1. 访问共享资源的代码区域称为临界区。自旋锁(spinlock)和互斥体(mutex)是保护内核临界区的两种基本机制。
2.互斥锁:pthread_mutex,属于sleep-waiting类型的锁。互斥量是实现最简单的锁类型,因此有一些教科书一般以互斥量为例对锁原语进行描述。互斥量的释放并不仅仅依赖于释放操作,还可以引入一个定时器属性。如果在释放操作执行前发生定时器超时,则互斥量也会释放代码块或共享存储区供其他线程访问。当有异常发生时,可使用try-finally语句来确保互斥量被释放。定时器状态或try-finally语句的使用可以避免产生死锁。
3.自旋锁:pin lock,属于busy-wait类型的锁。 旋转锁是一种非阻塞锁,由某个线程独占。采用旋转锁时,等待线程并不静态地阻塞在同步点,而是必须“旋转”,不断尝试直到最终获得该锁。旋转锁多用于多处理器系统中。这是因为,如果在单核处理器中采用旋转锁,当一个线程正在“旋转”时,将没有执行资源可供另一释放锁的线程使用。旋转锁适合于任何锁持有时间少于将一个线程阻塞和唤醒所需时间的场合。线程控制的变更,包括线程上下文的切换和线程数据结构的更新,可能比旋转锁需要更多的指令周期。旋转锁的持有时间应该限制在线程上下文切换时间的50%到100%之间(Kleiman,1996年)。在线程调用其他子系统时,线程不应持有旋转锁。对旋转锁的不当使用可能会导致线程饿死,因此需谨慎使用这种锁机制。旋转锁导致的饿死问题可使用排队技术来解决,即每个等待线程按照先进先出的顺序或者队列结构在一个独立的局部标识上进行旋转。自旋锁有在内核可抢占式或SMP的情况下才真正需要。
4.可见互斥锁和自旋锁有各自的应用场景:
1)如果要等待的时间较长,互斥体比自旋锁更合适。
2)因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。但自旋锁一直占用CPU,它在未获得锁的情况下,一直运行-自旋,所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。
3)如果临界区需要睡眠,只能使用互斥体,因为在获得自旋锁后进行调度、抢占以及在等待队列上睡眠都是非法的。
4)由于互斥体会在面临竞争的情况下将当前线程置于睡眠状态,因此,在中断处理函数中,只能使用自旋锁。
5. 实例说明:最近做的两个项目中都需要实现消息队列,一个消息队列只是简单的queue.insert和queue.pop,比较简单,没有什么业务逻辑,就使用了自旋锁;另外一个还需要进行队列的拼装和排序,业务逻辑比较复杂或耗时,就使用了互斥锁
线程的同步, 发生在多个线程共享相同内存的时候, 这时要保证每个线程在每个时刻看到的共享数据是一致的. 如果每个线程使用的变量都是其他线程不会使用的(read & write), 或者变量是只读的, 就不存在一致性问题. 但是, 如果两个或两个以上的线程可以read/write一个变量时, 就需要对线程进行同步, 以确保它们在访问该变量时, 不会得到无效的值, 同时也可以唯一地修改该变量并使它生效. 以上就是我们所说的线程同步. 线程同步有三种常用的机制: 互斥量(mutex), 读写锁(rwlock)和条件变量(cond). 互斥量有两种状态: lock和unlock, 它确保同一时间只有一个线程访问数据; 读写锁有三种状态: 读加锁, 写加锁, 不加锁, 只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁. 条件变量则给多个线程提供了一个会合的场所, 与互斥量一起使用时, 允许线程以无竞争的方式等待特定条件的发生.
互斥量 互斥量从本质上说就是一把锁, 提供对共享资源的保护访问. 1. 初始化: 在Linux下, 线程的互斥量数据类型是pthread_mutex_t. 在使用前, 要对它进行初始化: 对于静态分配的互斥量, 可以把它设置为PTHREAD_MUTEX_INITIALIZER, 或者调用pthread_mutex_init. 对于动态分配的互斥量, 在申请内存(malloc)之后, 通过pthread_mutex_init进行初始化, 并且在释放内存(free)前需要调用pthread_mutex_destroy.
2. 互斥操作: 对共享资源的访问, 要对互斥量进行加锁, 如果互斥量已经上了锁, 调用线程会阻塞, 直到互斥量被解锁. 在完成了对共享资源的访问后, 要对互斥量进行解锁. 首先说一下加锁函数:
再说一下解所函数:
3. 死锁: 死锁主要发生在有多个依赖锁存在时, 会在一个线程试图以与另一个线程相反顺序锁住互斥量时发生. 如何避免死锁是使用互斥量应该格外注意的东西. 总体来讲, 有几个不成文的基本原则:
读写锁
在线程同步系列的第一篇文章里已经说过, 读写锁是因为有3种状态, 所以可以有更高的并行性. 1. 特性: 一次只有一个线程可以占有写模式的读写锁, 但是可以有多个线程同时占有读模式的读写锁. 正是因为这个特性,
2. 适用性: 读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁. 3. 初始化和销毁:
#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); 成功则返回0, 出错则返回错误编号. 同互斥量以上, 在释放读写锁占用的内存之前, 需要先通过pthread_rwlock_destroy对读写锁进行清理工作, 释放由init分配的资源.
4. 读和写:
#include <pthread.h>
这3个函数分别实现获取读锁, 获取写锁和释放锁的操作. 获取锁的两个函数是阻塞操作, 同样, 非阻塞的函数为:
int pthread_rwlock_rdlock(pthread_rwlock_t * rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t * rwlock); int pthread_rwlock_unlock(pthread_rwlock_t * rwlock); 成功则返回0, 出错则返回错误编号.
#include <pthread.h>
非阻塞的获取锁操作, 如果可以获取则返回0, 否则返回错误的EBUSY.int pthread_rwlock_tryrdlock(pthread_rwlock_t * rwlock); int pthread_rwlock_trywrlock(pthread_rwlock_t * rwlock); 成功则返回0, 出错则返回错误编号. 条件变量 条件变量分为两部分: 条件和变量. 条件本身是由互斥量保护的. 线程在改变条件状态前先要锁住互斥量. 1. 初始化: 条件变量采用的数据类型是pthread_cond_t, 在使用之前必须要进行初始化, 这包括两种方式:
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t * restrict attr); int pthread_cond_destroy(pthread_cond_t * cond); 成功则返回0, 出错则返回错误编号. 当pthread_cond_init的attr参数为NULL时, 会创建一个默认属性的条件变量; 非默认情况以后讨论.
2. 等待条件:
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t * restric mutex); int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec * restrict timeout); 成功则返回0, 出错则返回错误编号. 这两个函数分别是阻塞等待和超时等待. 等待条件函数等待条件变为真, 传递给pthread_cond_wait的互斥量对条件进行保护, 调用者把锁住的互斥量传递给函数. 函数把调用线程放到等待条件的线程列表上, 然后对互斥量解锁, 这两个操作是原子的. 这样便关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道, 这样线程就不会错过条件的任何变化. 当pthread_cond_wait返回时, 互斥量再次被锁住.
3. 通知条件:
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t * cond); int pthread_cond_broadcast(pthread_cond_t * cond); 成功则返回0, 出错则返回错误编号. 这两个函数用于通知线程条件已经满足. 调用这两个函数, 也称向线程或条件发送信号. 必须注意, 一定要在改变条件状态以后再给线程发送信号. |