【问题标题】:How to make a thread wait for another one in linux?如何让一个线程在linux中等待另一个线程?
【发布时间】:2017-09-28 04:50:49
【问题描述】:

例如,我想创建 5 个线程并打印它们。如何让第四个在第二个之前执行?我尝试用互斥锁锁定它,但我不知道如何只锁定第二个,所以它给了我分段错误。

【问题讨论】:

  • 发布您的代码。还要看看信号量。

标签: c linux pthreads mutex


【解决方案1】:

通常,您定义操作的顺序,而不是执行这些操作的线程。这听起来像是微不足道的区别,但是当您开始实施它时,您会发现它产生了重大差异。这也是一种更有效的方法,因为您不会考虑需要的线程数量,而是要完成的操作或任务的数量,以及其中可以并行完成的数量,以及它们可能需要如何完成排序或排序。

不过,出于学习目的,查看排序线程可能更有意义。

OP 为每个工作线程函数传递一个指向字符串的指针。这行得通,但有点奇怪;通常你传递一个整数标识符:

#include <stdlib.h>
#include <inttypes.h>
#include <pthread.h>

#define  ID_TO_POINTER(id)  ((void *)((intptr_t)(id)))
#define  POINTER_TO_ID(ptr) ((intptr_t)(ptr))

ID 类型(我假设它是上面的有符号整数,通常是intlong)到指针的转换是通过两次强制转换完成的。第一个转换是&lt;stdint.h&gt; 中定义的intptr_t 类型(包含&lt;inttypes.h&gt; 时会自动包含),这是一个有符号整数类型,可以保存任何void 指针的值;第二个演员是空指针。如果您的 ID 是整数类型,无法在没有潜在信息丢失的情况下转换为/从 void 指针转换(通常在警告中描述为“不同大小”),中间转换避免了警告。

排序POSIX threads的最简单方法,与排序操作或任务或作业没有什么不同,是使用单个mutex作为锁来保护下一个应该运行的线程的ID,以及一个相关的condition variable 让线程等待,直到它们的 ID 出现。

剩下的一个问题是如何定义顺序。通常,您只需增加或减少 ID 值 - 减少意味着线程将按 ID 值的降序运行,但 -1 的 ID 值(假设您从 0 开始对线程进行编号)总是意味着“全部完成",无论使用多少线程:

static pthread_mutex_t  worker_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t   worker_wait = PTHREAD_COND_INITIALIZER;
static int              worker_id   = /* number of threads - 1 */;

void *worker(void *dataptr)
{
    const int id = POINTER_TO_ID(dataptr);

    pthread_mutex_lock(&worker_lock);
    while (worker_id >= 0) {
        if (worker_id == id) {

            /* Do the work! */
            printf("Worker %d running.\n", id);
            fflush(stdout);

            /* Choose next worker */
            worker_id--;
            pthread_cond_broadcast(&worker_wait);
        }

        /* Wait for someone else to broadcast on the condition. */
        pthread_cond_wait(&worker_wait, &worker_lock);
    }

    /* All done; worker_id became negative.
       We still hold the mutex; release it. */
    pthread_mutex_unlock(&worker_lock);

    return NULL;
}

请注意,我没有让工作人员在其任务完成后立即退出;这是因为我想稍微扩展一下示例:假设您要定义数组中的操作顺序:

static pthread_mutex_t  worker_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t   worker_wait = PTHREAD_COND_INITIALIZER;
static int              worker_order[] = { 0, 1, 2, 3, 4, 2, 3, 1, 4, -1 };
static int             *worker_idptr = worker_order;

void *worker(void *dataptr)
{
    const int id = POINTER_TO_ID(dataptr);

    pthread_mutex_lock(&worker_lock);
    while (*worker_idptr >= 0) {
        if (*worker_idptr == id) {

            /* Do the work! */
            printf("Worker %d running.\n", id);
            fflush(stdout);

            /* Choose next worker */
            worker_idptr++;
            pthread_cond_broadcast(&worker_wait);
        }

        /* Wait for someone else to broadcast on the condition. */
        pthread_cond_wait(&worker_wait, &worker_lock);
    }

    /* All done; worker_id became negative.
       We still hold the mutex; release it. */
    pthread_mutex_unlock(&worker_lock);

    return NULL;
}

看看变化有多大?

让我们考虑第三种情况:一个单独的线程,比如主线程,决定接下来运行哪个线程。在这种情况下,我们需要两个条件变量:一个供工作线程等待,另一个供主线程等待。

