目录速览
1、互斥锁
线程同步机制是多线程协调工作的关键和基础,即控制对共享资源的先后访问(线程同步);常见的线程同步机制包括:互斥锁、读写锁、条件变量和信号量等四种方法。本文将介绍互斥锁的使用。
(1)What(什么是互斥锁)
本质是一个对象,实现对临界区的串行访问(互斥访问)
(2)Why(互斥锁的用途)
互斥锁的用途:实现多线程对共享资源的先后访问
(3)How(如何使用互斥锁)
在Linux中,使用互斥锁机制对共享资源的先后访问顺序如下:定义互斥锁对象、初始化互斥锁对象、关锁、开锁、销毁锁…
step01:定义互斥锁
相当于创建互斥量对象
pthread_mutex mutex;
step02:初始化互斥锁
相当于初始化对象
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
- 参数mutex:待初始化的互斥量对象
- 参数attr:用于指定互斥锁的属性。如果为 NULL,则使用默认属性
- 返回值:初始化成功返回0,否则初始化失败
step03:关锁
本质是调用操作互斥锁对象mutex的一个关锁函数,其它线程在访问该共享资源时,如果互斥锁变量属于关锁状态,则将会被阻塞
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 参数mutex:互斥锁对象的地址
- 返回值:成功关锁(表示成功获取互斥锁变量)返回0,否则返回非0
Note:当锁处于开状态时,调用该函数会改变mutex的状态为关,并记录哪个线程对它上的锁;
当锁处于关状态时,调用该函数会失败,这些调用失败的线程都会阻塞在这把互斥锁对象上。
step04:开锁
本质时调用操作互斥锁对象mutex的一个开锁函数,当获取该互斥锁的对象完成了对共享资源的访问时调用
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 参数mutex:被开始的互斥锁对象
- 返回值:0表示开锁成功
step05:销毁锁
本质就是释放互斥锁对象,将资源归还给操作系统
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 参数mutex:被销毁的互斥锁对象
- 返回值:0表示销毁成功
(4)代码实践
可直接在Linux平台编译和运行
#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>
using namespace std;
pthread_mutex_t mutex;
vector<int> que;
void * threadProduce(void *arg)
{
int i = 0;
while(1){
pthread_mutex_lock(&mutex);
que.push_back(i);
cout<<"push: "<<i<<"(Length is "<<que.size()<<")"<<endl;
++i;
pthread_mutex_unlock(&mutex);
sleep(1);
}
return NULL;
}
void * threadConsume(void *arg)
{
while(1){
pthread_mutex_lock(&mutex);
if(que.size()!=0)
{
int iVal = que.back();
que.pop_back();
cout<<"pop: "<<iVal<<"(Length is "<<que.size()<<")"<<endl;
}
pthread_mutex_unlock(&mutex);
sleep(2);
}
return NULL;
}
int main()
{
pthread_mutex_init(&mutex,NULL);
pthread_t tidPro, tidCom;
pthread_create(&tidPro, NULL, threadProduce, NULL);
pthread_create(&tidCom, NULL, threadConsume, NULL);
pthread_join(tidPro, NULL);
pthread_join(tidCom, NULL);
}
2、读写锁
(1)What(什么是读写锁)
与互斥锁类似,读写锁的本质也是一个类对象,读锁对象允许读线程对临界区的并行访问,而写线程对临界区只能互斥访问(换言之,读写锁是互斥锁的升级版)
(2)Why(读写锁的作用)
在保护共享资源一致性的基础上提高了并发读的性能(换言之,在实现线程同步的基础上提高了并发读的性能)
(3)How(如何使用读写锁)
step01:定义读写锁及类型
本质就是创建一个读写锁对象,并指定锁的类型是读锁还是写锁
pthread_rwlock_t rwlock;
step02:初始化读写锁对象
本质就是给读写锁对象赋初始值
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr = NULL);
- 参数rwlock:要初始化的读写锁对象
- 参数attr:指定读写锁的属性,通常传递 NULL 以使用默认属性
- 返回值:0表示初始化成功
step03:写锁锁定临界区
本质就是调用函数修改读写锁对象的状态,写锁只允许线程互斥访问临界区,当获取失败时,对应的线程会阻塞在这个读写锁对象上
#include <pthread.h>
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
- 参数rwlock:被上写锁的读写锁对象
- 返回值:0表示上写锁成功
step04:读锁锁定临界区
本质就是调用函数修改读写锁对象的状态,读锁允许读线程共同访问临界区,当该锁处于写状态时,会导致上读锁失败,线程阻塞
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
- 参数rwlock:被上读锁的读写锁对象
- 返回值:0表示上读锁成功
step05:解锁
本质还是调用函数修改读写锁的状态,解锁之后,该读写锁对象对临界区的管理将会解除
#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
- 参数rwlock:将要被释放的读写锁对象
- 返回值:0表示释放读写锁成功
step06:销毁读写锁对象
本质就是释放读写锁占有的内存,以归还给操作系统
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
- 参数rwlock:被销毁的读写锁对象
- 返回值:0表示销毁成功
(4)读写锁的特征
- 如果是读锁锁定了临界区,那么可以并行读取临界区,因为读锁对读是共享的
- 如果是写锁锁定了临界区,那么只能串行访问临界区,因为写锁对读和写都是独占的
3、条件变量
(1)What(什么是条件变量)
一种线程同步机制,在线程中以睡眠的方式等待某一条件的发生,当条件不成立时挂起线程,条件成立时唤醒线程
(2)Why(条件变量的作用)
通过条件的满足与否来控制线程的等待与运行,目的是实现对线程的等待与唤醒以保证多线程之间对共享资源的安全有序访问(即实现线程同步)
(3)How(如何使用条件变量实现线程同步)
条件变量通常和互斥锁结合使用
step01:定义条件变量
在Linux中,条件变量的本质是一个数据结构(不同的操作系统其本质可能有一点要求)
pthread_cond_t cond;
step02:初始化条件变量
本质是给条件变量赋初始值
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr=NULL);
参数cond:待初始化的条件变量
参数attr:条件变量的属性,不传代表使用默认参数
返回值:初始化成功返回0,初始化失败返回错误码
step03:阻塞线程
本质就是调用函数改变条件变量的状态,阻塞当前线程,直到条件变量被唤醒
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
参数cond:要等待的条件变量
参数mutex:与条件变量关联的互斥锁
返回值:成功返回0,失败返回错误码
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
上述函数与 pthread_cond_wait 类似,但会在指定的超时时间timespec后自动唤醒
step04:唤醒线程
本质就是调用函数改变条件变量的状态,发送信号给条件变量,唤醒阻塞在当前条件变量下的一个线程
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
参数cond:要发送信号的条件变量
返回值:成功返回0,失败返回错误码
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
唤醒所有等待在该条件变量上的线程
step05:销毁条件变量
本质就是调用函数释放条件变量占用的内存空间
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
- 参数cond:要销毁的条件变量
- 返回值:成功返回0,失败返回错误码
说明:
在典型的使用场景中,线程在访问共享资源之前会先获取互斥锁,然后判断对应的条件变量是否满足,如果条件不满足,线程会调用条件变量的等待操作,并释放互斥锁进入阻塞状态;当其它线程修改了共享数据并满足条件时,它会通过条件变量的信号或广播操作来唤醒等待该条件的线程。被唤醒的线程重新获取互斥锁,检查条件是否满足,如果条件满足就继续执行临界区的操作。
(4)生产者-消费者模型
使用互斥锁和条件变量实现生产者-消费者模型。生产者-消费者模式是一种常见的并发编程模型,用于解决生产者和消费者之间的协作和同步问题。
A.What(什么是生产者-消费者模型)
一种常见的并发编程模型。模型包括三个重要元素:生产者、消费者和共享缓冲区。生产者不断生产数据,如果共享缓冲区已满,生产者会等待,直到缓冲区有空闲空间。消费者不断从缓冲区中获取数据,如果缓冲区为空,消费者会等待,直到有新的数据被生产者放入缓冲区。
B.Why(该模型的作用)
- 解耦了生产者和消费者的操作,使它们可以独立地运行
- 提高了系统的并发性和效率,生产者和消费者可以同时工作,不必相互等待
C.How(如何使用条件变量和互斥锁实现该模型)
下列示例中,生产者可以无限生产,消费者需要判断缓冲区是否有产品可以消费。
全局变量
pthread_cond_t cond; //条件变量,用于控制消费者线程
pthread_mutex_t mutex; //互斥锁
sturct Node * head = NULL; //缓冲区
生产者线程
void * produce(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
//TODO:向head中添加数据节点
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond); //唤醒消费线程
}
return NULL;
}
消费者线程
void * consume(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
while(head == NULL) //不满足消费条件,所以阻塞
{
pthread_cond_wait(&cond, &mutex);
}
pthread_mutex_unlock(&mutex);
}
return NULL;
}
main函数
void main()
{
pthread_mutex_init(&mutex);
pthread_cond_init(&cond);
pthread_t consumer[5], producer[5];
for(int i = 0; i<5; ++i)
{
pthread_create(&consumer[i], NULL, consume, NULL);
pthread_create(&producer[i], NULL, produce, NULL);
}
//TODO:释放工作
}
4、信号量
(1)What(什么是信号量)
提供一种计数器的方式控制对共享资源的访问;当计数器大于0时,请求资源成功并计数器-1;当计数器小于0时,线程阻塞,等待其它线程执行signal(V操作)唤醒它
(2)Why(信号量的作用)
- 实现线程的同步与互斥:通过信号量的设计,可以实现对共享资源的串行访问
- 实现线程的等待与通知机制:当信号量小于0时,当前线程将被阻塞;当信号量大于0时,会唤醒一个阻塞在信号量上的线程
(3)How(如何使用信号量实现线程同步)
step01:创建信号量
可以看作是创建一个信号量对象
sem_t sem;
step02:初始化信号量
可以看作是对信号量对象的一个初始化,这一过程会给信号量的计数器赋予一个初始值
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 参数sem:被初始化的信号量对象
- 参数pshared:默认为0,表示信号量用于线程同步;其它表示信号量用于进程同步
- 参数value:表示信号量的数量,常用用于表示共享资源的数量
- 返回值:成功返回0,失败返回-1,并设置错误码
step03:请求资源
请求获取共享资源,此时信号量的计数器减1;如果信号量小于1,请求失败,线程阻塞,直到信号量满足条件时解除阻塞
int sem_wait(sem_t *sem);
- 参数sem:请求共享资源,如果sem中的计数器大于0,则请求成功,否则线程阻塞
- 返回值:成功返回0,失败返回-1,并设置错误码
step04:释放资源
释放共享资源,此时信号量的计数器加1,此时会唤醒一个等待该共享资源的线程
int sem_post(sem_t *sem);
- 参数sem:释放共享资源,sem中的计数器+1
- 返回值:成功返回0,失败返回-1,并设置错误码
step05:销毁信号量
本质就是释放信号量对象的内存空间
int sem_destroy(sem_t *sem);
- 参数sem:将要被销毁的信号量对象
- 返回值:成功返回0,失败返回-1,并设置错误码
(4)代码实例
以下代码是对共享资源的互斥访问,共享资源的个数为5
#include <semaphore.h>
#include <unistd.h>
#include <iostream>
#include <pthread.h>
using namespace std;
pthread_mutex_t mutex;
sem_t semProc;
sem_t semComu;
struct Node{
int iVal;
Node * ptrNext;
static int iSize;
};
int Node::iSize = 0;
Node * head = NULL;
void *produce(void *arg){
int i = 0;
while(1){
sem_wait(&semProc);
pthread_mutex_lock(&mutex);
int iVal = i;
Node *node = new Node;
node->iVal = iVal;
node->ptrNext = head->ptrNext;
head->ptrNext = node;
Node::iSize++;
cout<<"Producing "<<iVal<<"("<<Node::iSize<<")"<<endl;
pthread_mutex_unlock(&mutex);
sem_post(&semComu);
++i;
sleep(1);
}
}
void *consume(void *arg){
while(1){
sem_wait(&semComu);
pthread_mutex_lock(&mutex);
if(head->ptrNext!=NULL)
{
cout<<"Comsuing "<<head->ptrNext->iVal;
Node *ptr = head->ptrNext;
head->ptrNext = ptr->ptrNext;
Node::iSize--;
delete ptr;
cout<<"("<<Node::iSize<<")"<<endl;
ptr = 0;
}
pthread_mutex_unlock(&mutex);
sem_post(&semProc);
sleep(2);
}
return NULL;
}
int main()
{
head = new Node;
pthread_t tidProc, tidComu;
sem_init(&semProc, 0, 5);
sem_init(&semComu, 0, 0);
pthread_create(&tidProc, NULL, produce, NULL);
pthread_create(&tidComu, NULL, conmuse, NULL);
pthread_join(tidProc, NULL);
pthread_join(tidComu, NULL);
return 0;
}
标签:线程,信号量,互斥,cond,pthread,mutex
From: https://blog.csdn.net/qq_42279379/article/details/140999342