例如,我想创建5个线程并打印它们。如何让第四个在第二个之前执行?我尝试用互斥锁锁定它,但我不知道如何只锁定第二个,所以它给了我分段错误。
答案 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类型(我假设为上面的有符号整数,通常是int
或long
)转换为指针。第一个转换为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程序员)更加熟悉如何使用互斥锁和条件变量,我展示了上面的例子。