文章目录

Ⅰ. 线程池的概念
线程池是一种多线程的并发模型,它包含了一组线程,用于执行多个任务,通常将任务存储在队列或者数组中,并通过一定的调度算法来实现任务的执行。线程池可以提高应用程序的性能,减少线程的创建和销毁所带来的开销,并能更好地控制并发线程的数量,从而避免因线程过多而导致系统性能下降的问题。
虽说线程过多会带来调度开销,进而影响缓存局部性和整体性能,但线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络 socket
等的数量。
🎏一个基本的线程池包含以下几个部分:
- 任务队列:用于存放待执行的任务。
- 线程池管理器:用于创建、销毁、管理线程池中的线程。
- 工作线程:用于执行任务。
- 线程同步机制:用于线程间的同步和通信。
💥线程池的工作流程如下:
- 当有任务需要执行时,先将任务加入任务队列中。
- 当线程池中有空闲线程时,从任务队列中取出一个任务分配给空闲线程执行。
- 当线程池中没有空闲线程时,新建一个线程执行任务。
- 当线程执行完任务后,如果线程池中有其他任务需要执行,则继续执行下一个任务,否则该线程就成为空闲状态,等待下一个任务的到来。
- 如果线程长时间处于空闲状态,线程池管理器可能会根据一定的策略销毁该线程,从而节省系统资源。
☢️线程池的应用场景:
-
需要大量的线程来完成任务,且完成任务的时间比较短。
WEB
服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet
连接请求,线程池的优点就不明显了。因为Telnet
会话时间比线程的创建时间大多了。 -
对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
-
接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。
虽然说线程池听起来复杂,但是其实本质还是一个生产者消费者模型,只是说我们现在要维护的是一个存储多线程的队列或者数组,让它们进行调度去完成任务!除此之外,我们还会在下面的实现当中用上我们之前封装好的线程库和互斥锁库,虽说我们可以直接使用 c++11
中的接口,但是这里为了了解实现原理,我们选择自己使用封装好的接口!
Ⅱ. 简单线程池模拟实现
线程池通过一个线程安全的阻塞任务队列加上一个或一个以上的线程实现,线程池中的线程可以从阻塞队列中获取任务进行任务处理,当线程都处于繁忙状态时可以将任务加入阻塞队列中,等到其它的线程空闲后进行处理。
下面我们实现一个简单的线程池,主要是下面的功能(下面不使用阻塞任务队列来实现):
-
封装好线程池类之后,我们只需要在主函数中调用
put
函数进行任务的放置即可让线程池自动调度空闲线程去处理任务! -
需要添加任务的话可以在
Task.hpp
中进行声明定义!
下面给出程序的调用流程图,帮助理解:
💥ThreadPool.hpp(线程池类)
#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include <pthread.h>
#include <unistd.h>
#include "LockGuard.hpp"
#include "Thread.hpp"
#include "ThreadData.hpp"
using namespace ThreadNS;
const int MAXCAP = 100; // 线程池最大线程数量
template <class T>
class ThreadPool
{
public:
ThreadPool(const int& maxcap = MAXCAP)
:_cap(maxcap)
{
// 初始化工作
pthread_cond_init(&_cond, nullptr);
pthread_mutex_init(&_mutex, nullptr);
for(int i = 0; i < _cap; ++i)
_threads.push_back(new Thread());
}
// 启动所有线程的函数
void run()
{
for(const auto& t : _threads)
{
// 使用ThreadData类装载线程池和名称,方便后面打印
ThreadData<T>* td = new ThreadData<T>(this, t->threadname());
t->start(handlerTask, td);
std::cout << t->threadname() << " start......" << std::endl;
}
}
// 向任务队列中放置任务的接口
void put(const T& in)
{
// 队列的操作是线程不安全的,所以加锁
LockGuard lock(&_mutex);
_task_queue.push(in);
pthread_cond_signal(&_cond); // 唤醒其中一个线程执行任务
}
// 拿取并且弹出任务队列中的任务
T take()
{
// 此时只会有一个线程执行,所以不需要加锁
T t = _task_queue.front();
_task_queue.pop();
return t;
}
~ThreadPool()
{
pthread_cond_destroy(&_cond);
pthread_mutex_destroy(&_mutex);
for(const auto& t : _threads)
delete t;
}
private:
// 线程将来在此获取来自任务队列中的任务和执行任务
static void* handlerTask(void* args)
{
ThreadData<T>* td = static_cast< ThreadData<T> *>(args);
while(true)
{
T t;
{
LockGuard lock(&td->_threadpool->_mutex);
while(td->_threadpool->_task_queue.empty())
{
pthread_cond_wait(&td->_threadpool->_cond, &td->_threadpool->_mutex); // 阻塞直到被put了任务后被唤醒
}
t = td->_threadpool->take(); // 拿取、弹出任务队列中的任务
}
// 这句执行语句的其实是细节:
// 因为执行语句其实很耗时间,所以如果放在锁也就是临界区,那么效率就会变得很低
// 所以要将其放到临界区外面,当一个线程在执行任务的时候,还可以继续从任务队列拿任务,提高并发效率
std::cout << td->_name << " 获取了一个任务: " << t.toTaskString() << " 并处理完成,结果是:" << t() << std::endl;
}
delete td;
return nullptr;
}
private:
int _cap; // 线程池容量
std::vector<Thread*> _threads; // 线程等待容器
std::queue<T> _task_queue; // 任务队列
pthread_cond_t _cond; // 用来线程等待和唤醒线程的条件变量
pthread_mutex_t _mutex; // 互斥锁,保护共享资源--任务队列
};
main.cpp(主函数)
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <memory>
#include <ctime>
#include <unistd.h>
using namespace ThreadNS;
int main()
{
srand((unsigned int)time(nullptr)); // 种随机种子
std::unique_ptr<ThreadPool<CalTask>> tp(new ThreadPool<CalTask>()); // 使用unique_ptr更安全
tp->run(); // 启动线程池
while(true)
{
int x = rand() % 1000;
int y = rand() % 2000;
char op = oper[rand() % oper.size()];
CalTask t(x, y, op, caltask);
tp->put(t); // 向线程池放置任务,会自动被处理
usleep(50000);
}
return 0;
}
ThreadData.hpp(封装线程池和名称的类)
#pragma once
#include <iostream>
#include <string>
#include "ThreadPool.hpp"
// 需要有ThreadPool的声明,不然会报错
template <class T>
class ThreadPool;
// 线程池与名称的封装,成员设为public给外部使用
template <class T>
class ThreadData
{
public:
ThreadData(ThreadPool<T>* threadpool, const std::string& name)
: _threadpool(threadpool), _name(name)
{}
public:
ThreadPool<T>* _threadpool;
std::string _name;
};
Thread.hpp(线程库封装类,之前写过的)
#pragma once
#include <iostream>
#include <functional>
#include <string>
#include <pthread.h>
#include <cstdio>
namespace ThreadNS
{
class Thread
{
using func_t = std::function<void*(void*)>;
public:
Thread()
{
char namebuffer[1024];
snprintf(namebuffer, sizeof namebuffer, "thread%d", _num++);
_threadname = namebuffer;
}
// 创建线程
void start(func_t callback, void *args = nullptr)
{
_callback = callback;
_args = args;
pthread_create(&_t, nullptr, start_routine, this);
}
// 等待线程
void join()
{
pthread_join(_t, nullptr);
}
std::string threadname()
{
return _threadname;
}
private:
// 在类内创建线程,想让线程执行对应的方法,需要将方法设置成为static
static void* start_routine(void* args) // 类内成员,有缺省参数!
{
Thread* _this = static_cast<Thread*>(args);
return _this->_callback(_this->_args);
}
private:
func_t _callback; // 线程执行函数
void* _args; // 线程函数参数
std::string _threadname; // 线程标识名称
pthread_t _t;
static int _num; // 用于标识几号线程
};
int Thread::_num = 1;
}
Task.hpp(任务类与任务函数,生产者消费者模型时候写过)
#pragma once
#include <iostream>
#include <functional>
#include <cstdio>
#include <string>
class CalTask
{
using func_t = std::function<int(int, int, char)>;
public:
CalTask()
{}
CalTask(int x, int y, 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;
};
std::string oper = "+-*/";
int caltask(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 << "除零错误" << std::endl;
result = -1;
}
else
{
result = x / y;
}
}
break;
default:
break;
}
return result;
}
LockGuard.hpp(锁封装类和守卫锁类,之前写过的)
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex(pthread_mutex_t* pmutex = nullptr)
: _pmutex(pmutex)
{}
void lock()
{
if(_pmutex != nullptr)
pthread_mutex_lock(_pmutex);
}
void unlock()
{
if(_pmutex != nullptr)
pthread_mutex_unlock(_pmutex);
}
private:
pthread_mutex_t* _pmutex;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t* pmutex)
: _mutex(pmutex)
{
_mutex.lock();
}
~LockGuard()
{
_mutex.unlock();
}
private:
Mutex _mutex;
};
Makefile
mythread:main.cpp
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f mythread
执行结果
Ⅲ. 单例模式的线程池
我们学过单例模式,这里就不详细讲解了。主要是因为线程池是一般不会去产生多个对象的,因为其占内存比较大,所以我们可以考虑将其实现为单例对象,这里我们用的是懒汉模式进行实现!
说的简单点,就是如下几个要点:
- 将拷贝构造和赋值重载封掉,并且将构造函数设为私有。
- 提供一个静态方法
GetInstance()
让外界调用,下面实现的是返回一个单例对象的指针。 - 这个单例对象也需要是静态的。
- 为了解决线程安全问题和效率问题,使用互斥锁+双重判断。
volatile
关键字防止过度优化,针对的是这个单例对象。
下面我们将上述写过的 ThreadPool.hpp
进行修改,只写出关于单例模式的详细细节(注释地方)!
#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include <pthread.h>
#include <unistd.h>
#include "LockGuard.hpp"
#include "Thread.hpp"
#include "ThreadData.hpp"
using namespace ThreadNS;
const int MAXCAP = 100; // 线程池最大线程数量
template <class T>
class ThreadPool
{
public:
void run()
{}
void put(const T& in)
{}
T take()
{}
~ThreadPool()
{}
static ThreadPool<T>* GetInstance()
{
// 使用双层判断来减少加锁开销
if(_ptp == nullptr)
{
pthread_mutex_lock(&_static_mutex);
if(_ptp == nullptr)
{
_ptp = new ThreadPool<T>();
}
pthread_mutex_unlock(&_static_mutex);
}
return _ptp;
}
private:
// 封掉拷贝构造和赋值重载
ThreadPool<T>& operator=(const ThreadPool<T>&) = delete;
ThreadPool(const ThreadPool<T>&) = delete;
// 构造函数设为私有
ThreadPool(const int& maxcap = MAXCAP)
:_cap(maxcap)
{}
private:
static void* handlerTask(void* args)
{}
private:
int _cap;
std::vector<Thread*> _threads;
std::queue<T> _task_queue;
pthread_cond_t _cond;
pthread_mutex_t _mutex;
static pthread_mutex_t _static_mutex; // 静态互斥锁,来保护生成单例模式
volatile static ThreadPool<T>* _ptp; // 静态对象指针,来生成单例对象,并用volatile修饰,防止编译器gu
};
// 静态对象类外初始化
template <class T>
pthread_mutex_t ThreadPool<T>::_static_mutex;
template <class T>
volatile ThreadPool<T>* ThreadPool<T>::_ptp = nullptr;
主函数:
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <memory>
#include <ctime>
#include <unistd.h>
using namespace ThreadNS;
int main()
{
srand((unsigned int)time(nullptr));
// 生成单例对象
ThreadPool<CalTask>* tp = ThreadPool<CalTask>::GetInstance();
tp->run();
// ThreadPool<CalTask>* tp2 = new ThreadPool<CalTask>(); ❌
// ThreadPool<CalTask> tp3; ❌
while(true)
{
int x = rand() % 1000;
int y = rand() % 2000;
char op = oper[rand() % oper.size()];
CalTask t(x, y, op, caltask);
tp->put(t);
usleep(50000);
}
return 0;
}
Ⅳ. STL和智能指针是不是线程安全的❓❓❓
一、对于STL
STL
中的容器通常是非线程安全的,因为它们没有内置的锁保护访问。这是因为 STL
的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。这意味着如果多个线程同时修改同一个容器,就会发生竞态条件,导致数据损坏或不一致。
但是,STL
提供了一些线程安全的容器,例如 C++11
引入的 std::mutex
和 std::lock_guard
等机制,可以用于保护容器的访问。此外,C++17
引入了一些新的线程安全的容器,例如 std::shared_mutex
和 std::shared_lock
等,它们允许多个线程同时读取容器,但只有一个线程可以写入容器。
而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如哈希表的锁表和锁桶)。
因此,如果需要在多个线程之间共享容器,应该采取适当的线程安全措施来避免竞态条件。
二、对于智能指针
C++11
中提供的 智能指针是线程安全 的(如 std::shared_ptr
和 std::unique_ptr
),因为它们使用原子计数器来追踪指向资源的引用计数。这意味着多个线程可以同时访问和修改同一个智能指针对象,而不会引起数据竞争或其他线程安全问题。
然而,智能指针所指向的资源本身可能不是线程安全的,因此需要根据具体情况进行考虑和处理。如果智能指针所管理的资源需要在多个线程之间共享或修改,那么需要采取一些措施来保证线程安全,例如使用互斥量或其他同步机制来控制资源的访问。
对于 unique_ptr
来说,由于只是在当前代码块范围内生效,因此不涉及线程安全问题。
对于 shared_ptr
来说,多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作 (CAS
) 的方式保证 shared_ptr
能够高效,原子的操作引用计数。