线程安全
文章目录
一:线程的执行方式
- 线程之间就像是比赛时的起跑者,如果没有裁判和比赛规则约束,谁都有可能抢跑.谁都有可能领先夺冠.
- 线程是抢占式执行的,正是因为这种抢占式的执行方式.引来了线程安全问题
- 如下面的程序
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
//互斥锁/互斥量
pthread_mutex_t mutex;
#define THREAD_NUM 2
int g_count=0;
void *ThreadEntry(void * arg){
(void) arg;
for(int i=0;i<500000;++i){
//如果当前锁已经被其他线程获取到了,
//当前线程再想获取就会在lock函数处阻塞
pthread_mutex_lock(&mutex);
++g_count;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(){
pthread_mutex_init(&mutex,NULL);
pthread_t tid[THREAD_NUM];
for(int i=0; i < THREAD_NUM ; ++i){
pthread_create(&tid[i],NULL,ThreadEntry,NULL);
}
for(int i=0;i<THREAD_NUM;++i){
pthread_join(tid[i],NULL);
}
printf("g_count = %d \n",g_count);
pthread_mutex_destroy(&mutex);
return 0;
}
【执行结果】
【结果分析】
- 该程序创建了两个线程,若线程安全的话得到的结果应该是2x500000,而不会出现这种情况
- 可以看到多次的执行效果都是不同的,那么多次执行相同的程序为什么会出现不同的结果呢?
- 原因是线程的抢占式执行引起的线程安全问题,
- 当线程A从内存中读到数据进行计算时,没等到线程A结束线程B就从内存中读取了和线程A相同的数据,最后当两个线程将结果写入到内存时,发现两者的执行结果是相同的所以只写了一次.这样导致程序在执行过程中好多次的执行过程都是"无用功".
1.线程安全相关知识
1.1临界资源
- 临界资源:多个线程执行共享的资源
- 临界区:每个线程内部,访问临界资源的代码,叫做 临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界区资源起到保护作用
- 原子性:不会被任何调度机制打断的操作,该操作是有两种状态要么完成,要么没完成
1.2.互斥量mutex
- 大部分情况,线程使用的数据都是局部数据,变量的地址空间在线程栈空间内,这样的话变量就归单个线程,其他线程无法获取这种变量
- 但有时多个线程会共享一个变量,这样的变量就是共享变量,可以通过数据的共享,完成线程之间的交互
1.3.线程锁🔒的引入
- 由于线程抢占式执行的特点,导致的问题如何解决呢?
做到这些需要解决三个问题
- 1.代码必须要有互斥行为,当代码进入临界区执行时,不允许其他线程进入该临界区
- 2.如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么允许一个线程进入该临界区
- 3.如果线程不在临界区中执行,那么该线程不能组织其他线程进入临界区
要做到这三点,本质上就是需要将临界区的资源锁起来.Linux中提供的锁叫做互斥量
2.解决线程的不安全问题
在临界区中使用"互斥机制"就能解决线程不安全的问题
互斥锁(单车道洗车车间门)
- 在自动洗车间洗车的时候,只有一辆车能够通过这个洗车道.所以当第一辆车进入洗车间后,洗车间系统就将入口上锁,其他的车只能在门外等待第一辆车洗车结束之后,才能进入洗车间洗车.这种加锁的方式有效的避免了车与车之间的碰撞等问题。
- 那么这个洗车间就相当于线程锁的有效段(进入才有效,出来就失效了) .当上一个线程拿到线程锁后若他没有释放锁,其他线程只能在线程锁外等待着获取锁的资格。
【过程总结】
- 1.先加锁
- 2.执行临界区代码
- 3.释放锁
同一时刻只能有一个线程获取到锁,只有这个获取到锁的线程才能执行临界区的代码,其他线程只能等待其释放锁之后才有获取锁的权利
线程执行时先去获取锁,两个线程同时执行.先获取到锁的线程先执行,后获取锁的线程等待获取锁.
待先获取锁的线程执行完后再对等待的线程加锁后执行。
【总结】
- 为了避免出现多个线程相互之间影响执行效果而设置的一种保障机制,使得先获取到锁的线程在不受外界影响的情况下执行临界区的代码.待其执行完之后.从而避免相互之间干扰的情况.
2.1.互斥量的接口
- 初始化互斥量的两种方法
- 1.静态分配
pthread_mutex_t mutex=PTHREAD_MUTEX_INITALIZER;
- 2.动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t*restrict attr);
- 销毁互斥量
- 销毁互斥量需要注意
- 使用pthread_mutex_initializer 初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁 的互斥量,要确保之后不会有线程再尝试加锁
int pthread_mutex_destory(pthread_mutex_t *mutex);
- 互斥量加索和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//返回值:成功返回0,失败返回错误码
- 互斥锁 pthread_mutex 挂起等待锁,一旦线程获取锁失败,就会挂起(进入到操作系统提供的一个等待队列中)
- 互斥锁能够保证线程安全,最终的程序效率会受到影响
- 除此之外,还有一个严重的问题–>死锁
【代码示例】
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
//互斥锁/互斥量
pthread_mutex_t mutex;
#define THREAD_NUM 2
int g_count=0;
void *ThreadEntry(void * arg){
(void) arg;
for(int i=0;i<50000;++i){
//如果当前锁已经被其他线程获取到了,
//当前线程再想获取就会在lock函数处阻塞
pthread_mutex_lock(&mutex);
++g_count;
pthread_mutex_unlock(&mutex);
//这个线程不会在其他线程释放锁之后立刻就能恢复执行
//而是在其他线程释放锁之后,由操作系统决定执行时间
}
return NULL;
}
int main(){
pthread_mutex_init(&mutex,NULL);
pthread_t tid[THREAD_NUM];
for(int i=0; i < THREAD_NUM ; ++i){
pthread_create(&tid[i],NULL,ThreadEntry,NULL);
}
for(int i=0;i<THREAD_NUM;++i){
pthread_join(tid[i],NULL);
}
printf("g_count = %d \n",g_count);
pthread_mutex_destroy(&mutex);
return 0;
}
2.2.死锁的两个场景
【死锁简介】
- 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待的状态
【死锁的四个必要条件】
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
【情景一】
- 1.一个线程在加锁后(没有解锁的情况下)再尝试加锁
- 即一个线程两个锁的现象
void *ThreadEntry(void * arg){
(void) arg;
for(int i=0;i<50000;++i){
pthread_mutex_lock(&mutex);
++g_count;
pthread_mutex_lock(&mutex);
//第一次获取锁之后没有释放就再次加锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
【情景二】
- 2.两个线程1、2有两把锁A、B.
.线程1先去获取锁A,再去获取锁B.同时线程2先去获取锁B ,再去获取锁A,也会死锁.
【经典故事】
多个进程多把锁的问题(哲学家吃饭)
- 哲学家的行为(五根筷子五个人)
- 五个人五只筷子,每个人拿起自己右手/左手边的筷子.每个人只能拿到一只筷子.(一只筷子没办法吃鸡).五个哲学家谁都不让谁,每人拿着一根筷子僵持着.导致的结果是谁都吃不了鸡.
如何解决这种尴尬场面(死锁问题)呢?
比较实用的死锁解决办法(从代码设计的角度来解绝死锁问题)
- 1.短: 让临界区代码尽量短
- 2.平: 临界区代码尽量不去调用其他复杂函数
- 3.快: 临界区代码执行速度尽量快,别做太耗时的操作
死锁的解决办法(针对哲学家就餐问题)
- 1.先给每根筷子编号,约定先拿编号小的筷子
这样的约定是破除死锁的常见办法,破除死锁中的环路条件
- 2.弄一个信号量(计数器),
申请资源的时候搞一个信号量,信号量记录的是当前可用资源的个数
如果当前数值为0了,申请资源操作就会等待
每个哲学家拿筷子的时候先进性P操作(其中的计数器记载的是可用资源的数目)
二:线程----->同步
【同步】:
- 线程是抢占式执行的,所以没有办法控制次序.因此同步控制着线程与线程之间执行顺序(主要还是抢占式执行的结果,有时需要线程和线程之间按照一定的顺序来执行)
【滑稽取钱】
- 一个人不离开ATM机一直取钱,一直占用ATM机,让外面的人一直等待着(线程饿死)
【同步】
-
取钱得时候ATM机没钱了,占用ATM机的滑稽只能退出机房在外等待,等到押炒员将钱拿过来再继续取钱
-
线程锁结束之后的情景
1.释放锁
2.等待条件就绪(1、2两步操作必须是原子的,否则就会错过其他线程的通知消息,会导致一直等待的情况发生)
3.重新获取锁,准备执行后续的操作
1.条件变量的使用
【简单介绍】
- 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了,只能等待其他线程改变状态。
- 例如一个线程访问队列时发现队列为空,它只能等待.只到其它线程将一个节点添加到队列中,这种情况就需要用到条件变量。
简单来说,对于条件变量函数来说.
只有在线程满足某种特性的情况下才能
使用,否则会一直等待着
【条件变量函数】
- 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
- 参数:
- cond:要初始化的条件变量
- attr:NULL
- 销毁
int pthread_cond_destroy(pthread_cond_t *cond)
大部分的情况下,条件变量要搭配互斥锁来使用
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
//互斥锁/互斥量
pthread_mutex_t mutex;
pthread_cond_t cond;
int g_count=0;
void *ThreadEntry1(void * arg){
(void) arg;
while(1){
printf("传球\n");
pthread_cond_signal(&cond);
//等待扣篮老哥的信号
usleep(789789);
}
return NULL;
}
void *ThreadEntry2(void *arg){
(void) arg;
while(1){
pthread_cond_wait(&cond,&mutex);
//TODO 搭配互斥锁
//执行这个pthread_cond_wait 函数就会导致线程被阻塞
//阻塞到其他线程发送一个通知
printf("扣篮\n");
usleep(123456);
}
return NULL;
}
int main(){
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
pthread_t tid1,tid2;
pthread_create(&tid1,NULL,ThreadEntry1,NULL);
pthread_create(&tid2,NULL,ThreadEntry2,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
2.生产者消费者模型
- 优点
- 解耦
- 支持并发
- 支持忙先不均
这是一个多线程场景中的典型应用,应用场景非常广泛
分工协作,提高效率
生产者负责产生数据,把数据放到交易场所中
消费者负责消费数据,把数据从交易厂所中取走
- 消费者之间为互斥关系(抢购一份东西)
- 生产者之间也为互斥关系(提供一份商品)
- 生产者与消费者之间互斥同步关系
3.c++提供的用法
c++queue模拟阻塞队列的生产消费模型
#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 NotifyProduct()
{
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);
// NotifyConsume();
UnLockQueue();
}
void PopData(int &data)
{
LockQueue();
while(IsEmpty()){
NotifyProduct();
std::cout << "queue empty, notify product data, consume stop." <<
std::endl;
ConsumeWait();
}
data = q.front();
q.pop();
// NotifyProduct();
UnLockQueue();
}
~BlockQueue()
{
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&full);
pthread_cond_destroy(&empty);
}
};
void *consumer(void *arg) {
BlockQueue *bqp = (BlockQueue*)arg;
int data;
for( ; ; ){
bqp->PopData(data);
std::cout << "Consume data done : " << data << std::endl;
}
}
//more faster
void *producter(void *arg) {
BlockQueue *bqp = (BlockQueue*)arg;
srand((unsigned long)time(NULL));
for( ; ; ){
int data = rand() % 1024;
bqp->PushData(data);
std::cout << "Prodoct data done: " << data << std::endl;
// sleep(1);
}
}
int main()
{
BlockQueue bq;
pthread_t c,p;
pthread_create(&c, NULL, consumer, (void*)&bq);
pthread_create(&p, NULL, producter, (void*)&bq);
pthread_join(c, NULL);
pthread_join(p, NULL);
return 0; }
- 运行结果
三.线程池(频繁申请和销毁大量空间)
/*threadpool.h*/
/* 线程池:
* 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线
程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够
保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络
sockets等的数量。
* 线程池的应用场景:
* 1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线
程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任
务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
* 2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
* 3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线
程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内
存到达极限,出现错误.
* 线程池的种类:
* 线程池示例:
* 1. 创建固定数量线程池,循环从任务队列中获取任务对象,
* 2. 获取到任务对象后,执行任务对象中的任务接口
* /*threadpool.hpp*/
#ifndef __M_TP_H__
#define __M_TP_H__
#include <iostream>
#include <queue>
#include <pthread.h>
#define MAX_THREAD 5
typedef bool (*handler_t)(int);
class ThreadTask
{
private:
int _data;
handler_t _handler;
public:
ThreadTask()
:_data(-1)
, _handler(NULL)
{}
ThreadTask(int data, handler_t handler) {
_data= data;
_handler = handler;
}
void SetTask(int data, handler_t handler) {
_data = data;
_handler = handler;
}
void Run() {
_handler(_data);
}
};
class ThreadPool
{
private:
int _thread_max;
int _thread_cur;
bool _tp_quit;
std::queue<ThreadTask *> _task_queue;
pthread_mutex_t _lock;
pthread_cond_t _cond;
private:
void LockQueue() {
pthread_mutex_lock(&_lock);
}
void UnLockQueue() {
pthread_mutex_unlock(&_lock);
}
void WakeUpOne() {
pthread_cond_signal(&_cond);
}
void WakeUpAll() {
pthread_cond_broadcast(&_cond);
}
void ThreadQuit() {
_thread_cur--;
UnLockQueue();
pthread_exit(NULL);
}
void ThreadWait(){
if (_tp_quit) {
ThreadQuit();
}
pthread_cond_wait(&_cond, &_lock);
}
bool IsEmpty() {
return _task_queue.empty();
}
static void *thr_start(void *arg) {
ThreadPool *tp = (ThreadPool*)arg;
while(1) {
tp->LockQueue();
while(tp->IsEmpty()) {
tp->ThreadWait();
}
ThreadTask *tt;
tp->PopTask(&tt);
tp->UnLockQueue();
tt->Run();
delete tt;
}
return NULL;
}
public:
ThreadPool(int max=MAX_THREAD):_thread_max(max), _thread_cur(max),
_tp_quit(false) {
pthread_mutex_init(&_lock, NULL);
pthread_cond_init(&_cond, NULL);
}
~ThreadPool() {
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_cond);
}
bool PoolInit() {
pthread_t tid;
for (int i = 0; i < _thread_max; i++) {
int ret = pthread_create(&tid, NULL, thr_start, this);
if (ret != 0) {
std::cout<<"create pool thread error\n";
return false;
}
}
return true;
}
bool PushTask(ThreadTask *tt) {
LockQueue();
if (_tp_quit) {
UnLockQueue();
return false;
}
_task_queue.push(tt);
WakeUpOne();
UnLockQueue();
return true;
}
bool PopTask(ThreadTask **tt) {
*tt = _task_queue.front();
_task_queue.pop();
return true;
}
bool PoolQuit() {
LockQueue();
_tp_quit = true;
UnLockQueue();
while(_thread_cur > 0) {
WakeUpAll();
usleep(1000);
}
return true;
}
};
#endif
/*main.cpp*/
bool handler(int data)
{
srand(time(NULL));
int n = rand() % 5;
printf("Thread: %p Run Tast: %d--sleep %d sec\n", pthread_self(), data, n);
sleep(n);
return true; }
int main()
{
int i;
ThreadPool pool;
pool.PoolInit();
for (i = 0; i < 10; i++) {
ThreadTask *tt = new ThreadTask(i, handler);
pool.PushTask(tt);
}
pool.PoolQuit();
return 0;
}