条件竞争
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
void* Print(char* str){
printf("%s ",str);
}
int main(){
pthread_t thread1,thread2;
pthread_create(&thread1,NULL,(void*)&Print,"Hello");
pthread_create(&thread2,NULL,(void*)&Print,"World");
return 0;
}
编译运行后,发现没有预期输出。
原因:主线程的退出会导致创建的线程退出
使用
pthread_create
函数创建两个线程,两个线程创建后,并不影响主线程的执行,所以这里就存在三个线程的竞争关系。主线程执行return 0;
先于另外两个线程的打印函数。所以看不见另外两个线程的输出。为了使return 0;
语句在另外两个进程后执行,可以采用sleep()
函数进行延迟,就可以得到输出了,这就是条件竞争。
在遇到条件竞争的问题中,采用sleep()
函数进行延迟似乎也能解决问题。实则不然,弊端很明显:
- 不能判断延迟的时间长度。
- 使程序执行卡顿。
最适当的解决方法是采用锁机制。
并发程序引起的共享内存的问题
有两个进程,两个进程共享全局变量s。两个进程都执行一个计数功能的函数,直观地看过去,th1运行时s++要执行10000次,th2运行时s++也要执行10000次,似乎计算得到的最后s应该是20000。但实际上是这样的吗?
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
int s = 0;
void *func(void*args){
int i;
for(i = 0; i < 10000; i++){
s++;
}
return NULL;
}
int main(){
pthread_t th1;
pthread_t th2;
pthread_create(&th1, NULL, func, NULL);
pthread_create(&th2, NULL, func, NULL);
pthread_join(th1, NULL);
pthread_join(th2, NULL);
printf("%s = %d\n", s);
return 0;
}
编译运行后,发现输出并不是20000,而是12657。
运行了3次这个程序,每次的结果都不同。 用这个演示来表示一下,多线程之间是资源共享的。
s++ 是有三个步的,读取s,s+1,写入s。
在程序运行的某个时刻,th1携带myfunc执行s++,读取s,此时s=100,进行s+1, 与此同时th2也开始读取s,此时的s还是等于100, 这时th1,执行写入s=101,th2执行s++,写入s ,s=101. th2中的s 就会覆盖掉 th1中的s 。这样造成了结果的误差。
原因:当我们执行s++,底层发生的事件其实是:内存中读取s→将s+1→将s写入到内存。这不是一个原子化操作,当两个线程交错运行的时候,很容易发生结果的丢失。因此最后的结果肯定是要小于20000的。这种情况有种专有名词,叫race condition。
为了解决这个问题,我们可以加锁。
#include <pthread.h>
int s = 0;
pthread_mutex_t lock; //锁的声明
void *func(void *args){
int i;
for(i = 0; i < 10000; i++){ //给临界区代码加锁实现原子化操作
pthread_mutex_lock(&lock);
s++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main(){
pthread_t th1;
pthread_t th2;
//锁初始化
pthread_mutex_init(&lock, NULL);
pthread_create(&th1, NULL, func, NULL);
pthread_createe(&th2, NULL, func, NULL);
pthread_join(th1, NULL);
pthread_join(th2, NULL);
printf("s = %d\n, s);
return 0;
}
改进后的代码如下,学过操作系统会很好理解,无非就是为了保证共享内存区(临界区)的原子化操作,我们可以在进这段代码之前加锁(pthread_mutex_lock),意味着其他线程看到这段内存被其他人占有的时候,就不去抢占,等这段内存被解锁(pthread_mutex_unlock)之后,它才有读写这段临界区的权利。
但其实这种方式的执行速度并不快,比如这段代码里,每个线程都要进行10000次加解锁的操作,它能解决内存读写冲突的问题,但是却牺牲了效率。
锁的作用是什么呢?
前面说过多线程是并发执行的,th1运行后 进行了加锁,th2这时候想要运行,就必须等待th1解锁之后才行。
(一个卫生间,多个人要用,第一个人进去之后,把门锁上了,后边的人就得排队等着,第一个方便完了,解锁开门出来,第二个人进去,继续锁门……)
锁 在提高程序的安全性的同时,也降低了程序的效率。
锁的使用方法
pthread_mutex_t lock; 声明一个锁
pthread_mutex_init(&lock,NULL); 对声明的锁进行初始化
pthread_mutex_lock(&lock); //上锁 此时其他线程就开始等待
pthread_mutex_lock(&unlock); //解锁 其他线程可以使用资源了
死锁
拿上边举例,th1运行后,th2会等待th1解锁,才能运行,如果程序出现错误中断了,th1没有执行完,重新启动后,th1又重新执行,这时候th2排在th1前边,需要等待th1的解锁 ,而th1又在等待th2结束。 这样就造成了相互等待的情况,这个就是死锁。
互斥锁机制
通过访问时对共享资源加锁的方法,防止多个线程同时访问共享资源。锁有两种状态:未上锁和已上锁。在访问共享资源时,进行上锁,在访问结束后,进行解锁。若在访问时,共享资源已被其它线程锁住了,则进入堵塞状态等待该线程释放锁再继续下一步的执行。这种锁我们称为互斥锁。
互斥锁相关函数介绍:
1、pthread_mutex_init :初始化一个互斥锁。
函数原型:int pthread_mutex_init(pthread_mutex_tmutex,constpthread_mutexattr_tattr);
2、pthread_mutex_lock:若所访问的资源未上锁,则进行lock,否则进入堵塞状态。
函数原型:intpthread_mutex_lock(pthread_mutex_t*mutex);
3、pthread_mutex_unlock:对互斥锁进行解锁。
函数原型:intpthread_mutex_unlock(pthread_mutex_t*mutex);
4、pthread_mutex_destroy:销毁一个互斥锁。
函数原型:intpthread_mutex_destroy(pthread_mutex_t*mutex);
生产者消费者模型
生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中生成产品,消费者从存储空间中取走产品。当存储空间为空时,消费者阻塞;当存储空间满时,生产者阻塞。(下面代码中存储空间为1)
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
int buf = 0;
pthread_mutex_t mut;
void producer(){
while(1){
pthread_mutex_lock(&mut);
if(buf == 0){
buf = 1;
printf("produced an item.\n");
sleep(1);
}
pthread_mutex_unlock(&mut);
}
}
void consumer(){
while(1){
pthread_mutex_lock(&mut);
if(buf == 1){
buf = 0;
printf("consumed an item.\n");
sleep(1);
}
pthread_mutex_unlock(&mut);
}
}
int main(void){
pthread_t thread1,thread2;
pthread_mutex_init(&mut,NULL);
pthread_create(&thread1,NULL,&producer,NULL);
consumer(&buf);
pthread_mutex_destroy(&mut);
return 0;
}
从执行结果可以看出,运行顺序井然有序。生产后必是消费,消费完后必是生产。由于互斥锁机制的存在,生产者和消费者不会同时对共享资源进行访问。
信号量机制
上面了解到的互斥锁有两种状态:资源为0和1的状态。当我们所拥有的资源大于1时,可以采用信号量机制。在信号量机制中,我们有n个资源(n>0)。在访问资源时,若n>=1,则可以访问,同时信号量-1,否则堵塞等待直到n>=1。其实互斥锁可以看出信号量的一种特殊情况(n=1)。
信号量相关函数的介绍:
头文件:semaphore.h
1、sem_init函数:初始化一个信号量。
函数原型:int sem_init(sem_t* sem, int pshared, unsigned int value);
参数:
sem:指定了要初始化的信号量的地址;
pshared:如果其值为0,就表示信号量是当前进程的局部信号量,否则信号量就可以在多个进程间共享;
value:指定了信号量的初始值;
返回值:成功=>0 , 失败=> -1;
2、 sem_post函数:信号量的值加1,如果加1后值大于0:等待信号量的值变为大于0的进程或线程被唤醒。
函数原型:int sem_post(sem_t* sem);
返回值:成功=>0 , 失败=> -1;
3、sem_wait函数:信号量的减1操作。如果当前信号量的值大于0,则可继续执行。如果当前信号量的值等于0,则会堵塞,直到信号量的值大于0.
函数原型:int sem_wait(sem_t* sem);
返回值:成功=>0 , 失败=> -1;
4、sem_destroy函数:销毁一个信号量。
函数原型:int sem_destroy(sem_t* sem);
返回值:成功=>0 , 失败=> -1;
5、sem_getvalue函数:获取信号量中的值。
函数原型:int sem_getvalue(sem_t* sem, int* sval);
获取信号量的值,并放在&sval上。
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<semaphore.h>
#include<unistd.h>
sem_t npro; //还可以生产多少
sem_t ncon; //还可以消费多少
void* producer(void* arg){
while(1){
int num;
sem_wait(&npro); //先判断是否可以生产
sem_post(&ncon); //生产一个,可消费数+1
sem_getvalue(&ncon,&num);
printf("produce one,now have %d items.\n",num);
sleep(0.7);
}
}
void consumer(void* arg){
while(1){
int num;
sem_wait(&ncon); //判断是否可以消费
sem_post(&npro); //消费一个,可生产数+1
sem_getvalue(&ncon,&num);
printf("consume one,now have %d items.\n",num);
sleep(1);
}
}
int main(void){
pthread_t thread1,thread2;
//init semaphore
sem_init(&npro,0,5); //设最大容量为5
sem_init(&ncon,0,0);
pthread_create(&thread1,NULL,&producer,NULL);
consumer(NULL);
return 0;
}
同样也可以解决条件竞争问题,而且使用范围更广了。
解决:
- pthread_join()函数
- 互斥锁机制
- 信号量机制