线程互斥与同步
多线程访问出现问题的例子
假设我们现在有1000张票,4个线程来抢。没有线程互斥与同步机制我们来看下会发生什么
void* Routine( void * args)
{
const char* name = static_cast<const char*> (args);
while (true)
{
if(tickets > 0)
{
usleep(1000);
std::cout << name << " get tickets" << tickets << std::endl;
tickets--;
}else
{
break;
}
// sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tids[3];
for(int i = 0 ; i < 3 ;i++)
{
char * name = new char[1024];
snprintf(name,1024,"thread-%d",i+1);
pthread_create(tids + i,nullptr,Routine,name);
}
for(int i = 0 ; i< 3; i++)
{
pthread_join(tids[i],nullptr);
}
return 0;
}
我们发现tickets 出现了负数 意味着没有票了还在卖~
原因解释
因为cpu的寄存器只有一套,而寄存器里面的数据可以有多套。
有一个前置知识就是 if 判断 和 – 操作都不是原子的。 所谓原子就是一个整体,这个步骤不能再被切割了。 再具体一点就是一条语句只对应一条汇编就是原子的。
有一个关键点就是这里usleep(1000) 三个线程 都有很大概率进入if条件了(因为在–前睡眠了)。但是 我们减减那里虽然可以并行但是没有睡眠概率比较小,就依次减减了。
如果我们在–前加sleep 加大并行的概率,那么我们的–操作中第二步减数据就被覆盖了,就不会出现每个线程都有效减一的情况。
解决这个问题
就要引入今天的主题了线程互斥~
我们先介绍几个概念
临界资源:就是是在多线程或多进程环境中,被多个线程或进程共享的一些资源。
临界区:界区是指一段代码或者程序的一个部分,这段代码需要访问共享资源,
对临界资源的保护就是对临界区代码的保护~
互搓锁
为了解决这个问题我们可以对临界区进行加锁~
互斥锁的接口
应用全局的互斥锁解决这个问题
全局锁只需要初始化 不需要destory
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
//当你声明一个 pthread_mutex_t 类型的全局变量或静态变量,并使用 PTHREAD_MUTEX_INITIALIZER 初始化时,你实际上已经在编译时为这个互斥锁分配了存储空间并初始化了它。
//这意味着在程序运行之前,互斥锁就已经处于一个有效且可用的状态。
// 就不需要用 pthread_mutex_init 了
void* Routine( void * args)
{
const char* name = static_cast<const char*> (args);
while (true)
{
pthread_mutex_lock(&gmutex);
if(tickets > 0)
{
usleep(1000);
std::cout << name << " get tickets" << tickets << std::endl; // 1
tickets--;
pthread_mutex_unlock(&gmutex); // 1
}else
{
pthread_mutex_unlock(&gmutex);
break;
}
// pthread_mutex_unlock(&gmutex); // 2
// 锁的位置用1 这样 不用 2 是因为如果减到0走了else 这个锁就没释放 其他线程就被阻塞在锁那里了
}
return nullptr;
}
int main()
{
pthread_t tids[4];
pthread_mutex_init(&gmutex,nullptr);
for(int i = 0 ; i < 4 ;i++)
{
char * name = new char[1024];
snprintf(name,1024,"thread-%d",i+1);
pthread_create(tids + i,nullptr,Routine,name);
}
for(int i = 0 ; i< 4; i++)
{
pthread_join(tids[i],nullptr);
}
return 0;
}
应用局部锁
因为我们为了传局部锁给每个线程和给每个线程名字。所以必须把这两个变量封装这一个类。才能同时都给线程~
class ThreadData
{
public:
char * _name;
pthread_mutex_t* _pmutex;
public:
ThreadData(char* name, pthread_mutex_t* pmutex)
:_name(name),_pmutex(pmutex)
{
}
};
void* Routine( void * args)
{
ThreadData * td = static_cast<ThreadData*> (args);
while (true)
{
pthread_mutex_lock(td->_pmutex);
if(tickets > 0)
{
usleep(1000);
std::cout << td->_name << " get tickets" << tickets << std::endl; // 1
tickets--;
pthread_mutex_unlock(td->_pmutex); // 1
}else
{
pthread_mutex_unlock(td->_pmutex);
break;
}
// pthread_mutex_unlock(&mutex); // 2
}
return nullptr;
}
int main()
{
pthread_t tids[4];
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,nullptr);
for(int i = 0 ; i < 4 ;i++)
{
char * name = new char[1024];
snprintf(name,1024,"thread-%d",i+1);
ThreadData td(name,&mutex);
pthread_create(tids + i,nullptr,Routine,&td);
}
for(int i = 0 ; i< 4; i++)
{
pthread_join(tids[i],nullptr);
}
pthread_mutex_destroy(&mutex);
return 0;
}
线程加锁注意事项
从原理的角度认识互斥锁
就是mutex 申请成功就返回,失败就就堵在pthread_mutex_lock() 函数内部。
堵塞在某个函数内部,这个其实我们经常遇见比如我们用的scanf 输入完成前就是堵塞在scanf函数内部
从实现的角度理解互斥锁
xchgb 交换 这条汇编 和我们写的swao不一样 它是用硬件实现的。
先把 寄存器al的值赋为0,然后把锁(所里面的内容1) 交换。 谁得到这个1就可以返回出pthread_muex_lock函数。
条件变量
直观认识条件变量
有一个vip自习室 只能运行一个人拿到锁进去~
午饭时间到了小明肚子开始饿了,准备出去吃午饭,于是他就把锁挂在墙上,但是他想这自习室这么好有空调,又安静~别人也想拿到锁进去。于是他放弃干饭,继续学习了。因为它离锁更近,一下就从墙上拿到了。此时小红和小刚就不同意了。
为了解决这个问题,图书管理员想了个办法,就是出去就重新排队!
这个队列就相当于条件变量!
让线程表现出一定的顺序关系称为同步~
一次只让一个线程访问临界资源称为互斥
互斥是为了避免数据的二义性,而同步是让线程访问公共资源更加合理。
图书管理员在哪喊号,就类似于唤醒条件变量下的线程!
这个顺序一定时先进先出吗?不一定 因为signal()操作会选择一个线程从该队列中唤醒。但是,具体选择哪个线程被唤醒通常依赖于底层操作系统调度器的策略,这可能基于先来先服务(FIFO)、优先级、或者其它一些算法。 所以你不能依赖于signal()唤醒特定的线程,除非你的系统明确支持某种特定的唤醒策略,并且你的程序逻辑可以处理任何可能的唤醒顺序
接口
熟悉接口的测试代码
pthread_mutex_t gmutex= PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;
void * Wait(void * args)
{
char *name = (char *)args;
while(true)
{
pthread_mutex_lock(&gmutex);
pthread_cond_wait(&gcond,&gmutex);
std::cout<<name<<std::endl;
// sleep(1);
pthread_mutex_unlock(&gmutex);
}
return nullptr;
}
int main()
{
pthread_t tids[5];
for(int i = 0; i < 5; i++)
{
char * name = new char[1024];
snprintf(name,1024,"thread-%d",i+1);
usleep(10000);
pthread_create(tids+i,nullptr,Wait ,(void*)name);
}
sleep(1);
while(true)
{
pthread_cond_signal(&gcond);
sleep(1);
}
for(auto tid: tids)
{
pthread_join(tid,nullptr);
}
return 0;
}
解决抢火车票每个线程不均匀的问题
int tickets = 10000;
class ThreadData
{
public:
char * _name;
pthread_mutex_t* _pmutex;
pthread_cond_t * _pcond;
public:
ThreadData(char* name, pthread_mutex_t* pmutex,pthread_cond_t * pcond)
:_name(name),_pmutex(pmutex),_pcond(pcond)
{
}
~ThreadData()
{
delete[] _name;
}
};
pthread_cond_t cond[4] = {PTHREAD_COND_INITIALIZER};
int next = 0; // cond 的下标
void* Routine( void * args)
{
ThreadData * td = static_cast<ThreadData*> (args);
while (true)
{
pthread_mutex_lock(td->_pmutex);
while(td->_pcond != cond+next) // 一定要注意最后唤醒的cond+next下的线程 否则会重新睡眠
{
pthread_cond_wait(td->_pcond,td->_pmutex);
}
if(tickets > 0)
{
std::cout << td->_name << " get tickets" << tickets << std::endl; // 1
tickets--;
next = (++next) % 4;
pthread_mutex_unlock(td->_pmutex); // 1
}else
{
next = (++next) % 4; // 按顺序退出,唤醒对应的条件变量下的进程
pthread_mutex_unlock(td->_pmutex);
break;
}
// pthread_mutex_unlock(&mutex); // 2
}
return nullptr;
}
int main()
{
pthread_t tids[4];
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,nullptr);
for(int i = 0 ; i < 4 ;i++)
{
char * name = new char[1024];
snprintf(name,1024,"thread-%d",i+1);
ThreadData *td = new ThreadData(name,&mutex,cond+i); // important! crucial
pthread_create(tids + i,nullptr,Routine,td);
}
int k = 0;
while(tickets > 0)
{
pthread_cond_signal(cond+k);
k++;
k %= 4;
}
int cur = next;
for(int i = 0 ; i< 4; i++)
{
pthread_cond_signal(cond+cur); // 把next对应的 线程唤醒
// 如果不是则会重新睡眠
cur = (++cur) % 4;
usleep(100);
}
//std::cout<<"开始 join"<<std::endl;
for(int i = 0 ; i< 4; i++)
{
pthread_join(tids[i],nullptr);
}
pthread_mutex_destroy(&mutex);
return 0;
}
这样每个线程就表现出了一定的顺序性即同步关系~
生产消费者模型
这个模型可以解决忙闲不均的问题。比如说供应商生产的很快,消费者消费的很慢,以至于把超市的货架都塞满了,我们让供应商先停止生产。超市做了个缓存,比如说要过年了工厂关门了,但是超市上还是有商品供我们选择。
效率高因为我们我们消费的时候,供应商正在生产,我们消费行为和供应商生产行为(准备发生的数据)并发了。 我们在用商品时(处理数据)和供应商放商品到货架上也并发了。
生产者消费者完成解耦,解耦是指减少生产者和消费者之间的直接依赖关系,使两者可以独立工作,某个生产者或者消费者异常了并不影响对方。
有一个关键点 记忆有一个口诀 321原则
1.一个交易场所(特点数据结构形式存在的一段内存空间)
2.两种角色 生产线程和消费线程
3.三种关系(生产者和生产者,生产者和消费者,消费者和消费者)
消费者和生产者之间是互斥的,比如我正在写hello word,你就来读了,可能你读到的就只有hello。我正在读你就来写了,可能你把之前写的覆盖了,我读到的就是垃圾数据。但是超市空了,消费者就必须阻塞,超市满了生产者就必须阻塞,他们又表现出同步关系。
生产者和生产者之间也是互斥,同行是冤家嘛~ 因为同时往一个队列里面写数据,可能造成覆盖。
消费者和消费者之间也是互斥的,因为例如,如果两个消费者同时尝试从队列中取出一个元素,可能会导致一个元素被取出两次,或者队列状态混乱。
我们用 互斥锁和条件变量来实现
#include"BlockQueue.hpp"
#include<pthread.h>
#include<ctime>
#include<sys/types.h>
#include<stdlib.h>
#include<unistd.h>
#include<iostream>
#include"task.hpp"
void * producer(void *args)
{
BlockQueue<task_t>* blockqueue = static_cast<BlockQueue<task_t>*>(args);
srand(time(nullptr)^getpid());
while(true)
{
// 生产
// int x = rand()%10 + 1;
// usleep(10000);
// int y = rand()%10 + 1;
// Task a(x,y) ;
blockqueue->equeue(DownLoad);
std::cout<<"prodecder:"<<"send task"<<std::endl;
sleep(1);
}
return nullptr;
}
void * consumer(void *args)
{
BlockQueue<task_t>* blockqueue = static_cast<BlockQueue<task_t>*>(args);
while(true)
{
//消费
task_t data;
blockqueue->pop(&data);
data();
//data.operator();
// task_t();
//std::cout<<"consumer:"<<data.solve()<<std::endl;
}
return nullptr;
}
int main()
{
pthread_t c[2],p[2];
BlockQueue<task_t>* blockqueue = new BlockQueue<task_t>();
for(int i = 0; i < 2; i++)
{
pthread_create(c+i,nullptr,consumer,(void*)blockqueue);
pthread_create(p+i,nullptr,producer,(void*)blockqueue);
}
for(int i = 0; i < 2; i++)
{
pthread_join(*(p+i),nullptr);
pthread_join(*(c+i),nullptr);
}
return 0;
}
#include<queue>
#include<pthread.h>
#include<iostream>
template <class T>
class BlockQueue
{
int _cap_max;
std::queue<T> q;
pthread_mutex_t _mutex;
pthread_cond_t _c_s; // 把消费者和生产者放在两个不同的条件变量下
pthread_cond_t _p_s;// 唤醒控制比较简单
private:
bool isFull()
{
return q.size() == _cap_max;
}
bool isEmpty()
{
return q.empty();
}
public:
BlockQueue()
:_cap_max(5)
{
pthread_mutex_init(&_mutex,nullptr);
pthread_cond_init(&_c_s,nullptr);
pthread_cond_init(&_p_s,nullptr);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_c_s);
pthread_cond_destroy(&_p_s);
}
void equeue(const T &in)
{
pthread_mutex_lock(&_mutex);
while(isFull())
{
// std::cout<<"isfull"<<std::endl;
pthread_cond_wait(&_p_s,&_mutex); // 阻塞的时候释放锁 返回的时候参与锁的竞争
}
q.push(in);
pthread_mutex_unlock(&_mutex); // 先解锁还是先唤醒都可以
// 如果sinal时消费者没有在wait里,signal没有意思 随
//便放
// 如果先唤醒(唤醒有意义的讨论就是在线程阻塞在wait里) 因为锁可能没释放
//消费者pthread_cond_wait(&_p_s,&_mutex);
//等待在锁那直到生产者解锁 如果直接释放了锁消费者直接运行
// 如果先解锁 生产者 再唤醒,消费者从条件变量里竞争锁,
// 生产者从下一次的equeue中的pthread_mutex_lock竞争
//锁 如果生产者直接成功了就完事大吉 没成功也无妨因为此
//时还是空的状态 消费者被条件变量的wait捕获 重新释放锁
pthread_cond_signal(&_c_s);
}
void pop(T * out)
{
pthread_mutex_lock(&_mutex);
while(isEmpty())
{
pthread_cond_wait(&_c_s,&_mutex);
}
*out = q.front();
q.pop();
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_p_s); /// 2
}
};
信号量
信号量本质是一个计数器,用来描述公共资源的数量~类似于电影票对资源进行预定
刚刚我们把用互斥锁和条件变量的维护的阻塞队列就是整体使用的!
认识信号量的接口
测试信号量接口的代码 三个线程按顺序抢票
#include <iostream>
#include <semaphore.h>
#include <string>
#include <unistd.h>
#include <vector>
#include "thread.hpp"
#include "lockerguard.hpp"
using namespace ThreadModel;
int cnt = 1;
int tickets = 10000;
class ThreadData
{
public:
char *_name;
sem_t *_psem;
public:
ThreadData(char *name, sem_t *psem) : _name(name), _psem(psem)
{
// 本线程可见 信号量初始值为0
// sem_init(&_sem,0,0);
}
~ThreadData()
{
delete[] _name;
}
};
sem_t sems[4];
int turn = 1;
void P(sem_t *psem)
{
sem_wait(psem);
}
void V(sem_t *psem)
{
sem_post(psem);
}
void *Routine(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
while (true)
{
int idx = td->_psem - sems;
while (idx != turn)
{
P(td->_psem); // 第2,3,线程全sleep
}
if(tickets > 0)
{
tickets--;
std::cout << td->_name << "get tickets" << tickets << std::endl;
turn = (++turn) % 3 + 1;
V(sems+turn);
}else
{
turn = (++turn) % 3 + 1;
V(sems+turn);
break;
}
}
return nullptr;
}
int main()
{
pthread_t tids[4];
for (int i = 0; i < 4; i++)
{
sem_init(sems + i, 0, 1);
}
for (int i = 1; i <= 3; i++)
{
char *name = new char[1024];
snprintf(name, 1024, "thread-%d", i);
ThreadData *td = new ThreadData(name, sems + i);
pthread_create(tids + i, nullptr, Routine, td);
}
for (int i = 1; i <= 3; i++)
{
pthread_join(tids[i], nullptr);
}
for (int i = 1; i <= 3; i++)
{
sem_destroy(sems + i);
}
return 0;
}
用信号量实现生产消费者模型
我们使用的数据结构是环形队列,因为这样可以不被整体使用。
环形队列
如果没有计数器,我们需要浪费一个格子,才能区分空和满,空:head = tail;满 (tail+ 1)% N = head。巧合的是信号量就相当于计数器了。size =0 时为空 size =N 时为满
1.队列为空的时候,谁先访问?
我们只能让生产者先生产
2.队列满了谁应该先访问?
让消费者来消费
3 . 不为空又不为满,head,tail 下标一定不在同一个位置 所以可以实现 生产者和消费者同时访问共享资源的不同部分
消费者和生产者认为的资源是不同的,消费者认为生产的数据是资源,生产者认为还剩的空间为资源。所以我们需要两个信号量,一个 sem_data 另一个sem_size
信号量的设置
我们把sem_data 初始化为0 sem_size 初始化为n
验证 最开始如果消费者先动,则sem_data 为 0 P操作时会阻塞等待,从而保证的生产者先生产。
当队列满的时候 sem_data = N ,sem_size = 0, 此时生产者再生产对sem_size进行P操作 则会阻塞等待,保证了队列满的时候 消费者先消费。
当不为空不为满的时候,生产者和消费者控制的head和tail都可以动。
如果head = tail 了就变为满或者空的情况了。
#include <vector>
#include <pthread.h>
#include <semaphore.h>
template<class T>
class BlockQueue
{
private:
void P(sem_t* sem)
{
sem_wait(sem);
}
void V(sem_t *sem)
{
sem_post(sem);
}
public:
BlockQueue(int cap = 4)
:_capmax(cap),_end(0),_start(0),_blockqueue(cap)
{
sem_init(&sem_data,0,0);
sem_init(&sem_cap,0,_capmax);
pthread_mutex_init(&_p_mutex,nullptr);
pthread_mutex_init(&_c_mutex,nullptr);
}
~BlockQueue()
{
sem_destroy(&sem_data);
sem_destroy(&sem_cap);
pthread_mutex_destroy(&_p_mutex);
pthread_mutex_destroy(&_c_mutex);
}
void equeue(const T& data)
{
// 生产者
P(&sem_cap);//并发的买票
pthread_mutex_lock(&_p_mutex); // 这里加锁是因为生产者之间要互斥,避免数据覆盖
_blockqueue[_end] = data;
_end++;
_end %= _capmax;
pthread_mutex_unlock(&_p_mutex);
V(&sem_data); // 空了,假如线程1还没释放信号量 线程2在P等待 直到释放信号量
//P(&sem_data); // 如果先释放信号量 可能线程1没解锁 线程2阻塞在锁里 直到线程1释放锁
// pthread_mutex_unlock(&_p_mutex); // 如果线程1解锁了 那直接ok
}
void pop( T * out)
{
// 消费者
P(&sem_data);
pthread_mutex_lock(&_c_mutex);// 消费者互斥是防止队列的一份数据给了两个消费者
*out = _blockqueue[_start];
_start++;
_start %= _capmax;
pthread_mutex_unlock(&_c_mutex);
V(&sem_cap);
}
private:
std::vector<T> _blockqueue;
int _start;
int _end;
int _capmax;
sem_t sem_data;
sem_t sem_cap;
pthread_mutex_t _c_mutex;
pthread_mutex_t _p_mutex;
};