为什么使用线程锁
在多线程应用程序中,当多个线程共享相同的内存时,如同时访问一个变量时,需要确保每个线程看到一致的数据视图,即保证所有线程对数据的修改是一致的。
如下两种情况不存在不一致的问题:
每个线程使用的变量都是其他线程不会读取和修改的
变量是只读的
当一个线程在修改变量的值时,其他线程在读取这个变量时可能会得到一个不一致的值。
一个典型的例子是,在一个多线程程序中,两个及以上个线程对同一个变量i执行i++操作,结果得到的值并不如顺序执行所预期的那样。这就是线程间不同步的一个例子。
可以用程序修改变量值时所经历的三个步骤解释这个现象:
从内存单元读入寄存器
在寄存器中对变量操作(加/减1)
把新值写回到内存单元
不能预期以上三步骤在一个总线周期内完成,所以也就不能指望多线程程序如预期那样运行。
开始使用线程锁
互斥量
多线程程序中可能会存在数据不一致的情况,那么如何保证数据一致呢?可以考虑同一时间只有一个线程访问数据。互斥量(mutex)就是一把锁。
多个线程只有一把锁一个钥匙,谁上的锁就只有谁能开锁。当一个线程要访问一个共享变量时,先用锁把变量锁住,然后再操作,操作完了之后再释放掉锁,完成。
当另一个线程也要访问这个变量时,发现这个变量被锁住了,无法访问,它就会一直等待,直到锁没了,它再给这个变量上个锁,然后使用,使用完了释放锁,以此进行。
这个即使有多个线程同时访问这个变量,也好象是对这个变量的操作是顺序进行的。
互斥变量使用特定的数据类型:pthread_mutex_t,使用互斥量前要先初始化,使用的函数如下:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
简单的使用可以使用默认的属性初始化互斥量,函数的后一个参数设置为NULL即可。
对互斥量加锁解锁的函数如下:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
函数pthread_mutex_trylock会尝试对互斥量加锁,如果该互斥量已经被锁住,函数调用失败,返回EBUSY,否则加锁成功返回0,线程不会被阻塞。
避免死锁
产生死锁的情况较多,如一个线程对变量a加锁后,试图对变量b加锁,另一个线程对变量b加了锁,试图对a加锁,这时两个线程都不释放锁,加不会加锁成功,造成两个线程处于死锁状态。
可以在设计中避免死锁的发生。如使用 pthread_mutex_timedlock函数,该函数允许线程阻塞特定时间,如果加锁失败就会返回ETIMEDOUT。函数原型如下:
#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timesec *restrict tsptr);
读写锁
读写锁与互斥量类似,但它允许更高的并行性。
互斥量只有两种状态:锁住和未锁住,且一次只有一个线程可以对它加锁。
读写锁可以有三种状态:读模式下加锁状态、写模式下加锁状态和不加锁状态。一次只有一个线程可以占有写模式的读写锁,但多个线程可以同时占有读模式的读写锁。
读写锁非常适合于对数据结构读的次数远远大于写的情况。
与互斥量相比,读写锁在使用之前必须初始化,在释放它们底层的内存之前必须销毁。使用读写锁相关的函数如下:
#include <pthread.h>
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_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);
条件变量
条件变量与互斥量一直使用时,允许线程以无竞争的方式等待特定的条件发生。条件变量是线程可用的另一种同步机制。
条件本身是由互斥量保护的。线程在改变条件状态之前必须产生锁住互斥量,其他线程在获得互斥量之前不会到这种改变,因为互斥量必须在锁定以后才能计算条件。
在使用条件变量之前必须先对它初始化。释放条件变量底层的内存空间之前,对条件变量反初始化。使用的相关函数如下:
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
自旋锁
与互斥量类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等阻塞状态。当锁被持有的时间较短,而且线程不希望在重新调度上花费太多成本的情况下使用自旋锁。
当自旋锁用在非抢占式内核中时是非常有用的,除了提供互斥机制以外,它们会阻塞中断,这样中断处理程序就不会让系统陷入死锁状态,因为它需要获取已被加锁的自旋锁。
在用户层,自旋锁并不非常有用。很多互斥量的实现非常高效,甚至与采用自旋锁是同行效率的。
使用自旋锁的相关函数如下。
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
总结
在多线程编程中,线程同步是必不可少的考虑环节。线程不同步导致的问题很难排查,甚至bug复现的都很困难。
所以在多线程编程中,应该在设计中考虑到数据同步问题,保证变量视图的一致性,避免出现不一致性情况。