什么是线程安全?
线程安全就是在多执行流中,对临界资源进行争抢访问,不会造成数据的二义性或者逻辑异常。
临界资源:多线程执行流共享的资源就是临界资源。
临界区:每个线程内部,访问临界资源的代码。
互斥:任何时刻,保证只有一个执行流访问临界资源。
原子性:不会被任何调度机制打断,该操作要么完成,要么未完成。
确保线程安全的方法
通过同步与互斥方式实现线程安全。
同步:通过条件判断实现对资源获取的合理性。
互斥:通过同一时间只有一个执行流能够访问资源,实现资源访问的安全性。
互斥
互斥锁是互斥方式的实现方式。
什么是互斥锁:
互斥锁就是只有0/1的计数器,描述临界资源的可访问/不可访问的状态。一个执行流在访问期间需要把资源状态标记为不可访问,结束后标记为可访问。
这些访问同一资源的执行流在访问前,先去访问互斥锁,判断访问状态。
操作流程:
- 定义互斥锁变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
- 初始化互斥锁变量
pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
- 在访问临界资源前加锁
pthread_mutex_lock(pthread_mutex_t *mutex);
如果不能加锁则阻塞等待。
pthread_mutex_trylock(pthread_mutex_t *mutex);
如果不能加锁则报错返回。–EBUSY。
- 对临界资源访问完毕后进行解锁。
pthread_mutex_unlock(pthread_mutex_t *mutex);
- 如果不使用互斥锁了则释放资源
thread_mutex_destroy(pthread_mutex_t *mutex);
注意:
- 互斥锁只能保护资源访问的安全性,不能保证访问的合理性。
- 所有线程访问同一临界资源,必须访问同一个互斥锁进行保护。
死锁
多个执行流因为资源争夺,但是因为推进顺序不当,陷入互相等待,导致程序流程无法继续推进。
另一篇文章讲解死锁:https://blog.csdn.net/lyly_h/article/details/108281483
同步
通过条件判断,决定执行流是否能够获取资源,保证资源获取的合理性。
条件变量:
当一个线程互斥地访问某个变量时,它可能发现在其他线程改变状态之前,它什么也做不了。
例如:
当一个线程访问队列时,发现队列为空,它只能等待,只能等待其他线程将一个节点添加到队列中。这种情况就需要用到条件变量。
提供了一个pcb等待队列,并且提供了使线程休眠已经唤醒的接口。
执行过程:
- 定义条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- 初始化条件变量
pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
- 在执行流不满足获取资源条件的时候调用接口执行流阻塞等待
thread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
条件变量搭配互斥锁一起使用。
- 在执行流满足获取资源条件之后唤醒等待执行流。
pthread_cond_broadcast(pthread_cond_t *cond);
(至少唤醒一个)
pthread_cond_signal(pthread_cond_t *cond);
(唤醒所有等待的执行流)
- 不使用条件变量则释放资源
pthread_cond_destroy(pthread_cond_t *cond)
生产者消费者模型
通过一个容器来解决生产者和消费者之间强耦合的问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生
产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的
321原则:一个场所、两种角色、三种关系。
应对场景:数据的产生与处理场景。
优点:
- 解耦合:降低各模块程序之间的强关联或依赖性。
- 支持并发:支持对线程生产以及多线程处理提高效率。
- 支持忙闲不均:产生快、处理慢有可能造成数据丢失,队列可以起到数据缓冲的作用。
采用C++代码模拟实现:
#include<iostream>
#include<queue>
#include<stdlib.h>
#include<pthread.h>
#define NUM 8
class BlockQueue
{
private:
std::queue<int> q;//线程队列容器
int cap;//队列中最大节点数量
pthread_mutex_t lock;//互斥锁
pthread_cond_t full;//消费者等待队列
pthread_cond_t empty;//生产者等待队列
private:
void LockQueue()
{
pthread_mutex_lock(&lock);
}
void UnLockQueue()
{
pthread_mutex_unlock(&lock);
}
void ProductWait()
{
pthread_cond_wait(&full, &lock);
}
void ConsumeWait()
{
pthread_cond_wait(&empty, &lock);
}
void NotifyProduce()
{
pthread_cond_signal(&full);
}
void NotifyConsume()
{
pthread_cond_signal(&empty);
}
bool IsEmpty()
{
return(q.size() == 0 ? true : false);
}
bool IsFull()
{
return(q.size() == cap ? true : false);
}
public:
BlockQueue(int _cap = NUM) :cap(_cap)
{//构造函数 初始化
pthread_mutex_init(&lock, NULL);
pthread_cond_init(&full, NULL);
pthread_cond_init(&empty, NULL);
}
void PushData(const int& data)
{//线程安全的数据入队操作
LockQueue();
while (IsFull())
{
NotifyConsume();
std::cout << "queue full, notify consume data, product stop" << std::endl;
ProductWait();
}
q.push(data);
UnLockQueue();
}
void PopData(const int& data)
{//出队
LockQueue();
while (IsEmpty())
{
NotifyProduct();
std::cout << "queue empty, notify product data, consume stop" << std::endl;
ConsumrWait();
}
data = q.front();
q.pop();
UnLockQueue();
}
~BlockQueue()
{//析构函数 释放资源
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&empty);
pthread_cond_destroy(&full);
}
};
void *consumer(void *arg)
{
BlockQueue *bpq = (BlockQueue*)arg;
int data;
for ( ; ; )
{
bpq->PopData(data);
std::cout << "consume data done:" << data << std::endl;
}
}
void *producer(void *arg)
{
BlockQueue *bpq = (BlockQueue*)arg;
srand((unsigned long)time(NULL));
for (;;)
{
int data = rand() % 1024;
bpq->PushData(data);
std::cout << "product data done:" << data << std::endl;
}
}
int main()
{
BlockQueue bp;
pthread_t c, p;
pthread_create(&c, NULL, consumer, (void*)&bp);
pthread_create(&p, NULL, producer, (void*)&bp);
pthread_join(c, NULL);
pthread_join(p, NULL);
return 0;
}
信号量
本质是一个计数器,和一个PCB等待队列。是用来实现线程(进程)间的同步与互斥。
信号量实现互斥:保证最高计数为1,表示同一时间只有一个线程能够访问资源,访问前计数+1,访问完毕后-1。
信号量实现同步:通过对资源的计数,判断资源有多少,访问是否合理。(有资源才能访问,没有资源则阻塞进程)。
实现步骤与接口:
- 定义信号量
sem_t;
- 初始化信号量
sem_init(sem_t *sem, int pshared, unsigned int value);
sem:定义的信号量
pshared:0–表示线程间共享,非0–表示进程间共享
val:信号量初始值
- 访问资源前先访问信号量,判断是否合理,不合理则阻塞,合理计数-1返回。
int sem_wait(sem_t *sem);
- 产生一个资源后,对计数+1,并且唤醒等待队列中等待的线程
int sem_post(sem_t *sem);
- 销毁一个信号量
int sem_destroy(sem_t *sem);