线程间无需特别的手段进行通信,因为线程间可以共享数据结构,也就是一个全局变量可以被两个线程同时使用。 不过要注意的是线程间需要做好同步。
目录
一、互斥锁
互斥锁也叫做互斥量,是一个二元变量,主要以排他的方式防止数据被并发访问。在访问共享资源前对互斥锁进行加锁,访问后释放互斥锁。若互斥锁已经被某线程加锁,其他试图再次对互斥锁加锁的线程都将会被阻塞直到当前线程释放该互斥锁。
如果释放互斥锁时有一个以上的线程等待加锁,那么这些线程都将会变成可运行状态,但只有一个变线程可以对互斥量加锁,其他线程抢占失败,只能再次阻塞等待它重新变成可用,这样,就可以保证一次只有一个线程可以向前执行。
1、初始化
初始化一个互斥锁(互斥量)mutex。
(1)动态初始化
#include <pthread.h>
/*动态初始化互斥锁*/
int pthread_mutex_init(pthread_mutex_t* mutex,pthread_mutexattr_t* attr);
函数参数:
mutex:传出参数,调用时应传 &mutex给该函数
attr:传入参数,互斥量属性。为NULL,则表示使用默认属性
返回值:
成功返回0,失败返回-1
(2)静态初始化
/*静态态初始化互斥锁,mutex必须为全局变量*/
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALZER;
静态初始化时,mutex必须为全局变量,有三种类型可供选择:
- PTHREAD_MUTEX_INITIALIZER ---创建快速互斥锁,默认类型
- PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP---创建递归互斥锁,允许同一线程循环给互斥量上锁
- PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP---创建检错互斥锁,如果该互斥量已经被上锁,那么后续的上锁将会失败而不会阻塞。
2、加锁
对已经完成初始化的互斥锁尝试加锁操作,使用pthread_mutex_lock函数对共享资源进行加锁时,如果加锁不成功,则线程就阻塞;而如果使用pthread_mutex_trylock,加锁不成功时不会阻塞,而是立即返回EBUSY错误。
#include <pthread.h>
/*以阻塞方式上锁*/
int pthread_mutex_lock(pthread_mutex_t* mutex);
/*以非阻塞方式上锁*/
int pthread_mutex_trylock(pthread_mutex_t* mutex);
函数参数:
mutex:已经初始化的互斥锁
返回值:
成功返回0,失败返回-1
3、解锁
对共享资源解锁,在解锁的同时,会将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度策略。默认情况下:先阻塞的线程会先被唤醒。解锁操作只能由占用该互斥锁的线程完成。
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t* mutex);
函数参数:
mutex:已经初始化的互斥锁
返回值:
成功返回0,失败返回-1
4、销毁互斥锁
销毁一个互斥锁,并释放资源
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t* mutex);
函数参数:
mutex:已经初始化的互斥锁
返回值:
成功返回0,失败返回-1
5、互斥锁属性
(1) 初始化属性
将互斥锁的属性初始化为缺省值,在执行过程中,线程系统会为每个属性对象分配存储空间。
// 初始化属性
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
函数参数:
attr:待初始化的互斥锁属性
返回值:
成功返回0,失败返回-1
(2)销毁属性
销毁互斥锁属性对象,只要执行过初始化的属性对象,必须成对执行销毁操作,否则将会导致内存泄漏。
// 销毁属性
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
函数参数:
attr:待销毁的互斥锁属性
返回值:
成功返回0,失败返回-1
(3)获取/设置互斥锁的共享属性
获取/设置互斥锁的共享属性
// 获取/设置互斥锁的共享属性
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr, int *pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
函数参数:
attr:待操作的互斥锁属性对象
pshared:互斥锁的共享属性,有以下两种取值类型:
- PTHREAD_PROCESS_PRIVATE(默认):由同一个进程创建的线程才能够处理该互斥锁
- PTHREAD_PROCESS_SHARED:多个进程中的线程之间共享互斥锁
返回值:
成功返回0,失败返回-1
(4)获取/设置互斥锁的范围
当持有互斥量的进程终止时,不同属性将有不同的响应。
// 获取/设置持有互斥锁的进程终止时的处理
int pthread_mutexattr_getrobust(const pthread_mutexattr_t *attr, int *robust);
int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr,int robust);
函数参数:
attr:待操作的互斥锁属性对象
robust:互斥锁属性的健壮性,取值有以下两种:
- PTHREAD_MUTEX_STALLED(默认):意味着持有互斥量的进程终止时不会有特别的动作
- PTHREAD_MUTEX_ROBUST:在进程终止后,等待中的线程如果调用了pthread_mutex_lock,会从阻塞中返回EOWNERREAD,而不是0.
返回值:
成功返回0,失败返回-1
(5)设置/获取互斥锁的类型
销毁互斥锁属性对象,只要执行过初始化的属性对象,必须成对执行销毁操作,否则将会导致内存泄漏。
// 获取/设置互斥锁的类型
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
函数参数:
attr:待销毁的互斥锁属性
type:互斥锁的类型,有以下四种类型选择:
- PTHREAD_MUTEX_NORMAL ---标准互斥量类型,不做特殊的错误检查和死锁检测。如果线程在不首先解除互斥锁的情况下,尝试重新锁定该互斥锁,则会产生死锁;尝试解除由其他线程锁定的互斥锁会产生不确定的行为;如果尝试解除锁定的互斥锁未锁定,则会产生不确定的行为。
- PTHREAD_MUTEX_ERRORCHECK ---此类型的互斥锁可提供错误检查。如果线程在不首先解除锁定互斥锁的情况下尝试重新锁定该互斥锁,则会返回错误。如果线程尝试解除锁定的互斥锁已经由其他线程锁定,则会返回错误。如果线程尝试解除锁定的互斥锁未锁定,则会返回错误。
- PTHREAD_MUTEX_RECURSIVE ---递归类型,此种类型允许同一线程在互斥量解锁之前对该互斥量进行多次加锁。递归互斥量维护锁的计数,在解锁次数与加锁次数不同的情况下,不会释放锁。如果线程尝试解除锁定的互斥锁已经由其他线程锁定,则会返回错误。 如果线程尝试解除锁定的互斥锁未锁定,则会返回错误。
- PTHREAD_MUTEX_DEFAULT---此类型提供默认特性,操作系统实现时会把它映射到上面三种中的一种上。linux系统映射到第一种标准类型上。
返回值:
成功返回0,失败返回-1
二、条件变量
条件变量是利用线程间共享的全局变量进行同步的一种机制,始终与互斥锁一起使用:一个线程等待”条件变量的条件成立”而挂起;另一个线程使”条件成立”(给出条件成立信号)。
条件变量可以以原子方式阻塞线程,直到某个特定条件为真,如果条件为假,线程通常会基于条件变量阻塞,并以原子方式释放等待条件变化的互斥锁。如果另一个线程更改了条件,该线程可能会向相关的条件变量发出信号,从而使一个或多个等待的线程执行以下操作:
- 唤醒
- 再次获取互斥锁
- 重新评估条件
1、初始化
初始化一个条件变量。
(1)动态初始化
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
函数参数:
cond:要初始化的条件变量
attr:条件变量属性,通常传NULL,表示使用默认属性
返回值:
成功返回0,失败返回非0值
(2)静态初始化
#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
静态初始化时,cond必须为全局变量
2、等待条件变量
阻塞等待一个条件变量,具体而言有以下两个动作:
- 阻塞等待条件变量cond满足;
- 解锁互斥锁mutex,相当于pthread_mutex_unlock(&mutex);
以上两个动作被合并为一个原子操作,这样便关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道, 这样线程就不会错过条件的任何变化。
当函数接收到“条件成立”的信号后,它并不会立即结束对线程的阻塞,而是先完成对互斥锁的“加锁”操作,然后才解除阻塞。
#include <pthread.h>
/*阻塞等待某个条件变量*/
int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex);
/*超时等待某个条件变量*/
int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex,struct timespec* abstime);
函数参数:
cond:已初始化的条件变量
mutex:与条件变量配合使用的互斥锁
abstime:超时等待的绝对时间,结构体为:
struct timespec { time_t tv_sec; /* 秒 */ long tv_nsec; /* 纳秒*/ }
返回值:
成功返回0,失败返回非0值
3、唤醒条件变量
唤醒至少一个阻塞在条件变量上的线程,以下两个函数都能解除线程的“阻塞”状态,区别在于:
- pthread_cond_signal() 函数至少解除一个线程的“阻塞”状态,如果等待队列中包含多个线程,优先解除哪个线程将由操作系统的线程调度程序决定;
- pthread_cond_broadcast() 函数可以解除等待队列中所有线程的“阻塞”状态。
由于互斥锁的存在,解除阻塞后的线程也不一定能立即执行。当互斥锁处于“加锁”状态时,解除阻塞状态的所有线程会组成等待互斥锁资源的队列,等待互斥锁“解锁”。
#include <pthread.h>
/*通知等待的第一个线程*/
int pthread_cond_signal(pthread_cond_t* cond);
/*通知等待的所有线程*/
int pthread_cond_broadcast(pthread_cond_t* cond);
函数参数:
cond:已初始化的条件变量
返回值:
成功返回0,失败返回非0值
4、销毁条件变量
销毁一个条件变量,值得注意的是销毁后的条件变量还可以调用 pthread_cond_init() 函数重新初始化后使用。
int pthread_cond_destroy(pthread_cond_t *cond);
函数参数:
cond:已初始化的条件变量
返回值:
成功返回0,失败返回非0值
5、条件变量属性
条件变量的属性结构体为:pthread_condattr_t,内容和互斥锁相差无几,如果需要精细化管理,请自行百度,这里就不在累述。
6、条件变量与互斥锁使用示例
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int pro = 0;
pthread_mutex_t mutex;
pthread_cond_t cond;
void init()
{
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
}
void* producer(void* date)
{
while(1)
{
pthread_mutex_lock(&mutex);
pro++;
printf("生产者生产了1个,现在共%d个\n",pro);
if(pro == 3)
{
pthread_cond_signal(&cond);
}
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
void* contomer(void* date)
{
while(1)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);
pro -= 3;
printf("消费者吃了3个\n");
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
int main()
{
void* ret;
pthread_t tid1,tid2;
init();
pthread_create(&tid1,NULL,producer,0);
pthread_create(&tid2,NULL,contomer,0);
pthread_join(tid1,&ret);
pthread_join(tid2,&ret);
return 0;
}
输出为:
从结果可以看出,当生产者生产了3个产品后,立即通知消费者来消费,紧接着,生产者继续生产,无线循环下去。里面需要注意的是消费者在调用pthread_cond_wait()后进入等待状态时,会把互斥锁解锁,并阻塞等待加锁后,才会继续消费。
三、读写锁
读写锁的操作与互斥锁基本相同,但占用资源相对于互斥锁要少很多,操作逻辑为:
当前读写锁的状态 | 线程发出“读”请求 | 线程发出“写”请求 |
---|---|---|
无锁 | 允许占用 | 允许占用 |
读锁 | 允许占用 | 阻塞线程执行 |
写锁 | 阻塞线程执行 | 阻塞线程执行 |
1、初始化
初始化一个读写锁。
(1)动态初始化
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t*rwlock, const pthread_rwlockattr_t *attr);
函数参数:
rwlock:传出参数,要初始化的读写锁
attr:读写锁属性,通常传NULL,表示使用默认属性
返回值:
成功返回0,失败返回非0值
(2)静态初始化
#include <pthread.h>
pthread_rwlock_t myRWLock = PTHREAD_RWLOCK_INITIALIZER;
静态初始化时,myRWLock必须为全局变量
2、读上锁
以读方式请求读写锁,当读写锁处于“无锁”或者“读锁”状态时,以上两个函数都能成功获得读锁;当读写锁处于“写锁”状态时:
- pthread_rwlock_rdlock() 函数会阻塞当前线程,直至读写锁被释放;
- pthread_rwlock_tryrdlock() 函数不会阻塞当前线程,直接返回 EBUSY
- pthread_rwlock_timerdlock函数最多阻塞abstime.tv_sec秒,如果没有获取到则会直接返回错误
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_timerdlock(pthread_rwlock_t *rwlock,const struct timespec * abstime);
函数参数:
rwlock:已初始化的读写锁
abstime:超时等待的绝对时间,结构体为:
struct timespec { time_t tv_sec; /* 秒 */ long tv_nsec; /* 纳秒*/ }
返回值:
成功返回0,失败返回非0值
3、写上锁
以写方式请求读写锁,当读写锁处于“无锁”状态时,三个函数都能成功获得写锁;当读写锁处于“读锁”或“写锁”状态时:
- pthread_rwlock_wrlock() 函数将阻塞线程,直至读写锁被释放;
- pthread_rwlock_trywrlock() 函数不会阻塞线程,直接返回 EBUSY。
- pthread_rwlock_timewrlock函数最多阻塞abstime.tv_sec秒,如果没有获取到则会直接返回错误
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_timewrlock(pthread_rwlock_t *rwlock,const struct timespec * abstime)
函数参数:
rwlock:已初始化的读写锁
abstime:超时等待的绝对时间,结构体为:
struct timespec { time_t tv_sec; /* 秒 */ long tv_nsec; /* 纳秒*/ }
返回值:
成功返回0,失败返回非0值
4、释放读写锁
无论是处于“无锁”、“读锁”还是“写锁”的读写锁,都可以使用如下函数释放读写锁:
int pthread_rwlock_unlock (pthread_rwlock_t* rwlock);
函数参数:
rwlock:要释放的读写锁
返回值:
成功返回0,失败返回非0值
5、销毁读写锁
当读写锁不再使用时,记得要销毁。
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);
函数参数:
rwlock:要销毁的读写锁
返回值:
成功返回0,失败返回非0值
5、读写锁属性
条件变量的属性结构体为:pthread_rwlockattr_t,内容和互斥锁相差无几,如果需要精细化管理,请自行百度,这里就不在累述。
四、自旋锁
自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
简单来说:自旋锁如果发现要使用的资源被占用就会一直查询这个资源使用的状态直到这个资源被其他线程释放。
从这里我们可以看到自旋锁的一个缺点:那就等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋锁的持有时间不能太长。
所以自旋锁适用于短时期的轻量级加锁。
1、初始化
初始化自旋锁。
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
函数参数:
lock:要初始化的自旋锁
pshared:设置自旋锁的共享属性,有以下两个值可供选择:
- PTHREAD_PROCESS_PRIVATE):由同一个进程创建的线程才能够处理该锁
- PTHREAD_PROCESS_SHARED:多个进程中的线程之间共享该锁
返回值:
成功返回0,失败返回非0值
2、加锁
对自旋锁尝试加锁:
- pthread_spin_lock() 函数将阻塞线程,直至锁被释放;
- pthread_spin_trylock() 函数不会阻塞线程,如果没有获取到就返回 0
int pthread_spin_lock ( pthread_spinlock_t *lock) ;
// 尝试获取指定的自旋锁,如果没有获取到就返回 0
int pthread_spin_trylock(pthread_spinlock_t *lock);
函数参数:
lock:要操作的自旋锁
返回值:
成功返回0,失败返回非0值
3、解锁
对自旋锁解锁
int pthread_spin_unlock(pthread_spinlock_t *lock);
函数参数:
lock:要操作的自旋锁
返回值:
成功返回0,失败返回非0值
4、销毁
将自旋锁销毁
int pthread_spin_destroy(pthread_spinlock_t *lock);
函数参数:
lock:要操作的自旋锁
返回值:
成功返回0,失败返回非0值
五、屏障
屏障(barrier)是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有合作线程都到达某一点,然后从该点继续执行。
举一个简单的例子,要排序1亿个数字,我们假如使用4个线程去做,每个线程排序数据的1/4,等待4个线程全部完成排序后,主线程再做合并即可。
1、初始化
将屏障变量进行初始化。
int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t * attr, unsigned int count);
函数参数:
barrier:待初始化的屏障变量
att:屏障属性,为NULL,则表示使用默认属性
count:指定允许所有线程再wait后继续运行时,必须到达屏障的线程数目
返回值:
成功返回0,失败返回非0值
2、等待其他线程
调用pthread_barrier_wait的线程在屏障计数count未满足条件时,会进入休眠状态。如果该线程是最后一个调用pthread_barrier_wait的线程,即满足了屏障计数,所有的线程都被唤醒,继续执行。
int pthread_barrier_wait(pthread_barrier_t *barrier);
函数参数:
barrier:已初始化的屏障变量
返回值:
成功返回0,失败返回非0值
3、销毁
销毁一个屏障变量
int pthread_barrier_destory(pthread_barrier_t * barrier);
函数参数:
barrier:已初始化的屏障变量
返回值:
成功返回0,失败返回非0值
4、屏障使用示例
伪代码为:
#include <pthread.h>
// 屏障变量
static pthread_barrier_t barrier;
void* fun1(void *arg) {
// 进行排序
// 等待其他线程完成操作
pthread_barrier_wait(&barrier);
}
void* fun2(void *arg) {
// 进行排序
// 等待其他线程完成操作
pthread_barrier_wait(&barrier);
}
void* fun3(void *arg){
// 进行排序
// 等待其他线程完成操作
pthread_barrier_wait(&barrier);
}
int main() {
pthread_t thread1;
pthread_t thread2;
pthread_t thread3;
const int thread_num = 4;
//init
pthread_barrier_init(&barrier, NULL, thread_num);
pthread_create(&thread1, NULL, fun1, NULL);
pthread_detach(thread1);
pthread_create(&thread2, NULL, fun2, NULL);
pthread_detach(thread2);
pthread_create(&thread3, NULL, fun3, NULL);
pthread_detach(thread3);
// 主线程中也排序其中1/4任务
pthread_barrier_wait(&barrier);
// 所有排序完成
pthread_barrier_destroy(&barrier);
return 0;
}
六、信号量
在学习信号量之前,我们必须先知道——Linux提供两种信号量:
- 内核信号量,由内核控制路径使用
- 用户态进程使用的信号量,这种信号量又分为:
- POSIX信号量:
- 有名信号量:其值保存在文件中, 所以它既可以用于线程间同步,也可以用于进程间的同步
- 无名信号量:其值保存在内存中,一般用在线程间同步。
- 系统V信号量:用于进程间同步
- POSIX信号量:
信号量其实也是一种锁,线程获取不到信号量的时候进入睡眠,直至有信号量释放出来时,才会被唤醒,进入临界区继续执行。信号量有二值信号量和计数信号量两种,其中二值信号量比较常用。
二值信号量表示信号量只有两个值,即0和1。信号量为1时,表示临界区可用,信号量为0时,表示临界区不可访问。
信号量API参考博客:https://blog.csdn.net/Chiang2018/article/details/123425121