文章目录
线程同步
线程同步概念
同步即协同步调,对公共区域数据按序访问。
线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。防止数据混乱,产生与时间有关的错误。
“同步”的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间、信号间等等都需要同步机制。
数据混乱的原因
资源共享(独享资源则不会)
调度随机(意味着数据访问会出现竞争)
线程间缺乏必要的同步机制
1和2不可避免,着手解决3,使多个线程在访问共享资源的时候,出现互斥
互斥量mutex
每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。
互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。
即使有了
mutex
,如果有线程不按规则来访问数据,依然会造成数据混乱。我们要尽量的减少锁的粒度,越小越好。访问共享数据前,加锁,访问结束后,要立即解锁。
主要应用函数
主要应用函数
pthread_mutex_t mutex //创建锁
1、pthread_mutex_init函数 //初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr); //动态初始化
eg: pthread_mutex_init(&mutex,NULL);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//静态初始化
在内核中的定义
#define PTHREAD_MUTEX_INITIALIZER \
{ { 0, 0, 0, 0, 0, 0, { 0, 0 } } }
2、pthread_mutex_destroy函数 //销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
3、pthread_mutex_lock函数 //加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
可理解为将mutex --(或 -1),操作后mutex的值为0,会阻塞
4、pthread_mutex_trylock函数 //尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
pthread_mutex_trylock加锁失败直接返回错误号(如:EBUSY),不阻塞。
5、pthread_mutex_unlock函数 //解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
可理解为将mutex ++(或 +1),操作后mutex的值为1
以上5个函数的返回值都是:成功返回0, 失败返回错误号。
pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。
pthread_mutex_t mutex; 变量mutex只有两种取值1、0。
restrict关键字
关键字
restrict
只用于限定指针;该关键字用于告知编译器,所有修改该指针所指向内容的操作全部都是基于(base on
)该指针的,即不存在其它进行修改操作的途径.
表示指针指向的内容只能通过这个指针进行修改.
restrict
关键字:用来限定指针变量。被该关键字限定的指针变量所指向的内存操作,必须由本指针完成。
void *memcpy( void * restrict dest , const void * restrict src, size_t n) ;
这是一个很有用的内存复制函数,由于两个参数都加了
restrict
限定,所以两块区域不能重叠,即dest
指针所指的区域,不能让别的指针来修改,即src
的指针不能修改.
pthread_mutex_t的定义
/* Data structures for mutex handling. The structure of the attribute
type is not exposed on purpose. */
typedef union
{
struct __pthread_mutex_s
{
int __lock;
unsigned int __count;
int __owner;
#if __WORDSIZE == 64
unsigned int __nusers;
#endif
/* KIND must stay at this position in the structure to maintain
binary compatibility. */
int __kind;
#if __WORDSIZE == 64
int __spins;
__pthread_list_t __list;
# define __PTHREAD_MUTEX_HAVE_PREV 1
#else
unsigned int __nusers;
__extension__ union
{
int __spins;
__pthread_slist_t __list;
};
#endif
} __data;
char __size[__SIZEOF_PTHREAD_MUTEX_T];
long int __align;
} pthread_mutex_t;
借助互斥锁管理共享数据实现同步
- 未加互斥锁
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <sys/mman.h>
#include <errno.h>
void* tfn(void* arg){
srand(time(NULL));
while(1){
printf("hello ");
sleep(rand() % 3);
printf("world \n");
sleep(rand() % 3);
}
return (void*)(1);
}
int main(int argc,char* argv[]){
int ret;//用于判断是否创建了线程
int i = 0;//用于循环创建线程
pthread_t tid;//用于create线程的传出参数
srand(time(NULL));
int* a;
ret = pthread_create(&tid,NULL,tfn,(void*)(i));
if(ret != 0){
fprintf(stderr,"pthread_create error: %s\n",strerror(ret));
}
while(1){
printf("HELLO ");
sleep(rand() % 3);
printf("WORLD \n");
sleep(rand() % 3);
}
ret = pthread_join(tid,(void**)&a);
if(ret != 0){
fprintf(stderr,"pthread_join error: %s\n",strerror(ret));
}
printf("a==%d\n",a);
pthread_exit(NULL);
}
/*执行结果
ubuntu@ubuntu:~/LearnCPP/pthread_test/mutex$ ./pthread_mutex1
HELLO hello world
WORLD
hello world
hello world
HELLO WORLD
hello HELLO world
^C
*/
- 加入互斥锁
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <sys/mman.h>
#include <errno.h>
pthread_mutex_t mutex;//定义一个互斥锁
void* tfn(void* arg){
srand(time(NULL));
while(1){
pthread_mutex_lock(&mutex);//加锁
printf("hello ");
sleep(rand() % 3);
printf("world \n");
pthread_mutex_unlock(&mutex);//解锁
sleep(rand() % 3);
}
return (void*)(1);
}
int main(int argc,char* argv[]){
int ret;//用于判断是否创建了线程
int i = 0;//用于循环创建线程
pthread_t tid;//用于create线程的传出参数
srand(time(NULL));
int* a;
ret = pthread_mutex_init(&mutex,NULL);//初始化锁
if(ret != 0){
fprintf(stderr,"mutex_init error:%s",strerror(ret));
}
ret = pthread_create(&tid,NULL,tfn,(void*)(i));
if(ret != 0){
fprintf(stderr,"pthread_create error: %s\n",strerror(ret));
}
while(1){
pthread_mutex_lock(&mutex);//加锁
printf("HELLO ");
sleep(rand() % 3);
printf("WORLD \n");
pthread_mutex_unlock(&mutex);//解锁
sleep(rand() % 3);
}
pthread_join(tid,(void**)&a);
if(ret != 0){
fprintf(stderr,"pthread_join error: %s\n",strerror(ret));
}
pthread_mutex_destroy(&mutex);//销毁互斥锁
//sleep(i);
printf("a==%d\n",a);
pthread_exit(NULL);
}
/*执行结果
ubuntu@ubuntu:~/LearnCPP/pthread_test/mutex$ ./pthread_mutex2
HELLO WORLD
hello world
HELLO WORLD
hello world
HELLO WORLD
HELLO WORLD
hello world
HELLO WORLD
^C
*/
//主线程和子线程在访问共享区时就没有交叉输出的情况了
死锁
死锁的情况:
1. 线程试图对同一个互斥量A加锁两次。对一个锁反复的lock。
2. 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁。一个线程不满足于拥有一把锁。
读写锁
与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享
读写锁是“写模式加锁”时, 解锁前,所有对该锁加锁的线程都会被阻塞。
读写锁是“读模式加锁”时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞, 写锁优先级高
读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。
读写锁非常适合于对数据结构读的次数远大于写的情况。
相较于互斥量而言,当读线程多的时候,提高访问效率。
总结:读共享,写独占,写优先级高
主要应用函数
1、pthread_rwlock_t rwlock;//创建读写锁
2、int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
eg:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;//静态初始化
pthread_rwlock_init(&rwlock, NULL);//动态初始化
成功返回 0,失败返回错误号
3、int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//销毁读写锁
eg:
pthread_rwlock_destroy(&rwlock);
成功返回 0,失败返回错误号
4、int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//加读锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);//尝试加读锁
pthread_rwlock_rdlock(&rwlock);
pthread_rwlock_tryrdlock(&rwlock);
成功返回 0,失败返回错误号
5、int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//加写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);//尝试加写锁
pthread_rwlock_wrlock(&rwlock);
pthread_rwlock_trywrlock(&rwlock);
成功返回 0,失败返回错误号
6、int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//解锁
pthread_rwlock_unlock(&rwlock);
成功返回 0,失败返回错误号
读写锁demo
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
int counter;
pthread_rwlock_t rwlock;//定义一个读写锁
void* write_func(void* arg){
int t;
int i = (int)arg;
while(1){
t = counter;//保存全局变量
usleep(1000);
pthread_rwlock_wrlock(&rwlock);//加写锁
printf("+++++++++%d,pthread:%lu,counter=%d,++counter:%d\n",i,pthread_self(),t,++counter);
pthread_rwlock_unlock(&rwlock);
usleep(9000);//给rd线程的机会
}
return NULL;
}
void* read_func(void* arg){
int t = (int)arg;//用于显示第几个线程
while(1){
pthread_rwlock_rdlock(&rwlock);//加读锁
printf("------------------%d,pthread:%lu,counter=%d\n",t,pthread_self(),counter);
pthread_rwlock_unlock(&rwlock);//解锁
usleep(2000);//给w线程机会
}
return NULL;
}
int main(int argc,char* argv[]){
int i = 0;//用于循环创建线程
pthread_t tid[8];//用于create线程的传出参数
pthread_rwlock_init(&rwlock,NULL);//初始化rwlock
for(i = 0;i<3;i++){
pthread_create(&tid[i],NULL,write_func,(void *)i);//创建写线程
}
for(i = 0;i<5;i++){
pthread_create(&tid[i+4],NULL,read_func,(void *)i);//创建读线程
}
for(i = 0;i<8;i++){
pthread_join(tid[i],NULL);//回收子线程
}
pthread_rwlock_destroy(&rwlock);
pthread_exit(NULL);
return 0;
}
/*执行结果,程序执行得很快,只截取了一点点。
ubuntu@ubuntu:~/LearnCPP/pthread_test/rw_mutex$ ./pthread_mutex2
------------------0,pthread:139967631570688,counter=0
------------------1,pthread:139967623177984,counter=0
------------------2,pthread:139967614785280,counter=0
------------------3,pthread:139967606392576,counter=0
------------------4,pthread:139967597999872,counter=0
+++++++++2,pthread:139967639963392,counter=0,++counter:1
+++++++++1,pthread:139967648356096,counter=0,++counter:2
+++++++++0,pthread:139967656748800,counter=0,++counter:3
......
*/
条件变量
条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所
主要应用函数
1、pthread_cond_t cond;//创建条件变量
2、pthread_cond_init函数
初始化一个条件变量
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
参2:attr表条件变量属性,通常为默认值,传NULL即可
pthread_cond_init(&cond,NULL);//动态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//静态初始化
3、pthread_cond_destroy函数
销毁一个条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_destroy(&cond);
4、pthread_cond_wait函数
阻塞等待一个条件变量
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
pthread_cond_wait(&cond,&mutex);
函数作用:
1.阻塞等待条件变量cond(参1)满足
2.释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);
1.2.两步为一个原子操作,不可以在分离的操作。
3.当被唤醒,pthread_cond_wait函数返回时,
解除阻塞并重新申请获取互斥锁 pthread_mutex_lock(&mutex);
5、pthread_cond_timedwait函数
限时等待一个条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
参3: 参看man sem_timedwait函数,查看struct timespec结构体。
struct timespec {
time_t tv_sec; /* seconds */ 秒
long tv_nsec; /* nanosecondes*/ 纳秒
}
形参abstime:绝对时间。
如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。
struct timespec t = {1, 0};
pthread_cond_timedwait (&cond, &mutex, &t);
只能定时到 1970年1月1日 00:00:01秒(早已经过去)
正确用法:
time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义timespec 结构体变量t
t.tv_sec = cur+1; 定时1秒
pthread_cond_timedwait (&cond, &mutex, &t);
6、pthread_cond_signal函数
唤醒至少一个阻塞在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);
7、pthread_cond_broadcast函数
唤醒全部阻塞在条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
以上6 个函数的返回值都是:成功返回0, 失败直接返回错误号。
pthread_cond_wait原理
条件变量的生产者消费者模型
生产者消费者模型,借助条件变量来实现这一模型是比较常见的一种方法。假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。
消费者等待商品,使用pthread_cond_wait(&cond,&mutex)
函数等待满足条件,生产者一有商品满足条件,通过pthread_cond_signal()
函数和pthread_cond_broadcast()
函数唤醒等待的线程。这个条件变量都要配合线程锁配合使用。
执行到pthread_cond_wait(&cond,&mutex)
函数时,他会阻塞等待条件的满足,然后就会解锁,此时生产者就会加锁成功,然后生产者满足条件后,就会唤醒消费者线程,进行加锁。
单消费者生产者demo
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
//商品
struct product {
struct product* next;
int num;
};
struct product *head;//头节点
//静态初始化一个条件变量和一个互斥量
pthread_cond_t products = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
//消费者线程
void* consumer(void* arg){
struct product *p;//消费的商品
while(1){
pthread_mutex_lock(&lock);//加锁
if(head == NULL){ //说明没有商品,要等待创建商品
pthread_cond_wait(&products,&lock);
//他会阻塞等待条件的满足,
//先会解锁,此时生产者就会加锁成功,
//然后生产者满足条件后,就会唤if醒消费者线程,进行加锁。
}
p = head;//头删法
head = p->next;
pthread_mutex_unlock(&lock);//解锁
printf("consumer num %d\n",p->num);//打印消费掉的数据
free(p); //p时消费者线程和生产者线程共享的,malloc在堆区,堆区共享
sleep(rand()%3);//睡眠
}
}
//生产者线程
void* producter(void* arg){
struct product *p;//生产的商品
while(1){
p = malloc(sizeof(struct product));
p->num = rand()%100 + 1;
printf("product num %d\n",p->num);//打印出生产的数据
pthread_mutex_lock(&lock);//加锁
p->next = head;//头插法
head = p;
pthread_mutex_unlock(&lock);//解锁
pthread_cond_signal(&products);//满足条件,唤醒消费者线程
sleep(rand()%3);
}
}
int main(int argc,char* argv[]){
pthread_t pid,cid;
srand(time(NULL));
//创建两个线程
pthread_create(&pid,NULL,producter,NULL);
pthread_create(&cid,NULL,consumer,NULL);
//回收两个线程
pthread_join(pid,NULL);
pthread_join(cid,NULL);
pthread_exit(NULL);
return 0;
}
//运行结果
/*
product num 73
consumer num 73
product num 87
consumer num 87
product num 89
product num 97
consumer num 97
product num 31
consumer num 31
product num 4
consumer num 4
product num 56
consumer num 56
consumer num 89
product num 64
product num 22
^C
*/
多个消费者
使用上面的demo会出现问题的,比如,两个消费者都阻塞在条件变量上,就是说没有数据可以消费。完
事儿都把锁还回去了,生产者此时生产了一个数据,会同时唤醒两个因条件变量阻塞的消费者,完事
两个消费者去抢锁。结果就是 A 消费者拿到锁,开始消费数据,B 消费者阻塞在锁上。之后 A 消费
完数据,把锁归还,B 被唤醒,然而此时已经没有数据供 B 消费了。所以这里有个逻辑错误,消费者
阻塞在条件变量那里应该使用 while 循环。这样 A 消费完数据后,B 做的第一件事不是去拿锁,而是
判定条件变量。
//消费者线程
void* consumer(void* arg){
struct product *p;//消费的商品
while(1){
pthread_mutex_lock(&lock);//加锁
while(head == NULL){ //说明没有商品,要等待创建商品 多消费者的时候这里使用while
pthread_cond_wait(&products,&lock);
//他会阻塞等待条件的满足,
//先会解锁,此时生产者就会加锁成功,
//然后生产者满足条件后,就会唤if醒消费者线程,进行加锁。
}
p = head;//头删法
head = p->next;
pthread_mutex_unlock(&lock);//解锁
printf("consumer num %d\n",p->num);//打印消费掉的数据
free(p); //p时消费者线程和生产者线程共享的,malloc在堆区,堆区共享
sleep(rand()%3);//睡眠
}
}
信号量
主要操作函数
互斥量相当于初始化为1,那么信号量相当于初始化值为
N
的互斥量,N值代表可以同时访问共享数据的线程数。应用于线程、进程间同步。
#include <semaphore.h>
1、sem_t sem;//创建信号量
2、int sem_init(sem_t *sem, int pshared, unsigned int value) //初始化信号量
参数:
sem: 信号量
pshared: 0: 用于线程间同步 1: 用于进程间同步
value:N 值。(指定同时访问的线程数)
sem_init(&sem,0,3);
3、int sem_destroy(sem_t *sem); //销毁一个信号量
4、int sem_wait(sem_t *sem); //给信号量加锁 --操作 类似于pthread_mutex_lock
//当信号量的值为 0 时,再次 -- 就会阻塞
5、int sem_post(sem_t *sem); //给信号量解锁 ++操作 类似于pthread_mutex_unlock
//当信号量的值为 N 时, 再次 ++ 就会阻塞
6、int sem_trywait(sem_t *sem); //尝试对信号量进行加锁 --操作 类似于pthread_mutex_trylock
//当信号量的值为 0 时,再次 -- 就会阻塞
7、int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); //尝试限时对信号量加锁
参2:abs_timeout采用的是绝对时间。
定时1秒:
time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义timespec 结构体变量t
t.tv_sec = cur+1; 定时1秒
t.tv_nsec = t.tv_sec +100;
sem_timedwait(&sem, &t); 传参
以上6 个函数的返回值都是:成功返回0, 失败直接返回错误号。
信号量实现生产者消费者模型
产品数量star_num,空格数量blanK_num,其中消费者不断的进行消费,将star_num–,blank_num++。同理生产者不断的生产,将star_num++,blank_num–。star_num和blank_num加减到0或者5的时候就不能在进行减或者加了。
sem信号量消费者模型demo
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#define NUM 5
int queue[NUM] = {0};//数据,比作为星星
sem_t blank_num,star_num;//创建空格和星星信号量
void* producer(void* arg){
int i = 0;
while(1){
sem_wait(&blank_num); //blank_num--
queue[i] = rand()%1000 +1;//生产星星
printf("produce star num = %d\n",queue[i]);
sem_post(&star_num); //star_num++
i = (i+1)%NUM; //借助数组的下标实现环形
sleep(rand()%2);
}
}
void* consumer(void* arg){
int i = 0;
while(1){
sem_wait(&star_num); //star_num--
printf("consumer star num = %d\n",queue[i]);
queue[i] = 0;//消费一个星星
sem_post(&blank_num); //blank_num++
i = (i+1)%NUM; //借助数组的下标实现环形
sleep(rand()%2);
}
}
int main(int argc,char** argv){
pthread_t pid,cid;
srand(time(NULL));
sem_init(&blank_num,0,NUM);//空格子初始化信号量5
sem_init(&star_num,0,0);//星星信号量初始化0
pthread_create(&pid,NULL,&producer,NULL);//创建生产者线程
pthread_create(&cid,NULL,&consumer,NULL);//创建消费者线程
pthread_join(pid,NULL);
pthread_join(cid,NULL);
sem_destroy(&blank_num);
sem_destroy(&star_num);
return 0;
}
//执行结果部分
/*
produce star num = 763
consumer star num = 763
produce star num = 715
consumer star num = 715
produce star num = 635
consumer star num = 635
produce star num = 1000
consumer star num = 1000
*/