线程池初步(非标准写法)
例
在上篇文章《线程与竞争》的例一中,我用了200个线程来判断质数,但是实际上需要这么多线程吗?
其实可以用分块、交叉分配、池类写法来实现若干个线程判断质数
我们知道,线程和线程之间是共用同一个进程空间的,所以线程之间通信只要设置一个全局变量就可以了,因为线程就是跑着的函数
但是呢,如果你用进程那一套东西,管道,共享内存这些的,相对于全局变量都是绕远路,对吧
先理解一个场景:
在一个公司里,老板有一个项目,可以安排给手下若干个员工,并且奖励非常的丰厚
员工一听,这么好的项目,大家都想要,所以大家都从摆烂的状态里醒过来,来抢占这个任务
那么,我们的实现方法就是,mian线程给任务,交由线程来抢占,然后某个线程抢到以后,就判断是不是质数
那么有几个问题,假如A线程抢到了30000000这个数
一、怎么告诉其他虎视眈眈的线程,这个数字已经被A线程抢占了
二、怎么告诉mian线程,30000000这个任务已经被取到,让mian线程继续下放任务
很简单,设置一个变量number,mian线程往这个变量上下发任务,比如放置30000000,然后让线程去抢夺,如果A线程抢到以后,就迅速保存这个数值
并且把number置0,当其他线程看到是0的时候就知道他们已经来晚一步了,任务已经被抢走了,当mian线程看到0的时候就知道任务已经被领走
就可以继续下放任务
那么,最后一个问题,当mian线程放完任务的时候,那个变量number是0,而线程们又眼巴巴的等着,这个时候怎么办呢?
所以还要有一个退出环节
下面给出规定:
num > 0:表示任务
num = 0:表示无任务
num = -1:表示提醒线程退出,mian线程回收
请看程序
判断质数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM 4 //假定有4个线程在线程池里
static int num = 0; //上游mian线程和下游被创建的线程的窗口,通过num的值来执行对应的业务
static pthread_mutex_t mut_num = PTHREAD_MUTEX_INITIALIZER; //创建互斥量并初始化
static void* thr_prime(void* p); //声明函数
int main()
{
int i, err;
pthread_t tid[THRNUM]; //创建4个线程表示
for (i = 0; i < THRNUM; i ++ )
{
err = pthread_create(tid + i, NULL, thr_prime, (void*)i); //创建线程,参数:(要回填的线程标识, 默认属性, 线程的执行函数, 传给线程执行函数的参数)
if (err) //检查是否创建线程失败
{
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
}
for (i = LEFT; i <= RIGHT; i++) //下发任务
{
pthread_mutex_lock(&mut_num); //锁上,如果已经被锁上了,那么就阻塞等待
//锁上的原因:多个线程(包括mian线程)都访问同一个变量地址,为了避免竞争
while (num != 0) //mian线程能执行到这里,说明num变量是mian线程独占的
{
pthread_mutex_unlock(&mut_num); //当num != 0的时候标识任务还没有被下游线程领走,那么就解锁给下游线程一点机会
sched_yield();
pthread_mutex_lock(&mut_num); //然后继续锁上
}
num = i; //下发任务
pthread_mutex_unlock(&mut_num); //mian线程到这里已经完成了任务的下发, 或者说后续对num变量的访问只是读不是写,所以这个时候可以解锁了
}
pthread_mutex_lock(&mut_num); //mian线程到这里已经完成了所有的任务,所以要把num改成-1, 更改的前提是num == 0也就是任务真的发好了
//既然要改num的值,那么就要确保num是mian线程独占的,所以要锁上
while (num != 0)
{
pthread_mutex_unlock(&mut_num); //如果不为0表示还没有发好,那就松开一会,然后再锁上
sched_yield();
pthread_mutex_lock(&mut_num);
}
num = -1; //真的发好了
pthread_mutex_unlock(&mut_num); //已经改好了num,此后mian线程对num只是读,并不需要写,所以解锁
for (i = 0; i < THRNUM; i++) pthread_join(tid[i], NULL); //收尸
pthread_mutex_destroy(&mut_num); //释放互斥量
exit(0); //结束程序
}
static void* thr_prime(void* p) //切记,线程只是一个运行的函数,和其他线程都是兄弟关系,不是父子,没有主次之分
{
int i, mark;
while (1)
{
pthread_mutex_lock(&mut_num); //下游线程抢到了任务,那么要确保是独占的,所以要锁上
while (num == 0) //num != 0 才代表有任务,如果为0那么就松开一会,让mian线程下放任务
{
pthread_mutex_unlock(&mut_num);
sched_yield();
pthread_mutex_lock(&mut_num);
}
if (num == -1) //如果任务都发好了,那么就可以结束了
{
pthread_mutex_unlock(&mut_num); //避免死锁!这点程序后讲吗这个非常重要
break;
}
i = num; //领取任务
num = 0; //告诉mian线程和其他线程,任务已经被领走了
pthread_mutex_unlock(&mut_num); //下游线程领走任务以后,对num就只是读不需要写,所以解锁
//判断质数,时间复杂度根号i
mark = 1;
for (int j = 2; j <= i / 2; j ++ )
if (!(i % j))
{
mark = 0;
break;
}
if (mark) printf("[%d]: %d is a primer\n", (int)p, i); //p是传过来的参数,取值范围是0, 1, 2, 3,所以可以人为的当作是线程的标识
}
pthread_exit(NULL); //结束
}
这里着重讲两个问题,其他的问题在注释里讲的比较清楚,至少我是这么觉得的
问题一:那三行是什么意思
pthread_mutex_unlock(&mut_num);
sched_yield();
pthread_mutex_lock(&mut_num);
问题二:
if (num == -1) //如果任务都发好了,那么就可以结束了
{
pthread_mutex_unlock(&mut_num); //避免死锁!这点程序后讲吗这个非常重要
break;
}
为什么break前要解锁,不是break完以后就可以直接pthread_exit结束吗
OK, 一个一个来
问题一:以mian线程为例,当mian线程下发任务的时候,他期待的是num = 0的状态,这样他就可以下发任务了
但是呢,在此之前,肯定要判断num的状态呀,如果num不是0的话,那么就松手(此前已经锁住了),然后迅速再锁上
那么,问题一还有一个子问题,为什么是while不是if ?很简单,万一松手的时候,别的线程没抢到怎么办
啊对!问题来了,其他线程没抢到怎么办?很简单啊,松手的时间长一点呗,sleep一会呗
我们来详细剖析一下sleep,当一个进程里出现sleep的时候,就会引发进程状态的颠簸,从runing态变成可中断的睡眠态,当sleep过后,又变成runing态
这个进程状态的变化是没有必要的,所以用sched_yield函数
最后,提一下sched_yield()函数, sched_yield()这个函数可以使用另一个级别等于或高于当前线程的线程先运行。
如果没有符合条件的线程,那么这个函数将会立刻返回然后继续执行当前线程的程序。
可以理解为非常非常短的sleep函数,并且并不会引起进程状态的颠簸
问题二:
仔细看程序,在判断num是否为-1的上面有一个while循环,可以看到,在if执行的时候,这个时候是这个当前这个线程独占num,
所以,如果当前这个线程没有解锁,直接跳出,然后结束线程,那么其他的线程只能干巴巴的等,并且这个线程还没人收尸
切记,从锁上到解锁都是临界区,在临界区里,我们要留心任何一个跳转指令,比如说:break,continue,goto这些
任何一个跳转指令,如果是跳转到临界区内,那无所谓,反正还在临界区内,还没有解锁,但是!
如果你跳转到了临界区外, 一定要先解锁,然后才能跳转
那么分析到这里,整个程序,大家看懂了吗?
其实还有一个很细节的地方
请看mian线程的最后
num = -1
这里是为了设置num = -1, 告诉其他线程,该结束了
显然,这个是某个线程(mian线程)对num的访问,所以前后都应该加锁,确保是mian线程独占,OK,没问题
那么问什么要判断num的状态呢?因为,当mian线程下发最后任务后,此时如果又被mian线程抢到了执行机会,那么他肯定直接把num赋值为1
这是不是意味着我们的程序有一个逻辑bug,最后一个任务,有可能会不执行,那么当我们做大型项目的时候,面对百万并发的时候,后果将把不堪设想
所以需要判断num的状态,使用while来判断(为什么使用while上面讲了)