【问题标题】:Why to pass mutex as a parameter to a function being called by a thread?为什么要将互斥锁作为参数传递给线程调用的函数?
【发布时间】:2011-09-26 12:49:47
【问题描述】:

在某些地方,我看到人们创建线程池并创建线程并使用这些线程执行函数。在调用该函数时 boost::mutex 是通过引用传递的。为什么这样做?我相信您可以在被调用函数本身中声明互斥体,也可以将其声明为类成员或全局成员。谁能解释一下?

例如

   myclass::processData()
   {
         boost::threadpool::pool pool(2);
         boost::mutex mutex;

         for (int i =0; data<maxData; ++data)
             pool.schedule(boost::bind(&myClass::getData, boost_cref(*this), boost::ref(mutex)));
    }

那么,

    myClass::getData(boost::mutex& mutex)
    {
         boost::scoped_lock(mutex)    // Why can't we have class member variable mutex or                                     
                                      //local mutex here
        //Do somethign Here
}

【问题讨论】:

  • 您应该添加一个使用它的特定示例。这很可能是由于示例中实现的语义,并且根本无法在没有上下文的情况下回答(除非您想要通用 只是因为可能有意义 答案)

标签: c++ multithreading boost mutex


【解决方案1】:

互斥体是不可复制的对象,虽然它们可以是类的成员,但它会使父类的复制能力大大复杂化。因此,如果多个类实例需要共享相同的数据,一种首选方法是将互斥体创建为静态数据成员。否则,如果互斥锁只需要在类本身的实例中锁定,您可以创建一个指向互斥锁的指针作为非静态数据成员,然后该类的每个副本都将拥有自己的动态分配的互斥锁(并且如果需要,请保持可复制)。

在上面的代码示例中,基本上发生的是通过引用将全局互斥锁传递到线程池中。这使得共享相同内存位置的所有线程能够使用完全相同的互斥锁在该内存上创建排他锁,而无需管理互斥锁本身的不可复制方面的开销。此代码示例中的互斥锁也可能是类myClass 的静态数据成员,而不是通过引用传入的全局互斥锁,假设每个线程都需要锁定一些可从每个线程全局访问的内存线程。

本地互斥锁的问题在于它只是互斥锁的本地可访问版本...因此,当线程锁定互斥锁以共享一些全局可访问的数据时,数据本身不受保护,因为所有其他线程将拥有自己的本地互斥锁,可以锁定和解锁。它破坏了互斥的全部意义。

【讨论】:

  • 这毫无意义,在许多应用程序中,将互斥锁作为非静态成员是有意义的。比将互斥锁作为静态成员更常见,并且没有理由不将指针或引用存储为成员。互斥锁应该在被保护的数据级别(即如果它保护静态数据,它必须是静态的,如果它保护成员属性,那么它可能应该是一个成员,如果它由不同的实例或不同的类型,那么它必须更通用
  • 对不起,我不是说你不能这样做......我只是说如果你让它成为一个非静态数据成员,创建一个正确可复制的类要困难得多(最后它并不是真正可复制的,您必须存储一个指向互斥锁的指针,然后在副本中解除分配和重新分配互斥锁,这不是一个“真正的”副本班级的每个成员)。我将修改我的答案以使这一点更清楚。
  • ... 这一切都取决于对象的定义(语义上)在大多数情况下,互斥锁不构成对象状态的一部分,但它是一种实用程序,可确保真实的在多线程环境中使用时,在中间不正确(不变量损坏)状态下无法看到类的状态。
  • 是的,我同意,但是在给出的代码示例中,虽然没有明确表达,但似乎因为指向静态 myClass 方法的指针被传递给 bind 而没有指针对于myClass 的实例,有一些数据在线程之间共享,这需要从多个myClass 实例访问的互斥锁。
【解决方案2】:

我相信您可以在被调用函数本身中声明互斥体,或者可以将其声明为类成员或全局成员。谁能解释一下?

在入口处创建一个新的互斥锁不会保护任何东西。

如果您正在考虑声明一个静态(或全局)互斥锁来保护非静态成员,那么您不妨将程序编写为单线程程序(好吧,有一些极端情况)。静态锁会阻塞除一个之外的所有线程(假设有比赛);相当于“一次最多可以有一个线程在此方法的主体中操作”。声明一个静态互斥体来保护静态数据是可以的。正如 David Rodriguez - dribeas 在另一个答案的 cmets 中简洁地表述的那样:“互斥锁应该处于受保护的数据级别”。

可以为每个实例声明一个成员变量,这将采用通用形式:

class t_object {
public:
    ...
    bool getData(t_data& outData) {
        t_lock_scope lock(this->d_lock);
        ...
        outData.set(someValue);
        return true;
    }

private:
    t_lock d_lock;
};

这种方法很好,而且在某些情况下理想。在大多数情况下,当您构建一个实例打算从其客户端抽象锁定机制和错误的系统时,这是有意义的。一个缺点是它可能需要更多的获取,并且通常需要更复杂的锁定机制(例如可重入)。通过更多的获取:客户端可能知道一个实例仅在一个线程中使用:在这种情况下为什么要锁定?同样,一堆小的线程安全方法会引入很多开销。使用锁定,您希望尽快进出保护区(无需引入许多获取),因此关键部分通常是比典型操作更大的操作。

如果公共接口需要将此锁作为参数(如您的示例所示),这表明您的设计可以通过私有化锁定来简化(使对象以线程安全的方式运行,而不是将锁作为外部持有的资源传递)。

使用外部(或绑定或关联)锁,您可以潜在地减少获取(或总锁定时间)。这种方法还允许您在事后向实例添加锁定。它还允许客户端配置锁的操作方式。客户端可以通过共享它们(在一组实例中)来使用更少的锁。即使是一个简单的组合示例也可以说明这一点(支持两种模型):

class t_composition {
public:
    ...
private:
    t_lock d_lock; // << name and data can share this lock
    t_string d_name;
    t_data d_data;
};

考虑到某些多线程系统的复杂性,将正确锁定的责任推给客户端可能是一个非常糟糕的主意

两种模型(绑定和作为成员变量)都可以有效地使用。在给定情况下哪个更好因问题而异。

【讨论】:

  • 谢谢贾斯汀。但是,这是否意味着如果要保护静态数据,则需要静态互斥锁,而如果要保护数据成员,则通常需要类级别的互斥锁?
  • 作为一种通用方法:是的,这是确保您的实现是线程安全的多种可用方法之一(另一种是 OP 中的方法)。如果您想最小化获取和锁定时间,粒度也很重要。在某些情况下,一个类级别的锁可能太小,这会导致许多不必要的获取。在许多情况下,较大的(全局或基于文档的)锁定并不理想,因为每次获取锁定的时间会增加,并且其他人在此期间等待的可能性要高得多。
【解决方案3】:

使用本地互斥锁是错误的:线程池可能调用多个函数实例,它们应该使用同一个互斥锁。班员没事。将互斥体传递给函数使其更通用和可读。调用者可以决定传递哪个互斥锁:类成员或其他任何东西。

【讨论】:

  • 这让我错了。如果由调用类决定如何使用互斥锁,它不应该不是接口定义的一部分吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-12-14
  • 1970-01-01
  • 2019-11-17
  • 2013-10-29
  • 2022-11-25
  • 1970-01-01
相关资源
最近更新 更多