static pthread_mutex_t  worker_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t   worker_wait = PTHREAD_COND_INITIALIZER;
static pthread_cond_t   worker_done = PTHREAD_COND_INITIALIZER;
static int              worker_id = 0;

void *worker(void *dataptr)
{
    const int id = POINTER_TO_ID(dataptr);

    pthread_mutex_lock(&worker_lock);
    while (worker_id >= 0) {
        if (worker_id == id) {

            /* Do the work! */
            printf("Worker %d running.\n", id);
            fflush(stdout);

            /* Notify we are done. Since there is only
               one thread waiting on the _done condition,
               we can use _signal instead of _broadcast. */
            pthread_cond_signal(&worker_done);
        }

        /* Wait for a change in the worker_id. */
        pthread_cond_wait(&worker_wait, &worker_lock);
    }

    /* All done; worker_id became negative.
       We still hold the mutex; release it. */
    pthread_mutex_unlock(&worker_lock);

    return NULL;
}

决定哪个工作线程首先运行的线程应该在创建工作线程时持有worker_lock 互斥锁,然后等待worker_done 条件变量。当第一个工作人员完成其任务时,它将在worker_cone 条件变量上发出信号,并在worker_wait 条件变量上等待。然后决策者线程应该将worker_id 更改为应该运行的下一个ID,并在worker_wait 条件变量上广播。这将继续,直到决策者线程将worker_id 设置为负值。例如:

int             threads; /* number of threads to create */
pthread_t      *ptids;   /* already allocated for that many */    
pthread_attr_t  attrs;
int             i, result;

/* Simple POSIX threads will work with 65536 bytes of stack
   on all architectures -- actually, even half that. */
pthread_attr_init(&attrs);
pthread_attr_setstacksize(&attrs, 65536);

/* Hold the worker_lock. */
pthread_mutex_lock(&worker_lock);

/* Create 'threads' threads. */
for (i = 0; i < threads; i++) {
    result = pthread_create(&(ptids[i]), &attrs, worker, ID_TO_POINTER(i));
    if (result) {
        fprintf(stderr, "Cannot create worker threads: %s.\n", strerror(result));
        exit(EXIT_FAILURE);
    }
}

/* Thread attributes are no longer needed. */
pthread_attr_destroy(&attrs);

while (1) {

    /* 
       TODO: Set worker_id to a new value, or
             break when done.
    */

    /* Wake that worker */
    pthread_cond_broadcast(&worker_wait);

    /* Wait for that worker to complete */
    pthread_cond_wait(&worker_done, &worker_lock);
}

/* Tell workers to exit */
worker_id = -1;
pthread_cond_broadcast(&worker_wait);

/* and reap the workers */
for (i = 0; i < threads; i++)
    pthread_join(ptids[i], NULL);

以上所有示例中都有一个非常重要的细节,如果没有大量实践,可能很难理解:互斥锁和条件变量如何交互(如果通过pthread_cond_wait() 配对)。

当一个线程调用pthread_cond_wait()时,它会原子地释放指定的互斥体,并等待条件变量上的新信号/广播。 “原子”意味着两者之间没有时间;两者之间不会发生任何事情。当接收到信号或广播时调用返回——不同之处在于信号只发送给一个随机服务员;而广播到达所有等待条件变量的线程 --,并且线程获取锁。你可以把这想象成信号/广播首先唤醒线程,但pthread_cond_wait() 只会在它重新获取互斥锁时返回。

上述所有示例都隐含地使用了此行为。特别是,您会注意到pthread_cond_signal()/pthread_cond_broadcast() 总是在持有worker_lock 互斥锁时完成;这确保了其他线程或多个线程仅在 worker_lock 互斥锁被解锁后才被唤醒并开始行动——无论是显式的,还是通过等待条件变量的持有线程。

我想我可能会绘制一个关于事件和动作顺序的有向图(使用 Graphviz),但是这个“答案”已经太长了。我建议你自己做——也许在纸上? ——因为当我学习所有这些东西时,这种可视化对我自己非常有用。

 
我必须承认,我确实对上述方案感到很不舒服。在任何时候,只有一个线程在运行,这基本上是错误:任何应该按特定顺序完成任务的作业都应该只需要一个线程。

不过,我展示了上述示例是为了让您(不仅仅是 OP,还有任何对 POSIX 线程感兴趣的 C 程序员)更熟悉如何使用互斥锁和条件变量。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-07-10
    • 1970-01-01
    • 2021-06-21
    • 2015-02-22
    • 1970-01-01
    • 2010-09-22
    相关资源
    最近更新 更多