目录
一、基于环形队列的生产消费模型
1.POSIX信号量
信号量是一个计数器,描述临界资源数量的计数器。
通常操作为++,--,这两个操作都是原子的。
加加代表归还资源,减减代表申请资源。
信号量你一旦申请成功,就会获得指定资源。
接上篇文章,我们谈到基于阻塞队列的生产者消费者模型,其中为了防止一个线程恶意竞争锁而导致其他线程饥饿,我们采用了线程同步的方式来解决问题。这次了解到了信号量我们是否也可以用它来解决上面的问题。
2.基于环形队列的生产消费模型
关于生产者和消费者访问同一个位置时,会出现两种情况:
为空:生产者先运行,消费者待命,等待有资源可以消费。
为满:消费者先运行,生产者待命,等待有空间可以生产。
这两步骤的运行都是由信号量来保证。
代码:
ringqueue.hpp:
#include <iostream>
#include <vector>
#include <ctime>
#include <unistd.h>
#include <semaphore.h>
using namespace std;
const int size = 8;
template <class T>
class Ringqueue
{
public:
Ringqueue(int cap = size)
: _ringqueue(cap), _proindex(0), _conindex(0)
{
// 初始化信号量
sem_init(&_dataspace, 0, _ringqueue.size());
sem_init(&_datanum, 0, 0);
}
void produce(const T &in)
{
// 等待信号量,如果有就会使该信号量减减继续如果没有就阻塞
sem_wait(&_dataspace);
_ringqueue[_proindex] = in;
// 增加信号量
sem_post(&_datanum);
_proindex++;
_proindex %= _ringqueue.size();
}
T consume()
{
sem_wait(&_datanum);
T out = _ringqueue[_conindex];
sem_post(&_dataspace);
_conindex++;
_conindex %= _ringqueue.size();
return out;
}
~Ringqueue()
{
// 销毁信号量
sem_destroy(&_dataspace);
sem_destroy(&_datanum);
}
private:
// queue
vector<T> _ringqueue;
// 信号量
// 空间大小
sem_t _dataspace;
// 数据个数
sem_t _datanum;
// 生产者生产的下标
uint32_t _proindex;
// 消费者消费的下标
uint32_t _conindex;
};
ringqueue.cc:
#include "ringqueue.hpp"
void *productor(void *args)
{
Ringqueue<int> *rqp = static_cast<Ringqueue<int> *>(args);
while (1)
{
sleep(1);
int data = rand() % 199;
rqp->produce(data);
cout << "Time : " << (unsigned long)time(nullptr) << ' ' << "thread : " << pthread_self()
<< ' ' << "produce data success,data is : " << data << endl;
}
}
void *concumer(void *args)
{
Ringqueue<int> *rqp = static_cast<Ringqueue<int> *>(args);
while (1)
{
int out = rqp->consume();
cout << "Time : " << (unsigned long)time(nullptr) << ' ' << "thread : " << pthread_self()
<< ' ' << "consume data success,data is : " << out << endl;
}
}
int main()
{
srand((unsigned long)time(nullptr) ^ getpid());
pthread_t con;
pthread_t pro;
Ringqueue<int> rq;
pthread_create(&con, nullptr, concumer, &rq);
pthread_create(&pro, nullptr, productor, &rq);
pthread_join(con, nullptr);
pthread_join(pro, nullptr);
return 0;
}
结果:
多线程:
只需要,在申请信号量时加上锁。
结果:
二、读者写者问题
在编写多线程时也会有这样一种模型,在这个模型中读的次数要比改写的次数要远远高于,并且在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地 降低我们程序的效率。所以要专门来提供方案来解决这类问题,方案就是读写锁。
解决问题之前,我们首先要了解读者写者的关系。
写者与写者:互斥关系
写者与读者:互斥关系
读者与读者:并发关系
为什么消费者与消费者之间的关系就是互斥,这里读者与读者可以并发,本质区别就是这是读并没有修改临界资源中的数据。
既然这样那就要针对处理:
读者-读锁 写者-写锁
我们来见识下读写锁的销毁与初始化:
读锁:
写锁:
解锁:
代码:
int count = 0;
pthread_rwlock_t rwmutex;
void *read(void *argc)
{
char *msg = static_cast<char *>(argc);
sleep(2);
while (1)
{
pthread_rwlock_rdlock(&rwmutex);
cout<<"thread : "<<pthread_self()<<" reader read : "<<count<<endl;
pthread_rwlock_unlock(&rwmutex);
sleep(1);
}
}
void *write(void *argc)
{
char *msg = static_cast<char *>(argc);
while (1)
{
sleep(1);
pthread_rwlock_wrlock(&rwmutex);
count++;
cout<<"writer write : count++"<<endl;
pthread_rwlock_unlock(&rwmutex);
}
}
int main()
{
pthread_t r,r1,r2,r3,r4,r5,w;
pthread_create(&r, nullptr, read, (void *)"reader");
pthread_create(&r1, nullptr, read, (void *)"reader");
pthread_create(&r2, nullptr, read, (void *)"reader");
pthread_create(&r3, nullptr, read, (void *)"reader");
pthread_create(&r4, nullptr, read, (void *)"reader");
pthread_create(&r5, nullptr, read, (void *)"reader");
pthread_create(&w, nullptr, write, (void *)"writer");
pthread_join(r, nullptr);
pthread_join(r1, nullptr);
pthread_join(r2, nullptr);
pthread_join(r3, nullptr);
pthread_join(r4, nullptr);
pthread_join(r5, nullptr);
pthread_join(w, nullptr);
pthread_rwlock_destroy(&rwmutex);
return 0;
}
结果:
三、线程池
1.线程池
这是一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着 监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。
我们来写写一个具有固定数量线程暴露出调用接口的线程池。
threadpool.hpp:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <queue>
#include <assert.h>
#include <cstdlib>
#include <memory>
#include <ctime>
#include <string>
#include "Task.hpp"
using namespace std;
const int gthreadnum = 8;
template <class T>
class Threadpool
{
public:
Threadpool(int num = gthreadnum) : _isStart(false), _threadnum(num)
{
assert(_threadnum > 0);
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
~Threadpool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
static void *fuc(void *argc)
{
pthread_detach(pthread_self());
Threadpool<T> *tp = static_cast<Threadpool<T> *>(argc);
while (1)
{
tp->lockqueue();
while (tp->isempty())
{
tp->waittask();
}
T t = tp->pop();
tp->unlockqueue();
int result = t.calculate();
int one, two;
char opr;
t.getelem(&one, &two, &opr);
cout << "Time : " << (unsigned long)time(nullptr) << ' ' << "thread : " << pthread_self()
<< ' ' << "do task success,data is : " << one << ' ' << opr << ' ' << two << " = " << result << endl;
}
}
void start()
{
assert(!_isStart);
for (int i = 0; i < _threadnum; i++)
{
pthread_t th1;
pthread_create(&th1, nullptr, fuc, this);
}
_isStart = true;
}
void push(const T &in)
{
lockqueue();
_taskqueue.push(in);
choicethread();
unlockqueue();
}
private:
void lockqueue()
{
pthread_mutex_lock(&_mutex);
}
void unlockqueue()
{
pthread_mutex_unlock(&_mutex);
}
bool isempty()
{
return _taskqueue.empty();
}
void waittask()
{
pthread_cond_wait(&_cond, &_mutex);
}
void choicethread()
{
pthread_cond_signal(&_cond);
}
T pop()
{
T temp = _taskqueue.front();
_taskqueue.pop();
return temp;
}
private:
bool _isStart;
uint32_t _threadnum;
queue<T> _taskqueue;
// 让线程互斥的获取任务队列的任务
pthread_mutex_t _mutex;
pthread_cond_t _cond;
};
threadpool.cc:
#include "threadpool.hpp"
#include "Task.hpp"
const std::string opera = "+-*/%";
int main()
{
unique_ptr<Threadpool<Task>> utp(new Threadpool<Task>());
utp->start();
srand((unsigned long)time(nullptr) ^ getpid());
while (1)
{
int one = rand() % 3212;
int two = rand() % 131;
char opr = opera[rand()%opera.size()];
cout << "Time : " << (unsigned long)time(nullptr) << ' ' << "thread : " << pthread_self() << ' ' << "produce task success,data is : " << one << ' ' << opr << ' ' << two << " -> ?" << endl;
Task t(one, two, opr);
utp->push(t);
sleep(1);
}
}
结果:
至于Task.hpp,可以去前几篇文章寻找一下。限于篇幅就不重复敲了。
四、线程安全的单例模式
懒汉方式实现单例模式(线程安全版本)
把上面的线程池改为单例模式
private:
Threadpool(int num = gthreadnum) : _isStart(false), _threadnum(num)
{
assert(_threadnum > 0);
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
Threadpool(const Threadpool<T> &) = delete;
void operator=(Threadpool<T> &) = delete;
public:
static Threadpool<T> *getinstance()
{
static pthread_mutex_t mutex;
if (instance == nullptr)
{
pthread_mutex_lock(&mutex);
if (instance == nullptr)
{
instance = new Threadpool<T>();
}
pthread_mutex_unlock(&mutex);
}
assert(instance != nullptr);
return instance;
}
private:
bool _isStart;
uint32_t _threadnum;
queue<T> _taskqueue;
// 让线程互斥的获取任务队列的任务
pthread_mutex_t _mutex;
pthread_cond_t _cond;
static Threadpool<T> *instance;
};
template <class T>
Threadpool<T> *Threadpool<T>::instance = nullptr;
结果:
虽然没有使用多个线程去调用getinstance(),但是这在多线程版本也是可行的。
五、线程安全与可重入
1.线程安全
一个函数被称为线程安全的,当且仅当被多个并发线程反复地调用时,它会一直产生正确的结果。如果一个函数不是线程安全的,我们就说它是线程不安全的。
四类线程不安全函数:
①不保护全局变量的函数。
在函数内部为临界区加上锁,就可以将它变为线程安全函数,这样做的缺点是增加程序运行时间。
②保持跨越多个调用的状态的函数
rand函数就是线程不安全的,因为当前调用的结果依赖于前次调用的中间结果。如果,我们为srand置一个种子,一个线程调用rand会得到,预期的随机数。如果多个线程同时调用rand函数,反而有可能得到相同的数字。因为,rand函数得出的伪随机数依赖于srand所给出的值,多线程调用rand函数,而同一时间srand函数,只给出一个值,多个线程拿到的值一样,造就随机值都是一样的。使得像这样的rand函数线程安全唯一的方式就是重写它,使得它不再使用任何static数据,而是依靠调用者在参数中传递状态信息。
③返回指向静态变量的指针的函数
某些函数,例如ctime和gethostbyname,将计算结果放在static变量中,然后返回一个指向这个变量的指针。如果我们从并发线程中调用这些函数,那么将可能发生灾难,因为正在被一个线程调用的结果会被另一个线程悄悄地覆盖了。解决方案两种方法:一种重写函数,调用者传递存放结果的地址。另一种选择就是使用加锁-复制技术。基本思想是将线程不安全函数与互斥锁联系起来。在每次调用线程不安全函数时,对它加锁,再把返回结果复制到一个私有的内存位置,然后解锁。
④调用线程不安全的函数的函数
调用第二类线程不安全的函数,这种依赖于跨越多次调用的状态的函数,会导致调用该函数的函数线程不安全。
2.可重入
同一个函数被不同执行流调用,当前一个流程还没有执行完,就有其他执行流再次进入,我们称之为重入。一个函数在重入的情况下,运算结果不会出现任何问题,则该函数被称为可重入函数,反之被称为不可重入函数。它有一个最重要的一个特性是:当它们被多个线程调用时,不会引用任何共享的数据。
线程安全和可重入两个概念很容易搞混,它们是这样的关系:
如果所有的函数参数都是传值传递的(既没有指针),并且所有的数据引用的都是本地的自动栈变量(既没有引用静态或全局变量),那么函数就是显式可重入的。
如果允许显式可重入函数中一些参数是引用传递(传递指针),那么就得到了一个隐式可重入函数,前提是这个指针指向非共享数据。
3.使用可重入的库函数
大多数Linux函数,包括定义在标准C库中的函数(例如:malloc、free、relloc、printf和scanf)都是线程安全的。小部分例外,但这小部分例外,Linux系统提供大多数线程不安全函数的可重入版本。可重入版本的名字总是以“_r”结尾。
像我们先前提到过的rand函数就有可重入版本rand_r。
六、死锁
1. 死锁
死锁是指一组进程中不同的进程占用不会释放的资源,但因互相去申请其他进程不会释放的资源而处于永久等待的状态。
比如:
void *fuc(void *argc)
{
while (1)
{
pthread_mutex_lock(&mutexA);
pthread_mutex_lock(&mutexB);
cout << "我是线程1"
<< "mythread id :" << pthread_self() << endl;
pthread_mutex_unlock(&mutexA);
pthread_mutex_unlock(&mutexB);
}
}
void *fuc1(void *argc)
{
while (1)
{
pthread_mutex_lock(&mutexA);
pthread_mutex_lock(&mutexB);
cout << "我是线程2"
<< "mythread id :" << pthread_self() << endl;
pthread_mutex_unlock(&mutexA);
pthread_mutex_unlock(&mutexB);
}
}
int main()
{
pthread_t td1, td2;
pthread_create(&td1, nullptr, fuc, NULL);
pthread_create(&td1, nullptr, fuc1, NULL);
pthread_join(td1, NULL);
pthread_join(td2, NULL);
pthread_mutex_destroy(&mutexA);
pthread_mutex_destroy(&mutexB);
这个代码跑起来并没有什么问题
但如果,线程1先去申请mutexA,同时线程2去申请mutexB,然后线程1要去申请mutexB,线程2要去申请mutexA,这就会出现问题。
void *fuc(void *argc)
{
while (1)
{
pthread_mutex_lock(&mutexA);
sleep(1);
pthread_mutex_lock(&mutexB);
...
void *fuc1(void *argc)
{
while (1)
{
pthread_mutex_lock(&mutexB);
sleep(1);
pthread_mutex_lock(&mutexA);
...
进程卡住了,两个线程互相去申请对方占用的锁,申请不到便会挂起。这就是死锁现象。
2.死锁四个必要条件
①互斥条件: 线程访问临界区是互斥的。一个资源每次智能被一个执行流使用。
②请求与保持条件: 一个执行流因申请资源而阻塞,而自己的申请到的资源未释放。
③不剥夺条件: 一个线程在未使用完资源时,不能强行剥夺。
④循环等待条件:若干个执行流都在等待对方的资源,而形成首尾相接的循环等待资源的关系。
3.避免死锁
①破坏四个死锁必要条件。
②加锁顺序一致。
③避免锁未释放的场景。
④资源一次性释放。
4.避免死锁的算法
①银行家算法
每一个新进程在执行之前,必须先声明自己需要的每种资源的最大单元个数。当进程申请某一部分资源的时候,os会判断是否可以分配,如果可以分配并且os在分配之后处于安全状态,才会将资源分配给进程,反之则不会。
②死锁检测算法
死锁会导致一个现象,首尾相接的互相申请的线程阻塞在一起,所以这个算法就是检查这种现象的。
我们在给线程加锁时不一定非要用pthread_mutex_lock(),也可以用pthread_mutex_trylock()
lock家族的成员,它是非阻塞式申请mutex,如果申请不到则返回,一般会尝试while循环调用,如果没成功可以在循环体内检测某些必要条件是否满足,避免死锁现象。
感谢观看,如有问题请指出。