Pthread线程同步总结——APUE学习笔记

pthread_create 创建线程

pthread_join 等待其他线程A结束,并能获取A的返回值

pthread_cancel 取消其他线程

一:互斥量 pthread_mutex_t

基本用法

斥量从本质上说是把锁,在访问共享资源前对互斥量进行设置(加锁),在访问完成后释放(解锁)互斥量。

对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程都会被阻塞直到当前线程释放该互斥锁。

如果释放互斥量时有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对互斥量加锁 ,其他线程就会看到互斥量依然是锁着的,只能回去再次等待它重新变为可用。

mutex的数据类型是pthread_mutex_t,且mutex的使用有几步:

初始化

加锁

解锁

销毁

初始化有两种方法,一是调用pthread_mutex_init函数,二是直接对mutex赋值PTHREAD MUTEX_INITIA ZER:

如果使用方法一初始化,则销毁互斥量时就不调用pthread_mutex_destroy函数了,方法二初始化的互斥量则需要。

// 初始化方法一:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 初始化方法二:
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);    // 参数二代表互斥量相关的属性
// 销毁:
pthread_mutex_destroy(&mutex);
// 加锁,如果已锁,则阻塞调用线程
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 尝试加锁,如果已锁,则返回EBUSY   
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

死锁问题

如果两个互斥量,在不同的线程中,锁的顺序不同,则有可能造成死锁,为了保证不出现死锁,有两种方法:

保证互斥量加锁顺序相同;

使用pthread_mutex_trylock函数,该函数会判断是否能获取互斥量,如果互斥量被锁,则直接放弃申请锁而返回EBUSY。

带超时时间的mutex加锁原型:

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

其中参数abstime是超时的时间点,所谓"绝对时间"。

二:读写锁 pthread_rwlock_t

读写锁有三种状态:

读锁:加读锁时,可允许其他线程读,但不能写;

写锁:加写锁时,其他线程读或写都不允许。

无锁

读写锁又称为共享互斥锁,读锁即共享锁,写锁即互斥锁。

// 初始化方法一
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
// 初始化方法二
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_tryrdlock(pthread_rwlock_t *rwlock);
// 加写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

和mutex一样,如果用方法二初始化,则需要在最后调用destroy函数销毁读写锁。

try相关的函数也同mutex相关的try函数,在锁被占用不能持有的情况下,不阻塞线程、直接返回EBUSY。

同样,也有timed锁提供超时锁,参数abstime同样也是绝对时间点:

int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abstime);

int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abstime);

三:条件变量 pthread_cond_t

注意点:

pthread_cond_signal函数可能只唤醒一个阻塞线程,也可能唤醒多个阻塞线程,APUE上的说法是:唤醒至少一个阻塞线程。

条件变量cond要和互斥量mutex搭配使用,在使用时,先将mutex上锁,然后再判断条件,继而决定是否进行pthread_cond_wait调用,最后再将mutex解锁。

难点:

pthread_cond_wait执行过程中,先将当前线程加入条件阻塞队列,再解锁mutex,这两个过程是原子操作的,解锁mutex是为了其他线程能对mutex上锁、这样其他线程才能进行条件判断及阻塞,或者更改条件;

pthread_cond_wait返回时(即其他线程调用了signal从而唤醒当前线程),mutex由阻塞过程中的解锁状态重新变为加锁状态;

while判断条件是为了防止虚假唤醒和惊群效应,详解如下:

虚假唤醒:

当某线程用pthread_cond_signal唤醒其他阻塞线程时,条件其实并未满足,此时虽然其他阻塞线程得到了唤醒的信号,但其条件不满足,其实不能往下运行。

如果仅在pthread_cond_wait前用if判断条件是否满足,被唤醒后,即使条件不满足,if判断也已完成,那么会开始运行,和期望不符。例如:

int x = 1;
pthread_mutex_lock(&mutex);
if (x > 0) {
    pthread_cond_wait(&cond, &mutex);    // cond为pthread_cond_t类型
}
pthread_mutex_unlock(&mutex);
run_other();
// 该程序意为当x>0时,程序阻塞,当x<=0时才开始运行run_other()函数

首次判断时x==1,此时进入if块内,执行wait函数,当前线程阻塞。此时如果外部有另一线程执行signal函数通知当前线程,不管x是否被改变,先前的if块都会执行完毕,即不管x是否满足x<=0条件,都会运行run_other函数,这显然于设想不符合,而使用while判断,则可在往下执行前再次判断,避免这种虚假唤醒:

while (x > 0) {    // 可循环判断,当wait返回后,可再判断一次x>0是否成立
    pthread_cond_wait(&cond);    // cond为pthread_cond_t类型
}

