【发布时间】:2017-09-28 04:50:49
【问题描述】:
例如,我想创建 5 个线程并打印它们。如何让第四个在第二个之前执行?我尝试用互斥锁锁定它,但我不知道如何只锁定第二个,所以它给了我分段错误。
【问题讨论】:
-
发布您的代码。还要看看信号量。
例如,我想创建 5 个线程并打印它们。如何让第四个在第二个之前执行?我尝试用互斥锁锁定它,但我不知道如何只锁定第二个,所以它给了我分段错误。
【问题讨论】:
通常,您定义操作的顺序,而不是执行这些操作的线程。这听起来像是微不足道的区别,但是当您开始实施它时,您会发现它产生了重大差异。这也是一种更有效的方法,因为您不会考虑需要的线程数量,而是要完成的操作或任务的数量,以及其中可以并行完成的数量,以及它们可能需要如何完成排序或排序。
不过,出于学习目的,查看排序线程可能更有意义。
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 类型(我假设它是上面的有符号整数,通常是int 或long)到指针的转换是通过两次强制转换完成的。第一个转换是<stdint.h> 中定义的intptr_t 类型(包含<inttypes.h> 时会自动包含),这是一个有符号整数类型,可以保存任何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 程序员)更熟悉如何使用互斥锁和条件变量。
【讨论】: