一、线程同步
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。“同”字从字面上容易理解为一起动作,其实不是,“同”字应是指协同、协助、互相配合。
如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A,A再继续操作。
在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。
注意:同一个进程内存的多个线程之间,除了栈内存是独立的,其他资源全部共享。
#include <stdio.h> #include <pthread.h> int num = 0; void* run(void* arg) { for(int i=0; i<1000000; i++) { // 加锁 num++; // 解锁 } } int main(int argc,const char* argv[]) { pthread_t tid1,tid2; pthread_create(&tid1,NULL,run,NULL); pthread_create(&tid2,NULL,run,NULL); pthread_join(tid1,NULL); pthread_join(tid2,NULL); printf("%d\n",num); }
线程A 线程A
读取
运算 读取
回写 运算
回写
二、互斥锁
注意:如果man手册中查不到这系列函数,可以安装以下内容: sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 功能:定义并初始化互斥锁 int pthread_mutex_init (pthread_mutex_t* mutex,const pthread_mutexattr_t* mutexattr); 功能:初始化一互斥锁,会被初始化为非锁定状态 int pthread_mutex_lock (pthread_mutex_t* mutex); 功能:加锁,当互斥锁已经是锁定状态时,调用者会阻塞,直到互斥被解开,当前线程才会加锁成功并返回。 int pthread_mutex_unlock (pthread_mutex_t* mutex); 功能:解锁,解锁后等待加锁的线程才能加锁成功。 int pthread_mutex_destroy (pthread_mutex_t* mutex); 功能:销毁锁 int pthread_mutex_trylock (pthread_mutex_t *__mutex) 功能:加测试锁,如果不加锁刚立即返回 int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout); 功能:倒计时加锁,如果超时还不加上则立即返回。 struct timespec{ time_t tv_sec; /* Seconds. */ long int tv_nsec; /* Nanoseconds.*/ 1秒= 1000000000 纳秒 };
#include <stdio.h> #include <pthread.h> /* 执行流程: 1、互斥锁被初始化为非锁定状态 2、线程1调用pthread_mutex_lock函数,立即返回,互斥量呈锁定状态; 3、线程2调用pthread_mutex_lock函数,阻塞等待; 4、线程1调用pthread_mutex_unlock函数,互斥量呈非锁定状态; 5、线程2被唤醒,从pthread_mutex_lock函数中返回,互斥量呈锁定状态 */ pthread_mutex_t mutex; //pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int num = 0; void* run(void* arg) { for(int i=0; i<1000000; i++) { pthread_mutex_lock(&mutex); num++; pthread_mutex_unlock(&mutex); } } int main(int argc,const char* argv[]) { pthread_mutex_init(&mutex,NULL); pthread_t pid1,pid2; pthread_create(&pid1,NULL,run,NULL); pthread_create(&pid2,NULL,run,NULL); pthread_join(pid1,NULL); pthread_join(pid2,NULL); pthread_mutex_destroy(&mutex); printf("%d\n",num); }
三、读写锁
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; 功能:定义并初始化读写锁 int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr); 功能:初始化读写锁 int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); 功能:加读锁,如果不能加则阻塞等待 int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); 功能:加写锁,如果不能加则阻塞等待 int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); 功能:解读写锁。 int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); 功能:尝试加读锁,如果不能加则立即返回 int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); 功能:尝试加写锁,如果不能加则立即返回 int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abstime); 功能:带倒计时加读锁,超时则立即返回 int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abstime); 功能:带倒计时加写锁,超时则立即返回 int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); 功能:销毁读写锁 使用读写锁的线程应根据后续的操作进行加锁,如果只对数据进行读取则只加读锁即可,只有对数据进行修改时才应该加写锁,与互斥锁的区别是,它能让只读的线程加上锁,使用原理与文件锁一样。 线程A 线程B 读锁 读锁 OK 读锁 写锁 NO 写锁 读锁 NO 写锁 写锁 NO
练习:使用读写锁来解决同步问题。
#include <stdio.h> #include <pthread.h> pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; int num = 0; void* run(void* arg) { for(int i=0; i<1000000; i++) { pthread_rwlock_wrlock(&rwlock); num++; pthread_rwlock_unlock(&rwlock); } } int main(int argc,const char* argv[]) { pthread_t tid1,tid2; pthread_create(&tid1,NULL,run,NULL); pthread_create(&tid2,NULL,run,NULL); pthread_join(tid1,NULL); pthread_join(tid2,NULL); pthread_rwlock_destroy(&rwlock); printf("%d\n",num); }
四、死锁问题
什么是死锁:
多个线程互相等待对方资源,在得到所需要的资源之前都不会释放自己的资源,然后造成循环等待的现象,称为死锁。
死锁产生四大必要条件:
1、资源互斥
2、占有且等待
3、资源不可剥夺
4、环路等待
以上四个条件缺一不可,只要有一个不满足就不能构成死锁。
#include <stdio.h> #include <unistd.h> #include <pthread.h> // 创建三个互斥锁并初始化 pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mutex3 = PTHREAD_MUTEX_INITIALIZER; void* run1(void* arg) { pthread_mutex_lock(&mutex1); usleep(100); pthread_mutex_lock(&mutex2); printf("没有构成死锁!!!\n"); pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex1); } void* run2(void* arg) { pthread_mutex_lock(&mutex2); usleep(100); pthread_mutex_lock(&mutex3); printf("没有构成死锁!!!\n"); pthread_mutex_unlock(&mutex3); pthread_mutex_unlock(&mutex2); } void* run3(void* arg) { pthread_mutex_lock(&mutex3); usleep(100); pthread_mutex_lock(&mutex1); printf("没有构成死锁!!!\n"); pthread_mutex_unlock(&mutex1); pthread_mutex_unlock(&mutex3); } int main(int argc,const char* argv[]) { // 创建三个线程 pthread_t tid1,tid2,tid3; pthread_create(&tid1,NULL,run1,NULL); pthread_create(&tid2,NULL,run2,NULL); pthread_create(&tid3,NULL,run3,NULL); // 主线程等待三个子线程结束 pthread_join(tid1,NULL); pthread_join(tid2,NULL); pthread_join(tid3,NULL); return 0; }
如休防止出现死锁:
构成死锁的四个条件只有一个不成立,就不会产生死锁了。
1、破坏互斥条件,让资源能够共享使用(准备多份)。
2、破坏占有且等待的条件,一次申请完成它所有需要的资源(把所有资源进行打包,用一把锁来代表,拿到这反锁就相当于拿到的所有资源),资源没有满足前不让它运行,一旦开始运行就一直归它所有, 缺点是系统资源会被浪费。
3、破坏不可剥夺的条件,当已经占有了一些资源,请求新的资源而获取不到,然后就释放已经获取到的资源,缺点是实现起来比较复杂,释放已经获取到的资源可能会造成前一阶段的工作浪费。
4、破坏循环等待的条件,采用顺序分配资源的方法,在系统中为资源进行编号,规定线程必须按照编号递增的顺序获取资源,缺点是资源必须相对稳定,这样就限制了资源的增加和减少。
#include <stdio.h> #include <unistd.h> #include <pthread.h> // 创建三个互斥锁并初始化 pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mutex3 = PTHREAD_MUTEX_INITIALIZER; void* run1(void* arg) { while(1) { pthread_mutex_lock(&mutex1); usleep(100); if(0 == pthread_mutex_trylock(&mutex2)) break; pthread_mutex_unlock(&mutex1); } printf("没有构成死锁!!!\n"); pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex1); } void* run2(void* arg) { while(1) { pthread_mutex_lock(&mutex2); usleep(100); if(0 == pthread_mutex_trylock(&mutex3)) break; pthread_mutex_unlock(&mutex2); } printf("没有构成死锁!!!\n"); pthread_mutex_unlock(&mutex3); pthread_mutex_unlock(&mutex2); } void* run3(void* arg) { while(1) { pthread_mutex_lock(&mutex3); usleep(100); if(0 == pthread_mutex_trylock(&mutex1)) break; pthread_mutex_unlock(&mutex3); } printf("没有构成死锁!!!\n"); pthread_mutex_unlock(&mutex1); pthread_mutex_unlock(&mutex3); } int main(int argc,const char* argv[]) { // 创建三个线程 pthread_t tid1,tid2,tid3; pthread_create(&tid1,NULL,run1,NULL); pthread_create(&tid2,NULL,run2,NULL); pthread_create(&tid3,NULL,run3,NULL); // 主线程等待三个子线程结束 pthread_join(tid1,NULL); pthread_join(tid2,NULL); pthread_join(tid3,NULL); return 0; }
检测死锁的方法:
总体思路:观察+分析
方法1:阅读代码,分析各线程的加锁步骤。
方法2:使用strace追踪程序的执行流程。
方法3:查看日志观察程序的业务执行过程。
方法4:使用gdb调试,查看各线程的执行情况。
1、把断点打在线程创建完毕后 2、run 3、info threads 查看所有线程 4、thread n 进程指定的线程 5、bt 查看线程堆栈信息 6、配合s/n单步调试
什么是死锁?
构成死锁的4个必要条件?
如何避免死锁?
如何判断程序是否陷入死锁?
五、原子操作
所谓的原子操作就是不可被拆分的操作,对于多线程对全局变量进行操作时,就再也不用再线程锁了,和pthread_mutex_t保护作用是一样的,也是线程安全的,有些编译器在使用时需要加-march=i686编译参数。
type __sync_fetch_and_add (type *ptr, type value); // + type __sync_fetch_and_sub (type *ptr, type value); // - type __sync_fetch_and_and (type *ptr, type value); // & type __sync_fetch_and_or (type *ptr, type value); // | type __sync_fetch_and_nand (type *ptr, type value); // ~ type __sync_fetch_and_xor (type *ptr, type value); // ^ 功能:以上操作返回的是*ptr的旧值 type __sync_add_and_fetch (type *ptr, type value); // + type __sync_sub_and_fetch (type *ptr, type value); // - type __sync_and_and_fetch (type *ptr, type value); // & type __sync_or_and_fetch (type *ptr, type value); // | type __sync_nand_and_fetch (type *ptr, type value); // ~ type __sync_xor_and_fetch (type *ptr, type value); // ^ 功能:以上操作返回的是*ptr与value计算后的值 type __sync_lock_test_and_set (type *ptr, type value); 功能:把value赋值给*ptr,并返回*ptr的旧值 __sync_lock_release(type *ptr); 功能:将*ptr赋值为0
#include <stdio.h> #include <pthread.h> int num = 0; void* run(void* arg) { for(int i=0; i<100000000; i++) { __sync_fetch_and_add(&num,1); } } int main(int argc,const char* argv[]) { pthread_t pid1,pid2; pthread_create(&pid1,NULL,run,NULL); pthread_create(&pid2,NULL,run,NULL); pthread_join(pid1,NULL); pthread_join(pid2,NULL); printf("%d\n",num); }
原子操作的优点:
1、速度贼快
2、不会产生死锁
原子操作的缺点:
1、该功能并不通用,有些编译器不支持。
2、type只能是整数相关的类型,浮点型和自定义类型无法使用。
练习1:
使用读写锁或互斥锁实现一个线程安全队列。
#include "queue.h" #include <stdlib.h> Node* create_node(TYPE data) { Node* node = malloc(sizeof(Node)); node->data = data; node->next = NULL; return node; } Queue* create_queue(void) { Queue* queue = malloc(sizeof(Queue)); pthread_rwlock_init(&queue->lock,NULL); queue->front = NULL; queue->rear = NULL; return queue; } bool empty_queue(Queue* queue) { pthread_rwlock_rdlock(&queue->lock); bool flag = NULL == queue->front; pthread_rwlock_unlock(&queue->lock); return flag; } void push_queue(Queue* queue,TYPE data) { Node* node = create_node(data); if(empty_queue(queue)) { pthread_rwlock_wrlock(&queue->lock); queue->front = node; queue->rear = node; } else { pthread_rwlock_wrlock(&queue->lock); queue->rear->next = node; queue->rear = node; } pthread_rwlock_unlock(&queue->lock); } bool pop_queue(Queue* queue) { if(empty_queue(queue)) return false; pthread_rwlock_wrlock(&queue->lock); Node* tmp = queue->front; queue->front = tmp->next; pthread_rwlock_unlock(&queue->lock); free(tmp); return true; } TYPE top_queue(Queue* queue) { pthread_rwlock_rdlock(&queue->lock); TYPE data = queue->front->data; pthread_rwlock_unlock(&queue->lock); return data; } void destroy_queue(Queue* queue) { while(!empty_queue(queue)) pop_queue(queue); pthread_rwlock_destroy(&queue->lock); free(queue); } int main(void) { Queue* queue = create_queue(); for(int i=0; i<10; i++) { push_queue(queue,i); printf("push %d\n",i); } while(!empty_queue(queue)) { printf("top %d\n",top_queue(queue)); pop_queue(queue); } }
练习2:
使用原子操作实现一个线程安全的无锁队列。
//queue->rear = (queue->rear+1)%queue->cap; if(queue->rear == queue->cap) { queue->rear = 0; } else { __sync_fetch_and_add(&queue->rear,1); } queue->front = (queue->front+1)%queue->cap; if(queue->front == queue->cap) { queue->front = 0; } else { __sync_fetch_and_add(&queue->front,1); }
六、生产者与消费者模型
生产者:生产数据的线程,这类的线程负责从用户端、客户端接收数据,然后把数据Push到存储中介。
消费者:负责消耗数据的线程,对生产者线程生产的数据进行(判断、筛选、使用、响应、存储)处理。
存储中介:也叫数据仓库,是生产者线程与消费者线程之间的数据缓冲区,用于平衡二者之间的生产速度与消耗速度不均衡的问题,通过缓冲区隔离生产者和消费者,与二者直连相比,避免相互等待,提高运行效率。
问题1:生产快于消费,缓冲区满,撑死。
解决方法:负责生产的线程通知负责消费的线程全速消费,然后进入休眠。
问题2:消费快于生产,缓冲区空,饿死。
解决方法:负责消费的线程通知负责生产的线程全速生产,然后进入休眠。
七、条件变量
条件变量是利用线程间共享的"全局变量"进行同步的一种机制,主要包括两个动作:
1、线程等待"条件变量的条件成立"而休眠;
2、等"条件成立"叫醒休眠的线程。
为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起,一般线程睡入条件变量,伴随着解锁动作,而线程从条件变量醒来时,伴随着加锁动作,如果加锁失败线程进入阻塞状态,而不是睡眠。
// 定义或创建条件变量 pthread_cond_t cond; // 初始化条件变量 int pthread_cond_init (pthread_cond_t* cond,const pthread_condattr_t* attr); //亦可pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 使调用线程睡入条件变量cond,同时释放互斥锁mutex int pthread_cond_wait (pthread_cond_t* cond,pthread_mutex_t* mutex); // 带倒计时的睡眠,时间到了会自动醒来 int pthread_cond_timedwait (pthread_cond_t* cond, pthread_mutex_t* mutex, const struct timespec* abstime); struct timespec { time_t tv_sec; // Seconds long tv_nsec; // Nanoseconds [0 - 999999999] }; // 从条件变量cond中叫醒一个线程,令其重新获得原先的互斥锁 int pthread_cond_signal (pthread_cond_t* cond); 注意:被唤出的线程此刻将从pthread_cond_wait函数中返回, 但如果该线程无法获得原先的锁,则会继续阻塞在加锁上。 // 从条件变量cond中唤醒所有线程 int pthread_cond_broadcast (pthread_cond_t* cond); // 销毁条件变量 int pthread_cond_destroy (pthread_cond_t* cond);
注意:使用互斥锁配合条件变量实现的生产者与消费者模型,能够平衡生产与消费的时间不协调,并且可以最大限度的节约运行资源。
#include <stdio.h> #include <unistd.h> #include <pthread.h> pthread_cond_t cond = PTHREAD_COND_INITIALIZER; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void* run(void* arg) { int index = 1; for(;;) { pthread_mutex_lock(&mutex); if(0 == index % 10) { printf("任务已完成,即将睡眠!\n"); pthread_cond_wait(&cond,&mutex); } printf("index = %d\n",index++); sleep(1); pthread_mutex_unlock(&mutex); } } int main(int argc,const char* argv[]) { pthread_t tid; pthread_create(&tid,NULL,run,NULL); printf("是否叫醒睡眠的线程?"); for(;;) { char cmd = getchar(); if('y' == cmd) { pthread_cond_signal(&cond); } } pthread_join(tid,NULL); return 0; }
八、信号量
多线程使用的信号量:
#include <semaphore.h> sem_t sem; int sem_init(sem_t *sem, int pshared, unsigned int value); 功能:给信号量设置初始值 pshared:信号量的使用范围 0 线程间使用 nonzero 进程之间使用 int sem_wait(sem_t *sem); 功能:信号量减1操作,如果信号量已经等于0,则阻塞 int sem_trywait(sem_t *sem); 功能:尝试对信号量减1操作,能减返回0成功,不能减返回-1失败,不会阻塞 int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); 功能:带倒计时的对信号减1操作,能减返回0成功,不能减超时返回-1失败,阻塞abs_timeout一段时间 int sem_post(sem_t *sem); 功能:对信号量执行加1操作 int sem_getvalue(sem_t *sem, int *sval); 功能:获取信号量的值 int sem_destroy(sem_t *sem); 功能:销毁信号量
多进程使用的信号量:
sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value); 功能:在内核创建一个信号量对象 name:信号量的名字 oflag: O_CREAT 不存在则创建信号量,存在则获取 O_EXCL 如果信号量已经存在,返回失败 mode:信号量的权限 value:信号量的初始值 sem_t *sem_open(const char *name, int oflag); 功能:获取信号,或相关属性 int sem_unlink(const char *name); 功能:删除信号量