Linux系统编程笔记:线程同步
线程同步
一、基本概念
1.1线程同步
线程不同步导致共享数据混乱,是由于资源共享、调度随机,缺乏同步机制。
情景:程序中有一个全局变量Val,且有两个线程,线程A需要对变量Val进行读取操作,线程B需要对变量Val进行写操作,此时可能会出现对Val资源的抢夺,会导致不能按照客户的意愿进行下去,这时就需要实行线程同步。
线程同步:协同步调,对公共区域数据按序访问。防止数据混乱,产生与时间有关的错误。
1.2线程同步的方式
常见的线程同步方法有:互斥锁、条件变量、信号量等。
二、同步方法:互斥锁(互斥量)
为避免资源争夺共享资源的问题,提出互斥量(mutex)来确保同一时间只有一个线程占有资源。更为全面的说法是,可以使用互斥量来保证对任意共享资源的原子访问,而保护共享变量是其常见的用法。
没有锁的资源争夺情景如下:
void *thread_fun1(void *arg){ //读全局变量val for(int i = 0;i<10;i++){ printf("read thread val=%d\n",val); } return NULL; } void *thread_fun2(void *arg){ //写全局变量val for(int i = 0;i <10;i++){ val++; printf("write thread doing val++ \n"); } return NULL; } int main(void){ pthread_t tid1,tid2; int ret; ret = pthread_create(&tid1,NULL,thread_fun1,NULL); //创建线程1 if(ret != 0){ printf("create failed\n"); exit(1); } ret = pthread_create(&tid2,NULL,thread_fun2,NULL); //创建线程2 if(ret != 0){ printf("create failed\n"); exit(1); } pthread_join(tid1,NULL); pthread_join(tid2,NULL); return 0; }
从图中看出,在读数据时,数据还被写了。
2.1锁的两种状态
互斥锁有两种状态:已锁定(locked)和未锁定(unlocked),任何时候,只有一个线程可以获得锁。可以换个角度看,锁在任何时候只有0,1两个状态的一种。
锁的初始状态为1,当有线程执行lock动作后,lock相当于执行减一操作,锁的状态变成0;
当有线程执行unlock动作后,unlock相当于执行加一操作,锁的状态右0变成1。
当一个线程A获得资源的锁之后,在没有解锁之前,另一个线程B想要操作同一资源时,会发生阻塞,直到线程A解锁。
2.2使用互斥锁的一般步骤
第一步:pthread_mutex_t mute 创建互斥锁
第二步:pthread_mutex_init; 初始化互斥锁
第三步:pthread_mutex_lock; 对资源加锁
第四步:操作资源
第五步:pthrad_mutext_unlock();解锁
第六步:pthead_mutex_destroy;销毁锁
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<pthread.h> #include<sys/stat.h> #include<string.h> int val = 0; //全局变量 pthread_mutex_t mutex; //第一步: 创建互斥锁 void *thread_fun1(void *arg){ //读全局变量val for(int i = 0;i<10;i++){ pthread_mutex_lock(&mutex); //第三步:锁; printf("read thread val=%d\n",val); //第四部:操作 pthread_mutex_unlock(&mutex); //第五步:解锁 } return NULL; } void *thread_fun2(void *arg){ for(int i = 0;i <10;i++){ pthread_mutex_lock(&mutex) ; //锁; val++; printf("write thread doing val++ \n"); pthread_mutex_unlock(&mutex); //解锁 } return NULL; } int main(void){ pthread_t tid1,tid2; int ret; pthread_mutex_init(&mutex,NULL); //第二步:初始化互斥锁 ret = pthread_create(&tid1,NULL,thread_fun1,NULL); //创建线程1 if(ret != 0){ printf("create failed\n"); exit(1); } ret = pthread_create(&tid2,NULL,thread_fun2,NULL); //创建线程2 if(ret != 0){ printf("create failed\n"); exit(1); } pthread_join(tid1,NULL); pthread_join(tid2,NULL); pthread_mutex_destroy(&mutex); //第六步:销毁互斥锁 return 0; }
结果:读写数据不会混乱
2.3互斥锁死锁
互斥锁能够有效的解决线程间资源抢夺的问题。但是,互斥锁使用的不好的时候,就会出现死锁。现象表现为互斥锁一致被占有,线程迟迟不能拿到锁,而一致阻塞中。
互斥锁死锁一般分为两种:
(1)线程自己将自己锁死
(2)多线程抢占锁资源被困
2.3.1 线程自己将自己锁死
同一个线程先后调用同一个pthread_lock()两次。由于第一次加锁后,锁一直处于0状态(被占用),当程序往下执行到第二次加锁时,就会阻塞在第二次加锁处,且不能解锁,就会一直处于阻塞状态。
int val = 0; //全局变量 pthread_mutex_t mutex; //互斥锁 void *thread_fun1(void *arg){ //读全局变量val pthread_mutex_lock(&mutex); //锁 printf("read thread val=%d\n",val); pthread_mutex_lock(&mutex); //锁 val++; printf("read thread val=%d\n",val); pthread_mutex_unlock(&mutex); //解锁 pthread_mutex_unlock(&mutex); //解锁 return NULL; } int main(void){ pthread_t tid; int ret; pthread_mutex_init(&mutex,NULL); //初始化互斥锁 ret = pthread_create(&tid,NULL,thread_fun1,NULL); //创建线程1 if(ret != 0){ printf("create failed\n"); exit(1); } pthread_join(tid,NULL); pthread_mutex_destroy(&mutex); //销毁互斥锁 return 0; }
结果:程序锁死
2.3.2 多线程抢占锁资源被困
线程A获取资源必须要得到锁1和锁2,同样线程B获取资源也必须要得到锁1和锁2。当线程A获得锁1,程B获得锁2时,由于线程A得不到锁2会阻塞,线程B得不到锁1也会阻塞。此时两个线程都处于阻塞状态。
int val = 0; //全局变量 pthread_mutex_t mutex1,mutex2; //互斥锁 void *thread_fun1(void *arg){ //读全局变量val printf("thread_fun1 running!!!\n"); pthread_mutex_lock(&mutex1); //锁1; sleep(1); pthread_mutex_lock(&mutex2); //锁2; printf("read thread val=%d\n",val); pthread_mutex_unlock(&mutex2); //解锁 pthread_mutex_unlock(&mutex1); //解锁 return NULL; } void *thread_fun2(void *arg){ //写全局变量 printf("thread_fun2 running!!!\n"); pthread_mutex_lock(&mutex2); //锁2; pthread_mutex_lock(&mutex1) ; //锁1; val++; printf("write thread doing\n"); pthread_mutex_unlock(&mutex1); //解锁 pthread_mutex_unlock(&mutex2); //解锁 return NULL; } int main(void){ pthread_t tid1,tid2; int ret; pthread_mutex_init(&mutex1,NULL); //初始化互斥锁 pthread_mutex_init(&mutex2,NULL); //初始化互斥锁 ret = pthread_create(&tid1,NULL,thread_fun1,NULL); //创建线程1 if(ret != 0){ printf("create failed\n"); exit(1); } ret = pthread_create(&tid2,NULL,thread_fun2,NULL); //创建线程2 if(ret != 0){ printf("create failed\n"); exit(1); } pthread_join(tid1,NULL); pthread_join(tid2,NULL); pthread_mutex_destroy(&mutex1); //销毁互斥锁 pthread_mutex_destroy(&mutex2); //销毁互斥锁 return 0; }
2.3.3避免死锁的方法
1.保证资源的获取顺序,要求每个线程获取资源的顺序一致;
2.当得不到所需资源时,放弃已获得的资源,等待。
2.4读写锁
锁只有一把。以读方式给数据加锁——读锁。以写方式给数据加锁——写锁。
读共享,写独占。
写锁优先级高。
相较于互斥量而言,当读线程多的时候,提高访问效率
相关API
pthread_rwlock_t rwlock; pthread_rwlock_init(&rwlock, NULL); pthread_rwlock_rdlock(&rwlock); try pthread_rwlock_wrlock(&rwlock); try pthread_rwlock_unlock(&rwlock); pthread_rwlock_destroy(&rwlock);
三、同步方法:条件变量
互斥锁防止多个线程同时访问同一共享资源。条件变量允许一个线程就某共享资源的状态变化通知其他线程,并让其他线程等待(阻塞)这一通知。条件变量通常结合互斥锁来使用。条件变量典型的场景时生产-消费模型,线程池等。
3.1条件变量要解决的问题
一个未使用条件变量的简单例子有助于理解条件变量的重要性。
int val = 0; //全局变量 //生产函数 void* thread_producer(void* argc){ pthread_mutex_lock(&mutex); //已经定义且初始化好的互斥锁 val++; //生产 pthread_mutex_unlock(&mutex); } //主函数消费函数 while(1){ pthread_mutex_lock(&mutex); while(val > 0 ){ val--; } pthread_mutex_unlock(&mutex); }
上诉代码虽然可行,但是由于主线程不停地循环检查val变量,故造成CPU资源的浪费。条件变量就可以解决这一问题:允许一个线程休眠(等待)直至接获另一线程的通知(收到信号)去执行某些操作。当val没有生产时,主线程休眠不去查询,当val生产了,就通知给主线程。
3.2相关函数
初始化条件变量:
pthread_cond_init(&cond, NULL); 动态初始化。 pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 静态初始化。
阻塞等待条件:
pthread_cond_wait(&cond, &mutex); 作用: 1. 阻塞等待条件变量满足 2. 解锁已经加锁成功的信号量 (相当于 pthread_mutex_unlock(&mutex)) 3. 当条件满足,函数返回时,重新加锁信号量 (相当于, pthread_mutex_lock(&mutex);)
唤醒阻塞线程
pthread_cond_signal(pthread_cond_t*cond); 唤醒阻塞在条件变量上的一个线程。 pthread_cond_broadcast(pthread_cond_t*cond); 唤醒阻塞在条件变量上的 所有线程。
3.3生产消费模型
模拟一个生产消费模型:当生产者没有生产数据给数据区时,消费者拿不到数据,一直阻塞;当生产者生产数据给数据区时,发送一个信号,消费者收到,开始从数据区消费数据。
程序如下:
struct data{ //数据 int val; struct data *next; }; struct data *head; //定义头 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //静态初始化互斥锁 pthread_cond_t product_con = PTHREAD_COND_INITIALIZER; //静态初始化条件变量 void *producer_fun(void *p){ //生产 struct data *pNode; while(1){ //生产一个数据 pNode = (struct data*)malloc(sizeof(struct data)); pNode->val = rand() % 100 +1; printf("produce ----------%d\n\n",pNode->val); pthread_mutex_lock(&mutex); //锁 //数据放入数据区 pNode->next = head; head = pNode; pthread_mutex_unlock(&mutex); pthread_cond_signal(&product_con); //唤醒等待线程 sleep(rand() % 5); } return NULL; } void *consumer_fun(void *p){ //消费 struct data *cNode; while(1){ pthread_mutex_lock(&mutex); //锁 while(head == NULL) { pthread_cond_wait(&product_con,&mutex); //阻塞 } //消费 cNode = head; head = cNode->next; pthread_mutex_unlock(&mutex); printf("consume ----------%d\n\n",cNode->val); free(cNode); sleep(rand() % 5); } return NULL; } int main(int argc,char *argv[]){ pthread_t cid,pid; int ret; srand(time(NULL)); ret = pthread_create(&cid,NULL,consumer_fun,NULL); if(ret != 0){ printf("create c failed\n"); exit(1); } ret = pthread_create(&pid,NULL,producer_fun,NULL); if(ret != 0){ printf("create p failed\n"); exit(1); } pthread_join(cid,NULL); pthread_join(pid,NULL); return 0; }
四、信号量
信号量应用于线程、进程间同步。互斥量只有2值,分别为0,1;信号量相当于初始化值为 N 的互斥量。 N值,表示可以同时访问共享数据区的线程数。
4.1相关函数
sem_t sem; 定义类型。 int sem_init(sem_t *sem, int pshared, unsigned int value); 参数: sem: 信号量 pshared: 0:用于线程间同步;1:用于进程间同步 value:N值。(指定同时访问的线程数) sem_destroy(); //销毁 sem_wait(); 一次调用,做一次-- 操作, 当信号量的值为 0 时,再次 -- 就会阻塞。 sem_post(); 一次调用,做一次++ 操作. 当信号量的值为 N 时, 再次 ++ 就会阻塞。
信号量的工作可以理解为停车场场景:
在一个停车场里,总共有N个停车位,已经使用m个;
每当进行sem_post()操作就相当于,有一辆车开出去,m的数量就会加一,但空余车位不会大于N;
每当进行sem_wait()操作就相当于,有一辆车开进去,m的数量就会减一,空余车位为0,不能再操作。
4.2生产消费模型
#define NUM 5 int queue[NUM]; //全局数组实现环形队列 sem_t blank_number, product_number; //空格子信号量, 产品信号量 void *producer(void *arg) { int i = 0; while (1) { sem_wait(&blank_number); //生产者将空格子数--,为0则阻塞等待 queue[i] = rand() % 1000 + 1; //生产一个产品 printf("----Produce---%d\n", queue[i]); sem_post(&product_number); //将产品数++ i = (i+1) % NUM; //借助下标实现环形 sleep(rand()%1); } } void *consumer(void *arg) { int i = 0; while (1) { sem_wait(&product_number); //消费者将产品数--,为0则阻塞等待 printf("-Consume---%d\n", queue[i]); queue[i] = 0; //消费一个产品 sem_post(&blank_number); //消费掉以后,将空格子数++ i = (i+1) % NUM; sleep(rand()%3); } } int main(int argc, char *argv[]) { pthread_t pid, cid; sem_init(&blank_number, 0, NUM); //初始化空格子信号量为5, 线程间共享 -- 0 sem_init(&product_number, 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_number); sem_destroy(&product_number); return 0; }
注:此段代码为黑马培训视频内容。
- LINUX C系统编程学习笔记-----------进程通信(二)
- linux 系统编程-学习笔记1
- linux 系统编程学习笔记四
- 【网络编程笔记】Linux系统常见的网络编程I/O模型简述
- Liunx 命令行与shell脚本编程大全 第八章学习笔记(Linux系统的包管理基础)
- Linux 编程学习笔记----文档管理系统
- 笔记9:Linux 文件系统编程
- LINUX C系统编程学习笔记-----------时间编程
- linux系统编程-学习笔记4-chmod/access/chown/link/truncate
- linux 系统编程-学习笔记10--进程间通信--管道/FIFO/消息队列/
- 【Linux系统编程】线程同步与互斥:互斥锁
- <<Linux内核完全剖析 --基于0.12内核>> 学习笔记 第4章 80x86保护模式及其编程 4.1 80x86系统寄存器和系统指令
- Linux IO系统编程系列笔记之lseek()函数
- Linux IO 系统编程系列课程笔记之fcntl函数
- LINUX C系统编程学习笔记-----------进程通信(一)
- linux 系统编程-学习笔记2-文件I/O-open-read
- LINUX C系统编程学习笔记-----------文件编程
- linux 系统编程-学习笔记6-main函数/atexit函数/动态库、静态库/
- linux应用编程笔记(5)系统调用文件编程方法实现文件复制
- Linux-Unix系统编程手册学习笔记