惊群效应:

当一个线程执行pthread_cond_signal函数,由于各操作系统实现不同,可能唤醒一个或多个线程,如果唤醒多个线程时,其中某些线程可能更改条件,导致另外一些线程虽然被唤醒,但实际上不符合运行条件,例如:

int x = 1;
pthread_mutex_lock(&mutex);
if (x > 0) {
    pthread_cond_wait(&cond, &mutex);    // cond为pthread_cond_t类型
}
pthread_mutex_unlock(&mutex);
x++;    // 此处修改了条件
run_other();

当两个线程A和B都执行上面这段函数时阻塞。此刻如果有线程C使得x<=0,并调用signal函数唤醒A/B。则A/B可能都被唤醒,假设A执行完后B执行。如果采用if,即使A中x++导致x>0,那么B也可以执行下去,因为先前阻塞时已进入if语句块中了,如此则导致B在不符合条件的情况下往下执行。扩展一下,如果本来不止AB两个线程在阻塞,而是多个线程在阻塞,那么因为使用if,所有的线程可能都被唤醒,接着不管条件x是否满足,全都往下执行,则发生了“惊群效应”。

解决方法就是不用if,改用while判断条件,当被唤醒后,由于是while,可再循环判断一次,确认条件是否满足。

以上,不管是为了防止虚假唤醒或是惊群效应,本质都是由于线程被唤醒是一回事、条件满足是另一回事,被唤醒不代表条件一定被满足,因此唤醒后需要再检查一遍条件,只有使用while才能在循环中再判断一次,而if不能,所以pthread_cond_wait函数常与while搭配使用。

四:自旋锁 pthread_spinlock_t

忙等和阻塞:

忙等:线程处于忙等状态,即不让出CPU,CPU一直循环校验(在自旋锁中,是检验锁持有者是否释放锁),因不让出CPU,因此不会涉及CPU切换到其他线程。

阻塞:线程自身阻塞,则让出CPU供其他线程使用,故而会引起线程调度。

以上,可知二者区别主要为是否让出CPU。自旋锁使用忙等,互斥量/读写锁使用阻塞,因此使用自旋锁,好处是避免线程调度(CPU切换),坏处就是会引起忙等,当忙等时间较长时,会造成CPU资源浪费,故不建议在持有锁时间较长的情况下使用自旋锁。

相关API

自旋锁各函数和互斥量相似,也是init/destroy/lock/unlock几个方法。

// 初始化方法:
pthread_spinlock_t spinlock;
pthread_spinlock_init(&spinlock, int pshared);    // 参数二代表自旋锁相关的属性
// 销毁:
pthread_spinlock_destroy(&spinlock);
// 加锁,如果已锁,则自旋
int pthread_spinlock_lock(pthread_spinlock_t *spinlock);
// 尝试加锁,如果已锁,则返回EBUSY  
int pthread_spinlock_trylock(pthread_spinlock_t *spinlock);
// 解锁
int pthread_spinlock_unlock(pthread_spinlock_t *spinlockx);

以上初始化函数中pshared参数(进程共享属性)有两个值:PTHREAD_PROCESS_PRIVATE标识自旋锁只能被初始化该锁的进程的内部线程使用;PTHREAD_PROCESS_SHARED表示该锁可被能访问锁底层内存的线程获取、即使属于不同的进程。

自旋锁使用场景:

通常多用在非抢占式任务中,因为在抢占式任务中,自旋过程可能被其他线程打断,此时虽使用了自旋锁,仍旧会有CPU切换,故而不能充分发挥自旋锁的优势。

五:屏障 pthread_barrier_t

原理:

屏障提供了这样一种机制:指定特定数目的线程,在线程执行中插入屏障,这些线程都到达屏障插入位置时,各线程才能继续往下走。

当某些线程领先于其他线程,先到达屏障时,则阻塞当前线程、等待其他线程,直到所有线程都到达屏障插入位置。

相关API:

// 初始化屏障
int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned int count);
// 销毁屏障
int pthread_barrier_destory(pthread_barrier_t *barrier);
// 设立屏障位置、执行屏障机制
int pthread_barrier_wait(pthread_barrier_t *barrier);

初始化函数中,attr是屏障属性,采用默认属性可传NULL;count参数是参与屏障的线程数量,只有count数量个线程到达屏障,各线程才会继续执行。

函数 pthread_barrier_wait 被调用时,如果屏障计数未到初始化时指定的数量,则当前线程进入阻塞状态,如果到达指定数量(即当前线程是最后一个到达屏障的),则所有阻塞在相同屏障点的线程都被唤醒、开始运行。

屏障可被重复使用,不过屏障计数不会改变。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值