同步的概念与进程同步的概念一致,让多个线程按照顺序协同执行。为什么线程需要线程同步呢?因为线程很多资源都是共享的,比如全局数据,内存,文件,数据库等等。当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。(同步大多都互斥)
还有一点,因为我们在一个线程中有时候进行的操作并不一定是原子操作,那么如果多个线程同时执行,就有可能会发生线程不安全,进行同步控制后,就可以实现线程安全。
同进程同步相似,线程这里也可以使用信号量来进行同步,但是与进程的还是有些不同
//信号量的创建与初始化,都在一个函数里,同进程的信号量不同
#include
int sem_init(sem_t *semid,int shared,unsigned int val);
//失败返回-1,成功返回0
semid:标识操作的信号量,一般是一个全局变量(共享)
shared:控制信号量的类型,指定此信号量是否在进程间共享,如果是0则是当前进程局部的信号量,否则这个信号量可以在多个进程中共享,但是Linux还不支持这种共享,所以一般默认值我们会给0。
val:设置信号量的初始值
//控制信号量的两个函数
#include
int sem_wait(sem_t *semid); //相当于p操作,给参数sem指定的信号量值减1,会阻塞
int sem_post(sem_t *Semid); //相当于v操作,给参数sem指定的信号量值加1
//这几个函数都在线程库中封装好了,不用像进程那样自己封装,方便用户调用
在这里说明一点,sem_wait调用的时候,如果信号量不为0则直接给信号量减1继续执行,不会阻塞的,但是如果信号量等于0,调用此函数就会阻塞,等待着其他线程的运行,直到信号零不为0可被调度。但是我们还有一个函数,与sem_wait行使作用一致但是不会阻塞。
#include
int sem_trywait(sem_t *semid);
//相当于p操作,给信号零减1,但是不会阻塞
对于信号量的销毁,我们线程库也为用户封装好了,用户可以直接调用
#include
int sem_destroy(sem_t *semid); //销毁信号量
信号量同步实例:
//两个线程交替打印
#include
#include
#include
#include
#include
#include
#include
sem_t sem; //信号量,设为全局量,使得线程共享
void *fun(void *arg)
{
printf("fun thread start\n");
int i=0;
for(;i<8;i++)
{
sem_wait(&sem);
printf("fun pthread running\n");
sleep(1);
sem_post(&sem);
}
printf("fun pthread end\n");
pthread_exit(NULL);
}
void destory()
{
printf("destory\n");
sem_destroy(&sem);
}
int main()
{
atexit(destory); //注册函数,进程结束会调用它所注册的函数
int n=sem_init(&sem,0,1);
pthread_t id;
int res=pthread_create(&id,NULL,fun,NULL);
assert(res==0);
printf("main pthread start\n");
int i=0;
for(;i<10;i++)
{
sem_wait(&sem);
printf("main pthread runnning\n");
sleep(1); //想让打印的慢一点而已
sem_post(&sem);
}
printf("main pthread end\n");
pthread_exit(NULL);
}
运行结果:
可以看出我所写的这个函数并没有按照我们期望的那样交替输出,而是先输出了我们先执行的线程,再执行另一个线程,这是为啥呢?我们不是做了同步控制了吗,他怎么不按套路出牌呢?不要着急,我们再给大家展示一个更加诡异的骚操作。
我们给打印输出那里post之后再加上一个sleep(1),看看有什么神奇之处发生吗?
我们再来执行一下,看看结果
是不是很神奇,它竟然加上了一个sleep函数就好了,这是为啥呢?别着急,可能有的同学会问你这里加上一个sleep程序就好了,那么我要是只有下面这个sleep而没有打印处的那个sleep会不会受影响,答案是是的,会受影响,继续测试,下面给大家讲解为什么他会受影响。
将打印处的sleep屏蔽掉
结果:
可以看出结果又出现错误啦,本喵喵可没有胡说呀!亲测的好吧!!!
为什么我上面这些代码,有的正确有的错误仅仅是因为一个sleep函数,就会影响呢?
首先我们来说第一个sleep函数(即打印处的那个让打印速度变慢的sleep函数),这个sleep就是为了控制打印速度,让其打印慢点。由于CPU处理速度是非常快的,我们根本感知不到,所以如果没有这个sleep他就可能会感知不到只有一句打印的代码,然后就会将信号量给某一个线程获取到多次,打印结果就和我们想象的不同了,所以我们在线程拥有这个信号量处理的代码中一定要有一定的时间让系统感知到,否则就可能会被处理多次。
另一点,对于信号量post之后的sleep,它的目的就是为了让我这个线程post之后让另一个线程获取到这个信号量资源,因为函数在调度的时候需要一定的时间,那个时间肯定是比我这个当前线程的循环直接上去再次调用自身的wait慢,所以我就需要我当前这个线程等一等,让另一个线程获取到这个信号量,而不是被自身获取到。有的同学可能会问,那么为啥我进程那块就不需要我调用sleep,我就用信号量这样操作就不会出错呢?大家记的吗,进程的信号量是在哪里?没错,进程的信号量是在内核中维护的,是内核进行管理的,实际上内核为使用信号量的进程维护了一个队列,需要使用某个信号量的进程就会按照顺序访问,而不会像线程这样不管之前“排队”的顺序,谁先拿到就是谁调度。所以进程那块的信号量管理操作都是内核帮我们做了,所以我们不需要处理那么多,但是线程就会根据谁先获取到信号量就先调度谁,所以第一个获取到信号量的线程他就会将自己的信息输出完,才会将信号量交给其他线程,这就会与我们预想的不同。
所以如果我们要用这种方式处理交替打印,还是要注意这两个点(sleep),但是这其实是没有意义的,我们难道能在项目中通过sleep来同步吗,当然不行了,这是非常不可靠的,而且sleep是一个不可中断的操作,它有一个计时器,你不管暂不暂停它,他都会一直执行,直到时间跑完,就结束了。
那么我们如果要实现交替打印我们如何操作呢?
我们可以用两个信号量来进行同步控制
#include
#include
#include
#include
#include
#include
#include
sem_t sem1; //控制主线程打印的信号量
sem_t sem2; //控制函数线程打印的信号量
int flag1=1; //判断主线程是否退出,防止阻塞函数线程
int flag2=1; //判断函数线程是否退出,防止阻塞主线程
void *fun(void *arg)
{
printf("fun thread start\n");
int i=0;
for(;i<3;i++)
{
if(flag1!=0)
sem_wait(&sem2); //阻塞自身,等待主线程唤醒
printf("fun thread running\n");
sem_post(&sem1); //唤醒主线程
}
flag2=0; //通知主线程函数线程结束
printf("fun thread end\n");
pthread_exit(NULL);
}
void destory()
{
printf("destory\n");
sem_destroy(&sem1);
sem_destroy(&sem2);
}
int main()
{
atexit(destory); //注册函数
int n=sem_init(&sem1,0,1); //一定要一个信号量为1,要不然就阻塞了
int m=sem_init(&sem2,0,0);
assert(n==0&&m==0);
pthread_t id;
int res=pthread_create(&id,NULL,fun,NULL);
assert(res==0);
printf("main thread start\n");
int i=0;
for(;i<5;i++)
{
if(flag2!=0) //当函数线程退出时就直接输出,否则会阻塞,没办法结束
sem_wait(&sem1); //减1操作,让自身阻塞,等待函数线程将其唤醒
printf("main thread running\n");
sem_post(&sem2); //对sem2加1,唤醒函数线程打印
}
flag1=0; //通知函数线程主线程结束了,可以直接打印,不用wait阻塞了
printf("main thread end\n");
pthread_exit(NULL);
}
运行结果
所以对于几个线程交替打印,我们就需要几个信号量来进行控制。
另一种用在多线程程序种的同步访问方法是使用互斥量。它允许程序员锁住某个对象,使得每次只能有一个线程访问它。为了控制对关键代码的访问,必须在进入这段代码之前锁住一个互斥量,然后在操作完成之后解锁它,使其他线程对资源进行加锁访问
互斥锁与信号量的基本函数很相似
#include
int pthread_mutex_init(pthread_mutex_t *mutex,pthread_mutexattr *attr); //初始化锁
int pthread_mutex_lock(pthread_mutex_t *mutex); //对资源加锁,阻塞版本
int pthread_mutex_unlock(pthread_mutex_t *mutex); //对资源解锁
int pthread_mutex_destroy(pthread_mutex_t *mutex); //销毁锁
//成功返回0,失败返回-1
与信号量相似, 这几个函数的参数都是一个先前声明过的对象的指针,对互斥锁来说,这个对象的类型是pthread_mutex_t类型。对于pthread_mutex_init函数中的attr是设定互斥量的属性,一般我们给定NULL默认值,交由系统设定。
与信号量相同,对于它的加锁函数同样有一个非阻塞版本,即pthread_mutex_trylock函数
#include
int pthread_mutex_trylock(pthread_mutex_t*mutex);
//与pthread_mutex_lock作用相同,但是是非阻塞版本
互斥锁同步实例:
//主线程循环获取用户输入 函数线程获取用户输入的字符个数
#include
#include
#include
#include
#include
#include
#include
pthread_mutex_t mutex;
char buff[128]={0};
void *fun(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
if(strncmp(buff,"end",3)==0)
break;
printf("read count is:%d\n",strlen(buff)-1);
pthread_mutex_unlock(&mutex);
sleep(2);
}
}
int main()
{
pthread_t id;
int res=pthread_create(&id,NULL,fun,NULL);
assert(res==0);
pthread_mutex_init(&mutex,NULL);
while(1)
{
memset(buff,0,128);
pthread_mutex_lock(&mutex);
printf("please input:");
fflush(stdout);
fgets(buff,128,stdin);
pthread_mutex_unlock(&mutex);
if(strncmp(buff,"end",3)==0)
break;
sleep(1);
}
pthread_mutex_destroy(&mutex);
}
抛出一个问题,如果有很多个线程都使用同一互斥锁,在某一个线程释放这个锁后,其他的线程都会被唤醒,然后都来抢夺这个锁资源,当然最后只有一个线程能够获取到,那么这种在一时间之内唤起很多线程的现象就是惊群现象,那么如何解决这个问题呢?大家可以点击我的这个链接,没有什么惊喜。。。。
惊群现象解决
条件变量是线程可用的另一种同步机制,即等待某一条件的发生,和信号相似。它是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号),唤起挂起的线程等待。为了防止竞争(保护条件变量),条件变量的使用总是和一个互斥锁结合在一起。(简单的来说条件变量就是某一线程必须得在某一条件发生后其锁住互斥量才可以令事件发生,否则条件没发生锁住互斥量还是无法完成自己的操作)
互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。当两个线程操作同一临界区时,只通过互斥锁保护,若A线程已经加锁,B线程再加锁时候会被阻塞,直到A释放锁,B再获得锁运行。但是在线程B被CPU调度的时候必须不停的主动获得锁、检查、释放锁、再获得锁、再检查、再释放,一直到满足运行的条件的时候才可以(而此过程中其他线程一直在等待该线程的结束),而且互斥锁在加锁操作时涉及上下文的切换,所以这种方式是比较消耗系统的资源的。
加上条件变量,互斥锁的这个问题就可以得到很好的改善。
条件变量同样是阻塞,当条件不满足时,该线程会自动的释放锁并把自身置于等待状态,让出CPU的控制权给其它线程。其它线程此时就有机会去进行操作;一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程,退出阻塞,此时再进行加锁的操作。条件变量就是一种通知模型的同步方式,大大的节省了CPU的计算资源,减少了线程之间的竞争,而且提高了线程之间的系统工作的效率。
总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制。
可能到这里,大家有可能会问,既然条件变量是一个同步机制,我们为啥还需要互斥锁和其一起使用才可以?
你可能忘记了,既然我们说条件变量是一个变量(全局),那么他也就是一个资源,对,虽然他在条件没有发生会阻塞,但是他并不是互斥的,我在同一时间可能会有多个线程区修改它,那么就必须给他加一个锁,使它每次只有一个线程修改它。比如,没有锁的时候,如果A线程查看到这个条件变量使满足的,可是再A进行下一步动作之前,B修改了这个条件,以至于条件不再满足A线程,这样A进行下一步的话就会出现异常了。
可以总结为:条件变量用于某个线程需要在某种条件成立时才去保护它将要操作的临界区,这种情况从而避免了线程不断轮询检查该条件是否成立而降低效率的情况,实现了效率提高。在条件满足时,自动退出阻塞,再加锁进行操作。
//初始化条件变量
#include
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
//成功返回0,失败返回错误编号
cond为条件变量,cond_attr值通常为NULL,且被忽略。
#include
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); //等待条件变量,自动解锁互斥量,并等待条件变量触发
int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex,const struct timespec *timeout); //与pthread_cond_wait相似,只是多了一个等待时间
int pthread_cond_signal(pthread_cond_t *cond); //激活某个等待该条件的某个线程
int pthread_cond_broadcast(pthread_cond_t *cond); //激活某个等待该条件的所有线程
//成功返回0,失败返回错误编号
对于pthread_cond_wait函数:传递给pthread_cond_wait的互斥量对条件变量保护,调用者把锁住的互斥锁传给函数,函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作是原子操作。pthread_cond_wait函数返回时,互斥量再次被锁住。
mutex:是对其进行保护的互斥锁的地址
pthread_cond_timewait函数:与pthread_cond_wait函数相似,只是timeout指定了等待的时间,通过timespec结构指定。
调用pthread_cond_signal或者 pthread_cond_broadcast,也称向线程发送信号。ps:posix规范为了简化实现,允许pthread_cond_signal在实现的时候可以唤醒不止一个线程
#include
int pthread_cond_destroy(pthread_cond_t *cond); //删除条件变量
//成功返回0,失败返回错误编号
条件变量测试实例:
#include
#include
#include
#include
#include
#include
//judge为不为0时,条件发生,输出一句话
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
int judge=0;
void *fun(void *arg)
{
pthread_mutex_lock(&mutex);
while(!judge)
{
pthread_cond_wait(&cond,&mutex);
}
pthread_mutex_unlock(&mutex);
printf("fun get\n");
}
int main()
{
pthread_t id;
int res=pthread_create(&id,NULL,fun,NULL);
assert(res==0);
pthread_mutex_init(&mutex,NULL);
sleep(2);
pthread_mutex_lock(&mutex);
judge=1;
printf("main change over\n");
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
return 0;
}