多线程已经成为服务器开发不可或缺的重要知识点了,那么怎样协调各个线程之间的工作就变得至关重要,于是这篇文章就来总结一下线程同步的方法。
什么是线程同步?
“同”字应是指协同、协助、互相配合。主旨在协同步调,按预定的先后次序运行。线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。
“同步”的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。
因此, 所有“多个控制流,共同操作一个共享资源” 的情况,都需要同步。
那么怎样达到线程同步就需要一些方法,这里讲解了互斥量,条件变量,信号量线程同步方法。
互斥锁mutex
互斥锁的原理很容易理解,我们给一个共享资源分配一把锁,每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。
通过这样:资源还是共享的,线程间也还是竞争的,但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。但,应注意:同一时刻,只能有一个线程持有该锁。
我们先用一个用互斥量使用步骤开始讲解:
1,pthread_mutex_t 函数 ,创造一个互斥锁。
2,pthread_mutex_init 函数,初始化。
3,pthread_mutex_lock / pthread_mutex_trylock 函数,加锁。
4,访问共享数据
5,pthread_mutex_unlock 函数,解锁
6,pthread_mutex_destroy 函数,销毁锁
pthread_mutex_t mutex;
没啥好说的,就是创造一个互斥锁mutex。
pthread_mutex_init 函数
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 函数作用是:初始化一个互斥锁(互斥量) ---> 初值可看作 1
参 1:传出参数,调用时应传 &mutex。
restrict 关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改。
参 2:互斥量属性。是一个传入参数,通常传 NULL,选用默认属性(线程间共享)。 参 APUE.12.4 同步属性
pthread_mutex_lock 函数
int pthread_mutex_lock(pthread_mutex_t *mutex); 函数作用是:加锁。可理解为将 mutex--(或 -1),操作后 mutex 的值为 0。
另外,lock与trylock的区别是:lock加锁失败会导致阻塞,trylock加锁失败不会阻塞会返回-1 。
pthread_mutex_unlock 函数
int pthread_mutex_unlock(pthread_mutex_t *mutex); 解锁。可理解为将 mutex ++(或 +1),操作后 mutex 的值为 1。
pthread_mutex_destroy 函数
int pthread_mutex_destroy(pthread_mutex_t *mutex); 销毁一个互斥锁
lock 与 unlock :
lock 尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。
unlock 主动解锁函数, 同时将阻塞在该锁上的所有线程 全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<pthread.h> 4 #include<stdlib.h> 5 #include<unistd.h> 6 7 //互斥锁设为全局变量 8 pthread_mutex_t mutex; 9 10 //子线程 11 void *tfn(void *arg) { 12 srand(time(NULL)); 13 while (1) { 14 pthread_mutex_lock(&mutex); //加锁,不行则阻塞 15 printf("hello "); 16 printf("world "); 17 pthread_mutex_unlock(&mutex); 18 sleep(rand()%3); 19 } 20 return NULL; 21 } 22 23 int main() 24 { 25 pthread_t tid; 26 srand(time(NULL)); 27 28 pthread_mutex_init(&mutex,NULL); //初始化互斥锁 29 30 pthread_create(&tid,NULL,tfn,NULL); //创造子线程 31 32 //主线程 33 while (1) { 34 pthread_mutex_lock(&mutex); //互斥锁加锁,不行则阻塞 35 printf("HELLO "); 36 printf("WORLD "); 37 pthread_mutex_unlock(&mutex); //互斥锁解锁 38 sleep(rand()%3); 39 } 40 41 //最后记得回收子线程,销毁互斥锁 42 pthread_join(tid,NULL); 43 pthread_mutex_destroy(&mutex); 44 return 0; 45 }
互斥锁读写锁
条件变量
条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。
函数类似互斥锁,我们也从条件变量使用步骤开始:
1,pthread_cond_t 函数, 用于定义条件变量
2,pthread_cond_init 函数
3,pthread_cond_wait / pthread_cond_timedwait 函数
4,pthread_cond_signal / pthread_cond_broadcast 函数
5,唤醒之后,重新申请互斥锁,然后操作共享资源
6,pthread_cond_destroy 函数
pthread_cond_init 函数
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); 初始化一个条件变量
参1:创造的条件变量 参2:attr 表条件变量属性,通常为默认值,传 NULL 即可
pthread_cond_wait 函数
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex); 阻塞等待一个条件变量
函数作用:
1. 阻塞等待条件变量 cond(参 1)满足
2. 释放已掌握的互斥锁(解锁互斥量)相当于 pthread_mutex_unlock(&mutex);
注意:1 2. 两步为 一个 原子操作。
3. 当被唤醒,pthread_cond_wait 函数返回时,解除阻塞并重新申请获取互斥锁 pthread_mutex_lock(&mutex);
pthread_cond_timedwait 函数
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec*restrict abstime); 限时等待一个条件变量
与pthread_cond_wait函数相比就是多了struct timespec这个结构体:
struct timespec {
time_t tv_sec; /* seconds */ 秒
long tv_nsec; /* nanosecondes*/ 纳秒 ,注意这里是纳秒,timeval这个结构体这里是微秒
}
并且要注意这里的abstime是绝对时间,所以正确用法是:
time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义 timespec 结构体变量 t
t.tv_sec = cur+1; 定时 1 秒
pthread_cond_timedwait (&cond, &mutex, &t); 传参
pthread_cond_signal 函数
int pthread_cond_signal(pthread_cond_t *cond); 唤醒至少一个阻塞在条件变量上的线程
tpthread_cond_broadcast 函数
int pthread_cond_broadcast(pthread_cond_t *cond); 唤醒全部阻塞在条件变量上的线程
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<string.h> 4 #include<unistd.h> 5 #include<errno.h> 6 #include<pthread.h> 7 8 //线程不能perror,有自己独特的错误处理方式 9 void error_thread(int ret,char *str) { 10 if (ret!=0) { 11 fprintf(stderr,"%s:%s ",str,strerror(ret)); 12 pthread_exit(NULL); 13 } 14 } 15 16 //把生产的物品做成链表 17 struct msg{ 18 int num; 19 struct msg *next; 20 }; 21 struct msg *head; 22 23 pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; //互斥量 24 pthread_cond_t has_data=PTHREAD_COND_INITIALIZER; //条件变量 25 26 //生产者线程 27 void *producer(void *arg) { 28 while (1) { 29 //生产者生产一个数据 30 struct msg *mp=malloc(sizeof(struct msg)); 31 mp->num = rand()%1000+1; 32 printf("--produce %d ",mp->num); 33 34 //利用互斥量对公共区域做互斥 35 pthread_mutex_lock(&mutex); //加互斥锁 36 mp->next=head; //写公共区域 37 head=mp; 38 pthread_mutex_unlock(&mutex); //解互斥锁 39 40 //条件变量:唤醒阻塞在条件变量has_data的线程 41 pthread_cond_signal(&has_data); 42 43 sleep(rand()%3); 44 } 45 return NULL; 46 } 47 48 //消费者线程 49 void *consumer(void *arg) { 50 while (1) { 51 struct msg *mp; 52 53 // 54 pthread_mutex_lock(&mutex); 55 while (head==NULL) 56 pthread_cond_wait(&has_data,&mutex); 57 58 //消费生产品,读写公共区域 59 mp=head; 60 head=mp->next; 61 printf("-------------consumer id %lu :%d ",pthread_self(),mp->num); 62 free(mp); 63 64 sleep(rand()%3); 65 } 66 return NULL; 67 } 68 69 int main(int argc,char *argv[]) { 70 int ret; 71 pthread_t pid,cid; 72 73 srand(time(NULL)); 74 75 ret=pthread_create(&pid,NULL,producer,NULL); //生产者 76 if (ret!=0) 77 err_thread(ret,"pthread_create producer error"); 78 79 ret=pthread_create(&cid,NULL,consumer,NULL); //消费者 80 if (ret!=0) 81 err_thread(ret,"pthread_create producer error"); 82 83 return 0; 84 }
从生产者消费者问题我们可以看出条件变量配合使用的一点好处:
相较于 mutex 而言,条件变量可以减少竞争。
如直接使用 mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。
信号量
信号量可以看作是进化版的互斥锁(1 --> N),由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。
信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。
同样的,我们以使用步骤开始讲解:
1,sem_t sem; 规定信号量 sem 不能 < 0。
2,sem_init 函数 初始化
3,sem_wait / sem_trywait / sem_timedwait 函数 ,信号量尝试减一,准备访问共享资源
4,访问操作共享资源
5,sem_post 函数 ,访问完成,信号量加一
6,sem_destroy 函数 销毁信号量
sem_init 函数
int sem_init(sem_t *sem, int pshared, unsigned int value); 初始化一个信号量
参 1:sem 信号量
参 2:pshared 取 0 用于线程间;取非 0(一般为 1)用于进程间
参 3:value 指定信号量初值>=0
sem_wait 函数
int sem_wait(sem_t *sem); 给信号量加锁 --
sem_tryt wait 函数
int sem_trywait(sem_t *sem); 尝试对信号量加锁 -- (与 sem_wait 的区别类比 lock 和 trylock,即阻塞与非阻塞)
sem_timedwait 函数
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); 限时尝试对信号量加锁 --
参 2:abs_timeout 采用的是绝对时间。 注意这里也是timespec,参考上面条件变量,也是绝对时间+纳秒。
sem_post 函数
int sem_post(sem_t *sem); 给信号量解锁 ++
sem_destroy 函数
int sem_destroy(sem_t *sem); 销毁一个信号量
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<unistd.h> 4 #include<pthread.h> 5 #include<semaphore.h> 6 7 #define NUM 5 8 9 int queue[NUM]; 10 sem_t blank_number,product_number; 11 12 void *producer(void *arg) { 13 int i=0; 14 while (1) { 15 //这里是信号量处理生产过程 16 sem_wait(&blank_number); //空位数信号量-- 17 queue[i]=rand()%1000+1; //开始生产 18 printf("----Produce----%d ",queue[i]); 19 sem_post(&product_number); //产品数信号量++ 20 21 i=(i+1)%NUM; 22 sleep(rand()%1); 23 } 24 } 25 26 void *consumer(void *arg) { 27 int i=0; 28 while (1) { 29 //这里是信号量处理消费过程 30 sem_wait(&product_number); //产品数信号量-- 31 //开始消费 32 printf("---consume--%d ",queue[i]); 33 queue[i]=0; 34 sem_post(&blank_number); //空位数信号量++ 35 36 i=(i+1)%NUM; 37 sleep(rand()%3); 38 } 39 } 40 41 int main(int argc,char *argv[]) 42 { 43 pthread_t pid,cid; 44 45 //参二0为线程1为进程,参三可以理解为信号量初始值 46 sem_init(&blank_number,0,NUM); //空位数 47 sem_init(&product_number,0,0); //产品数 48 49 //创建生产者消费者线程 50 pthread_create(&pid,NULL,producer,NULL); 51 pthread_create(&cid,NULL,consumer,NULL); 52 53 //回收线程 54 pthread_join(pid,NULL); 55 pthread_join(cid,NULL); 56 57 //销毁信号量 58 sem_destroy(&blank_number); 59 sem_destroy(&product_number); 60 61 return 0; 62 }