一、同步
1.1 线程不同步会产生的问题
如下代码和结果所示,当线程没有同步时,多个线程抢占CPU资源,可能导致各种问题的发生。
/* 使用多线程实现卖票: 有三个窗口,一共是100张票 */ #include<stdio.h> #include<string.h> #include<unistd.h> #include <pthread.h> int tickets=0; void* saletic(void*arg){ //卖票 while(tickets<100){ usleep(6000); printf("%ld 正在卖第%d张门票\n", pthread_self(), tickets); ++tickets; } return NULL; } int main(){ //创建3个子线程 pthread_t tid1; pthread_t tid2; pthread_t tid3; pthread_create(&tid1, NULL, saletic, NULL); pthread_create(&tid2, NULL, saletic, NULL); pthread_create(&tid3, NULL, saletic, NULL); //回收子线程,阻塞的函数 // pthread_join(tid1,NULL); // pthread_join(tid2,NULL); // pthread_join(tid3,NULL); //或者设置线程分离 pthread_detach(tid1); pthread_detach(tid2); pthread_detach(tid3); //退出主线程 pthread_exit(NULL); return 0; }
1.2 线程同步概念
线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正在由其他线程修改的变量。临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作,也就是同时访问同一共享资源的其他线程不应中断该片段的执行。
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作,而其他线程则处于等待状态。线程同步会降低线程的并发效率,但是对于保证数据的安全性,是必须的。
二、锁
2.1互斥锁(量)概念
为避免线程更新共享变量时出现问题,可以使用互斥量(mutex是mutual exclusion的缩写)来确保同时仅有一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共享资源的原子访问。
互斥量有两种状态∶已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。
一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量,每一线程在访问同一资源时将采用如下协议︰
--针对共享资源锁定胡互斥量
--访问共享锁
--对互斥量解锁
2.2 互斥锁相关函数及同步代码
/* 互斥量的类型pthread mutex_t int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr) ; 功能:初始化互斥量 参数: -mutex:需要初始化的互斥量变量 -attr:互斥量相关属性,NULL -restrict:C语言修饰符,被修饰的指针不能由另外的指针进行操作 int pthread_mutex_destroy (pthread _mutex_t *mutex); 功能:释放互斥量 参数: -mutex:互斥量 int pthread_mutex_lock (pthread _mutex_t *mutex) ; 功能:加锁、阻塞,若有一个线程加锁,其他线程加锁等待 int pthread_mutex_trylock (pthread_mutex_t *mutex); 功能:尝试加锁,如果失败不会阻塞,会直接返回 int pthread_mutex_unlock (pthread_mutex_t *mutex); 功能:解锁 */ #include<stdio.h> #include<string.h> #include<unistd.h> #include <pthread.h> int tickets=0; //创建一个互斥量 pthread_mutex_t mutex; void* saletic(void*arg){ //卖票 while(1){ //加锁 pthread_mutex_lock(&mutex); if(tickets<1000){ usleep(60); printf("%ld 正在卖第%d张门票\n", pthread_self(), tickets); ++tickets; } else { //解锁 pthread_mutex_unlock(&mutex); break; } //解锁 pthread_mutex_unlock(&mutex); } return NULL; } int main(){ //初始化互斥量 pthread_mutex_init(&mutex,NULL); //创建3个子线程 pthread_t tid1; pthread_t tid2; pthread_t tid3; pthread_create(&tid1, NULL, saletic, NULL); pthread_create(&tid2, NULL, saletic, NULL); pthread_create(&tid3, NULL, saletic, NULL); //回收子线程,阻塞的函数 // pthread_join(tid1,NULL); // pthread_join(tid2,NULL); // pthread_join(tid3,NULL); //或者设置线程分离 pthread_detach(tid1); pthread_detach(tid2); pthread_detach(tid3); //退出主线程 pthread_exit(NULL); pthread_mutex_destroy(&mutex); return 0; }
2.3 死锁
有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
可能产生死锁的场景:1.忘记释放锁,2.重复加锁(相同的锁),3.多线程多锁,抢占锁资源。下图为多线程多锁的场景
以下是多线程多锁的死锁实例代码:
/* 死锁 */ #include<stdio.h> #include<string.h> #include<unistd.h> #include <pthread.h> int tickets=0; //创建两个互斥量 pthread_mutex_t mutex1; pthread_mutex_t mutex2; void* workA(void*arg){ //加锁1 pthread_mutex_lock(&mutex1); sleep(1); //加锁2 pthread_mutex_lock(&mutex2); printf("work A.......\n"); //解锁2 pthread_mutex_unlock(&mutex2); //解锁1 pthread_mutex_unlock(&mutex1); return NULL; } void* workB(void*arg){ //加锁1 pthread_mutex_lock(&mutex2); sleep(1); //加锁2 pthread_mutex_lock(&mutex1); printf("work B.......\n"); //解锁2 pthread_mutex_unlock(&mutex1); //解锁1 pthread_mutex_unlock(&mutex2); return NULL; } int main(){ //初始化两个互斥量 pthread_mutex_init(&mutex1,NULL); pthread_mutex_init(&mutex2,NULL); //创建2个子线程 pthread_t tid1; pthread_t tid2; pthread_create(&tid1, NULL, workA, NULL); pthread_create(&tid2, NULL, workB, NULL); //设置线程分离 pthread_detach(tid1); pthread_detach(tid2); //退出主线程 pthread_exit(NULL); //释放锁 pthread_mutex_destroy(&mutex1); pthread_mutex_destroy(&mutex2); return 0; }
2.4 读写锁
当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。在对数据的读写操作中,更多的是读操作,写操作较少,例如对数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
读写锁有以下特点:
----如果有其他线程读数据,则允许其他线程执行读操作,但不允许写操作。
----如果有其他线程写数据,则其他线程都不允许读、写操作。
----写是独占的,写的优先级高。
以下代码为读写锁案例:
/* 读写锁的类型 pthread_rwlock_t int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t * restrict attr) ; 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<string.h> #include<unistd.h> #include <pthread.h> //创建一个共享数据 int num=1; pthread_rwlock_t rwlock; void* writeNum(void*arg){ while(1){ pthread_rwlock_wrlock(&rwlock); num++; printf("++++write id: %ld , num : %d\n", pthread_self(), num); pthread_rwlock_unlock(&rwlock); usleep(100); } return NULL; } void* readNum(void*arg){ while(1){ pthread_rwlock_rdlock(&rwlock); printf("=====read id: %ld , num : %d\n", pthread_self(), num); pthread_rwlock_unlock(&rwlock); usleep(100); } return NULL; } int main(){ pthread_rwlock_init(&rwlock,NULL); //3个写线程 pthread_t wtids[3]; //5个读线程 pthread_t rtids[5]; for(int i=0;i<3;++i){ pthread_create(&wtids[i], NULL, writeNum, NULL); } for(int i=0;i<5;++i){ pthread_create(&rtids[i], NULL, readNum, NULL); } //设置线程分离 for(int i=0;i<3;++i){ pthread_detach(wtids[i]); } for(int i=0;i<5;++i){ pthread_detach(rtids[i]); } pthread_rwlock_destroy(&rwlock); pthread_exit(NULL); return 0; }