线程同步主要是指,在多个线程共同操作同一块内存时,保证线程的执行是顺序的,不会因为抢夺时间片而互相串扰导致与预期结果不符合。
建立起临界区的概念,即我要保证临界区内的代码是一气呵成执行的,不被其他线程所干扰。线程同步主要有4个重要的手段:互斥锁mutex,读写锁rw_lock,条件变量cond,信号量sem。
互斥锁mutex
在临界区的上界进行上锁pthread_mutex_lock,在临界区的下界进行解锁pthread_mutex_unlock。互斥锁可以看做是一个卫生间,别人进去锁上门,那么外边的人想要得到临界区的资源,就必须阻塞等待。但还有一种情况,就是看到卫生间上锁,他不阻塞,直接离开去做自己接下来的事情,这种操作等于pthread_mutex_trylock(),试一下不行就走,返回一个上锁的状态信息。
互斥锁的优点是容易理解,上锁解锁对称代码明晰,缺点是容易造成死锁。通常造成死锁的情况有以下几种
- 某线程在进入临界区后进行上锁。该线程在上锁后进入临界区,不小心再次上锁,这时会造成该线程的堵塞,但又无法释放锁资源,造成死锁。多由对别的函数的调用中有对锁的使用造成;
- 忘记解锁,即上锁后没有对应的解锁操作;
- 线程1对A资源上锁,线程2对B资源上锁,此时线程1访问B,线程2访问A,二者都阻塞且无法释放资源造成死锁;
读写锁
读写锁和互斥锁的区别在于允许对于读操作的并行,适用于读操作很多的情况。读写锁其实是同一个锁,pthread_rwlock_t rwlock,但有两种上锁的情况。一种是上读锁int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);一种是上写锁int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)。如果两个线程分别上读锁和写锁,写锁进入临界区的优先级更高,而读锁会被阻塞。
如果调用读锁rdlock,如果读写锁是打开的,则上锁成功;检查到读锁,那上锁依旧会成功,因为读锁是共享的;如果检查到是写锁,那么上锁失败,处于阻塞;
如果调用wrlock,如果读写锁是打开的,则上锁成功;上锁检查无论是读锁还是写锁,都会阻塞;
读写锁同样有trylock,与上述一致;
条件变量
严格来说条件变量并不是控制线程同步,而是阻塞线程,单使用条件变量无法实现线程同步,需要配合互斥锁。一般和互斥锁一起用于实现生产者-消费者模型。
//线程阻塞函数,哪个线程调用这个函数,就会阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
通过函数原型可以看出,该函数在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现数共享资源的数据混乱。该函数会对这个互斥锁做以下几件事情:
1.在阻塞线程时候,如果线程已经对互斥锁mutex上锁,那么会将这把锁打开,这样做是为了避免死锁;
2.当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个mutex互斥锁锁上,继续向下访问临界区;
信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 资源释放, 线程销毁之后调用这个函数即可
// 参数 sem 就是 sem_init() 的第一个参数
// 第二个参数为0表示线程同步,非0为进程同步
// 第三个参数为初始资源数
一个sem_wait用于资源-1,资源<0则阻塞;一个sem_post用于资源+1