我想在C(Linux)中使用以下命令创建多线程程序:
恢复:我需要完成无限多的任务(每个线程一个任务),我想知道如何在C中使用pthread实现它。
这是我的代码:
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#define MAX_THREADS 50
pthread_t thread[MAX_THREADS];
int counter;
pthread_mutex_t lock;
void* doSomeThing(void *arg)
{
pthread_mutex_lock(&lock);
counter += 1;
printf("Job %d started\n", counter);
pthread_mutex_unlock(&lock);
return NULL;
}
int main(void)
{
int i = 0;
int ret;
if (pthread_mutex_init(&lock, NULL) != 0)
{
printf("\n mutex init failed\n");
return 1;
}
for (i = 0; i < MAX_THREADS; i++) {
ret = pthread_create(&(thread[i]), NULL, &doSomeThing, NULL);
if (ret != 0)
printf("\ncan't create thread :[%s]", strerror(ret));
}
// Wait all threads to finish
for (i = 0; i < MAX_THREADS; i++) {
pthread_join(thread[i], NULL);
}
pthread_mutex_destroy(&lock);
return 0;
}
如何使这个循环无限?
for (i = 0; i < MAX_THREADS; i++) {
ret = pthread_create(&(thread[i]), NULL, &doSomeThing, NULL);
if (ret != 0)
printf("\ncan't create thread :[%s]", strerror(ret));
}
我需要这样的东西:
while (1) {
if (thread_number > MAX_THREADS_NUMBER)
sleep(1);
ret = pthread_create(...);
if (ret != 0)
printf("\ncan't create thread :[%s]", strerror(ret));
}
答案 0 :(得分:1)
您当前的程序基于简单的调度设计:初始线程创建工作线程,为每个线程分配要执行的任务。您的问题是,如何使这项工作适用于任意数量的任务,任意数量的工作线程。答案是,你没有:你所选择的设计基本上不可能进行这样的修改。
即使我要回答你陈述的问题,也不会使程序按照你喜欢的方式行事。它可能会在时尚之后起作用,但它就像一个方形轮子的自行车:不太实用,也不健壮 - 在你不再嘲笑它看起来多么愚蠢之后甚至没有乐趣。
正如我在对原始问题的评论中所写的那样,解决方案是改变底层设计:从简单的调度到thread pool方法。
实现一个线程池需要两件事:第一,是改变你的观点,从启动一个线程并让它执行一个任务,到&#34; pool&#34;抓住任务执行,然后回到&#34;池&#34;他们完成了之后。理解这是困难的部分。第二部分,为每个线程实现一种获取新任务的方法很简单:这通常以数据结构为中心,受某种锁的保护。但是,确切的数据结构取决于实际工作的内容。
假设你想要并行化Mandelbrot set的计算(或者更确切地说,转义时间,或者在一个点被裁定为在集合之外所需的迭代次数;维基百科页面包含准确的伪代码)。这是&#34;令人尴尬的平行&#34;问题;可以在没有任何依赖性的情况下解决子问题(这里,每个点)的那些。
以下是我在这种情况下如何处理线程池的核心。首先,需要为每个点记录逃逸时间或迭代计数。我们假设我们使用unsigned int
。我们还需要点数(它是一个2D数组),一种计算与每个点相对应的复数的方法,以及一些知道哪些点已被计算或正在计算的方法。加上互斥锁定,这样只有一个线程会立刻修改数据结构。所以:
typedef struct {
int x_size, y_size;
size_t stride;
double r_0, i_0;
double r_dx, i_dx;
double r_dy, i_dy;
unsigned int *iterations;
sem_t done;
pthread_mutex_t mutex;
int x, y;
} fractal_work;
构建fractal_work
的实例时,x_size
和y_size
是iterations
地图中的列数和行数。点x
,y
的迭代次数(或转义时间)存储在iterations[x+y*stride]
中。该点的复坐标的实部为r_0 + x*r_dx + y*r_dy
,虚部为i_0 + x*i_dx + y*i_dy
(允许您自由缩放和旋转分形)。
当一个线程抓取下一个可用点时,它首先锁定mutex
,然后复制x
和y
值(为自己工作)。然后,它增加x
。如果x >= x_size
,它会将x
重置为零,并增加y
。最后,它解锁mutex
,并计算该点的转义时间。
但是,如果x == 0 && y >= y_size
,则帖子在done
信号量上发布并退出,让初始线程知道分形已完成。 (初始线程只需要为它创建的每个线程调用sem_wait()
一次。)
线程工作者函数如下所示:
void *fractal_worker(void *data)
{
fractal_work *const work = (fractal_work *)data;
int x, y;
while (1) {
pthread_mutex_lock(&(work->mutex));
/* No more work to do? */
if (work->x == 0 && work->y >= work->y_size) {
sem_post(&(work->done));
pthread_mutex_unlock(&(work->mutex));
return NULL;
}
/* Grab this task (point), advance to next. */
x = work->x;
y = work->y;
if (++(work->x) >= work->x_size) {
work->x = 0;
++(work->y);
}
pthread_mutex_unlock(&(work->mutex));
/* z.r = work->r_0 + (double)x * work->r_dx + (double)y * work->r_dy;
z.i = work->i_0 + (double)x * work->i_dx + (double)y * work->i_dy;
TODO: implement the fractal iteration,
and count the iterations (say, n)
save the escape time (number of iterations)
in the work->iterations array; e.g.
work->iterations[(size_t)x + work->stride*(size_t)y] = n;
*/
}
}
程序首先为工作线程创建fractal_work
数据结构,对其进行初始化,然后创建一些线程,为每个线程提供{{1}的地址。 } 结构体。然后它也可以调用fractal_work
本身,以及#34;加入线程池&#34;。 (此池自动&#34;排水&#34;,即当分形中的所有点都完成时,线程将返回/退出。)
最后,主线程在fractal_worker()
信号量上调用sem_wait()
,与创建工作线程一样多次,以确保完成所有工作。
上面done
结构中的确切字段无关紧要。但是,它是线程池的核心。通常,至少有一个互斥锁或rwlock保护工作细节,以便每个工作线程获得唯一的工作细节,以及某种标志或条件变量或信号量,让原始线程知道任务现在已完成。
在多线程服务器中,通常只有一个描述工作队列的结构(或变量)实例。它甚至可能包含最小和最大线程数等内容,允许工作线程控制自己的编号,以动态响应可用的工作量。这听起来很神奇,但实际上很容易实现:当一个线程完成它的工作,或者在没有工作的池中被唤醒,并且持有互斥锁时,它首先检查有多少排队的工作,以及当前的工作是什么工作线程的数量是。如果有超过最小线程数,并且没有工作要做,则线程会减少线程数并退出。如果少于最大线程数,并且还有很多工作要做,则线程首先创建一个新线程,然后抓取下一个要处理的任务。 (是的,任何线程都可以在流程中创建新线程。它们也是平等的。)
使用一个或多个线程池来实现工作的实用多线程应用程序中的许多代码都是某种簿记。线程池方法非常关注数据,并且需要对数据执行计算。我确定在那里有更好的线程池示例;困难的部分是为应用程序执行一个好任务,因为数据结构是如此依赖于任务,并且许多计算非常简单以至于并行化它们没有任何意义(因为创建新线程确实具有很小的计算成本,当一个线程在相同或更短的时间内完成相同的工作时,浪费时间创建线程是愚蠢的。)
另一方面,许多受益于并行化的任务需要在工作者之间共享信息,这需要大量思考才能正确实现。 (例如,虽然存在有效并行化分子动力学模拟的解决方案,但大多数模拟器仍然可以在不同的步骤中计算和交换数据,而不是同时进行。正如你所看到的那样,这很难做到。)
除非您理解这个概念,否则所有这些意味着您无法期望能够编写代码。事实上,真正理解这些概念是困难的部分:编写代码相对容易。
即使在上面的示例中,也存在一些跳闸点:发布信号量和释放互斥锁的顺序是否重要? (嗯,这取决于等待分形完成的线程是什么 - 实际上,如果它还在等待。)如果它是条件变量而不是信号量,那么线程必不可少。对分形完成感兴趣的是等待条件变量,否则会错过信号/广播。 (这也是我使用信号量的原因。)