问题 在pthread中,如何可靠地将信号传递给另一个线程?


我正在尝试在pthread中编写一个简单的线程池程序。然而,似乎 pthread_cond_signal 不阻止,这会产生问题。例如,假设我有一个“生产者 - 消费者”计划:

pthread_cond_t my_cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t my_cond_m = PTHREAD_MUTEX_INITIALIZER;

void * liberator(void * arg)
{
    // XXX make sure he is ready to be freed
    sleep(1);

    pthread_mutex_lock(&my_cond_m);
    pthread_cond_signal(&my_cond);
    pthread_mutex_unlock(&my_cond_m);

    return NULL;
}

int main()
{
    pthread_t t1;
    pthread_create(&t1, NULL, liberator, NULL);

    // XXX Don't take too long to get ready. Otherwise I'll miss 
    // the wake up call forever
    //sleep(3);

    pthread_mutex_lock(&my_cond_m);
    pthread_cond_wait(&my_cond, &my_cond_m);
    pthread_mutex_unlock(&my_cond_m);

    pthread_join(t1, NULL);

    return 0;
}

如前所述 XXX 标记,如果我带走了 sleep 然后打电话 main() 可能因为错过了来电而失速 liberator()。当然, sleep 不是一种非常强大的方法来确保这两者。

在现实生活中,这将是一个工作线程告诉经理线程它已准备好工作,或者经理线程宣布新工作可用。

你会如何在pthread中可靠地完成这项工作?


@ Borealid的答案有点作品,但他对这个问题的解释可能会更好。我建议任何看这个问题的人阅读评论中的讨论,以了解正在发生的事情。

特别是,我自己会修改他的答案和这样的代码示例,以使其更清楚。 (因为Borealid的原始答案,在编译和工作时,让我很困惑)

// In main
pthread_mutex_lock(&my_cond_m);

// If the flag is not set, it means liberator has not 
// been run yet. I'll wait for him through pthread's signaling 
// mechanism

// If it _is_ set, it means liberator has been run. I'll simply 
// skip waiting since I've already synchronized. I don't need to 
// use pthread's signaling mechanism
if(!flag) pthread_cond_wait(&my_cond, &my_cond_m);

pthread_mutex_unlock(&my_cond_m);

// In liberator thread
pthread_mutex_lock(&my_cond_m);

// Signal anyone who's sleeping. If no one is sleeping yet, 
// they should check this flag which indicates I have already 
// sent the signal. This is needed because pthread's signals 
// is not like a message queue -- a sent signal is lost if 
// nobody's waiting for a condition when it's sent.
// You can think of this flag as a "persistent" signal
flag = 1;
pthread_cond_signal(&my_cond);
pthread_mutex_unlock(&my_cond_m);

8922
2017-08-04 02:10


起源



答案:


使用同步变量。

main

pthread_mutex_lock(&my_cond_m);
while (!flag) {
    pthread_cond_wait(&my_cond, &my_cond_m);
}
pthread_mutex_unlock(&my_cond_m);

在线程中:

pthread_mutex_lock(&my_cond_m);
flag = 1;
pthread_cond_broadcast(&my_cond);
pthread_mutex_unlock(&my_cond_m);

对于生产者 - 消费者问题,这将是消费者在缓冲区为空时休眠,并且生产者在其满时时休眠。记得拿锁 之前 访问全局变量。


7
2017-08-04 02:15



