首先,总结一下:
-
pthread_mutex_lock(&mutex):
如果mutex 是空闲的,那么这个线程会立即抓取它。
如果mutex 被抓取,那么这个线程会一直等到mutex 空闲,然后抓取它。
-
pthread_mutex_trylock(&mutex):
如果mutex 是空闲的,那么这个线程会抓取它。
如果mutex 被抓取,则调用会立即返回EBUSY。
-
pthread_mutex_unlock(&mutex):
发布mutex。
-
pthread_cond_signal(&cond):
唤醒一个等待条件变量cond的线程。
-
pthread_cond_broadcast(&cond):
唤醒所有等待条件变量 cond 的线程。
-
pthread_cond_wait(&cond, &mutex):
这必须用mutex 抓取。
调用线程将暂时释放mutex并等待cond。
当cond被广播,或者被发送信号并且这个线程恰好是被唤醒的那个,那么调用线程将首先重新抓取mutex,然后从调用中返回。
重要的是要注意,调用线程要么 一直抓到mutex,或 正在等待cond。中间没有间隔。
让我们看一个实际运行的示例代码。我们将按照 OP 的代码行创建它。
首先,我们将使用一个结构来保存每个工作函数的参数。由于我们希望在线程之间共享互斥锁和条件变量,因此我们将使用指针。
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <pthread.h>
#include <limits.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
/* Worker function work. */
struct work {
pthread_t thread_id;
pthread_mutex_t *lock; /* Pointer to the mutex to use */
pthread_cond_t *wait; /* Pointer to the condition variable to use */
volatile int *done; /* Pointer to the flag to check */
FILE *out; /* Stream to output to */
long id; /* Identity of this thread */
unsigned long count; /* Number of times this thread iterated. */
};
线程工作者函数接收到一个指向上述结构的指针。每个线程迭代循环一次,然后等待条件变量。当被唤醒时,如果完成标志仍然为零,则线程迭代循环。否则,线程退出。
/* Example worker function. */
void *worker(void *workptr)
{
struct work *const work = workptr;
pthread_mutex_lock(work->lock);
/* Loop as long as *done == 0: */
while (!*(work->done)) {
/* *(work->lock) is ours at this point. */
/* This is a new iteration. */
work->count++;
/* Do the work. */
fprintf(work->out, "Thread %ld iteration %lu\n", work->id, work->count);
fflush(work->out);
/* Wait for wakeup. */
pthread_cond_wait(work->wait, work->lock);
}
/* *(work->lock) is still ours, but we've been told that all work is done already. */
/* Release the mutex and be done. */
pthread_mutex_unlock(work->lock);
return NULL;
}
要运行上述代码,我们还需要一个 main():
#ifndef THREADS
#define THREADS 4
#endif
int main(void)
{
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t wait = PTHREAD_COND_INITIALIZER;
volatile int done = 0;
struct work w[THREADS];
char *line = NULL, *p;
size_t size = 0;
ssize_t len = 0;
unsigned long total;
pthread_attr_t attrs;
int i, err;
/* The worker functions require very little stack, but the default stack
size is huge. Limit that, to reduce the (virtual) memory use. */
pthread_attr_init(&attrs);
pthread_attr_setstacksize(&attrs, 2 * PTHREAD_STACK_MIN);
/* Grab the mutex so the threads will have to wait to grab it. */
pthread_mutex_lock(&lock);
/* Create THREADS worker threads. */
for (i = 0; i < THREADS; i++) {
/* All threads use the same mutex, condition variable, and done flag. */
w[i].lock = &lock;
w[i].wait = &wait;
w[i].done = &done;
/* All threads output to standard output. */
w[i].out = stdout;
/* The rest of the fields are thread-specific. */
w[i].id = i + 1;
w[i].count = 0;
err = pthread_create(&(w[i].thread_id), &attrs, worker, (void *)&(w[i]));
if (err) {
fprintf(stderr, "Cannot create thread %d of %d: %s.\n", i+1, THREADS, strerror(errno));
exit(EXIT_FAILURE); /* Exits the entire process, killing any other threads as well. */
}
}
fprintf(stderr, "The first character on each line controls the type of event:\n");
fprintf(stderr, " e, q exit\n");
fprintf(stderr, " s signal\n");
fprintf(stderr, " b broadcast\n");
fflush(stderr);
/* Let each thread grab the mutex now. */
pthread_mutex_unlock(&lock);
while (1) {
len = getline(&line, &size, stdin);
if (len < 1)
break;
/* Find the first character on the line, ignoring leading whitespace. */
p = line;
while ((p < line + len) && (*p == '\0' || *p == '\t' || *p == '\n' ||
*p == '\v' || *p == '\f' || *p == '\r' || *p == ' '))
p++;
/* Do the operation mentioned */
if (*p == 'e' || *p == 'E' || *p == 'q' || *p == 'Q')
break;
else
if (*p == 's' || *p == 'S')
pthread_cond_signal(&wait);
else
if (*p == 'b' || *p == 'B')
pthread_cond_broadcast(&wait);
}
/* It is time for the worker threads to be done. */
pthread_mutex_lock(&lock);
done = 1;
pthread_mutex_unlock(&lock);
/* To ensure all threads see the state of that flag,
we wake up all threads by broadcasting on the condition variable. */
pthread_cond_broadcast(&wait);
/* Reap all threds. */
for (i = 0; i < THREADS; i++)
pthread_join(w[i].thread_id, NULL);
/* Output the thread statistics. */
total = 0;
for (i = 0; i < THREADS; i++) {
total += w[i].count;
fprintf(stderr, "Thread %ld: %lu events.\n", w[i].id, w[i].count);
}
fprintf(stderr, "Total: %lu events.\n", total);
return EXIT_SUCCESS;
}
如果您将以上内容保存为example.c,您可以使用例如将其编译为example。 gcc -Wall -O2 example.c -lpthread -o example.
要正确直观地掌握操作,请在终端中运行示例,并在其旁边的窗口中显示源代码,并在您提供输入时查看执行进度。
您甚至可以运行诸如 printf '%s\n' s s s b q | ./example 之类的命令来快速连续运行一系列事件,或者运行 printf 's\ns\ns\nb\nq\n' | ./example 等命令,从而缩短事件之间的时间。
经过一些实验,您有望发现并非所有输入事件都会导致它们各自的动作。这是因为退出事件(上面的q)不是同步的:它不会等待所有待处理的工作完成,而是告诉线程立即退出。这就是为什么即使对于完全相同的输入,事件的数量也会有所不同。
(另外,如果您在条件变量上发出信号并立即对其进行广播,线程往往只会被唤醒一次。)
您可以通过延迟退出来缓解这种情况,例如使用(printf '%s\n' s s b s s s ; sleep 1 ; printf 'q\n' ) | ./example.
但是,还有更好的方法。条件变量不适用于可数事件;它真的很像旗帜。信号量会更好,但是你应该小心不要溢出信号量;它只能是从 0 到 SEM_VALUE_MAX,包括 0 到 SEM_VALUE_MAX。 (因此,您可以使用信号量来表示 pending 作业的数量,但可能不是每个/所有线程工作人员完成的迭代次数。)线程中待办工作的队列泳池时尚,是最常见的方法。