目录
Linux 线程间共享进程的地址空间和资源,于是线程间就可能因为竞争这些资源而产生问题。Linux线程由多种同步方式来解决这类竞争问题,下面我们将介绍最常用的几种:互斥锁,读写锁,条件变量,自旋锁,屏障。Linux 线程间共享进程的地址空间和资源,于是线程间就可能因为竞争这些资源而产生问题。Linux线程由多种同步方式来解决这类竞争问题,下面我们将介绍最常用的几种:互斥锁,读写锁,条件变量,自旋锁,屏障。
5.1 互斥锁
互斥锁类似于进程中的二值信号量,用来保证同一时间内某个资源只能被一个线程占用。使用方式就是:线程在使用某个资源前对其加锁,后续企图使用这个资源的线程阻塞;当前线程使用完后释放资源,第一个开始阻塞的线程占用该资源,以此类推。Pthread线程库中提供了一组API来完成互斥锁的操作。
5.1.1 初始化和销毁
互斥锁的初始化有静态初始化和动态初始化两种。使用动态初始化时,在内存释放前需要先销毁该互斥锁。互斥变量用pthread_mutex_t表示,静态初始化就是将其设置成PTHREAD_MUTEX_INITIALIZER。动态初始化和销毁可以由下面的函数完成:
#inlcude<pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t * mutex);
//执行成功返回0,失败返回错误编号
5.1.2 Lock和Unlock
#inlcude<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);
//执行成功返回0,失败返回错误编号
Trylock和 lock的区别在于lock函数调用lock函数的线程会被阻塞,但是调用trylock的函数如果申请不到资源会直接返回EBASY。
5.1.3 互斥量属性
在初始化互斥锁前,我们还可以指定互斥锁的属性,主要有三种,实际上这三种属性是也是线程同步属性的共性,它们分别是线程共享属性,健壮性属性,类型属性。因为Linux系统只支持第三种类型属性,所以这里我们只详细介绍下类型属性。
在设置互斥锁属性之前,我们先要对一个属性对象进行初始化,使用完之后需要反初始化:
#inlcude<pthread.h>
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t * mutex);
//执行成功返回0,失败返回错误编号
- 进程共享属性
前面有介绍过进程可以通过共享内存的方式进行通信。在共享段中的数据,我们也可以通过互斥量来保证同步。
- PTHREAD_PROCESS_PRIVATE:只能实现多一个进程中多个线程之间的同步;
- PTHREAD_PROCESS_SHARED:通过在共享段中创建互斥量,可以实现多个进程中的线程实现同步。
共享属性可以通过下面的一组函数去设置和获取:
#inlcude<pthread.h>
int pthread_mutexattr_getpshared(cosnt pthread_mutexattr_t *attr, int *restrict pshared);
int pthread_mutexattr_setpshared(cosnt pthread_mutexattr_t *attr, int restrict pshared);
//执行成功返回0,失败返回错误编号
2. 健壮属性
线程健壮属性也与多进程间同步有关。当持有互斥量的进程突然终止时,其它进程中等待该互斥量而阻塞的线程会是什么行为呢?健壮属性就是用来处理这种情况:
- PTHREAD_MUTEX_STALLED: 持有互斥量进程终止时,不采取任何行为,这种情况下再去使用互斥量的行为是未定义的,等待该互斥量的线程可能会被有效拖住;
- PTHREAD_MUTEX_ROBUST: 持有互斥量进程终止时,其它进程再试图去lock时会直接返回EOWNERREAD,这样我们就可以针对这种情况去对这个互斥量做恢复状态的操作。
我们可以用下面的这组函数来设置健壮属性:
#inlcude<pthread.h>
int pthread_mutexattr_getpRobust(cosnt pthread_mutexattr_t *attr, int *restrict robust);
int pthread_mutexattr_setpRobust(cosnt pthread_mutexattr_t *attr, int restrict robust);
//执行成功返回0,失败返回错误编号
设置可Robust属性,当线程lock时接收到了EOWNERREAD返回值,我们就可以知道是由于之前上锁的进程异常终止而没有unlocl了。此时我们可以在本线程的unlock前 调用以下的函数,此后这个互斥量就又可以正常使用了。
#inlcude<pthread.h>
int pthread_mutex_consistent(pthread_mutex_t * mutex);
//执行成功返回0,失败返回错误编号
3. 类型属性
互斥量的类型属性主要是针对以下几种情况:同一线程对同一互斥锁没有解锁前重新加锁;线程对另一个线程加锁的互斥量进行解锁(不占用解锁);线程对已经解锁的互斥量进行解锁。我们先来看下有几种类型属性:
- PTHREAD_MUTEX_NORMAL:标准类型,不做任何特殊的错误检查或这死锁检测;
- PTHREAD_MUTEX_ERRORCHECK: 提供错误检查
- PTHREAD_MUTEXT_RECURSIVE:允许同一线程对同一互斥量递归加锁,当然也需要相应次数的解锁后,该互斥锁才算释放;
- PTHREAD_MUTEX_DEFAULT:不同的操作系统可以将其映射成上述三种之一,linux下等同于NORMAL类型。
同样的,Pthread线程库对设置和获取类型属性也定义了一组函数:
#inlcude<pthread.h>
int pthread_mutexattr_getptype(cosnt pthread_mutexattr_t *attr, int *restrict type);
int pthread_mutexattr_settype(cosnt pthread_mutexattr_t *attr, int restrict type);
//执行成功返回0,失败返回错误编号
有些讲解中有提到健壮性和类型信息,在这些宏定义后都含有_np的字样,那是glibc2.4引入的,现在已经基本上废弃不用了。
5.1.4 例程
本节中我们再来简单改编下APUE书中图12-8的程序示例,该示例使用递归类型的互斥量保证了线程间数据的同步。这个例程是在main函数中分别设置两个10s 和 5s 的定时器,让两个线程分别等待10s和5s后执行入口函数。
需要注意的是:在mian函数中执行完创建线程的函数后,一定要有等待线程执行完之后的操作,否则,进程一旦执行完先退出,线程会自动退出,无法再执行。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include <pthread.h>
#include <time.h>
#include <sys/time.h>
#include<unistd.h>
#define err_exit(err,string) printf("%d %s\n",err,string);
int makethread(void *(*fn)(void *), void *arg)
{
int err;
pthread_t tid;
pthread_attr_t attr;
err = pthread_attr_init(&attr);
if(err != 0)
{
printf("make thread init attr fail\n");
return err;
}
err = pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
if(err != 0)
{
printf("make thread init set detach fail\n");
return err;
}
err = pthread_create(&tid, &attr, fn, arg);
if(err != 0)
{
printf("make thread creat thread fail\n");
return err;
}
pthread_attr_destroy(&attr);
return err;
}
struct to_info {
void (*to_fn)(void *); /* function */
void *to_arg; /* argument */
struct timespec to_wait; /* time to wait */
};
#define SECTONSEC 1000000000 /* seconds to nanoseconds */
#if !defined(CLOCK_REALTIME) || defined(BSD)
#define clock_nanosleep(ID, FL, REQ, REM) nanosleep((REQ), (REM))
#endif
#ifndef CLOCK_REALTIME
#define CLOCK_REALTIME 0
#define USECTONSEC 1000 /* microseconds to nanoseconds */
void
clock_gettime(int id, struct timespec *tsp)
{
struct timeval tv;
gettimeofday(&tv, NULL);
tsp->tv_sec = tv.tv_sec;
tsp->tv_nsec = tv.tv_usec * USECTONSEC;
}
#endif
void *
timeout_helper(void *arg)
{
struct to_info *tip;
tip = (struct to_info *)arg;
clock_nanosleep(CLOCK_REALTIME, 0, &tip->to_wait, NULL);
(*tip->to_fn)(tip->to_arg);
free(arg);
return(0);
}
void
timeout(const struct timespec *when, void (*func)(void *), void *arg)
{
struct timespec now;
struct to_info *tip;
int err;
clock_gettime(CLOCK_REALTIME, &now);
if ((when->tv_sec > now.tv_sec) ||
(when->tv_sec == now.tv_sec && when->tv_nsec > now.tv_nsec)) {
tip = malloc(sizeof(struct to_info));
if (tip != NULL) {
tip->to_fn = func;
tip->to_arg = arg;
tip->to_wait.tv_sec = when->tv_sec - now.tv_sec;
if (when->tv_nsec >= now.tv_nsec) {
tip->to_wait.tv_nsec = when->tv_nsec - now.tv_nsec;
} else {
tip->to_wait.tv_sec--;
tip->to_wait.tv_nsec = SECTONSEC - now.tv_nsec +
when->tv_nsec;
}
err = makethread(timeout_helper, (void *)tip);
if (err == 0)
return;
else
free(tip);
}
}
/*
* We get here if (a) when <= now, or (b) malloc fails, or
* (c) we can't make a thread, so we just call the function now.
*/
printf("Prepare to excute\n");
(*func)(arg);
}
pthread_mutexattr_t attr;
pthread_mutex_t mutex;
void
func1(void *arg)
{
pthread_mutex_lock(&mutex);
printf("function 1 excute\n");
pthread_mutex_unlock(&mutex);
}
void
func2(void *arg)
{
pthread_mutex_lock(&mutex);
printf("function 2 excute\n");
pthread_mutex_unlock(&mutex);
}
int main(void)
{
int err, condition, arg;
struct timespec when;
if ((err = pthread_mutexattr_init(&attr)) != 0)
err_exit(err, "pthread_mutexattr_init failed");
if ((err = pthread_mutexattr_settype(&attr,
PTHREAD_MUTEX_RECURSIVE)) != 0)
err_exit(err, "can't set recursive type");
if ((err = pthread_mutex_init(&mutex, &attr)) != 0)
err_exit(err, "can't create recursive mutex");
/* continue processing ... */
pthread_mutex_lock(&mutex);
/*
* Check the condition under the protection of a lock to
* make the check and the call to timeout atomic.
*/
condition = 1;
if (condition) {
/*
* Calculate the absolute time when we want to func1.
*/
clock_gettime(CLOCK_REALTIME, &when);
when.tv_sec += 10; /* 10 seconds from now */
timeout(&when, func1, (void *)((unsigned long)arg));
when.tv_sec -= 5; /* 5 seconds from now */
timeout(&when, func2, (void *)((unsigned long)arg));
condition = 0;
}
pthread_mutex_unlock(&mutex);
/* continue processing ... */
sleep(20);
exit(0);
}
5.2 读写锁
读写锁类似于互斥锁,但是它允许更高的并行性。读写锁允许多个线程同时拥有读锁,但是只有一个线程可以由写锁,即读和写时互斥的,但是读和读之间并不是互斥。这种锁非常适用于对数据结构读次数大于写的情况。读写锁对应的数据结构时pthread_rwlock_t。
5.2.1 读写锁初始化
#inlcude<pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const phread_rwlockattr_t *attr);
int pthread_ rwlock _destroy(pthread_rwlock_t *rwlock);
//执行成功返回0,失败返回错误编号
类似于互斥锁的初始化有动态和静态两种方式,读写锁的初始化也有静态和动态两种。上面的函数提供了动态初始化和反初始化的方法,我们也可以直接将其静态的设置为PTHREAD_RWLOCK_INITIALIZER.
5.2.2 读写锁的lock与unlock
#inlcude<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);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//执行成功返回0,失败返回错误编号
通过以上的函数我们分别可以进行读加锁、写加锁以及解锁。当加锁失败时会阻塞中,当然我们也提供了一组trylock,当加锁失败时不会阻塞,而是返回EBUSY。
5.2.3 超时读写锁
#include<pthread.h>
#include<time.h>
int pthread_rwlock_timerdlock(pthread_rwlock_t *rwlock, const struct timespec *tpsr);
int pthread_rwlock_timewrlock(pthread_rwlock_t *rwlock, const struct timespec *tpsr);
//执行成功返回0,失败返回错误编号
超时读写锁会在加锁失败后的指定时间段内直接返回。
5.2.4 读写锁属性
读写锁只支持进程的共享属性,原理同互斥信号量相同,这里就不再过多介绍。读写锁的进程共享属性的初始化,设置分别由下面两组函数实现。
#include<pthread.h>
int pthread_rwlockattr_init(pthread_rwattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwattr_t *attr);
int pthread_rwlockattr_getpshared(cosnt pthread_ rwlockattr _t *attr, int *restrict pshared);
int pthread_ rwlockattr _setpshared(cosnt pthread_ rwlockattr _t *attr, int restrict pshared);
//执行成功返回0,失败返回错误编号
5.3 条件变量
条件变量是另一种线程同步机制,通常与互斥量一起使用,允许线程以无竞争的方式等待条件的发生。可以分两点进行说明:
为什么需要条件变量?我们考虑下面的情况,线程A和线程B都需要获取同一个资源,但是线程A又需要线程B执行到某一步,才能继续执行。这时候如果先执行线程A,lock住这个资源;那线程B就无法获取该资源,这样A,B两个线程就可能都会阻塞住。我们看下实现这种思想的伪代码:
pthread_mutex_lock(&mutex)
while(condition is false)
{
pthread_mutex_unlock(&mutex)
wait(conditon to true);
pthread_mutex_lock(&mutex)
}
这里的condition我们就可以用条件变量来实现。
那为什么需要同互斥量一起使用呢?上面的程序中如果在unlock之后线程被切走,其它的线程执行了” conditon to true”。这时此线程还没有执行到wait函数,也就永远没有机会再等待条件满足,此线程会一直阻塞。这时如果unlock 和wait是原子操作就可以避免这个问题。
5.3.1 初始化
#inlcude<pthread.h>
int pthread_cond_init(pthread_cond_t *cond, const phread_cond_t *attr);
int pthread_ cond _destroy(pthread_cond_t * cond);
//执行成功返回0,失败返回错误编号
同样,条件变量也可以直接被静态初始化为PTHREAD_COND_INITIALIZER。
5.3.2 Wait
#inlcude<pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, const phread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, const phread_mutex_t *mutex, const struct timespec * tsptr);
//执行成功返回0,失败返回错误编号
Wait函数其实就是上面介绍的unlock 和wait 的原子操作。Timedwait 多了个超时功能,到时间到了之后,wait函数就不会再等待。此时timewait函数将重新获取互斥量,并返回ETIMEOUT。需要注意的是在调用wait之前一定要先对mutex加锁。
5.3.3 Signal
#inlcude<pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
//执行成功返回0,失败返回错误编号
Signal函数至少可以唤醒一个等待该条件的线程,broadcast则会唤醒所有等待该条件的线程。
5.3.4 条件变量属性
条件变量属性有共享进程属性和时钟属性两种。共享进程属性同其他同步机制一样,不做过多介绍。时钟属性控制timedwait函数采用哪个时钟。条件变量属性对应的数据结构时:pthread_condattr_t。
- 条件变量属性初始化
#include<pthread.h>
int pthread_condattr_init(pthread_condattr_t * attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
//执行成功返回0,失败返回错误编号
2. 共享进程属性
#inlcude<pthread.h>
int pthread_condattr_getpshared(cosnt pthread_mutexattr_t *attr, int *restrict pshared);
int pthread_condattr_setpshared(cosnt pthread_mutexattr_t *attr, int restrict pshared);
//执行成功返回0,失败返回错误编号
3. 时钟属性
#inlcude<pthread.h>
int pthread_condattr_getclock(cosnt pthread_condattr_t *attr,clockid_t *clock_id);
int pthread_condattr_setclock(cosnt pthreadcondattr_t *attr, int restrict clock_id);
//执行成功返回0,失败返回错误编号
可选的时钟属性有以下几种:
CLOCK_REALTIME | 实时系统时间 |
CLOCK_MONOTONIC | 不带负跳数的实时系统时间 |
CLOCK_PROCESS_CPUTIME_ID | 调用进程的CPU时间 |
CLOCK_THREAD_CPUTIME_ID | 调用线程的CPU时间 |
5.4 自旋锁
自旋锁和互斥锁类似,都是用来保证同一时刻只有一个调用者可以访问某个资源。但自旋锁和互斥锁的调度机制不一样,对已经加锁的情况,另一个调用者再企图去加锁,如果是互斥锁,该调用者会阻塞,但自旋锁不会,它会一直在那里轮训,直到其它调用者解锁。所以自旋锁其实不是用于多线程并发,而是用于多处理器并发。
鉴于自旋锁的特性,它通常被用在内核中,而不是用户层。内核中常用的情景是中断处理程序,在非抢占式的内核中,中断处理程序一般不能休眠,所以自旋锁通常是其唯一可用的同步原语。
如果用在多线程编程中,必须是在非抢占式调度的系统内。如果是这种场景下,线程被切走,此时该线程进入阻塞队列,如果还有没解锁的自旋锁,那阻塞在锁上的其它线程自旋的时间可能就会超过预期。
自旋锁只有进程共享属性,它的一些常用的操作接口如下:
#inlcude<pthread.h>
int pthread_spin_init(pthread_ spinlock _t *restrict lock,int pshared);
int pthread_ spin _destroy(pthread_ spinlock _t * lock);
int pthread_spin_lock(pthread_ spinlock _t *restrict lock);
int pthread_spin_trylock(pthread_ spinlock _t *restrict lock);
int pthread_spin_unlock(pthread_ spinlock _t *restrict lock);
//执行成功返回0,失败返回错误编号
对于自旋锁还有两点需要说明:
- 同一调用者递归对自旋锁加锁必定会引起死锁;
- 自旋时时一直占用CPU的,所以自旋锁一般只适用于占用资源时间很短的场景。
5.5 屏障
屏障的原理是让很多线程同时执行到某个点,才能继续执行下去。即每个线程都运行到了设置屏障的地方,才能继续执行。
5.1 Barrier的使用
初始化的时候需要制定count数,只有count数目的线程都执行到了屏障的地点,所有的线程才能继续执行下去。最后一个执行wait 函数的线程返回值为PTHEAD_BARRIER_SERIAL_THREAD,其它线程返回值都是0。此时count计数被reset 成0。该barrier对象可以继续使用。Barrier对应的数据结构是pthread_barrier_t。
#inlcude<pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrict barrier, pthread_barrierattr_t *attr,
unsigned int count);
int pthread_ barrier_destroy(pthread_ barrier_t * barrier);
int pthread_barrier_wait(pthread_barrier_t *barrier);
5.2 Barrier属性
Barrier也只有进程共享属性,其对应的数据结构是:pt
#include<pthread.h>
int pthread_barrierattr_init(pthread_barrierattr_t * attr);
int pthread_condattr_destroy(pthread_barrierattr_t *attr);
int pthread_barrierattr_getpshared(cosnt pthread_barrierattr_t *attr, int *restrict pshared);
int pthread_barrierattr_setpshared(cosnt pthread_barrierattr_t *attr, int pshared);
//执行成功返回0,失败返回错误编号
ead_barrierattr_t。