是否 pthread_cond_broadcast 块?因为我遇到的问题与工作线程有关(liberator) 不阻止所以 main 会错过的 pthread_cond_signal (因为它不是一个消息队列 - 它只是一次抓住任何人等待)。我真的不明白为什么添加旗帜有助于解决问题 - 毕竟, liberator 还是开了一枪,如果 main 甚至没有达到新的 while 在那个时间循环(因此还没有等待),它将永远错过唤醒呼叫(因为线程不会再次触发它)。我错过了什么? - kizzx2
我添加了自己的答案。虽然它还涉及一个额外的变量和一个while循环,但这些并不是问题的核心。 IMO,这个答案没有解释工作线程在原始程序中没有睡眠(它应该)的关键点。这个答案的代码也有误导性,因为添加了一个 while 循环到等待状态显然没有帮助解决问题 - 因为原始问题是 main 陷入困境! - kizzx2
@ kizzx2:我觉得你真的不懂。您的实际问题是您的代码不会在事件发生时继续 liberator 之前运行 main 到达它 pthread_cond_wait 呼叫。解决方案是为了 main 等待 只要 如果某些条件(解放器生效)尚未成立。你不想等待等待;你必须等待什么。的目的 while 循环(而不是 if如果你被外围吵醒,只是回去睡觉。 - Borealid
@Borealid:在这种特殊情况下,只有一个线程 liberator 这恰好是一个 pthread_cond_signal  - 没有其他人可以进行“无关”的叫醒。 main 应该更加关注它 惯于 醒来,而不是被疏远唤醒。正如你所说,问题是 liberator 发送信号 之前  main 在等待。如何添加一个 while 循环和标志解决问题?如果 liberator 之前发送信号 main 甚至有机会检查标志或进入while循环,程序停止所有相同。 .... - kizzx2
@Borealid:因此,根据第一原则,解决方案就是这样 liberator 负责 确保 那个 pthread_cond_signal  将 到达等待的人,而不是在空气中迷失。正如你所说的那样(用文字而不是代码) liberator 是关于发送信号,它必须检查是否 main 是睡觉还是不睡觉。如果 main 在睡觉, liberator 可以继续发送信号。如果 main 是 不 还在等 liberator 必须等待 main 至 pthread_cond_wait 第一。这种“等待”行动可以通过几种手段来实现,例如传统手段 while 循环(轮询).. - kizzx2


答案:


使用同步变量。

main

pthread_mutex_lock(&my_cond_m);
while (!flag) {
    pthread_cond_wait(&my_cond, &my_cond_m);
}
pthread_mutex_unlock(&my_cond_m);

在线程中:

pthread_mutex_lock(&my_cond_m);
flag = 1;
pthread_cond_broadcast(&my_cond);
pthread_mutex_unlock(&my_cond_m);

对于生产者 - 消费者问题,这将是消费者在缓冲区为空时休眠,并且生产者在其满时时休眠。记得拿锁 之前 访问全局变量。


7
2017-08-04 02:15



是否 pthread_cond_broadcast 块?因为我遇到的问题与工作线程有关(liberator) 不阻止所以 main 会错过的 pthread_cond_signal (因为它不是一个消息队列 - 它只是一次抓住任何人等待)。我真的不明白为什么添加旗帜有助于解决问题 - 毕竟, liberator 还是开了一枪,如果 main 甚至没有达到新的 while 在那个时间循环(因此还没有等待),它将永远错过唤醒呼叫(因为线程不会再次触发它)。我错过了什么? - kizzx2
我添加了自己的答案。虽然它还涉及一个额外的变量和一个while循环,但这些并不是问题的核心。 IMO,这个答案没有解释工作线程在原始程序中没有睡眠(它应该)的关键点。这个答案的代码也有误导性,因为添加了一个 while 循环到等待状态显然没有帮助解决问题 - 因为原始问题是 main 陷入困境! - kizzx2
@ kizzx2:我觉得你真的不懂。您的实际问题是您的代码不会在事件发生时继续 liberator 之前运行 main 到达它 pthread_cond_wait 呼叫。解决方案是为了 main 等待 只要 如果某些条件(解放器生效)尚未成立。你不想等待等待;你必须等待什么。的目的 while 循环(而不是 if如果你被外围吵醒,只是回去睡觉。 - Borealid
@Borealid:在这种特殊情况下,只有一个线程 liberator 这恰好是一个 pthread_cond_signal  - 没有其他人可以进行“无关”的叫醒。 main 应该更加关注它 惯于 醒来,而不是被疏远唤醒。正如你所说,问题是 liberator 发送信号 之前  main 在等待。如何添加一个 while 循环和标志解决问题?如果 liberator 之前发送信号 main 甚至有机会检查标志或进入while循环,程序停止所有相同。 .... - kizzx2
@Borealid:因此,根据第一原则,解决方案就是这样 liberator 负责 确保 那个 pthread_cond_signal  将 到达等待的人,而不是在空气中迷失。正如你所说的那样(用文字而不是代码) liberator 是关于发送信号,它必须检查是否 main 是睡觉还是不睡觉。如果 main 在睡觉, liberator 可以继续发送信号。如果 main 是 不 还在等 liberator 必须等待 main 至 pthread_cond_wait 第一。这种“等待”行动可以通过几种手段来实现,例如传统手段 while 循环(轮询).. - kizzx2


我找到了解决方案 这里。对我来说,理解问题的棘手问题在于:

  1. 生产者和消费者必须能够双向沟通。无论哪种方式还不够。
  2. 这种双向通信可以打包成一个pthread条件。

为了说明,上面提到的博客文章表明这​​实际上是有意义和理想的行为:

pthread_mutex_lock(&cond_mutex);
pthread_cond_broadcast(&cond):
pthread_cond_wait(&cond, &cond_mutex);
pthread_mutex_unlock(&cond_mutex);

这个想法是,如果生产者和消费者都使用这种逻辑,那么他们中的任何一个都可以安全地睡觉,因为每个人都能够唤醒另一个角色。换句话说,在一个典型的生产者 - 消费者场景中 - 如果消费者需要睡觉,那是因为生产者需要醒来,反之亦然。将此逻辑打包在单个pthread条件中是有道理的。

当然,上面的代码有一个意想不到的行为,即当一个工作线程实际上只想唤醒生产者时,它也会唤醒另一个睡眠工作线程。这可以通过@Borealid建议的简单变量检查来解决:

while(!work_available) pthread_cond_wait(&cond, &cond_mutex);

在工作人员广播时,所有工作线程将被唤醒,但是一个接一个(因为隐式互斥锁定在 pthread_cond_wait)。由于其中一个工作线程将消耗工作(设置 work_available 回到 false),当其他工作线程醒来并实际开始工作时,工作将无法进行,因此工作人员将再次入睡。

这是我测试的一些评论代码,对于任何感兴趣的人:

// gcc -Wall -pthread threads.c -lpthread

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <assert.h>

pthread_cond_t my_cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t my_cond_m = PTHREAD_MUTEX_INITIALIZER;

int * next_work = NULL;
int all_work_done = 0;

void * worker(void * arg)
{
    int * my_work = NULL;

    while(!all_work_done)
    {
        pthread_mutex_lock(&my_cond_m);

        if(next_work == NULL)
        {
            // Signal producer to give work
            pthread_cond_broadcast(&my_cond);

            // Wait for work to arrive
            // It is wrapped in a while loop because the condition 
            // might be triggered by another worker thread intended 
            // to wake up the producer
            while(!next_work && !all_work_done)
                pthread_cond_wait(&my_cond, &my_cond_m);
        }

        // Work has arrived, cache it locally so producer can 
        // put in next work ASAP
        my_work = next_work;
        next_work = NULL;
        pthread_mutex_unlock(&my_cond_m);

        if(my_work)
        {
            printf("Worker %d consuming work: %d\n", (int)(pthread_self() % 100), *my_work);
            free(my_work);
        }
    }

    return NULL;
}

int * create_work()
{
    int * ret = (int *)malloc(sizeof(int));
    assert(ret);
    *ret = rand() % 100;
    return ret;
}

void * producer(void * arg)
{
    int i;

    for(i = 0; i < 10; i++)
    {
        pthread_mutex_lock(&my_cond_m);
        while(next_work != NULL)
        {
            // There's still work, signal a worker to pick it up
            pthread_cond_broadcast(&my_cond);

            // Wait for work to be picked up
            pthread_cond_wait(&my_cond, &my_cond_m);
        }

        // No work is available now, let's put work on the queue
        next_work = create_work();
        printf("Producer: Created work %d\n", *next_work);

        pthread_mutex_unlock(&my_cond_m);
    }

    // Some workers might still be waiting, release them
    pthread_cond_broadcast(&my_cond);

    all_work_done = 1;
    return NULL;
}

int main()
{
    pthread_t t1, t2, t3, t4;

    pthread_create(&t1, NULL, worker, NULL);
    pthread_create(&t2, NULL, worker, NULL);
    pthread_create(&t3, NULL, worker, NULL);
    pthread_create(&t4, NULL, worker, NULL);

    producer(NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    return 0;
}

4
2017-08-04 14:02



这怎么不是公认的答案?不能接受的。 - étale-cohomology