目录
一、什么是线程池
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建的线程集合(线程池)中分配这些任务进行执行。线程池的主要目的是重用线程,减少线程创建和销毁的开销,提高系统效率,并简化并发编程的复杂度。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
基本组成:
任务队列:用于存储待处理的任务。当有新的任务提交时,它们会被放入这个队列中等待线程来执行。
工作线程集合:预先创建好的一组线程。这些线程处于等待状态,随时准备从任务队列中取出任务并执行。
任务提交接口:允许外部向线程池提交任务。这通常涉及将任务添加到任务队列中。
管理机制:负责线程的创建、销毁、调度以及任务队列的管理。它还需要监控线程的状态,如空闲、忙碌等,以便有效分配任务。(任务多时增加线程,任务负荷小的时候在一定阈值内可以关闭一些线程)
优点:
提高性能:减少线程创建和销毁的开销。
资源控制:更好地管理线程资源,防止过多线程消耗系统资源。
简化编程模型:提供统一的接口给开发者提交任务,无需直接管理线程。
线程池的应用场景:
线程池在软件开发中有着广泛的应用,尤其适用于需要高效管理和复用线程资源的场景。以下是一些典型的线程池应用场景:
1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB 服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet 会话时间比线程的创建时间大多了。2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。 突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限 ,出现错误。
二、线程池的模拟实现
线程封装
要实现一个线程池,我们先来实现一个基于C++模板类的简单线程封装,实现基本的线程创建、运行和等待(Join)功能。
Thread.hpp
#pragma once
#include<iostream>
#include<string>
#include<functional>
#include<pthread.h>
template<class T>
using func_t=std::function<void(T&)>;
template<class T>
class Thread
{
public:
Thread(const std::string &threadnamae,func_t<T> func,T data)
:_tid(0),_threadname(threadnamae),_func(func),_data(data)
{
}
static void *threadRoute(void *args)
{
Thread *ts=static_cast<Thread*>(args);
ts->_func(ts->_data);
return nullptr;
}
bool Start()
{
int n=pthread_create(&_tid,nullptr,threadRoute,this);
if(n==0)
{
_isrunning=true;
return true;
}
else
return false;
}
bool Join()
{
if(!_isrunning)
return true;
int n=pthread_join(_tid,nullptr);
if(n==0)
{
_isrunning=false;
return true;
}
return false;
}
std::string ThreadName()
{
return _threadname;
}
bool IsRunning()
{
return _isrunning;
}
~Thread()
{}
private:
pthread_t _tid;
std::string _threadname;
bool _isrunning;
func_t<T> _func;
T _data;
};
LockGuard(RAII)思想
访问阻塞队列一定会涉及到加锁,我们首先可以设计一个LockGuard(RAII)思想,利用类出作用域自动销毁来实现解锁,防止忘记解锁造成死锁。
LockGuard.hpp
#pragma once
#include <pthread.h>
class Mutex
{
private:
pthread_mutex_t* _mutex;
public:
Mutex(pthread_mutex_t* lock)
:_mutex(lock)
{
}
void Lock()
{
pthread_mutex_lock(_mutex);
}
void Unlock()
{
pthread_mutex_unlock(_mutex);
}
~Mutex()
{
}
};
class LockGuard
{
private:
Mutex _mutex;
public:
LockGuard(pthread_mutex_t *lock)
:_mutex(lock)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
};
线程池
实现了一个基于C++模板类的线程池(Threadpool
),使用了单例模式来确保全局唯一,并且整合了简单的任务队列管理和线程同步机制。
单例模式:通过私有化构造函数、拷贝构造函数和赋值运算符,以及静态成员变量instance
和互斥锁sing_lock
,实现了线程池的单例模式。保证了程序中只有一个Threadpool
实例。
避免线程安全问题
static Threadpool<T> *GetInstance()
{
if (instance == nullptr) //双层检查,避免竞争锁再检查
{
LockGuard lockguard(&sig_lock);
if (instance == nullptr)
{
std::cout << "创建单例成功\n";
instance = new Threadpool<T>();
}
}
return instance;
}
旨在减少在多线程环境下的同步开销。具体来说,它通过在加锁前先检查实例是否已经创建,从而尽量避免每次调用都进行线程锁定,提高效率。
单例模式:通过私有化构造函数、拷贝构造函数和赋值运算符,以及静态成员变量
instance
和互斥锁sing_lock
,实现了线程池的单例模式。保证了程序中只有一个Threadpool
实例。构造函数:初始化线程池时,根据指定的线程数(默认为5)创建相应数量的工作线程。每个线程通过
Thread
类(在"thread.hpp"中定义)创建,传入线程名称、线程运行函数(ThreadRun
)以及线程数据。线程同步:使用互斥锁
_mutex
和条件变量_cond
来同步线程,ThreadWait
使线程等待直到有任务到来,而ThreadWake
唤醒等待的线程。任务队列管理:通过
Push
方法向任务队列中添加任务,并自动唤醒等待的线程;Join
方法用于等待所有工作线程完成它们的任务。资源清理:析构函数中清理了互斥锁和条件变量,释放了系统资源。
threadpool.hpp
#pragma once
#include <iostream>
#include <queue>
#include <vector>
#include <pthread.h>
#include <functional>
#include "thread.hpp"
#include "LockGuard.hpp"
static const int defaultnum = 5;
class ThreadData
{
public:
ThreadData(const std::string &name)
: _threadname(name)
{
}
~ThreadData()
{
}
std::string threadname() const
{
return _threadname;
}
private:
std::string _threadname;
};
template <class T>
class Threadpool
{
private: // 单例模式,私有化构造
Threadpool(int threadnum = defaultnum)
: _threadnum(threadnum)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
for (int i = 0; i < _threadnum; i++)
{
std::string threadname = "thread-";
threadname += std::to_string(i+1);
ThreadData td(threadname);
_threads.emplace_back(threadname, std::bind(&Threadpool<T>::ThreadRun, this, std::placeholders::_1), td);
std::cout << threadname.c_str() << "is be created...\n";
}
}
Threadpool(const Threadpool<T> &tp) = delete;
const Threadpool<T> &operator=(const Threadpool<T>) = delete;
public:
static Threadpool<T> *GetInstance()
{
if (instance == nullptr) //双层检查,避免竞争锁再检查
{
LockGuard lockguard(&sig_lock);
if (instance == nullptr)
{
std::cout << "创建单例成功\n";
instance = new Threadpool<T>();
}
}
return instance;
}
bool Start()
{
for (auto &thread : _threads)
{
thread.Start();
std::cout << thread.ThreadName().c_str() << " is running ...\n";
}
return true;
}
void ThreadWait(const ThreadData &td)
{
std::cout << td.threadname().c_str() << " no task sleeping...\n";
pthread_cond_wait(&_cond, &_mutex);
}
void ThreadWake()
{
pthread_cond_signal(&_cond);
}
void ThreadRun(ThreadData &td)
{
while (true)
{
T t;
{
LockGuard lockguard(&_mutex);
while (_q.empty())
{
ThreadWait(td);
std::cout << td.threadname().c_str() << "is weakup\n";
}
t = _q.front();
_q.pop();
}//取完任务就可以解锁了
t();
printf("%s handler task %s done, result is : %s\n",
td.threadname().c_str(), t.PrintTask().c_str(), t.PrintResult().c_str());
}
}
void Push(T&in)
{
printf( "other thread push a task, task is : %s\n", in.PrintTask().c_str());
LockGuard lockguard(&_mutex);
_q.push(in);
ThreadWake();
}
void Join()
{
for (auto &thread : _threads)
{
thread.Join();
}
}
~Threadpool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
private:
std::queue<T> _q;
std::vector<Thread<ThreadData>> _threads;
pthread_cond_t _cond;
pthread_mutex_t _mutex;
int _threadnum;
static Threadpool<T> *instance;
static pthread_mutex_t sig_lock;
};
template<class T>
Threadpool<T>* Threadpool<T>::instance=nullptr;
template<class T>
pthread_mutex_t Threadpool<T>::sig_lock=PTHREAD_MUTEX_INITIALIZER;
线程安全:通过互斥锁和条件变量保证了任务队列操作的原子性和线程间的协调。
任务包装:任务通过函数对象
std::function
或可调用对象T
的形式添加到线程池,要求任务类型必须可调用,且调用时无参数(或参数已绑定)。单例设计模式:确保了线程池在整个应用程序中只被实例化一次,便于集中管理和控制线程资源。
测试代码:
#pragma once
#include <iostream>
#include <string>
const int defaultvalue = 0;
enum
{
ok = 0,
div_zero,
mod_zero,
unknow
};
const std::string opers = "+-*/";
class Task
{
public:
Task()
{
}
Task(int x, int y, char op)
: data_x(x), data_y(y), oper(op), result(defaultvalue), code(ok)
{
}
void Run()
{
switch (oper)
{
case '+':
result = data_x + data_y;
break;
case '-':
result = data_x - data_y;
break;
case '*':
result = data_x * data_y;
break;
case '/':
{
if (data_y == 0)
code = div_zero;
else
result = data_x / data_y;
}
break;
case '%':
{
if (data_y == 0)
code = mod_zero;
else
result = data_x % data_y;
}
break;
default:
code = unknow;
break;
}
}
void operator()()
{
Run();
}
std::string PrintTask()
{
std::string s;
s = std::to_string(data_x);
s += oper;
s += std::to_string(data_y);
s += "=?";
return s;
}
std::string PrintResult()
{
std::string s;
s = std::to_string(data_x);
s += oper;
s += std::to_string(data_y);
s += "=";
s += std::to_string(result);
s += " [";
s += std::to_string(code);
s += "]";
return s;
}
~Task()
{
}
private:
int data_x;
int data_y;
char oper; // + - * / %
int result;
int code; // 结果码,0: 结果可信 !0: 结果不可信,1,2,3,4
};
Main.cc
#include<iostream>
#include<memory>
#include<ctime>
#include"threadpool.hpp"
#include"Task.hpp"
#include<sys/types.h>
#include<unistd.h>
int main()
{
Threadpool<Task>::GetInstance()->Start();
srand((uint64_t)time(nullptr)^getpid());
while(true)
{
int x = rand() % 100 + 1;
usleep(1234);
int y = rand() % 200;
usleep(1234);
char oper = opers[rand() % opers.size()];
Task t(x,y,oper);
Threadpool<Task>::GetInstance()->Push(t);
sleep(1);
}
Threadpool<Task>::GetInstance()->Join();
return 0;
}
初始化线程池:首先通过
Threadpool<Task>::GetInstance()->Start();
初始化并启动线程池。这里Task
是任务类型,意味着线程池将处理的任务是Task
类型的对象。随机数生成:使用
srand
函数设置了随机数种子,结合当前时间和进程ID,以增加随机性。随后,通过rand()
生成一系列随机数来模拟不同的任务参数。任务创建:在无限循环中,每隔一秒创建一个新的
Task
对象。每个任务由两个整数参数x
和y
以及一个操作符oper
构成,其中操作符来自一个预定义的集合opers[]
。任务提交:通过
Threadpool<Task>::GetInstance()->Push(t);
将创建的Task
对象提交给线程池。线程池会根据其内部机制安排工作线程执行这些任务。
运行结果如下图,先创建5个线程,然后开始是没有任务的,五个线程都sleep,然后每隔一秒都生成一个任务,唤醒一个等待的线程来执行该任务。
三、线程安全问题
STL中的容器是否是线程安全的?
不是 .原因是 , STL 的设计初衷是将性能挖掘到极致 , 而一旦涉及到加锁保证线程安全 , 会对性能造成巨大的影响, 而且对于不同的容器, 加锁方式的不同 , 性能可能也不同 ( 例如 hash 表的锁表和锁桶 ),因此 STL 默认不是线程安全 . 如果需要在多线程环境下使用 , 往往需要调用者自行保证线程安全。
C++11及以前:大多数STL容器在C++11标准之前并不是线程安全的。在多线程环境中同时读写同一个容器可能会导致数据竞争和未定义行为。
C++11开始:STL容器的元素读取操作(如迭代器遍历)通常被认为是线程安全的,只要不同时修改容器。但直接修改容器(如插入、删除)依然需要外部同步机制(如互斥锁)来保证线程安全。
智能指针是否是线程安全的?
对于 unique_ptr, 由于只是在当前代码块范围内生效 , 因此不涉及线程安全问题。对于 shared_ptr, 多个对象需要共用一个引用计数变量 , 所以会存在线程安全问题 . 但是标准库实现的时候考虑到了这个问题, 基于原子操作 (CAS) 的方式保证 shared_ptr 能够高效 , 原子的操作引用计数。
虽然STL和智能指针为C++编程带来了便利,但在多线程环境下使用时,我们需要清楚地了解它们的线程安全边界,并采取适当的同步措施来确保程序的正确性。