线程同步的概念
代码举例1
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void* thr1(void* arg)
{
while(1)
{
printf("hello");//hello在缓冲区中,遇到\n才会输出出来
sleep(rand()%3);//范围是0-2
printf("world\n");
sleep(rand()%3);
}
}
void* thr2(void* arg)
{
while(1)
{
printf("HELLO");
sleep(rand()%3);//失去CPU
printf("WORLD\n");
sleep(rand()%3);
}
}
int main()
{
pthread_t tid[2];
pthread_create(&tid[0],NULL,thr1,NULL);
pthread_create(&tid[1],NULL,thr2,NULL);
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
return 0;
}
程序运行结果如下:
造成这种数据混乱的原因是什么呢?
调度随机、线程间缺乏必要的同步机制
代码举例2
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
char buf[20];
void* thr1(void* arg)
{
int i=0;
for(;i<20;i++)
{
usleep(rand()%3);
buf[i]='0';
}
return NULL;
}
void* thr2(void* arg)
{
int i=0;
for(;i<20;i++)
{
usleep(rand()%3);
buf[i]='1';
}
return NULL;
}
int main()
{
memset(buf,0x00,sizeof(buf));
pthread_t tid[2];
pthread_create(&tid[0],NULL,thr1,NULL);
pthread_create(&tid[1],NULL,thr2,NULL);
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
printf("buf is %s.\n",buf);
return 0;
}
造成这种数据混乱的原因是什么呢?
资源共享、调度随机、线程间缺乏必要的同步机制
解决同步的问题:加锁(互斥量机制)。
因此,即使有了mutex,如果有线程不按规矩来访问数据,依然会造成数据混乱。
mutex 互斥量机制的相关函数
使用互斥量机制之前,需要进行初始化,有两种初始化方式:
1、动态初始化方式
2、静态初始化方式
动态初始化和静态初始化会放在不同的情况下使用。
比如你的互斥量就是凭空定义出来的变量时,使用静态初始化方式就够用了。
比如你的互斥量是位于某个结构体中的,你就必须使用动态初始化方式。
pthread_mutex_lock是死心眼,必须要加锁,当锁被别人占用时,就死等(阻塞等待)。
而pthread_mutex_trylock则是尝试加锁,如果加不上,就走。
程序结束且不再对互斥量操作了,要摧毁锁。
互斥量机制相关函数的使用
同一时间段只能有一个线程执行的那段代码称之为临界区
//全局变量--共享数据
char buf[20];
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
/*上锁的目的主要是为了操作共享数据*/
void* thr1(void* arg)
{
//上锁
pthread_mutex_lock(&mutex);
/*进入临界区--不要一直占用临界区(比如延时)*/
int i=0;
for(;i<20;i++)
{
buf[i]='0';
}
//解锁
pthread_mutex_unlock(&mutex);
/*退出临界区*/
return NULL;
}
通过下面这个例子来理解互斥锁锁住的是一段代码。
练习:有四个线程,这四个线程拼命的往终端上输出abcd.
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#define THRNUM 4
void* thr_func(void *p)
{
int c = 'a'+(int)p;
while(1)
{
write(1,&c,1);
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid[THRNUM];
int i,err;
for(i=0;i<THRNUM;i++)
{ //传给线程执行函数的参数
err = pthread_create(tid+i,NULL,thr_func,(void *)i);
if(err)
{
fprintf(stderr,"pthread_create():%s\n",strerror(err));
exit(1);
}
}
alarm(3);
for(i=0;i<THRNUM;i++)
{
pthread_join(tid[i],NULL);
}
exit(0);
}
程序执行结果如下:
我们从打印结果可以看出输出的序列中要么是单个字母序列,要么就是两个字母序列,就是没有三个字母序列或者四个字母序列的,那到底是什么原因呢?是因为,我们的虚拟机是双核的,只能模拟两个线程并发执行的情形,不能实现三个线程并发。
程序并没有像我们期待的那样输出abcd序列,那怎么解决这个问题呢?
定义互斥量,让打印 ‘a’ 字符的线程去解锁打印 ‘b’ 字符的线程,以此类推。
那这样就需要四个互斥量。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#define THRNUM 4
//定义四个互斥量
static pthread_mutex_t mut[THRNUM];
static int next(int n)
{
if(n+1 == THRNUM)
{
return 0;
}
return n+1;
}
void* thr_func(void *p)
{
int n =(int)p;//第n把锁
int c = 'a'+ n;
while(1)
{
//把自己给锁上
pthread_mutex_lock(mut+n);
write(1,&c,1);
pthread_mutex_unlock(mut+next(n));
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid[THRNUM];
int i,err;
for(i=0;i<THRNUM;i++)
{
//创建四把锁
pthread_mutex_init(mut+i,NULL);
pthread_mutex_lock(mut+i);
//创建了四个线程,i是传给线程执行函数的参数
err = pthread_create(tid+i,NULL,thr_func,(void *)i);
if(err)
{
fprintf(stderr,"pthread_create():%s\n",strerror(err));
exit(1);
}
}
//主线程执行到这里后,四个线程都被创建出来了,但都因为lock而被阻塞。
//解开第一个锁
pthread_mutex_unlock(mut+0);
alarm(3);
for(i=0;i<THRNUM;i++)
{
pthread_join(tid[i],NULL);
}
exit(0);
}
需要注意代码执行逻辑和顺序。尤其需要注意的是那个while循环体中执行流程。
程序中,当四个线程都被创建出来了,但都因为lock而被阻塞。当主线程执行了unlock后,第一个线程不再被阻塞,将自己再次给锁起来后,输出字符 a ,然后将下一个进程给解锁,需要注意,因为有while的存在,以此类推。
看下输出结果。
线程池(任务池)
"池"可以想象成就是一个容器保存着各种我们需要的对象。
标准的进程池或线程池是将进程或线程所在的位置模拟成 “池” 的实现。
我们要实现的目标:
利用线程完成对30000000–30000200之间质数的查找。
我们这里要做的并不是一个标准的线程池,相当于是做一个任务池。
定义一个全局变量num,上游的main线程利用num将任务(这里是一个个数)进行发放。下游定义3个线程来抢任务,比如上游将30000000扔了下去,那么下游就开始抢。那就看谁抢的快了。抢的快的那个人将30000000拿走。上游再将30000001放进去,如果执行30000000那个完成的够快,则再一起抢,否则的话,剩下的两个线程再一起抢,从而最大限度的利用当前所有的资源。
我们再定义几个状态,
当num大于0时,表明是任务,而当num等于0时,则表明当前没有任务,任务为空。这个0由取走任务的那个线程来写(该线程取走这个(任务)值后,立刻将num写成0),当下游的两个线程一看num=0,就知道当前没有任务。也就不再抢了。当上游线程发现num=0时,就会把下一个任务给放进去。依此类推。
总有一个时刻,任务会发放完毕。当任务结束,规定num的值为-1,
由main线程将num的值写为-1。下游的线程一看num=-1,就知道任务发放完毕了,就准备退出,然后上游线程(主线程)等着给他们收尸。
注意下述程序中有可能出现的死锁
查询法典型特点:加锁查看,解锁等待
代码编写如下:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM 4
int num=0;
//从名字上体现我做互斥量是为了保障对num的使用,是一种独占资源的形式在使用。
pthread_mutex_t mut_num = PTHREAD_MUTEX_INITIALIZER;
void* thr_prime(void *p)
{
int i,j,mark;
while(1)
{
pthread_mutex_lock(&mut_num);
while(num == 0)//说明没有新的任务
{
//解锁等待
pthread_mutex_unlock(&mut_num);
sched_yield();//作用是出让调度器给别的线程,可以理解为非常非常短的sleep,且不会引起调度颠簸
//在出让调度器的这段时间,如果主线程将num的值写为不为0的数,说明下发了任务或任务结束,且由于num不为0,也就将不再执行while循环.
pthread_mutex_lock(&mut_num);
}
//退出循环体,即将结束线程
if(num == -1)
{
//需要注意
//如果不写这句话就会造成死锁
//一个锁对应两种情况的解锁
pthread_mutex_unlock(&mut_num);
//临界区内的任何一个跳转语句一定要记得解锁之后再跳转
break;
}
i = num;//领取任务
num=0;
pthread_mutex_unlock(&mut_num);
//拿到num值之后,不是0和-1时才去执行下面代码。
mark = 1;
for(j=2;j<i/2;j++)
{
if(i%j == 0)
{
mark = 0;
break;
}
}
if(mark)
{
printf("[%d]%d is a primer.\n",(int)p,i);
}
}
pthread_exit(NULL);
}
int main()
{
int i,err;
pthread_t tid[THRNUM];
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);
//不能冒昧的直接将num的值给i,需要先进行判断
//不能是if来判断,而是应该循环判断
while(num!=0)
{
//需要让别人有机会将num的值变成0,所以需要先解锁
pthread_mutex_unlock(&mut_num);
//直接解锁就立刻去加锁,很有可能这把锁还是被你抢走,
//所以需要有个停顿,让进程有机会将num的值给写0.
//那么可以使用sleep(1);吗?需要注意如果使用sleep的话,
//可能会引起当前进程调度的颠簸,因为使用sleep后,当前进程
//会由running态切换到可中断的睡眠状态,当sleep时间结束后,
//这个进程又会被切换到running态,从而引起进程调度颠簸。
//使用sched_yield();可以使进程平稳过渡。
sched_yield();//作用是出让调度器给别的线程,可以理解为非常非常短的sleep,且不会引起调度颠簸
//在出让调度器的这段时间,如果有线程将num的值写为0,说明有线程领取任务,且由于num=0,将不再执行while循环.
//再马上加锁
pthread_mutex_lock(&mut_num);
}
num = i;
//当前下发任务完成
pthread_mutex_unlock(&mut_num);
}
//所有任务下发完毕,main线程需要将num的值写为-1
//但这里依然不能掉以轻心,首先,因为是要修改num的值,所以依然要加锁解锁
//其次,当我们发放完最后一个任务时,也就是执行了num=i之后,接着执
//行pthread_mutex_unlock(&mut_num);然后执行i++,此时,i的值不满足循环条件,从而跳出循环体
//继而执行下面抢锁命令,因为它们属于同一线程,在抢锁有优势,所以有很大希望抢到锁,但是,
//i=30000200(最后一个任务)还没有执行,就被你修改为-1了,这是不对的,所以这里也要不停的去判断。
pthread_mutex_lock(&mut_num);
//当num==0,说明最后一个任务也执行完毕了。此时,main线程才可以将num=-1
while(num != 0)
{
pthread_mutex_unlock(&mut_num);
sched_yield();
pthread_mutex_lock(&mut_num);
}
num = -1;
pthread_mutex_unlock(&mut_num);
for(i = 0;i< THRNUM;i++)
{
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&mut_num);
exit(0);
}
程序运行结果如下:
尽管使用 “池” 的方法来筛质数是非常可靠的一种方式,但是,从打印输出的结果来看,程序有些卡顿,且这段时间的CPU占用率非常的高,这是因为上下游都在等待,上游在不停的加锁查看,下游也在不停的加锁查看,都在等待num的值变化。也就是说大量资源被耗尽在不停的查询上,所以属于忙等法。
程序还有更好的方法是采用通知的方法,也就是当上游完成了,告诉下游,下游完成了,通知上游。采用通知的方法需要使用到一个机制 – 条件变量。
条件变量
条件变量中涉及到一个类型 pthread_cond_t
#include <pthread.h>
//与互斥量一样,条件变量也可以静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//动态初始化
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
参数:cond是条件变量,cond_attr是属性,不需要设置时填NULL。
//条件变量的销毁
int pthread_cond_destroy(pthread_cond_t *cond);
参数:cond是条件变量
具体用法还涉及到
怎么样去发一个消息呢?
//把所有的等待都叫醒
int pthread_cond_broadcast(pthread_cond_t *cond);
参数:cond是条件变量
//叫醒任意一个等待
int pthread_cond_signal(pthread_cond_t *cond);
参数:cond是条件变量
需要注意:这里的signal和信号那个signal无关。
还有
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
参数:cond是条件变量,mutex是互斥量
pthread_cond_wait 是死心眼,一直等待,直到有人来打断我为止。
pthread_cond_timedwait 是规定的时间内等不到就直接结束。其中参数abstime是超时设置,表示在我等待的abstime时间内,还没有发生我等待的行为的话,就结束。
1、使用条件变量来修改令牌桶的代码(见令牌桶章节)
2、使用条件变量来修改筛质数的代码
前面我们使用互斥量实现的本质是查询法的筛质数,上游和下游都不停的抢num,看状态。
上游抢num,期待它是0,下游抢num,期待它不是0。
现在,我们把它变成通知法,应该是这样一个逻辑。
比如当前下游一直处于一个等待状态。上游把任务放到num之后,给下游正在wait的几个线程发一个通知,使用signal就可以,因为下游几个线程的目的是一样的。叫醒任何一个,叫他取任务都可以。
如果现在下游都在忙,我的signal没有打断任何一个wait,那应该怎么办?
答:最先完成任务的程序执行到wait这个位置,再做处理。
当下游的某个线程将num取走之后,会使用pthread_cond_broadcast函数给main线程和其他正在等待的线程都发一个通知。
main线程接到通知后,知道任务被取走了,要再发任务或结束任务。
其他正在等待的线程收到通知,知道任务被别人取走了,继续等待.
具体看代码如何实现。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM 4
int num=0;
//从名字上体现我做互斥量是为了保障对num的使用,是一种独占资源的形式在使用。
pthread_mutex_t mut_num = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_num = PTHREAD_COND_INITIALIZER;
void* thr_prime(void *p)
{
int i,j,mark;
while(1)
{
pthread_mutex_lock(&mut_num);
while(num == 0)//说明没有新的任务
{
pthread_cond_wait(&cond_num, &mut_num);
}
//退出循环体,即将结束线程
if(num == -1)
{
//需要注意
//如果不写这句话就会造成死锁
//一个锁对应两种情况的解锁
pthread_mutex_unlock(&mut_num);
//临界区内的任何一个跳转语句一定要记得解锁之后再跳转
break;
}
i = num;//领取任务
num=0;
//给所有线程发通知,主线程收到通知,知道要再发任务或结束任务了,
//其他正在等待的线程收到通知,知道任务被别人取走了,继续等待.
//解锁之前发通知可以先让主线程准备好抢锁.让其他线程继续保持等待状态.
//所以应该使用pthread_cond_broadcast
pthread_cond_broadcast(&cond_num);
pthread_mutex_unlock(&mut_num);
//拿到num值之后,不是0和-1时才去执行下面代码。
mark = 1;
for(j=2;j<i/2;j++)
{
if(i%j == 0)
{
mark = 0;
break;
}
}
if(mark)
{
printf("[%d]%d is a primer.\n",(int)p,i);
}
}
pthread_exit(NULL);
}
int main()
{
int i,err;
pthread_t tid[THRNUM];
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);
//不能冒昧的直接将num的值给i,需要先进行判断
//不能是if来判断,而是应该循环判断
while(num!=0)
{
pthread_cond_wait(&cond_num, &mut_num);
}
num = i;
//给任意一个线程发就行
pthread_cond_signal(&cond_num);
//当前下发任务完成
pthread_mutex_unlock(&mut_num);
}
pthread_mutex_lock(&mut_num);
//当num==0,说明最后一个任务也执行完毕了。此时,main线程才可以将num=-1
while(num != 0)
{
pthread_cond_wait(&cond_num, &mut_num);
}
num = -1;
//给所有线程发,任务结束,让他们都准备退出吧.
pthread_cond_broadcast(&cond_num);
pthread_mutex_unlock(&mut_num);
for(i = 0;i< THRNUM;i++)
{
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&mut_num);
pthread_cond_destroy(&cond_num);
exit(0);
}
3、使用条件变量来修改顺序输出abcd的代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#define THRNUM 4
static int num = 0;
//定义互斥量
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
//定义条件变量
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static int next(int n)
{
if(n+1 == THRNUM)
{
return 0;
}
return n+1;
}
void* thr_func(void *p)
{
int n =(int)p;
int c = 'a'+ n;
while(1)
{
pthread_mutex_lock(&mut);
//当num的值不等于n时就等待
while(num != n)
{
pthread_cond_wait(&cond, &mut);
}
write(1,&c,1);
//打印完之后,我要让num的值变成num的下一个人的编号
num = next(num);
//然后通知下一个人,怎么通知呢?
//实际上a在打印的时候,bcd都在等待,直接用broadcast叫醒所有
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mut);
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid[THRNUM];
int i,err;
for(i=0;i<THRNUM;i++)
{
//创建了四个线程,i是传给线程执行函数的参数
err = pthread_create(tid+i,NULL,thr_func,(void *)i);
if(err)
{
fprintf(stderr,"pthread_create():%s\n",strerror(err));
exit(1);
}
}
//主线程执行到这里后,四个线程都被创建出来了.
alarm(3);
for(i=0;i<THRNUM;i++)
{
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&mut);
pthread_cond_destroy(&cond);
exit(0);
}