【问题标题】:Modelling boost::Lockable with semaphore rather than mutex (previously titled: Unlocking a mutex from a different thread)建模 boost::Lockable 与信号量而不是互斥锁(以前的标题:从不同的线程解锁互斥锁)
【发布时间】:2011-02-14 20:11:56
【问题描述】:

我正在使用 C++ boost::thread 库,这在我的情况下意味着我正在使用 pthreads。正式地,必须从锁定它的同一个线程解锁互斥锁,我想要能够锁定一个线程然后解锁另一个线程的效果。有很多方法可以做到这一点。一种可能性是编写一个允许这种行为的新互斥类。

例如:

class inter_thread_mutex{
  bool locked;
  boost::mutex mx;
  boost::condition_variable cv;
public:
  void lock(){
    boost::unique_lock<boost::mutex> lck(mx);
    while(locked) cv.wait(lck);
    locked=true;
  }

  void unlock(){
    {
      boost::lock_guard<boost::mutex> lck(mx);
      if(!locked) error();
      locked=false;
    }
    cv.notify_one();
  }
// bool try_lock(); void error(); etc.
}

我应该指出,上面的代码并不能保证 FIFO 访问,因为如果一个线程调用 lock() 而另一个调用 unlock(),第一个线程可能会先于其他正在等待的线程获取锁。 (想想看, boost::thread 文档似乎没有为互斥锁或条件变量做出任何明确的调度保证)。但现在让我们先忽略它(以及任何其他错误)。

我的问题是,如果我决定走这条路,我是否能够使用这样的互斥锁作为 boost Lockable 概念的模型。例如,如果我使用 boost::unique_lock&lt; inter_thread_mutex &gt; 进行 RAII 样式访问,然后将此锁传递给boost::condition_variable_any.wait() 等,会不会出错?

一方面我不明白为什么不这样做。另一方面,“我不明白为什么不”通常是一种非常糟糕的方式来确定某事是否可行。

我问的原因是,如果事实证明我必须为 RAII 锁和条件变量以及其他任何东西编写包装类,那么我宁愿找到其他方法来实现相同的效果。

编辑: 我想要的那种行为基本上如下。我有一个对象,每当修改它时都需要锁定它。我想从一个线程锁定对象,并对其进行一些工作。然后我想在告诉另一个工作线程完成工作时保持对象锁定。因此,当工作线程完成时,第一个线程可以继续执行其他操作。当工作线程完成后,它会解锁互斥锁。

而且我希望过渡是无缝的,这样在线程 1 开始工作和线程 2 完成工作之间,没有其他人可以得到互斥锁。

inter_thread_mutex 之类的东西似乎可以工作,并且它还允许程序与其交互,就好像它是一个普通的互斥锁一样。所以这似乎是一个干净的解决方案。如果有更好的解决方案,我也很乐意听到。

再次编辑: 我需要锁的原因是有多个主线程,锁在那里防止它们以无效的方式同时访问共享对象。 所以代码已经在主线程级别使用了循环级别的无锁操作序列。此外,在最初的实现中,没有工作线程,互斥锁是普通的 Kosher 互斥锁。

inter_thread_thingy 是作为优化提出的,主要是为了提高响应时间。在许多情况下,保证操作 A 的“第一部分”发生在操作 B 的“第一部分”之前就足够了。举个愚蠢的例子,假设我打对象 1 并给它一个黑眼圈。然后我告诉对象 1 改变它的内部结构以反映所有的组织损伤。在继续打对象 2 之前,我不想等待组织损伤。但是,我确实希望组织损伤作为同一操作的一部分发生;例如,在此期间,我不希望任何其他线程以使组织损坏无效操作的方式重新配置对象。 (是的,这个例子在很多方面都不完美,不,我不是在做游戏)

所以我们对模型进行了更改,其中可以将对象的所有权传递给工作线程以完成操作,它实际上工作得很好;每个主线程都能够完成更多操作,因为它不需要等待它们全部完成。而且,由于主线程级别的事件排序仍然是基于循环的,因此编写高级主线程操作很容易,因为它们可以基于操作完成的假设(更准确地说,关键的“当相应的函数调用返回时,排序逻辑所依赖的第一部分完成)。

最后,我认为使用带有增强锁的 RAII 使用 inter_thread mutex/semaphore thingies 来封装使整个事情正常工作所需的必要同步会很好。

