多线程:当通过条件变量进行通信时,哪些变量需要MUTEX保护?

发布于 2025-02-09 07:26:37 字数 1841 浏览 3 评论 0原文

我对条件变量和相关的静音锁之间的相互作用有一个疑问(这是由我在演讲中提出的简化示例引起的,在此过程中使自己感到困惑)。两个线程正在交换数据(假设int n指示数组大小,而double *d通过共享变量在其过程中通过共享变量进行了。我使用其他int flag(最初flag = 0)来指示数据(nd)准备就绪(flag = 1),pthread_mutex_t mtx和条件变量pthread_cond_t cnd cnd

此部分来自接收器线程,该线程等待flag变为1在Mutex锁定下,但后来处理n> D无保护:

while (1) {
    pthread_mutex_lock(&mtx);
    while (!flag) {
      pthread_cond_wait(&cnd, &mtx);
    }
    pthread_mutex_unlock(&mtx);
    // use n and d
}

此部分来自发送者线程,该线程设置了nd事先而无需通过Mutex锁定的保护,但是设置flag n octerx锁定时:

n = 10;
d = malloc(n * sizeof(float));
pthread_mutex_lock(&mtx);
flag = 1;
pthread_cond_signal(&cnd);
pthread_mutex_unlock(&mtx);

很明显,您需要发件人中的互斥X,因为否则您会有“丢失的唤醒呼叫”问题(请参阅 https://stackoverflow.com/a/4544494/3852630 )。

我的问题是不同的:我不确定必须设置哪些变量(在发件人线程中)或读取(在接收器线程中) 在由Mutex Lock保护的区域内,以及哪些变量不需要受Mutex锁的保护。在双方保护flag是否足够,还是nd也需要保护?

pthread_cond_signal()的呼叫中,应保证发件人和接收器之间的内存visiblity(请参见下面的规则),因此应在那里(结合pthread_cond_wait_wait()>) /代码>)。

我知道这是一个不寻常的情况。通常,我的应用程序从接收器中的列表中修改了发送者中的任务列表,并在接收器的列表中修改了一个pop任务,并在双方的列表操作中保护了相关的静音锁定。但是,我不确定上面的情况需要什么。危险可能是编译器(不知道对变量的并发访问)优化了对变量的写入和/或读取访问权限?如果nd不受MUTEX的保护,还有其他问题吗?

感谢您的帮助!

David R. Butenhof:使用Posix线程编程,第89页:“线程值可以看到的任何内存值何时是信号...条件变量也可以通过该信号唤醒的任何线程都可以看到。”

I have a question on the interplay between condition variables and associated mutex locks (it arose from a simplified example I was presenting in a lecture, confusing myself in the process). Two threads are exchanging data (lets say an int n indicating an array size, and a double *d which points to the array) via shared variables in the memory of their process. I use an additional int flag (initially flag = 0) to indicate when the data (n and d) is ready (flag = 1), a pthread_mutex_t mtx, and a condition variable pthread_cond_t cnd.

This part is from the receiver thread which waits until flag becomes 1 under the protection of the mutex lock, but afterward processes n and d without protection:

while (1) {
    pthread_mutex_lock(&mtx);
    while (!flag) {
      pthread_cond_wait(&cnd, &mtx);
    }
    pthread_mutex_unlock(&mtx);
    // use n and d
}

This part is from the sender thread which sets n and d beforehand without protection by the mutex lock, but sets flag while the mutex is locked:

n = 10;
d = malloc(n * sizeof(float));
pthread_mutex_lock(&mtx);
flag = 1;
pthread_cond_signal(&cnd);
pthread_mutex_unlock(&mtx);

It is clear that you need the mutex in the sender since otherwise you have a "lost wakeup call" problem (see https://stackoverflow.com/a/4544494/3852630).

My question is different: I'm not sure which variables have to be set (in the sender thread) or read out (in the receiver thread) inside the region protected by the mutex lock, and which variables don't need to be protected by the mutex lock. Is it sufficient to protect flag on both sides, or do n and d also need protection?

Memory visiblity (see the rule below) between sender and receiver should be guaranteed by the call to pthread_cond_signal(), so the necessary pairwise memory barriers should be there (in combination with pthread_cond_wait()).

I'm aware that this is an unusual case. Typically my applications modify a task list in the sender and pop tasks from the list in the receiver, and the associated mutex lock protects of the list operations on both sides. However I'm not sure what would be necessary in the case above. Could the danger be that the compiler (which is not aware of the concurrent access to variables) optimizes away the write and/or read access to the variables? Are there other problems if n and d are not protected by the mutex?

Thanks for your help!

David R. Butenhof: Programming with POSIX Threads, p.89: "Whatever memory values a thread can see when is signals ... a condition variable can also be seen by any thread that is awakened by that signal ...".

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

风筝有风,海豚有海 2025-02-16 07:26:37

在低级别的内存排序级别上,规则不是根据“由静音的区域”而定,而是根据同步而指定的。每当两个不同的线程访问相同的非原子对象时,除非它们都是读取的,否则必须在两者之间进行同步操作,以确保其中一个访问肯定会发生在另一个访问之前。

实现同步的方法是让一个线程在访问共享变量后执行释放操作(例如解锁Mutex),并让另一个线程在访问访问之前执行获取操作(例如锁定Mutex)该程序逻辑保证获取的方式必须发生在发布后。

在这里,你有。访问nd(发件人代码的最后一行)后,发件人线程确实执行Mutex Unlock。并且接收器在访问它们之前确实会执行静音锁(内部pthread_cond_wait)。 flag的设置和测试可确保,当我们退出时(!flag)循环时,接收器的最新lop lop确实发生了发件人。因此实现了同步。

编译器和CPU不得执行任何会失败的优化,因此,他们无法优化访问nd的访问,或在同步周围重新排序运营。通常通过将发布/获取操作视为障碍来确保这一点。在释放屏障之前,必须对释放屏障之前的任何可能共享的对象进行任何访问,并在屏障之后发生的任何事物之前进行释放屏障,并冲洗到连贯的内存/缓存(在这种情况下,在任何其他线程都可能将MUTEX视为已解锁之前) 。如果需要特殊的CPU说明来确保全球可见性,则编译器必须发出。反之亦然,以获取障碍:在收购屏障之后以程序顺序发生的任何访问都不能在其前重新排序。

换句话说,编译器将释放屏障视为可能读取所有记忆的操作。因此,所有变量必须在此之前写出,以便在此时内存的实际内容将与抽象机器所具有的内容相匹配。同样,一个获取屏障被视为可能会写入所有内存的操作,并且后来必须从内存中重新加载所有变量。唯一的例外是局部变量,编译器可以证明没有其他线程可以合法地知道其地址。这些可以安全地保存在寄存器中或以其他方式重新排序。


的确,在同步锁定操作之后,接收器恰好再次解锁了穆特X,但这在这里并不重要。该解锁不会与此特定程序中的任何内容同步,并且对其执行没有影响。同样,为了同步访问nd,发件人在访问它们之前还是之后是否锁定了穆特克斯。 (尽管发件人在写入flag之前都必须锁定mutex;这就是我们确保接收器对flag的任何早期读物的确确实发生在写作之前确实发生了而不是与它进行比赛。)

“对共享变量的访问应在由静音的关键区域内访问”只是一个高级抽象,这是确保通过不同线程访问的一种方法,始终具有同步的解锁 -锁在它们之间。如果可以一遍又一遍地访问变量,则通常您需要锁定,然后再解锁此类访问,这等同于“关键部分”原理。但是这个原则本身并不是基本规则。

也就是说,在现实生活中,您可能会尽可能地遵循此原则,因为这将使编写正确的代码和避免使用细微的错误变得更容易,并且同样使其他程序员更容易验证您的代码是否正确。因此,即使在此程序中绝对不需要访问n,d要受到穆特克斯的“保护”,但无论如何,这样做可能是明智的,除非有重要和可以避免获得可衡量的福利(性能或其他福利)。


条件变量在此处的避免比赛中不起作用,除非pthread_cond_wait锁定了Mutex。从功能上讲,这等同于让接收器简单地执行“锁静脉锁;测试标志;解锁互斥”的紧密自旋环,而不会浪费所有这些CPU周期。

而且我认为Butenhof的报价是错误的,或者充其量是误导性的。我的理解是,pthread_cond_signal本身不能保证是任何形式的障碍,实际上没有内存排序效果。 POSIX并未直接解决内存排序,但对于标准C等效cnd_signal,情况就是这种情况。拥有pthread_cond_signal确保全局可见性是没有意义的,除非您可以通过假设所有这些访问在相应的PTHREAD_COND_WAIT_WAIT返回时可以使用它。但是,由于PTHREAD_COND_WAIT可能会流行醒来,因此您永远无法安全地做出这样的假设。

在此程序中,发件人中必要的释放屏障不是来自pthread_cond_signal,而是来自后续pthread_mutex_unlock

At the low level of memory ordering, the rules aren't specified in terms of "the region protected by a mutex", but in terms of synchronization. Whenever two different threads access the same non-atomic object, then unless they are both reads, there must be a synchronization operation in between, to ensure that one of the accesses definitely happens before the other.

The way to achieve synchronization is to have one thread perform a release operation (such as unlocking a mutex) after accessing the shared variables, and have the other thread perform an acquire operation (such as locking a mutex) before accessing them, in such a way that program logic guarantees the acquire must have happened after the release.

And here, you have that. The sender thread does perform a mutex unlock after accessing n and d (last line of the sender code). And the receiver does perform a mutex lock before accessing them (inside pthread_cond_wait). The setting and testing of flag ensures that, when we exit the while (!flag) loop, the most recent lock of the mutex by the receiver did happen after the unlock by the sender. So synchronization is achieved.

The compiler and CPU must not perform any optimization that would defeat this, so in particular they can't optimize away the accesses to n and d, or reorder them around the synchronizing operations. This is usually ensured by treating the release/acquire operations as barriers. Any accesses to potentially shared objects that occur in program order before a release barrier must actually be performed and flushed to coherent memory/cache prior to anything that comes after the barrier (in this case, before any other thread may see the mutex as unlocked). If special CPU instructions are needed to ensure global visibility, the compiler must emit them. And vice versa for acquire barriers: any access that occurs in program order after an acquire barrier must not be reordered before it.

To say it another way, the compiler treats the release barrier as an operation that may potentially read all of memory; so all variables must be written out before that point, so that the actual contents of memory at that point will match what an abstract machine would have. Likewise, an acquire barrier is treated as an operation that may potentially write all of memory, and all variables must be reloaded from memory afterwards. The only exception would be local variables for which the compiler can prove that no other thread could legally know their address; those can be safely kept in registers or otherwise reordered.


It's true that, after the synchronizing lock operation, the receiver happened to unlock the mutex again, but that isn't relevant here. That unlock doesn't synchronize with anything in this particular program, and it has no impact on its execution. Likewise, for purposes of synchronizing access to n and d, it didn't matter whether the sender locked the mutex before or after accessing them. (Though it was important that the sender locked the mutex before writing to flag; that's how we ensure that any earlier reads of flag by the receiver really did happen before the write, instead of racing with it.)

The principle that "accesses to shared variables should be inside a critical region protected by a mutex" is just a higher-level abstraction that is one way to ensure that accesses by different threads always have a synchronizing unlock-lock pair in between them. And in cases where the variables could be accessed over and over again, you normally would want a lock before, and an unlock after, every such access, which is equivalent to the "critical section" principle. But this principle is not itself the fundamental rule.

That said, in real life you probably do want to follow this principle as much as possible, since it will make it easier to write correct code and avoid subtle bugs, and likewise make it easier for other programmers to verify that your code is correct. So even though it is not strictly necessary in this program for the accesses to n,d to be "protected" by the mutex, it would probably be wise to do so anyway, unless there is a significant and measurable benefit (performance or otherwise) to be gained by avoiding it.


The condition variable doesn't play a role in the race avoidance here, except insofar as the pthread_cond_wait locked the mutex. Functionally, it is equivalent to having the receiver simply do a tight spin loop of "lock mutex; test flag; unlock mutex", but without wasting all those CPU cycles.

And I think that the quote from Butenhof is mistaken, or at best misleading. My understanding is that pthread_cond_signal by itself is not guaranteed to be a barrier of any kind, and in fact has no memory ordering effect whatsoever. POSIX doesn't directly address memory ordering, but this is the case for the standard C equivalent cnd_signal. There would be no point in having pthread_cond_signal ensure global visibility unless you could make use of it by assuming that all those accesses were visible by the time the corresponding pthread_cond_wait returns. But since pthread_cond_wait can wake up spuriously, you cannot ever safely make such an assumption.

In this program, the necessary release barrier in the sender comes not from pthread_cond_signal, but rather from the subsequent pthread_mutex_unlock.

只有一腔孤勇 2025-02-16 07:26:37

保护双方的标志是否足够,还是N和D也需要保护?

原则上,如果您使用MUTEX,CV和flag,以使作者不修改nd*D在设置之后在mutex保护下,读者无法访问nd*d直到观察flag的修改(在同一Mutex的保护下)之后,您可以依靠读者观察作者的最后写入值n <n < /code>,<代码> d*d。这或多或少是手工卷的信号量。

实际上,您应该使用您选择的系统同步对象(静音,信号量,等)来保护共享数据。这样做更容易推理,并且不太容易产生错误。通常,这也更简单。

Is it sufficient to protect flag on both sides, or do n and d also need protection?

In principle, if you use your mutex, CV, and flag in such a way that the the writer does not modify n, d, or *d after setting flag under mutex protection, and the reader cannot access n, d, or *d until after it observes the modification of flag (under protection of the same mutex), then you can rely on the reader observing the writer's last-written values of n, d, and *d. This is more or less a hand-rolled semaphore.

In practice, you should use whichever system synchronization objects you have chosen (mutexes, semaphores, etc.) to protect all the shared data. Doing so is easier to reason about and less prone to spawn bugs. Often, it's simpler, too.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文