线程同步
线程的主要优势在于,能够通过全局变量来共享信息。不过这种便捷的共享是有代价的:
必须确保多个线程不会修改统一变量,或者某一线程不会读取正在被其他线程修改的变量。
(某个线程在修改某个全局变量时 不允许其他线程修改或访问这个全局变量 确保修改或访问操作的原子性)
临界区是指访问某一共享资源的代码片段,并且这段代码的操作执行应为原子操作,也就是同时访问同一共享资源的情况(临界区代码段必须一口气执行完—原子性 其他线程不能中断这一执行 在执行完之前不允许其他线程运行相同的代码段),其他线程不允许中断该代码片段的执行(则这段代码就是临界区) 临界资源就是票 临界区就是对票访问和修改的代码段
线程同步:即当有一个线程在对共享资源的内存进行操作时,其他线程都不允许对这个内存地址进行操作(读和写都不允许) 都处于等待状态,直到该线程完成操作,其他线程才能对该内存地址进行操作。 A执行临界区代码时B,C线程都不能执行这段代码,当A执行这段代码时,B或C正好运行到这段代码,B或C只能等着,等A运行完后B或C才能运行这段代码, 多线程对于临界区代码的执行不再是并发的而是串行(一个执行完另个再执行)的。
多线程卖票,票的仓库是同一个但是有三个窗口买票,不能出现多个窗口卖的为同一张票。
互斥量(互斥锁)
为避免线程更新共享变量时出现问题,可以使用互斥量(mutex —- mutual exclusion)来确保同一时刻仅有一个线程可以访问某项共享资源。可已使用互斥量来保证对任意共享资源的原子访问。
互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。任何时刻至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁将可能阻塞线程或报错失败这取决于加锁时使用的方法。
一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一共享资源(可能由多个相关变量组成) 会使用不同的互斥量,每一线程在访问同一资源时将采用如下协议:
针对共享资源锁定互斥量
访问共享资源
对互斥量解锁
多个线程试图执行临界区代码,只有一个线程能够持有该互斥量(其他线程将遭到阻塞),即同一时刻只有一个线程能执行临界区代码
互斥量类型 pthread_mutex_t
/*多线程卖票,票的仓库是同一个总共有100张票 但是有三个窗口(子线程)买票,不能出现多个窗口卖的为同一张票,int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr)初始化互斥量mutex 需要初始化的互斥量变量 attr 要设置的互斥量属性restrict 为C语言关键字 是个修饰符被修饰的指针 不允许别的的指针变量得到此指针地址后 对那块内存(数据)进行操作比如 int *restrict a = xxx; int *b=a; b++ or *b = 1 or b--都不行都会报错保证了只有的被restrict修饰的指针能对其指向的数据进行操作!!!!int pthread_mutex_destroy(pthread_mutex_t *mutex)释放互斥量资源int pthread_mutex_lock(pthread_mutex_t *mutex)加锁 哪个线程调用了这个函数 那么这个线程就是传入的mutex这个互斥量的所有者如果这个锁已经被别的线程获得了 那么会导致阻塞,直到锁被另外的进程释放 获得这个锁后才能继续执行int pthread_mutex_trylock(pthread_mutex_t *mutex)尝试加锁 锁已经被别的线程获得了 也没关系是非阻塞的直接返回int pthread_mutex_unlock(pthread_mutex_t *mutex)解锁man手册中找不到pthread_mutex_init, pthread_mutex_destroy等APIsudo apt-get install glibc-docsudo apt-get install manpages-posix-dev*/#include <stdio.h>#include <pthread.h>#include <string.h>#include <unistd.h>int tickets = 1000; //仓库里的票是三个线程共有的资源 三个线程都要修改和访问 所以肯定要设置为全局变量pthread_mutex_t mutex; //全局互斥量void *sell_tick(void *arg);int main(){//初始化互斥量pthread_mutex_init(&mutex, NULL);// 创建3个子线程 主线程只用于管理系统资源pthread_t tid1,tid2, tid3;int ret = pthread_create(&tid1, NULL, sell_tick, NULL);if (ret != 0){printf("%s\n", strerror(ret));}ret = pthread_create(&tid2, NULL, sell_tick, NULL);if (ret != 0){printf("%s\n", strerror(ret));}ret = pthread_create(&tid3, NULL, sell_tick, NULL);if (ret != 0){printf("%s\n", strerror(ret));}//主线程等待子线程结束 回收子线程资源int *t_return;//返回值是二级指针 是因为callback的返回值类型被规定为一级指针void*pthread_join(tid1, (void **)&t_return); //阻塞回收子线程资源pthread_join(tid2, (void **)&t_return); //阻塞回收子线程资源pthread_join(tid3, (void **)&t_return); //阻塞回收子线程资源//也可以直接讲子线程状态设置为分离 当子线程结束系统回收子线程资源 不需要主线程来回收 join后无法分离// pthread_detach(tid1);// pthread_detach(tid2);// pthread_detach(tid3);//子线程的返回值保存在 t_return中printf("son thread return %d\n", *t_return);pthread_mutex_destroy(&mutex); //释放互斥量资源pthread_exit(NULL); //让主线程退出 当主线程退出时 不会影响其他正常运行的线程//主线程结束 pthread_exit下面的代码是不会执行的 即不会打印main thread exsitprintf("main thread exsit\n"); //不会执行//当主线程return 相当于进程exit所有子线程全部结束!!!return 0; //不会执行}void *sell_tick(void *arg){//卖票//多线程对于共享的资源(tickets)的访问和修改 一定要做线程同步//当某个线程在对共享资源进行访问或修改时 都不允许其他线程对共享资源做访问或修改 保证操作的原子性//临界区 三个线程同时在此代码段对tickets修改或访问while (1) //标准C中没有布尔 也就没有true{pthread_mutex_lock(&mutex); //加锁if (tickets > 0){printf("%ld 号线程 卖出了第%d张票\n", pthread_self(), tickets);tickets--;}else{pthread_mutex_unlock(&mutex);break;}pthread_mutex_unlock(&mutex); //解锁//加锁 买完一张票后 解锁 三个线程再重新争夺锁}//临界区return NULL;}
死锁
有时一个线程需要同时访问两个或更多不同的临界资源,而每个资源又都由不同的互斥量管理,当超过一个线程加锁同一组互斥量时就可能发生死锁。
两个或两个以上的线程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用它们都无法推进下去,此时称系统处于死锁状态或系统产生了死锁。
死锁的几种场景:
ABC三个线程
忘记释放锁
A拿到锁后,执行完临界区代码忘记释放锁,那么BC线程都无法执行临界区代码都阻塞在临界区入口拿锁的那行代码上(A执行结束后 再次运行到拿锁这句话 A也拿不到这个已经拥有的锁 也会阻塞)。
重复加同一个锁
pthread_mutex_lock(&mutex); //加锁(拿锁)
…
pthread_mutex_lock(&mutex); //加锁(拿锁)
….临界区代码
线程执行完第一条加锁后已经拿到mutex这个锁了,执行第二条加锁又是拿mutex这个锁,但是这个锁当前线程一直拿着还没释放,所以当前线程是拿不到这同一个锁的被阻塞在这第二次调用。 这两次拿锁执行可能在两个函数中,这种错误经常出现。
多线程多锁,抢占锁资源
虚线是获取锁失败
线程A访问资源1 先为其上了A锁
线程B访问资源1但是由于A拥有A锁 B无法获取A锁
线程B访问资源2为其上了B锁 (B在访问资源1后没拿到锁 也没被阻塞 说明用的是trylock)
在A释放A锁的语句前,A需要拿B锁但B拿着所以A无法拿到B锁。
在B释放B锁的语句前,B需要拿A锁但A拿着所以B无法拿到A锁。
两个线程谁都不让步导致死锁
#include <stdio.h>#include <pthread.h>#include <string.h>#include <unistd.h>//两个全局互斥量资源pthread_mutex_t mutex1;pthread_mutex_t mutex2;void *work1(void *arg);void *work2(void *arg);int main(){//初始化互斥量pthread_mutex_init(&mutex1, NULL);pthread_mutex_init(&mutex2, NULL);// 创建3个子线程 主线程只用于管理系统资源pthread_t tid1, tid2;int ret = pthread_create(&tid1, NULL, work1, NULL);if (ret != 0){printf("%s\n", strerror(ret));}ret = pthread_create(&tid2, NULL, work2, NULL);if (ret != 0){printf("%s\n", strerror(ret));}//主线程等待子线程结束 回收子线程资源int *t_return;//返回值是二级指针 是因为callback的返回值类型被规定为一级指针void*pthread_join(tid1, (void **)&t_return); //阻塞回收子线程资源pthread_join(tid2, (void **)&t_return); //阻塞回收子线程资源//子线程的返回值保存在 t_return中printf("son thread return %d\n", *t_return);pthread_mutex_destroy(&mutex1); //释放互斥量资源 不是解锁unlockpthread_mutex_destroy(&mutex2); //释放互斥量资源 不是解锁unlockpthread_exit(NULL); //让主线程退出 当主线程退出时 不会影响其他正常运行的线程return 0; //不会执行}void *work1(void *arg){pthread_mutex_lock(&mutex1);sleep(1);pthread_mutex_lock(&mutex2);printf("1...\n");pthread_mutex_unlock(&mutex2);pthread_mutex_unlock(&mutex1);return NULL;}void *work2(void *arg){pthread_mutex_lock(&mutex2);sleep(1);pthread_mutex_lock(&mutex1);printf("2...\n");pthread_mutex_unlock(&mutex1);pthread_mutex_unlock(&mutex2);return NULL;}//1,2拿到1,2锁后都睡眠了1秒 这个1秒保证了在两个线程执行到第二条加锁语句时线程1,2已经都拿到了锁//1拿到1锁,2拿到2锁 接下来第二条加锁语句1,2线程分别想拿对方的锁但因为锁都没有释放//导致两个线程分别都阻塞在各自的第二条加锁语句上 出现了死锁现象
读写锁
当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住,但是考虑一种情形,当前持有互斥锁的线程只是要读临界资源(共享资源),而同时有其他几个线程也想读取这个临界资源,但是由于互斥锁定排它性,所有其他线程都无法获取锁,也就无法读临界资源,但是实际上多个线程同时访问读共享资源并不会导致问题。
在对数据的读写操作中,更多的是读操作,写操作较少例如数据库数据的读写应用。为了满足当前能够允许多个线程读出,但只允许一个线程写入的需求,线程提供了读写锁来实现。
读写锁的特点:
如果有线程拿到锁在读数据,则允许其他线程执行读操作(读可以并发读),但不允许写操作。
如果有线程拿到锁在写数据,则其它线程不允许读和写。
写是独占的当多个线程在竞争锁时(锁被一个线程拿了 其他线程运行到拿锁函数阻塞了 到锁被释放 则线程开始竞争锁 在这种情况时) 写锁的优先级更高,优先将锁给要写的线程
读写锁还是一把锁,但是可以控制其行为
/*读写锁类型pthread_rwlock_tint pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);初始化读写锁rwlock 需要初始化的读写锁变量 attr 要设置的读写锁变量属性restrict 为C语言关键字 是个修饰符被修饰的指针 不允许别的的指针变量得到此指针地址后 对那块内存(数据)进行操作比如 int *restrict a = xxx; int *b=a; b++ or *b = 1 or b--都不行都会报错保证了只有的被restrict修饰的指针能对其指向的数据进行操作!!!!int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)释放互斥量资源int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)上读锁 如果此时有线程拿到写锁了 将会阻塞int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock)尝试上读锁 非阻塞int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)上写锁 会阻塞int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock)尝试上写锁 非阻塞int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)读锁和写锁的解锁 就是这一个函数案例:8个线程操作同一全局变量 3个线程不定时写全局变量 5个线程不定时地读这个全局变量*/#include <stdio.h>#include <pthread.h>#include <string.h>#include <unistd.h>int tickets = 0; //全局共享数据pthread_rwlock_t rwlock; //全局读写锁量void *writeNum(void *arg);void *readNum(void *arg);int main(){//初始化互斥量pthread_rwlock_init(&rwlock, NULL);// 创建3个子线程 主线程只用于管理系统资源pthread_t wtids[3], rtids[5]; //线程数组 3个写 5个读for (int i = 0; i < 3; i++){int ret = pthread_create(&wtids[i], NULL, writeNum, NULL);if (ret != 0){printf("%s\n", strerror(ret));}//不join了 直接将8个线程都分离 由系统回收pthread_detach(wtids[i]);}// for (int i = 0; i < 3; i++)// {// int ret = pthread_create(&rtids[i], NULL, readNum, NULL);// pthread_detach(rtids[i]);// }for (int i = 0; i < 5; i++){int ret = pthread_create(&rtids[i], NULL, readNum, NULL);if (ret != 0){printf("%s\n", strerror(ret));}//不join了 直接将8个线程都分离 由系统回收pthread_detach(rtids[i]);}//pthread_exit(NULL); //让主线程退出 当主线程退出时 不会影响其他正常运行的线程return 0; //不会执行}void *writeNum(void *arg){while (1){pthread_rwlock_wrlock(&rwlock);tickets++;printf("%ld write %d \n", pthread_self(), tickets);pthread_rwlock_unlock(&rwlock);usleep(1000); //1000微秒 1毫秒 一个写线程写过一次后 至少有1ms不参与写锁的竞争}return NULL;}void *readNum(void *arg){while (1){pthread_rwlock_rdlock(&rwlock); // 当有线程获得读锁后 其他线程读并发 写阻塞(写线程拿不到写锁)printf("%ld read %d \n", pthread_self(), tickets);pthread_rwlock_unlock(&rwlock);usleep(1000);}return NULL;}