【问题讨论】:

  • Y我们的代码将不起作用,您仍在锁定从不同线程解锁 mx,这将导致未定义的行为。但是信号量会做你想做的事,查一下。
  • 其实我认为代码可以工作。 boost 锁是作用域的,这意味着它们在销毁时释放互斥锁。所以 boost::mutex 由锁定它的同一个线程释放。内部互斥锁的目的只是锁定对锁定布尔值的访问。但是,我确实同意我需要的是信号量。实际上,这个 inter_thread_mutex 实际上更像是一个二进制信号量,我称之为互斥量。
  • 这段代码看起来很像这段代码picturel.com/ucr/node32.html,用于使用互斥锁实现信号量,该互斥锁从这个精彩的 SO 答案stackoverflow.com/questions/62814/… 链接到关于信号量与互斥锁的关系。 OP 的问题本质上是,unique_lock 等是否适用于信号量以及互斥体。

标签: c++ mutex boost-thread


【解决方案1】:

man pthread_unlock(这是在 OS X 上,Linux 上的类似措辞)有答案:

姓名 pthread_mutex_unlock -- 解锁互斥锁 概要 #include 整数 pthread_mutex_unlock(pthread_mutex_t *mutex); 描述 如果当前线程持有互斥锁,那么 pthread_mutex_unlock() 函数解锁互斥锁。 使用互斥锁调用 pthread_mutex_unlock() 调用线程不持有会导致 未定义的行为。 ...

我的反问是——你想用这个解决什么样的同步问题?很可能有一个更简单的解决方案。

pthreadsboost::thread(建立在它之上)都不能保证竞争线程获取竞争互斥锁的任何顺序。

【讨论】:

  • 我明白这一点。这就是为什么我正在考虑编写一个可以从不同线程解锁的新互斥类。我在问题中编写的代码完成了这一点。我的问题是上面描述的 inter_thread_mutex 是否可以与其他 boost::thread 对象,特别是 RAII 锁和条件变量一起用作 Lockable 概念。
  • @dan:你要找的不是互斥锁。您正在寻找信号量或条件变量之类的东西。
  • @jalf:是的,我认为你是对的,我真正想要的是信号量。
  • 我很确定一个有效的实现将允许来自任何线程的pthread_mutex_unlock。 (但检查一下也无妨。)
【解决方案2】:

对不起,我不明白。如果另一个线程可以解锁,下面代码中第 [1] 行中的互斥锁的状态是什么?

inter_thread_mutex m;

{
  m.lock();
  // [1]
  m.unlock();
}

这毫无意义。

【讨论】:

  • 如果另一个线程解锁它,那是程序中的一个错误。这个想法是能够将互斥锁锁定在一个线程中,然后将锁传递给另一个线程,然后将其解锁。
  • @dan 你能举个例子说明“将锁传递”给不同的线程来解锁是如何有用的吗? “通过”究竟意味着什么?
  • @Logan:“传递”锁的想法是向第二个线程发出信号,开始做一些需要互斥锁所有权的事情。一旦第二个线程启动,它会返回信号,并且第一个线程释放互斥锁的所有权而不解锁它。然后第二个线程在完成后将其解锁。但正如其他人所指出的那样,似乎互斥锁似乎不适合用于此目的。
  • @dan 我想第一个线程将等待第二个线程的信号。那么第二个线程不需要解锁第一个线程的互斥量,所以不需要inter_thread_mutex。
【解决方案3】:

有几种方法可以解决这个问题。我要建议的两个方法都涉及向对象添加一条额外的信息,而不是添加一种机制来从拥有它的线程之外的线程中解锁线程。

1)您可以添加一些信息来指示对象的状态:

enum modification_state { consistent, // ready to be examined or to start being modified
                          phase1_complete, // ready for the second thread to finish the work
                        };

// first worker thread
lock();
do_init_work(object);
object.mod_state = phase1_complete;
unlock();
signal();
do_other_stuff();

// second worker thread
lock()
while( object.mod_state != phase1_complete )
  wait()
do_final_work(obj)
object.mod_state = consistent;
unlock()
signal()

// some other thread that needs to read the data
lock()
while( object.mod_state != consistent )
  wait();
read_data(obj)
unlock()

与条件变量一起工作得很好,因为显然你不是在写自己的锁。

2) 如果你有一个特定的线程,你可以给对象一个所有者。

  // first worker
  lock();
  while( obj.owner != this_thread() ) wait();
  do_initial_work(obj);
  obj.owner = second_thread_id;
  unlock()
  signal()

  ...

这与我的第一个解决方案几乎相同,但在添加/删除阶段更灵活,在添加/删除线程方面不太灵活。

老实说,我不确定线程​​间互斥锁将如何帮助您。您仍然需要一个信号量或条件变量来表示将工作传递给第二个线程。

