目录
1.线程同步
在保证数据安全的前提下,让县城能够按照某种特定的顺序访问临界资源,从而有效地避免饥饿问题,叫做同步。
1.1 条件变量
- 当一个线程互斥的访问某个变量时,他可能发现在其它线程改变状态之前,他什么都做不了。
- 例如,一个线程访问队列时,发现队列为空,他只能等待,直到其他的线程将节点添加到队列中。这种情况就需要用到条件变量。
1.2 接口介绍
条件变量初始化:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:cond:要初始化的条件变量
attr:nullptr
声明一个条件变量:pthread_cond_t cond;
条件变量销毁:
int pthread_cond_destroy(pthread_cond_t *cond);
当线程在申请共享资源时,由于条件不满足未申请到共享资源,让该线程在条件变量下等待的方法:
等待条件满足:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
参数:
cond:要在这个条件变量上等待
mutex:正在使用的互斥量,该函数会以原子性的方式将锁释放,并将自己挂起;在被 唤醒返回的时候,会自动地获取所传入的锁
当pthread_cond_wait函数被调用的时候,会以原子性的方式,将锁释放,并将自己挂起,若不释放锁则会导致其他的进程一直处于阻塞等待锁资源的状态,这个进程在挂起后,将锁释放供其他进程使用
当pthread_cond_wait函数调用完返回的时候,会自动的重新获取你传入的锁
当条件满足后,我们需要将等待的继承唤醒,接口如下
唤醒等待:
int pthread_cond_signal(pthread_cond_t *cond); -- 唤醒单个等待在该条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t *cond); -- 唤醒所有等待在该条件变量上的线程
该函数可以放在临界区里面,也可以放在外面
2.生产者消费者模型
2.1 模型介绍
上图所示,我们可以将生产者看作若干个线程,消费者看作若干个线程 。仓库可以看成是缓冲区,又来让生产者线程往里存数据,消费者取数据。该模型遵从"321"原则:
- 3种关系:生产者和生产者(互斥)、消费者和消费者(互斥)、生产者和消费者(互斥和同步)
- 2种角色:生产者线程、消费者线程
- 1个交易场所:一段特定结构的缓冲区
生产者消费者模型特点
- 生产线程和消费线程解耦
- 支持生产者和消费者的忙闲不均问题 -- 因为存在一段缓冲区
- 提高效率
2.2 基于BlockQueue的生产者消费者模型
生产者往队列中放数据,消费者从队列中取数据。当队列为空时,消费者进程会变为阻塞状态,等待生产者进程往里写数据。队列满时,生产者进程变为阻塞状态,等待消费者进程从里面读走数据。
Thread1和Thread2可以为多个线程。下面为实现代码:
代码1(BlockQueue.hpp):
#pragma once
#include <iostream>
#include <queue>
# include <pthread.h>
const int gmaxCap = 5;
template <class T>
class BlockQueue
{
public:
BlockQueue(const int &maxCap = gmaxCap)
:_maxCap(maxCap)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_consumerCond, nullptr);
pthread_cond_init(&_productorCond, nullptr);
}
void Push(const T& in) // 输入型参数一般为const引用型
{
pthread_mutex_lock(&_mutex);
// 细节2:充当条件判断的语法一定是while 不能是if :当线程被唤醒后,由于是while 会返回来再进行一次判断 不啊毛南族条件才会继续执行 健壮性更高
while(is_full())
{
// 1.细节1 pthread_cond_wait这个函数的第二个参数 必须是我们正在使用的互斥锁
// a.pthread_cond_wait:该函数会以原子性的方式将锁释放,并将自己挂起
// b.pthread_cond_wait:该函数在被唤醒返回的时候 会自动重新获取你传入的锁
// 若此时缓冲队列是满的 则无法生产 生产者进行等待
pthread_cond_wait(&_productorCond, &_mutex);
}
// 走到这里一定是不满的
_q.push(in);
// 阻塞队列一定有数据 则可以唤醒消费者来获取数据
// 细节3:pthread_cond_signal:该函数可以放在临界区内部 也可以放在外部
pthread_cond_signal(&_consumerCond); // 这里可以有一定的唤醒策略
pthread_mutex_unlock(&_mutex);
}
void pop(T *out) // 输出型参数一般为*型 输入输出型一般为引用型
{
pthread_mutex_lock(&_mutex);
while(is_empty())
{
// 若此时缓冲队列是空的 无法获取数据 消费者阻塞等待
pthread_cond_wait(&_consumerCond, &_mutex);
}
// 走到这儿保证队列数据不空
*out = _q.front();
_q.pop();
// 此时 决对能保证队列里至少有一个空位 唤醒生产者来生产
pthread_cond_signal(&_productorCond);
pthread_mutex_unlock(&_mutex);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_consumerCond);
pthread_cond_destroy(&_productorCond);
}
private:
bool is_empty()
{
return _q.empty();
}
bool is_full()
{
return _q.size() == _maxCap;
}
private:
std::queue<T> _q; // 封装队列
int _maxCap; // 队列最大容量
pthread_mutex_t _mutex; // 把队列保护起来 STL不是线程安全的
pthread_cond_t _productorCond; // 生产者对应的条件变量
pthread_cond_t _consumerCond; // 消费者对应的条件变量
};
代码2(MainCp.cc):
#include "BlockQueue.hpp"
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
void *consumer(void *_bq)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(_bq);
while(true)
{
// 消费活动
int data;
bq->pop(&data);
std::cout << "消费数据:" << data << std::endl;
sleep(1);
}
return nullptr;
}
void *productor(void *_bq)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int>*>(_bq);
while(true)
{
// 生产活动
int data = rand() % 10 + 1; // 1-10 先用随机数构建数据
bq->Push(data);
std::cout << "生产数据:" << data << "相加的任务!" << std::endl;
}
return nullptr;
}
int main()
{
srand((unsigned long)time(nullptr) ^ getpid());
BlockQueue<int> *bq = new BlockQueue<int>();
pthread_t c, p;
pthread_create(&c, nullptr, consumer, bq);
pthread_create(&p, nullptr, productor, bq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
上述代码为生产者生产快的情况,结果如下:
消费者消耗快的结果:
上面的代码演示了传输数据的情况,我们还可以传输任务,代码如下:
BlockQueue.hpp与上述相同。
代码3(Task.hpp):
#pragma once
#include <iostream>
#include <functional>
class Task
{
using func_t = std::function<int(int, int)>;
// typedef std::function<int(int, int)> func_t;
public:
Task()
{}
Task(int x, int y, func_t func)
:_x(x)
,_y(y)
,_callback(func)
{}
int operator()()
{
int result = _callback(_x, _y);
return result;
}
private:
int _x;
int _y;
func_t _callback;
};
代码4(MainCp.cc):
#include "BlockQueue.hpp"
#include "Task.hpp"
#include <ctime>
#include <sys/types.h>
#include <unistd.h>
int myAdd(int x, int y)
{
return x + y;
}
void *consumer(void *_bq)
{
BlockQueue<Task> *bq = static_cast<BlockQueue<Task>*>(_bq);
while(true)
{
// 消费活动
int data;
Task t;
bq->pop(&t);
std::cout << "消费数据:" << t() << std::endl;
sleep(1);
}
return nullptr;
}
void *productor(void *_bq)
{
BlockQueue<Task> *bq = static_cast<BlockQueue<Task>*>(_bq);
while(true)
{
// 生产活动
int x = rand() % 10 + 1; // 1-10 先用随机数构建数据
int y = rand() % 5 + 1;
Task t(x, y, myAdd);
bq->Push(t);
std::cout << "生产数据:" << "发送" << x << "与" << y << "相加的任务!" << std::endl;
}
return nullptr;
}
int main()
{
srand((unsigned long)time(nullptr) ^ getpid());
BlockQueue<Task> *bq = new BlockQueue<Task>();
pthread_t c, p;
pthread_create(&c, nullptr, consumer, bq);
pthread_create(&p, nullptr, productor, bq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
结果如下:
根据上面的代码,可以发现:在阻塞队列中,无论有几个生产/消费者,只能由一个生产/消费者能进入队列存/取数据。那么创建多线程的生产/消费者模型的意义是什么呢?--- 多线程的生产/消费者模型是为了在获取、构建任务/处理任务时并行执行的,从而提高效率。因为获取和处理任务可能是及其耗时的。
2.3 POSIX信号量
用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
在上面的代码中,对临界资源的访问与操作时,有可能其时不就绪的-即临界资源不可访问。但是我们无法提前得知,只能先加锁,再检测临界资源是否可用,再决定下一步如何进行。只要我们对资源整体加锁,就默认是对资源的整体访问,此时只有一个线程可以访问这部分资源。但是,公共资源是允许多个线程同时访问不同的区域的。那么如何访问呢?
2.3.1 信号量
(1).什么是信号量?---- 本质就是一个计数器,衡量临界资源中资源数量的多少。 只要拥有信号量就一定能够拥有一部分的临界资源。
(2).申请信号量 ---- 程序员编码保证不同的线程并发的访问公共资源的不同区域,那么再访问真正的临界资源前,就可以得知公共资源的使用情况,申请成功就有资源,失败则阻塞等待资源被释放。 ---- 申请信号量的本质:对临界资源中特定小块资源的预定机制。
信号量本身也是公共资源!
申请资源 --- P操作 --- 信号量-- 归还资源 --- V操作 --- 信号量++ 这两个操作具有原子性。
2.3.2 信号量接口
信号量初始化
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem:信号量
pshared:是否共享,0线程共享 非0进程共享
value:信号量的初始值
返回值:0成功 -1失败
信号量销毁:
int sem_destroy(sem_t *sem);
等待信号量:
int sem_wait(sem_t *sem); //P操作
发布信号量
int sem_post(sem_t *sem); // V操作
2.4 基于环形队列的生产消费模型
环形队列就不在赘述了,感兴趣的可以去数据结构学习。
在环形队列中,大部分情况单生产和单消费是可以并发执行的。只有队满/空时会发生互斥同步问题。
核心工作:消费者不能超过生产者,身缠这不能超消费者一圈,生产者和消费者什么时候在一起?
信号量是用来衡量临界资源中资源数量的。
- 对于生产者,看中队列中的剩余空间。 -- 空间资源定义一个信号量
- 对于消费者,看重队列中的剩余数据。 -- 数据资源定义一个信号量
下面为实现改模型的代码:
代码4(RingQueue.hpp):
#pragma once
# include <iostream>
# include <vector>
# include <cassert>
# include <semaphore.h>
# include <pthread.h>
static const int gcap = 10;
template <class T>
class RingQueue
{
private:
void P(sem_t &sem)
{
int n = sem_wait(&sem);
assert(n == 0);
(void)n;
}
void V(sem_t &sem)
{
int n = sem_post(&sem);
assert(n == 0);
(void)n;
}
public:
RingQueue(const int &cap = gcap)
:_queue(gcap)
,_cap(cap)
{
int n = sem_init(&_p_sapceSem, 0, _cap);
assert(n == 0);
n = sem_init(&_c_dataSem, 0, 0);
assert(n == 0);
_producttorStep = _consumerStep = 0;
pthread_mutex_init(&_pmutex, nullptr);
pthread_mutex_init(&_cmutex, nullptr);
}
void Push(const T &in)
{
// 加锁和申请信号量 谁先谁后比较合适
P(_p_sapceSem); // 申请到了空间信号量 意味着一定能进行正常的生产
pthread_mutex_lock(&_pmutex);
_queue[_producttorStep++] = in;
_producttorStep %= _cap;
pthread_mutex_unlock(&_pmutex);
V(_c_dataSem);
}
void Pop(T *out)
{
P(_c_dataSem);
pthread_mutex_lock(&_cmutex);
*out = _queue[_consumerStep++];
_consumerStep %= _cap;
pthread_mutex_unlock(&_cmutex);
V(_p_sapceSem);
}
~RingQueue()
{
sem_destroy(&_p_sapceSem);
sem_destroy(&_c_dataSem);
pthread_mutex_destroy(&_pmutex);
pthread_mutex_destroy(&_cmutex);
}
private:
std::vector<T> _queue;
int _cap; // 环形队列容量
sem_t _p_sapceSem; // 生产者 看中的是空间资源
sem_t _c_dataSem; // 消费者 看中的是数据资源
int _producttorStep; // 生产者位置
int _consumerStep; // 消费者位置
// 为了实现多消费多生产 需要加两把锁
// 这里的两把锁分别对多生产者、多个消费者进行加锁,防止同时有多个生产者/消费者进入临界区 造成影响
pthread_mutex_t _pmutex;
pthread_mutex_t _cmutex;
};
代码5(Task.hpp):
#pragma once
#include <iostream>
#include <functional>
#include <cstdio>
#include <string>
class Task
{
using func_t = std::function<int(int, int, char)>;
// typedef std::function<int(int, int)> func_t;
public:
Task()
{}
Task(int x, int y, const char op, func_t func)
:_x(x)
,_y(y)
,_op(op)
,_callback(func)
{}
std::string operator()()
{
int result = _callback(_x, _y, _op);
char buffer[1024];
snprintf(buffer, sizeof(buffer), "%d %c %d = %d", _x ,_op, _y, result);
return buffer;
}
std::string toTaskString()
{
char buffer[1024];
snprintf(buffer, sizeof(buffer), "%d %c %d = ?", _x, _op, _y);
return buffer;
}
private:
int _x;
int _y;
char _op;
func_t _callback;
};
const std::string oper = "+-*/%";
int myMath(int x, int y, char op)
{
int result = 0;
switch(op)
{
case '+':
result = x + y;
break;
case '-':
result = x - y;
break;
case '*':
result = x * y;
break;
case '/':
{
if(y == 0){
std::cerr << "div zero error!" << std::endl;
result = -1;
}
else result = x / y;
}
break;
case '%':
{
if(y == 0){
std::cerr << "mod zero error!" << std::endl;
result = -1;
}
else result = x % y;
}
break;
default:
// do nothing
break;
}
return result;
}
代码6(main.cc):
# include "RingQueue.hpp"
# include "Task.hpp"
# include <pthread.h>
# include <unistd.h>
# include <ctime>
# include <cstdlib>
# include <sys/types.h>
using namespace std;
string SelfName()
{
char buffer[128];
snprintf(buffer, sizeof buffer, "0x%x", pthread_self());
return buffer;
}
void *ProductorRoutine(void *args)
{
// RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
while(true)
{
// Version1
// sleep(2);
// int data = rand() % 10 + 1;
// rq->Push(data);
// cout << "生产完成,生产的数据是:" << data << endl;
// Version2
// 模拟构建一个任务
int x = rand() % 1000;
int y = rand() % 1500;
char op = oper[rand() % oper.size()];
Task t(x, y, op, myMath);
// 生产任务
rq->Push(t);
// 输出提示
cout << SelfName() << "生产者排发了一个任务:" << t.toTaskString() << " " << endl;
}
}
void *ConsumerRoutine(void *args)
{
// RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
while(true)
{
// verson1
// int data;
// rq->Pop(&data);
// cout << "消费完成,消费的数据是:" << data << endl;
// Version2
sleep(2);
Task t;
// 消费任务
rq->Pop(&t);
string ret = t();
cout << SelfName() <<"消费者消费了一个任务:" << ret << " " << endl;
}
}
int main()
{
srand((unsigned int)time(nullptr) ^ getpid() ^ pthread_self() ^ 0x71727374);
// RingQueue<int> * rq = new RingQueue<int>();
RingQueue<Task> * rq = new RingQueue<Task>();
// 单生产 单消费 多生产 多消费 --》 只要保证,最终进入临界区的是一个生产者一个消费者就好了
pthread_t c, p; // c:消费者 p:生产者
pthread_create(&p, nullptr, ProductorRoutine, rq);
pthread_create(&c, nullptr, ConsumerRoutine, rq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
delete rq;
return 0;
}
上述代码创建了一个生产者一个消费者进程,生产者负责派发任务--两个数的加减乘除取模运算--,消费者负责拿到任务并得到结果,然后打印到屏幕中。
结果如下:
因为我们的代码为生产者比较快,消费者慢,所以一开始会填满队列,等待消费者消费,然后继续操作。
3.线程池
线程池:一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着 监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利 用,还能防止过分调度。
线程池的使用场景:
- 需要大量的线程来完成任务,且完成任务的时间比较短。
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用
线程池示例:
- 创建固定数量的线程池,循环从任务队列中获取任务对象
- 获取到任务对象后,执行任务对象的任务接口
下面为线程池的示例代码:Task.hpp与上述代码5相同、
代码7(LockGuard.hpp):
#pragma once
# include <iostream>
# include <pthread.h>
class Mutex
{
public:
Mutex(pthread_mutex_t *lock_p = nullptr)
:_lock_p(lock_p)
{}
void lock()
{
if(_lock_p) pthread_mutex_lock(_lock_p);
}
void unlock()
{
if(_lock_p) pthread_mutex_unlock(_lock_p);
}
private:
pthread_mutex_t *_lock_p;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t *mutex)
:_mutex(mutex)
{
_mutex.lock(); // 在构造函数中进行加锁
}
~LockGuard()
{
_mutex.unlock(); // 解锁
}
private:
Mutex _mutex;
};
代码8(Thread.hpp):
# pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <functional>
#include <pthread.h>
namespace ThreadNs
{
typedef std::function<void* (void*)> func_t;
const int num = 1024;
class Thread
{
private:
// 在类内创建线程,想让线程执行对应的方法,需要将方法设置为static static类型的无this指针
static void *start_routine(void *args) // 类内成员,有缺省参数 其实是有两个参数的
{
Thread *thread = static_cast<Thread*>(args);
return thread->callBack();
}
public:
Thread()
{
char namebuffer[num];
snprintf(namebuffer, sizeof(namebuffer), "thread-%d", _threadNum++);
_name = namebuffer;
}
void start(func_t func, void *args = nullptr)
{
_func = func;
_args = args;
int n = pthread_create(&_tid, nullptr, start_routine, this); //TODO
assert(n == 0); // 编译debug的方式发布的时候存在,release方式发布,assert就不存在了,n是一个定义但是没有使用的变量,有的编译器会警告
(void)n;
}
void join()
{
int n = pthread_join(_tid, nullptr);
assert(n == 0);
(void)n;
}
void *callBack()
{
return _func(_args);
}
std::string threadname()
{
return _name;
}
~Thread()
{
}
private:
std::string _name;
pthread_t _tid;
func_t _func;
void *_args;
static int _threadNum;
};
int Thread::_threadNum = 1;
}
代码9(ThreadPool.hpp):
#pragma once
# include "Thread.hpp"
# include "LockGuard.hpp"
# include <vector>
# include <queue>
# include <mutex>
# include <pthread.h>
# include <unistd.h>
using namespace ThreadNs;
const int pnum = 5;
template<class T>
class ThreadPool;
template <class T>
class ThreadData
{
public:
ThreadPool<T> *threadpool;
std::string name;
public:
ThreadData(ThreadPool<T> *tp, const std::string& n)
:threadpool(tp)
,name(n)
{ }
};
template<class T>
class ThreadPool
{
private:
static void *handlerTask(void *args)
{
ThreadData<T> *td = static_cast<ThreadData<T>*>(args);
while(true)
{
T t;
// td->threadpool->lockQueue();
{
LockGuard lockguard(td->threadpool->mutex());
while(td->threadpool->isQueueEmpty())
{
td->threadpool->threadWait();
}
t = td->threadpool->pop(); // pop的本质 是从公共队列中拿到当前线程自己独立的栈中
}
// td->threadpool->unlockQueue();
std::cout << td->name << " 获取了一个任务:" << t.toTaskString() << " 并处理完成,结果是:" << t() << std::endl; // 处理任务 要放在解锁之后 不然处理任务就变为串行了 而我们是要让不同进程并发的处理任务
}
delete td;
return nullptr;
}
public:
void lockQueue() { pthread_mutex_lock(&_mutex); }
void unlockQueue() { pthread_mutex_unlock(&_mutex); }
bool isQueueEmpty() { return _task_queue.empty(); }
void threadWait() { pthread_cond_wait(&_cond, &_mutex); }
T pop()
{
T t = _task_queue.front();
_task_queue.pop();
return t;
}
pthread_mutex_t* mutex()
{
return &_mutex;
}
ThreadPool(const int &num = pnum)
:_num(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
for(int i = 0; i < _num; i++)
{
_threads.push_back(new Thread());
}
}
ThreadPool& operator=(const ThreadPool&) = delete;
ThreadPool(const ThreadPool&) = delete;
public:
void run()
{
for(const auto& t : _threads)
{
ThreadData<T> *td = new ThreadData<T>(this, t->threadname());
t->start(handlerTask, td);
std::cout << t->threadname() << "start..." << std::endl;
}
}
void Push(const T &in)
{
LockGuard lockguard(&_mutex);
// pthread_mutex_lock(&_mutex);
_task_queue.push(in);
pthread_cond_signal(&_cond);
// pthread_mutex_unlock(&_mutex);
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
for(const auto &t : _threads) delete t;
}
static ThreadPool<T>* getInstance()
{
// 保证线程安全
if(nullptr == tp)
{
_singLock.lock();
if(nullptr == tp)
{
tp = new ThreadPool<T>();
}
_singLock.unlock();
}
return tp;
}
private:
int _num; // 线程池中线程的数量
std::vector<Thread*> _threads; // 管理一批线程
std::queue<T> _task_queue; // 任务队列
pthread_mutex_t _mutex; // 锁
pthread_cond_t _cond; // 条件变量
static ThreadPool<T> *tp;
static std::mutex _singLock;
};
template<class T>
ThreadPool<T>* ThreadPool<T>::tp = nullptr;
template<class T>
std::mutex ThreadPool<T>::_singLock;
代码10(main.cc):
# include "Task.hpp"
# include "ThreadPool.hpp"
# include <memory>
int main()
{
std::unique_ptr<ThreadPool<Task>> tp(new ThreadPool<Task>());
tp->run();
int x, y;
char op;
while(1)
{
std::cout << "请输入数据1# ";
std::cin >> x;
std::cout << "请输入数据2# ";
std::cin >> y;
std::cout << "请输入将进行的运算# ";
std::cin >> op;
Task t(x, y, op, myMath);
std::cout << "刚刚录入的任务:" << t.toTaskString() << ",确认提交吗?[y/n]# " << std::endl;
char confirm;
std::cin >> confirm;
if(confirm == 'y')
tp->Push(t);
sleep(1);
}
return 0;
}
代码介绍:我们自己手动的输入一个=-*/%运算任务,然后将任务放入任务队列中等待线程来读取/处理任务。线程池中有_num个线程。结果如下,可以看到我们一开始在没有任务时,在线程池中创建了5个线程,输入任务后线程依次处理任务。
4.线程安全的单例模式
单例模式是一种 "经典的, 常用的, 常考的" 设计模式。
针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是设计模式。
单例模式:一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全 局访问点,该实例被所有程序模块共享。在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据。
常见的单例模式有两种:
- 懒汉模式:如果单例对象构造十分耗时或者占用很多资源,比如加载插件、初始化网络连接、读取文件等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。所以这种情况使用懒汉模式(延迟加载)更好。
- 饿汉模式:就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。-- 可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定。
懒汉方式实现单例模式(线程安全版本):
class Singleton
{
Singleton& operator=(const Singleton&) = delete;
Singleton(const Singleton&) = delete;
public:
static Singleton* getInstance()
{
if(_pInstance == nullptr) // 降低加锁时的资源浪费 提升性能
{
_mutex.lock();
if(_pInstance == nullptr)
_pInstance = new Singleton();
_mutex.unlock();
}
return _pInstance;
}
private:
Singleton(){} // 私有化默认构造函数
static Singleton *_pInstance;
static Mutex _mutex;
};
Singleton* Singleton::_pInstance = nullptr;
Mutex Singleton::_mutex;
饿汉方式实现单例模式(线程安全版本):
class Singleton
{
Singleton& operator=(const Singleton&) = delete;
Singleton(const Singleton&) = delete;
public:
static Singleton* getInstance()
{
return &_instance;
}
private:
Singleton(){} // 私有化默认构造函数
static Singleton _instance;
};
Singleton Singleton::_instance; // 在程序入口之前就完成单例对象的初始化
5.STL,智能指针和线程安全
STL中的容器是否是线程安全的?
不是. 原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响. 而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶). 因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.
智能指针是否是线程安全的?
对于 unique_ptr, 只是在当前代码块范围内生效, 因此不涉及线程安全问题. 对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这 个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.
6.其他常见的各种锁
- 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行 锁等),当其他线程想要访问数据时,被阻塞挂起。
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前, 会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不 等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
- 自旋锁,公平锁,非公平锁
自旋锁接口--与之前的锁用法完全相同:
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared); // psharead默认为0
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
7.读者写者问题
读者写者问题同样遵循321原则
- 写者之间: 互斥
- 读写之者:互斥、同步
- 读者之间:没有关系
与生产者消费者模型的区别:消费者会拿走数据,但读者不会。
场景:一次发布,很长时间不做修改,大部分时间是被读取的,少量时间在写入
接口:
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
读者:int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
写者:
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
在任一时刻,只能有一个写者写/多个读者读。