本篇主要讲Linux环境下的多线程同步内核对象。
(1)linux线程同步之互斥体:linux互斥体的用法与windows的临界区对象类似,使用数据结构 pthread_mutex_t表示互斥体对象(定义于pthread.h头文件中),初始化方式有两种:
1.使用 PTHREAD_MUTEX_INITIALIZER 直接给互斥体变量赋值,如:pthread_mutex_t mymutex = PTHREAD_MUTEX_INITIALIZER;
2.使用pthread_mutex_init函数初始化,如果互斥量是动态分配的或者需要给互斥量设置属性,函数签名:
int pthread_mutex_init(pthread_mutex_t* restrict mutex,const pthread_mutexattr_t* restrict attr);
//参数 mutex 即我们需要初始化的 mutex 对象的指针,参数 attr 是需要设置的互斥体属性,设置NULL表示默认属性PTHREAD_MUTEX_NORMAL(普通锁),此外还有
PTHREAD_MUTEX_ERRORCHECK(检错锁),PTHREAD_MUTEX_RECURSIVE(嵌套锁),普通锁则是线程独占,有线程占用的情况下其他线程调用上锁函数阻塞,检错锁是已
经上锁的线程对互斥体对象重复加锁,上锁返回EDEADLK,允许同一个线程对其持有的互斥体重复加锁,每加锁一次互斥体对象的锁引用就会新增一次,解锁会减少一次,当计数为0时
其他线程才能获得该锁。
互斥体对象销毁:int pthread_mutex_destroy(pthread_mutex_t* mutex); 成功则返回0 ,注意:使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量无须销毁,要去销毁一
个已经加锁或正在被条件变量使用的互斥体对象。
对于互斥体的加锁和解锁操作一般使用以下三个函数:
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_mutexattr_settype/pthread_mutexattr_gettype 设置或获取想要的属性类型:
int pthread_mutexattr_settype(pthread_mutexattr_t* attr,int type);
int pthread_mutexattr_gettype(const pthread_mutexattr_t* restrictattr,int* restricttype);
如下使用例子:
#include<pthread.h>
#include<stdio.h>
#include<errno.h>
int main(){
pthread_mutex_t mymutex;
pthread_mutex_init(&mymutex, NULL);
int ret = pthread_mutex_lock(&mymutex);
ret = pthread_mutex_destroy(&mymutex);
if (ret != 0)
{
if (errno == EBUSY)
printf("EBUSY\n");
printf("Failed to destroy mutex.\n");
}
ret = pthread_mutex_unlock(&mymutex);
ret = pthread_mutex_destroy(&mymutex);
if (ret == 0)
{
printf("Succeed to destroy mutex.\n");
}
return 0;
}
(2)linux线程同步之信号量:与windows的Semaphore 对象使用原理一样,linux的信号量也可以资源多份,可同时被多个线程访问,头文件semaphore.h,常用的一组API函数:
int sem_init(sem_t* sem,int pshared,unsigned int value); //初始化信号量,参数 sem 传入初始化信号量地址;参数 pshared表示该信号量是否可以被初始化该信号量的进程 fork 出
来的子进程共享,取值为 0 (不可以共享)、1(可以共享);参数 value 用于设置信号量初始状态下资源的数量;初始化成功返回0,失败返回-1;
int sem_destroy(sem_t* sem); //销毁信号量
int sem_post(sem_t* sem); //将信号量的资源计数加一,并解锁sem_wait而阻塞的线程
int sem_wait(sem_t* sem); // 如果当前信号量资源计数为 0,函数会阻塞调用线程;直到信号量对象的资源计数大于 0 时被唤醒,唤醒后将资源计数递减 1,然后立即返回
int sem_trywait(sem_t* sem); //sem_wait函数的非阻塞版,当前信号量对象的资源计数等于 0,函数会立即返回不会阻塞调用线程,返回值是 ﹣1,错误码 errno 被设置成 EAGAIN
int sem_timedwait(sem_t* sem,conststructtimespec* abs_timeout); // 带有等待时间的版本,等待时间在第二个参数 abs_timeout 中设置,不能设置为NULL,否则会奔溃
注意:1.sem_wait、sem_trywait、sem_timedwait 函数将资源计数递减一时会同时锁定信号量对象,因此当资源计数为 1 时,如果有多个线程调用 sem_wait 等函数等待该信号量
时,只会有一个线程被唤醒。当 sem_wait 函数返回时,会释放对该信号量的锁。
2.sem_wait、sem_trywait、sem_timedwait 函数调用成功后返回值均为 0,调用失败返回 ﹣1,可以通过错误码 errno 获得失败原因。
3.sem_wait、sem_trywait、sem_timedwait 可以被 Linux 信号中断,被信号中断后,函数立即返回,返回值是 ﹣1,错误码 errno 为 EINTR。
(3)linux线程同步之条件变量:为了让条件变量和互斥对象两者为原子操作,则两者需要同时结合使用,否则会出现CPU的调度导致线程获得互斥对象但是却错过了条件变量唤醒的信
号导致线程阻塞。条件变量常用API函数如下:
int pthread_cond_init(pthread_cond_t* cond,const pthread_condattr_t* attr); //初始化函数 也可使用 pthread_cond_t cond = PTHREAD_COND_INITIALIZER代替;
int pthread_cond_destroy(pthread_cond_t* cond); //销毁函数
int pthread_cond_wait(pthread_cond_t* restrict cond,pthread_mutex_t* restrict mutex); //条件变量不满足,线程阻塞
int pthread_cond_timedwait(pthread_cond_t* restrictcond,pthread_mutex_t* restrictmutex,const struct timespec* restrict abstime); //等待的非租塞函数,指定时间返回
int pthread_cond_signal(pthread_cond_t* cond); // 一次唤醒一个线程,具体哪个线程视情况而定,返回值非0为失败
int pthread_cond_broadcast(pthread_cond_t* cond); //广播唤醒,一次唤醒多个线程 ,返回值非0为失败
注意:1.当 pthread_cond_wait 函数阻塞时,它会释放其绑定的互斥体,并阻塞线程,因此在调用该函数前应该对互斥体有个加锁操作,当收到条件信号时, pthread_cond_wait
会返回并对其绑定的互斥体进行加锁,因此在其下面一定有个对互斥体进行解锁的操作。
2.条件变量的虚假唤醒:操作系统可能会在一些情况下唤醒条件变量,即使没有其他线程向条件变量发送信号,等待此条件变量的线程也有可能会醒来。pthread_cond_wait
是 futex 系统调用,属于阻塞型的系统调用,当系统调用被信号中断的时候,会返回 ﹣1,并且把 errno 错误码置为 EINTR。很多这种系统调用为了防止被信号中断都会重启系统调用
(即再次调用一次这个函数)。
3.如果一个条件变量信号条件产生时(调用 pthread_cond_signal 或pthread_cond_broadcast),没有相关的线程调用 pthread_cond_wait 捕获该信号,那么该信号条件就
会永久性地丢失了,再次调用 pthread_cond_wait 会导致永久性的阻塞,因此,一定要确保等待的线程在产生条件变量信号的线程发送条件信号之前调用 pthread_cond_wait。
(4) linux线程同步之读写锁:读写锁在 Linux 系统中使用类型 pthread_rwlock_t 表示,
读锁用于共享模式:
如果当前读写锁已经被某线程以读模式占有了,其他线程调用pthread_rwlock_rdlock(请求读锁)会立刻获得读锁;
如果当前读写锁已经被某线程以读模式占有了,其他线程调用pthread_rwlock_wrlock(请求写锁)会陷入阻塞。
写锁用的是独占模式:
如果当前读写锁被某线程以写模式占有,无论调用pthread_rwlock_rdlock还是pthread_rwlock_wrlock都会陷入阻塞,即写模式下不允许任何读锁请求通过,也不允许任何写锁
请求通过,读锁请求和写锁请求都要陷入阻塞,直到线程释放写锁。
读写锁初始化和销毁的API:
int pthread_rwlock_init(pthread_rwlock_t* rwlock,const pthread_rwlockattr_t* attr); //参数 rwlock 即需要初始化和销毁的读写锁对象的地址,参数 attr 用于设置读写锁的属性,设置
NULL表示默认属性,返回非0 为失败,当不需要动态创建或者设置非默认属性的读写锁对象,可使用 pthread_rwlock_t myrwlock = PTHREAD_RWLOCK_INITIALIZER; 初始化
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock); //销毁读写锁
请求读锁和写锁API数:
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_timedrdlock(pthread_rwlock_t* rwlock,conststructtimespec* abstime);
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_timedwrlock(pthread_rwlock_t* rwlock,conststructtimespec* abstime);
无论是读锁还是写锁,锁的释放都是一个接口:
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
读写锁的属性类型是 pthread_rwlockattr_t ,glibc 引入了如下接口来查询和改变读写锁的类型:
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t* attr,intpref);
int pthread_rwlockattr_getkind_np(constpthread_rwlockattr_t* attr,int* pref);
pthread_rwlockattr_setkind_np 的第二个参数 pref 即设置读写锁的类型,其取值有如下几种:
enum{
PTHREAD_RWLOCK_PREFER_READER_NP, //读者优先(即同时请求读锁和写锁时,请求读锁的线程优先获得锁)
PTHREAD_RWLOCK_PREFER_WRITER_NP, //不要被名字所迷惑,也是读者优先
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP, //写者优先(即同时请求读锁和写锁时,请求写锁的线程优先获得锁)
PTHREAD_RWLOCK_DEFAULT_NP = PTHREAD_RWLOCK_PREFER_READER_NP // 默认,读者优先
};
初始化和销毁 pthread_rwlockattr_t 对象:
int pthread_rwlockattr_init(pthread_rwlockattr_t* attr);
in tpthread_rwlockattr_destroy(pthread_rwlockattr_t* attr);
初始化一个写者优先的读写锁的例子:
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, &attr);
(5)C++11/14/17线程资源同步对象:C/C++提供的直接使用操作系统的功能函数虽然强大,也存在功能限制,同样的代码不能通知兼容linux和windows,因此C++11以及后续更新封装
同步辅助类std::mutex、std::condition_variable、std::lock_guard、std::unique_lock,极大的方便了跨平台开发。
常用的比如 std::mutex (C++11互斥对象)、std::shared_mutex(C++17共享的互斥量),均提供了加锁(lock)、尝试加锁(trylock)和解锁(unlock)的方法,为了避免死锁,例
如std::mutex.lock() 和 std::mutex::unlock() 方法需要成对使用,为了防止函数出口过多导致加锁后没有解锁导致死锁,推荐 RAII 技术封装加锁和解锁的两个接口,同时C++11也提供如
下封装:
互斥量管理 版本 作用
lock_guard C++11 基于作用域的互斥量管理
unique_lock C++11 更加灵活的互斥量管理
shared_lock C++14 共享互斥量的管理
scope_lock C++17 多互斥量避免死锁的管理
注意:1.比如 void func(){
std::lock_guard<std::mutex> guard(mymutex);
//在这里放被保护的资源操作
}
mymutex 的类型是 std::mutex,在 guard 对象的构造函数中,会自动调用 mymutex.lock() 方法加锁,当该函数出了作用域后,调用 guard 对象时析构函数时会自动调用
mymutex.unlock() 方法解锁 ,因此 mymutex 生命周期必须长于函数 func 的作用域。
2.重复加锁可能会造成程序奔溃,如果一个 std::mutex 对象已经调用了 lock() 方法,再次调用时,其行为是未定义的,“行为未定义”即在不同平台上可能会有不同的行为。
std::condition_variable表示条件变量,与linux环境下的原生条件变量一样,提供了等待条件变量满足的 wait 系列方法(wait、wait_for、wait_until 方法),发送条件信号使用
notify 方法(notify_one 和 notify_all 方法),当然使用 std::condition_variable 对象时需要绑定一个 std::unique_lock 或 std::lock_guard 对象,但是C++ 11 中 std::condition_variable 不
再需要显式调用方法初始化和销毁。