【讨论】:

  • 谢谢。我正在使用的当前实现类似于上面的(1)。我只是想也许可以通过 inter_thread_mutex 之类的东西来简化它。但看起来答案可能是否定的。实际上 inter_thread_mutex 类实际上比互斥量更接近二进制信号量。另外,是的,我使用条件变量来发出信号,所以我当然不会试图避免 cvs。只是寻找一种简洁的方法来模拟“将锁定资源的所有权”从一个线程传递到另一个线程的想法。
【解决方案4】:

对您已有的内容进行小修改:在您的inter_thread_whatever 中存储您想要锁定的线程的ID 怎么样?然后解锁它,并向该线程发送一条消息,说“我希望你执行任何试图获取此锁的例程”。

那么lock 中的条件变为while(locked || (desired_locker != thisthread &amp;&amp; desired_locker != 0))。从技术上讲,您在第一个线程中“释放了锁”,并在第二个线程中“再次获取”,但是任何其他线程都无法在两者之间抓住它,所以就好像您直接从一对一。

有一个潜在的问题,如果一个线程退出或被杀死,而它是你想要的锁的储物柜,那么那个线程就会死锁。但是您已经在谈论第一个线程等待来自第二个线程的消息说它已成功获取锁,所以大概您已经计划好了如果从未收到该消息会发生什么。在该计划中,添加“重置 inter_thread_whatever 上的 desired_locker 字段”。

不过,这一切都非常棘手,我不相信我的提议是正确的。有没有办法让“主”线程(指导所有这些助手的那个)可以确保它不会命令对受此锁保护的任何内容执行更多操作,直到第一个操作完成(或失败并且一些 RAII 的事情通知你)?如果您可以在消息循环级别处理它,您就不需要锁。

【讨论】:

  • 感谢 cmets。 (1) 我考虑过存储一个线程 ID,但我认为(可能是错误的)逻辑上管理所有权会更简洁。在思想实验中,显式使用线程 ID 似乎最终会产生更多的复杂性(例如,在您讨论的错误处理期间)。 (2) 是的,存在从未收到 ack 的问题;简而言之,在这种情况下,会发送一条“nack”或 ack-fail 消息,然后它会恢复并释放锁。
  • ...(3) 编辑了原始消息,以更详细地说明主线程锁定的整体工作原理。
【解决方案5】:

我认为将您的inter_thread_mutex (binary_semaphore) 视为可锁定的模型并不是一个好主意。主要问题是您的inter_thread_mutex 的主要功能击败了Locakble 概念。如果inter_thread_mutex 是可锁定模型,您将在 [1] 中期望 inter_thread_mutex m 被锁定。

// thread T1
inter_thread_mutex m;

{
  unique_lock<inter_thread_mutex> lk(m);
  // [1]
}

但是当 T1 在 [1] 中时,另一个线程 T2 可以执行 m.unlock(),因此保证被打破。

二进制信号量可以用作Lockables,只要每个线程在解锁之前尝试锁定。但你班级的主要目标恰恰相反。

这是 Boost.Interprocess 中的信号量不使用锁定/解锁来命名函数,而是等待/通知的原因之一。奇怪的是,这些是条件使用的相同名称:)

【讨论】:

  • "您会在 [1] 中期望 inter_thread_mutex m 被锁定" 除非您将 lk 转移到其他线程!我看不出这里有什么问题。设计简单、正确、明显,任何替代解决方案都可能要复杂得多。
【解决方案6】:

互斥锁是一种描述互斥代码块的机制。这些代码块跨越线程边界是没有意义的。试图以这种反直觉的方式使用这样的概念只会导致问题。

这听起来很像您在寻找不同的多线程概念,但如果没有更多细节,就很难知道是什么。

【讨论】:

  • "互斥锁是一种用于描述互斥代码块的机制。" 一点也不。 “互斥代码块”是临界区的定义。互斥体用于对数据的独占访问
  • 因为您觉得有必要对次要语义投反对票:“关键部分本身并不是一种互斥机制或算法。” (en.wikipedia.org/wiki/Mutex)。互斥锁是一种防止临界区同时执行的机制。几乎普遍这是因为他们访问相同的资源。无论如何,关键部分和互斥是密切相关的概念,确切的语义并没有完全改变我的观点,即我认为原作者正在寻找不同的原语。
猜你喜欢
  • 2012-12-25
  • 1970-01-01
  • 1970-01-01
  • 2012-05-07
  • 2011-07-24
  • 2018-05-23
  • 2010-11-22
相关资源
最近更新 更多