为什么传递对Mutex类的引用不是一个好的设计

Why is passing references to the Mutex class not a good design?

本文关键字:一个 Mutex 为什么 引用      更新时间:2023-10-16

从这里开始:我定义的Mutex类中的逻辑错误以及我在生产者-消费者程序pthreads 中使用它的方式

将引用(!)传递给互斥类的方式显然是在找麻烦,它挑战了任何类型的封装。

为什么这是个问题?我应该先传递值,然后编写复制构造函数吗?

在这种情况下,缺少封装会造成什么危害?我应该如何封装什么?

此外,为什么传递对Mutex类的引用不是一个好的设计?

传递对锁的引用是个坏主意——你不"使用"锁,你只获取它,然后再把它还给它。移动它会使您很难跟踪(关键)资源的使用情况。传递对互斥变量而不是锁的引用可能没那么糟糕,但它仍然会让人更难知道程序的哪些部分可能会死锁,因此需要避免。

请用简单的语言举例说明——为什么传递参考文献是个坏主意?

我认为这是一个糟糕的抽象和封装。默认情况下,mutex通常是在删除复制构造函数的情况下构建的,具有多个引用同一逻辑对象的互斥对象是容易出错的,也就是说,它可能导致死锁和其他竞争条件,因为程序员或读取器可以假设它们是不同的实体。

此外,通过指定您正在使用的内部互斥,您将暴露线程的实现细节,从而破坏mutex类的抽象。如果您使用的是pthread_mutex_t,那么您很可能使用的是内核线程(pthreads)。

封装也被破坏了,因为Mutex不是一个单独的封装实体,而是分散在几个(可能是悬挂的)引用中。

如果你想把pthread_mutex_t封装到一个类中,你可以按照的方式来做

class Mutex {
public:
    void lock(); 
    void unlock();
    // Default and move constructors are good! 
    // You can store a mutex in STL containers with these
    Mutex();
    Mutex(Mutex&&);
    ~Mutex();
    // These can lead to deadlocks!
    Mutex(const Mutex&) = delete;
    Mutex& operator= (const Mutex&) = delete;
    Mutex& operator= (Mutex&&) = delete;
private:
    pthread_mutex_t internal_mutex;
};

互斥对象是指在实现文件中声明的共享作用域中共享,而不是在本地声明并在函数中作为引用传递。理想情况下,只将参数传递给所需的线程构造函数。在与所讨论的函数(本例中为线程执行)相同的"级别"传递对作用域中声明的对象的引用通常会导致代码错误。如果声明互斥对象的作用域不再存在,会发生什么?mutex的析构函数会使互斥锁的内部实现无效吗?如果互斥体通过传递到达另一个模块,而该模块启动自己的线程并认为互斥体永远不会阻塞,会发生什么,这可能会导致严重的死锁。

另外,您希望使用互斥移动构造函数的一种情况是在互斥工厂模式中,如果你想创建一个新的互斥体,你可以调用一个函数,这个函数会返回一个互斥体,然后你会把它添加到互斥体列表中,或者通过某种共享数据传递给请求它的线程(前面提到的列表对这个共享数据来说是个好主意)。然而,正确地获得这样一个互斥体工厂模式可能非常棘手,因为您需要锁定对互斥体公共列表的访问。尝试一下应该很有趣!

如果作者的意图是避免全局作用域,那么在实现文件中将其声明为静态对象就足够抽象了。

我会将其提炼为单独的问题:1.什么时候通过引用传递任何对象是合适的?2.什么时候共享互斥体是合适的?

  1. 将对象作为参数传递的方式反映了您希望如何在调用者和被调用者之间共享对象的生存期。如果通过引用传递,则必须假设被调用者仅在调用期间使用对象,或者,如果引用由被调用者存储,则被调用者的生存期比引用短。如果处理动态分配的对象,您可能应该使用智能指针,它(除其他外)允许您更明确地传达您的意图(请参阅Herb Sutter对此主题的处理)。

  2. 应避免共享互斥对象。无论是通过引用还是任何其他方式传递,都是如此。通过共享互斥,对象允许自己在内部受到外部实体的影响。这违反了基本的封装,这已经足够合理了。(关于封装的优点,请参阅任何关于面向对象编程的文本)。共享互斥锁的一个真正后果是死锁的可能性。

    举个简单的例子:

    • A拥有互斥
    • A与B共享互斥
    • B在调用A上的函数之前获得锁
    • A的函数试图获得锁
    • 死锁

从设计的角度来看,您为什么要共享互斥锁?互斥锁保护可能被多个线程访问的资源。该互斥对象应该隐藏(封装)在控制该资源的类中。互斥只是这个类保护资源的一种方式;这是一个只有类才应该知道的实现细节。相反,共享控制资源的类的实例,并允许它以任何方式确保自身内的线程安全。