如何让一个线程在linux中等待另一个?

时间:2017-04-30 15:01:38

标签: c linux pthreads mutex

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

1 个答案:

答案 0 :(得分:3)

通常,您定义操作顺序,而不是执行这些操作的线程。这可能听起来像是一个微不足道的区别,但是当你开始实现它时,你会发现它会带来很大的不同。它也是更有效的方法,因为您没有考虑所需的线程数,而是要考虑要完成的操作或任务的数量,以及可以并行完成的操作或任务的数量,以及它们可能需要的方式有序或有序的。

但是,出于学习目的,可能需要考虑订购线程。

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)转换为指针。第一个转换为intptr_t中定义的<stdint.h>类型(当您包含<inttypes.h>时会自动包含该类型),这是一个有符号整数类型,可以保存任何void指针的值;第二个演员是一个无效指针。如果您的ID是一个整数类型,无法转换为void指针而不会丢失信息(通常在警告中描述为“不同大小”),则中间转换会避免出现警告。

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

剩下的一个问题是如何定义订单。通常,您只需递增或递减ID值 - 递减意味着线程将按ID值的降序运行,但ID值为-1(假设您将线程从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的线程应该在创建工作线程时保留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程序员)更加熟悉如何使用互斥锁和条件变量,我展示了上面的例子。