线程同步
同属于一个进程的不同线程是共享内存的,因而在执行过程中需要考虑数据的一致性。
假设:进程有一变量i=0,线程A执行i++,线程B执行i++,那么最终i的取值是多少呢?似乎一定是i=2;其实不然,如果没有考虑线程同步,i的取值可能是1.我们先考虑自加操作的过程:a,首先将内存中i的值copy到寄存器;b,对寄存器中i的copy进行自加;c,将寄存器中自加的结果返回到内存中。回到例子,如果线程A执行完abc三个步骤,线程B在执行者三个步骤,那么结果就应该为2.但是自加不是原子操作,假如执行过程是a(A), a(B), b(A),b(B),c(A),c(B)。那么执行的过程就是AB都把i原值i=0copy到寄存器,进行自加,得到结果1,然后分别有把值1赋值到内存中的i,导致i结果为1.
之所以可能出现上面非期望的结果(i=1而不是i=2),是因为i++不是原子操作。为了解决这个问题,引入互斥锁。
互斥锁 Mutexes
互斥锁通过pthreads的互斥接口来保证数据在某一时间里只能被一个线程访问。
互斥锁的使用过程大致是:1)访问共享资源前加锁,2)访问共享资源,3)访问加速后解锁。(在这个过程中互斥量扮演一个锁的角色)
互斥量使用结构体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);
Both return: 0 if OK, error number on failure
动态创建的互斥量mutex才需要使用
pthread_mutex_destroy,
静态创建互斥量时,可以直接mutex=
PTHREAD_MUTEX_INITIALIZER 进行初始化,而不调用
pthread_mutex_init
函数。
参数attr表示互斥量的属性。
加锁与解锁:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict tsptr);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
All return: 0 if OK, error number on failure
使用
pthread_mutex_lock
时,如果互斥量已经被其他线程加锁,那么会阻塞,知道该互斥量被解锁再对互斥量加锁。
使用
pthread_mutex_trylock
时,如果互斥量已经被其他线程加锁
,则函数返回错误。如果未被加锁,那么就对互斥量加锁。
使用
pthread_mutex_timedlock
时,尝试加锁,阻塞(最多阻塞到时间点tsptr),如果在tsptr还无法加锁,则返回错误。 关于tsptr,它是一个时间点,比如我们要等待3min,则结果tsptr应该是当前时间加上3min。
死锁与避免死锁
产生死锁的几种情况:
1)线程对同一互斥量加锁两次,类似下面的情况:
pthread_mutex_lock(mutex);
pthread_mutex_lock(mutex);
pthread_mutex_unlock(mutex);
pthread_mutex_unlock(mutex);
在第二行发生阻塞,死锁。
2)多个互斥量,且不同线程各自锁住一个互斥量,并都在请求另一个互斥量时阻塞:
//thread A
pthread_mutex_lock(mutex1);
pthread_mutex_lock(mutex2);
pthread_mutex_unlock(mutex2);
pthread_mutex_unlock(mutex1);
//thread B
pthread_mutex_lock(mutex2);
pthread_mutex_lock(mutex1);
pthread_mutex_unlock(mutex1);
pthread_mutex_unlock(mutex2);
A锁住mutex1,在请求mutex2时,B已经锁住了mutex2. 而B在请求mutex1时,又被A锁住了。
避免死锁
对于上面两种死锁情况,第一种是比较容易避免的,因为大多数情况下不会对同一个互斥量加锁两次,产生该类错误一般是程序员问题。
针对第二种情况, 我们通过要求同一进程的各个进程都对所有的互斥量以相同的顺序进行加锁,来避免产生死锁。也就是对于任何一个线程
pthread_mutex_lock
(
mutex1
);
一定 在
pthread_mutex_lock
(
mutex2
);
之前。
一般我们使用一个散列列表锁来帮助我们实现,类似:
struct foo {
int f_count;
pthread_mutex_t f_lock;
int f_id;
/* ... more stuff here ... */
};
读写锁
读写锁的作用是针对不同方式的共享资源访问,提供了更高的并行性。
关键点:
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);
Both return: 0 if OK, error number on failure
加锁与解锁:
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
All return: 0 if OK, error number on failure
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
Both return: 0 if OK, error number on failure
#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrictrwlock,const struct timespec*restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrictrwlock,const struct timespec *restric ttsptr);
读写锁的接口用法基本与互斥锁一样,知识其逻辑功能分读和写两种情况而已。
条件变量
条件变量允许线程以无竞争的方式等待某个条件的发生。
条件变量用结构体:pthread_cond_t表示。
条件变量的接口函数:
#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);
Both return: 0 if OK, error number on failure
这是初始化和去初始化的过程。同样可以使用PTHREAD_COND_INITIALIZER对静态创建的条件变量进行初始化。
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,
const
struct
timespec
*
restrict tsptr
);
pthread_cond_wait
是理解条件变量的关键。该函数会发生下面过程
1)解锁互斥量mutex
2)阻塞等待条件变量唤醒,关于唤醒函数后面会介绍
3)加锁互斥量mutex。
关于
pthread_cond_timedwait
注意
tsptr
是表示一个时间点,比如等待3min中,则tsptr的值应该是当前时间加上3min
唤醒函数:
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
Both return: 0 if OK, error number on failure
Example:
#include <pthread.h>
struct msg {
struct msg *m_next;
/* ... more stuff here ... */
};
struct msg *workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
void
process_msg(void)
{
struct msg *mp;
for (;;) {
pthread_mutex_lock(&qlock);
while (workq == NULL)
pthread_cond_wait(&qready, &qlock);
mp = workq;
workq = mp->m_next;
pthread_mutex_unlock(&qlock);
/* now process the message mp */
}
}
void
enqueue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);
}
Spin lock 自旋锁
自旋锁与互斥锁类似。只是起阻塞的方式不同。对于互斥锁而言,如果互斥锁被其他线程锁住了,那么本线程要加锁时通过睡眠方式等待。而对于自旋锁,它加锁时,如果自旋锁被其它线程加锁,则本线程以循环方式等待,也就是在那里一直循环。
自旋锁的作用是提高加锁的效率。它一般用于使用者保持锁时间较短的情况。也就是说加锁和解锁时间很短的情况。
关于自旋锁的接口:
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock,intpshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
Both return: 0 if OK, error number on failure
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
All return: 0 if OK, error number on failure
Barriers 计数锁
计数锁用
pthread_barrier_
表示,它充当栏杆,阻塞线程,直到指定数目的线程到齐。
接口:
#include <pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrictbarrier,const pthread_barrierattr_t *restrict attr,unsigned int count);
int pthread_barrier_destroy(pthread_barrier_t *barrier);
Both return: 0 if OK, error number on failure
参数count用于指定要等待的线程数目。
#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);
Returns: 0 orPTHREAD_BARRIER_SERIAL_THREADif OK, error number on failure
pthread_barrier_wait 用来是线程进入等待,然后检测计数锁是否满足释放条件了。
具体通俗的例子,8个人参加赛跑(count=0),每个运动员到起跑线 执行
pthread_barrier_wait
等待其它运动员,并通知裁判(相当于内核)我到了,裁判在8个运动员都到了时,开始比赛
pthread_barrier_wait
返回。