1. C++11的线程新特性
1.1 thread 类
在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。
thread类的常见函数如下:
1.1.1 thread类的构造函数
- 构造函数接受可调用对象作为参数
void fun(int a){
cout << a << endl;
}
int main(){
thread th1(fun, 10);
cout << "th1 number is " << th1.get_id() << endl;
th1.join();
return 0;
}
输出结果为:
th1 number is 0x16d06b000
10
- 构造函数接受lambda函数构造为参数
int main(){
thread th1([]{cout << "lambda func" << endl;});
cout << "th1 number is " << th1.get_id() << endl;
th1.join();
return 0;
}
结果为:
th1 number is 0x16f883000
lambda func
1.1.2 get_id()
获取线程ID,类型为std::thread::id,用于获取当前线程的ID,每个线程仅有一个唯一的ID。
1.1.3 join()
"join()"是"std::thread"的一个成员函数,用于等待线程执行完毕。调用"join()"函数将阻塞当前线程,知道被调用的线程执行完毕。
在使用线程时,如果你创建了一个线程但没有调用该线程的 join()
方法,主线程会在子线程执行完成之前继续执行。这可能导致一些问题,具体取决于程序的设计和执行流程。
- 主线程可能提前结束: 如果主线程在子线程执行完成之前结束,子线程可能被操作系统强制终止,这可能导致子线程没有完成它的任务。这在需要等待子线程完成任务后再执行其他操作时是一个潜在的问题。
- 资源泄漏: 如果子线程分配了资源(如内存),但在主线程结束时没有机会释放这些资源,可能会导致资源泄漏。
- 潜在的不确定行为: 线程可能在主线程结束时被终止,这可能导致线程中的操作不完全执行或者数据结构可能处于不一致的状态。
1.1.4 detach()
"detach()"用于将线程与实际的线程分离,分离后线程在后台继续执行,与主线程无关。主线程不再追踪和控制分离的线程。
例如:
void myFunction() {
// 线程执行的函数
for (int i = 0; i < 5; ++i) {
std::cout << "Thread executing..." << std::endl;
}
}
int main() {
std::thread myThread(myFunction); // 创建线程并执行 myFunction
// 其他操作...
myThread.join(); // 等待线程执行完成
//myThread.detach(); // 线程分离
std::cout << "Main thread continues..." << std::endl;
chrono::seconds(3) // 等待detach执行完成
return 0;
}
join的结果如下:
Thread executing...
Thread executing...
Thread executing...
Thread executing...
Main thread continues...
deatch的结果如下:
Main thread continues...
Thread executing...
Thread executing...
Thread executing...
Thread executing...
Thread executing...
1.1.5 joinable()
"joinable()"是"std::thread"类的一个成员函数,用于检查线程是否可以被"join()"或"detach()"。如果线程对象尚未与实际的线程关联,或者已经被"join()"或 "detach()",则该函数返回"false“,否则返回”true“。
1.2 atomic原子操作
原子操作主要用来解决线程中共享数据时候出现的线程同步问题。
多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问
题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数
据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦.
1.3 锁机制
如果是想保证某一段代码的安全性,只能用锁机制来设置代码了,原子操作只能保证某一个变量的原子性,不能保证一段代码的原子性。
1.3.1 lock_guard与unique_lock
C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock。打破了传统的加锁解锁机制。比如:
int ThreadProc1()
{
for (int i = 0; i < 100; i++)
{
g_lock.lock();
++number;
cout << "thread 1 :" << number << endl;
g_lock.unlock();
}
return 0;
}
我们可以用lock_guard()或者unqiue_lock()来修改:
int ThreadProc1()
{
for (int i = 0; i < 100; i++)
{
mutex<int> locker;
lock_guard<mutex> lock(locker);
++number;
cout << "thread 1 :" << number << endl;
}
return 0;
}
lock_guard() 与 unqiue_lock() 的区别是什么呢?
-
所有权和灵活性:
-
std::lock_guard
是一个轻量级的互斥量封装,它在构造时锁住互斥量,在析构时释放锁。它是一个用于自动化互斥量的管理的工具,但其锁定和解锁的时机是固定的。 -
std::unique_lock
提供了更多的控制权,它可以在构造时锁住互斥量,也可以在构造时不锁住,在后续的代码中手动调用lock()
来锁住,同时也可以在析构时选择是否自动解锁。
-
-
锁定和解锁的时机:
-
std::lock_guard
在构造时锁住互斥量,在析构时自动解锁,没有显式的锁定和解锁方法。 -
std::unique_lock
具有更大的灵活性。可以在构造时锁定互斥量,也可以在构造时不锁定,后续手动调用lock()
进行锁定,同时可以通过unlock()
进行解锁。
-
1.4 条件变量
我们看一个简单的判断奇数偶数程序来看看条件变量的用法,在这之前先看下条件变量的函数有哪些。
线程1输出偶数,线程2输出奇数:
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
using namespace std;
int main()
{
int i = 0;
int n = 100;
mutex mut;
int flag = true;
condition_variable cv;
//打印奇数
thread t1([&] {
while (i < n)
{
unique_lock<mutex> lock(mut);
while (flag == true)
cv.wait(lock); //通过条件变量的方式让该线程阻塞
cout << "t1: " << this_thread::get_id() << ": " << i << endl;
i++;
flag = true;
cv.notify_one();//唤醒另外一个线程
}
});
//打印偶数
thread t2([&] {
while (i <= n)
{
unique_lock<mutex> lock(mut);
while (flag == false)
cv.wait(lock);
cout << "t2: " << this_thread::get_id() << ": " << i << endl;
i++;
flag = false;
cv.notify_one();
}
});
t1.join();
t2.join();
return 0;
}
首先当0进来的时候,0是偶数,函数走到th1的wait判断条件,th1在这里阻塞。函数执行th2,当th2走到falg时候不会阻塞并且发现0是偶数可以输出。然后设置标识位位false,并且通知线程1可以执行。线程1执行。如此循环。
2. 线程池
2.1 线程池的实现方法
线程池的实现方法大概思路应该是构建两个大类,一个类用来操作线程池,另一个类用来操纵队列,因为我们要把所有的线程保存在任务队列中。
2.2 任务队列的设计
任务队列,存储需要处理的任务,由工作的线程来处理这些任务
- 通过线程池提供的API函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除
- 已处理的任务会被从任务队列中删除
- 线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程
任务队列设计应该包含这几个部分与功能:
首先应该维护一个队列,其次需要有添加线程函数,删除线程函数,取出一个线程的函数,最后需要一个返回当前队列个数的函数。另外对立的插入删除需要线程安全的条件下进行,所以需要用互斥锁保证线程安全。
#pragma once
#include<iostream>
#include<queue>
#include<mutex>
#include<functional>
using namespace std;
typedef function<void(void *)> callback;
template<class T>
class Task {
public:
Task() {
function = nullptr;
arg = nullptr;
}
Task(callback f, void *arg) {
function = f;
this->arg = (T *)arg;
}
callback function;
T *arg;
};
template<class T>
class TaskQueue {
public:
TaskQueue();
~TaskQueue();
void addTask(const Task<T> &task);
Task<T> takeTask();
inline unsigned int taskNumber() {
return taskQueue.size();
}
private:
queue<Task<T>> taskQueue;
mutex mtx;
};
template<class T>
TaskQueue<T>::TaskQueue() {}
template<class T>
TaskQueue<T>::~TaskQueue() {}
template<class T>
void TaskQueue<T>::addTask(const Task<T> &task) {
lock_guard<mutex> lock(mtx);
taskQueue.push(task);
}
template<class T>
Task<T> TaskQueue<T>::takeTask() {
lock_guard<mutex> lock(mtx);
Task<T> task;
if (!taskQueue.empty()) {
task = taskQueue.front();
taskQueue.pop();
}
return task;
}
2.3 工作任务线程的设计
- 线程池中维护了一定数量的工作线程, 他们的作用是是不停的读任务队列, 从里边取出任务并处理
- 工作的线程相当于是任务队列的消费者角色,
- 如果任务队列为空, 工作的线程将会被阻塞 (使用条件变量/信号量阻塞)
- 如果阻塞之后有了新的任务, 由生产者将阻塞解除, 工作线程开始工作
2.4 管理者线程的设计
- 它的任务是周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测
- 当任务过多的时候, 可以适当的创建一些新的工作线程
- 当任务过少的时候, 可以适当的销毁一些工作的线程
#pragma once
#include<iostream>
#include<queue>
#include<mutex>
#include<thread>
#include<string.h>
#include<atomic>
#include<vector>
#include<condition_variable>
#include"task_queue.hpp"
#include<signal.h>
using namespace std;
//线程池结构体
template<class T>
class ThreadPool {
public:
ThreadPool();
explicit ThreadPool(unsigned int min, unsigned int max);
~ThreadPool();
void addTask(const Task<T> &task);
unsigned int getBusyNum(); //忙的线程个数
unsigned int getAliveNum(); //活的线程个数
private:
void threadExit();//线程退出
static void * worker(void *arg);//工作线程调用的任务函数
static void * manager(void *arg);//管理者线程调用的任务函数
const static unsigned int NUMBER = 2;//一次加入的线程数
//任务队列
TaskQueue<T> *taskQueue;
thread *managerThread; //管理者线程
vector<thread> workGroup; //工作线程组
unsigned int minNum; //最小线程数
unsigned int maxNum; //最大线程数
atomic<unsigned int> busyNum; //忙的线程的个数
atomic<unsigned int> liveNum; //存活的线程的个数
atomic<unsigned int> exitNum; //要销毁的线程个数
mutex mutexPool; //锁整个线程池
condition_variable notEmpty; //任务队列是不是空了 利用条件变量通知
bool isShutdown; //线程池销毁 true
};
template<class T>
ThreadPool<T>::ThreadPool() {}
template<class T>
ThreadPool<T>::ThreadPool(unsigned int min, unsigned int max) {
taskQueue = new TaskQueue<T>;
if (nullptr == taskQueue) {
cout << "new taskQueue fail..." << endl;
return;
}
workGroup.reserve(max);//分配最大容量
minNum = min;
maxNum = max;
busyNum = 0;
liveNum = min;
exitNum = 0;
isShutdown = false;
//管理者线程
managerThread = new thread(manager, this);
//if (managerThread->joinable()) {
// // 管理线程是守护线程,这里可以将其分离
// managerThread->detach();
//}
if (nullptr == managerThread) {
cout << "new managerThread fail..." << endl;
return;
}
//工作线程
for (unsigned int i = 0; i < min; ++i) {
workGroup.push_back(thread(worker, this));
}
}
template<class T>
ThreadPool<T>::~ThreadPool() {
isShutdown = true;
//唤醒工作线程 主动关闭
if (liveNum > 0) {
notEmpty.notify_all();
}
if (taskQueue) {
delete taskQueue;
}
if (managerThread != nullptr) {
//设置managerThread为守护线程时
//c++运行库可以保证managerThread相关资源回收
if (managerThread->joinable()) {
managerThread->join();
}
delete managerThread;
}
if (!workGroup.empty()) {
threadExit();
}
cout << "ThreadPool end!" << endl;
}
template<class T>
void * ThreadPool<T>::worker(void *arg) {
cout << this_thread::get_id() << " worker thread starting..." << endl;
ThreadPool *pool = static_cast<ThreadPool *>(arg);
while (true) {
unique_lock<mutex> poolLock(pool->mutexPool);
while (0 == pool->taskQueue->taskNumber() && !pool->isShutdown) {
cout << this_thread::get_id() << " waiting task..." << endl;
pool->notEmpty.wait(poolLock);//阻塞等待任务,唤醒后自动上锁
//判断是否销毁线程
if (pool->exitNum > 0 && pool->liveNum > pool->minNum) {
pool->exitNum--;
pool->liveNum--;
poolLock.unlock();
cout << this_thread::get_id() << " thread end work" << endl;
return NULL;
}
}
if (pool->isShutdown) {
poolLock.unlock();
cout << this_thread::get_id() << " thread end work" << endl;
return NULL;
}
//取出一个任务
Task<T> task = pool->taskQueue->takeTask();
poolLock.unlock();
pool->busyNum++;
task.function(task.arg);//执行函数
delete task.arg;
pool->busyNum--;
}
}
template<class T>
void * ThreadPool<T>::manager(void *arg) {
cout << this_thread::get_id() << " manager thread starting..." << endl;
ThreadPool *pool = static_cast<ThreadPool *>(arg);
int checkInterval = 3;
while (!pool->isShutdown) {
this_thread::sleep_for(chrono::seconds(checkInterval));//每3秒检查
unique_lock<mutex> poolLock(pool->mutexPool);
unsigned int queueSize = pool->taskQueue->taskNumber();
//唤醒等待任务的线程
unsigned int sleepThreadNum = pool->liveNum - pool->busyNum;
if (pool->busyNum < queueSize) {
while (sleepThreadNum > 0) {
pool->notEmpty.notify_one();
sleepThreadNum--;
}
}
//添加线程
if (queueSize > pool->liveNum && pool->liveNum < pool->maxNum) {
for (unsigned int counter = 0; counter < NUMBER
&& pool->liveNum < pool->maxNum; ++counter) {
pool->workGroup.push_back(thread(worker, pool));
pool->liveNum++;
}
}
//检查时间自适应
//还需完善
if ((queueSize * 2 < pool->liveNum || queueSize > 2 * pool->liveNum)
&& checkInterval > 1) {
checkInterval--;
}
poolLock.unlock();
//销毁线程
if (pool->busyNum * 2 < pool->liveNum && pool->liveNum > pool->minNum) {
pool->exitNum = NUMBER;
for (int i = 0; i < NUMBER; ++i) {
pool->notEmpty.notify_one();
}
}
//代表有些线程以经结束了工作,需要从容器中删除,回收资源
if (pool->workGroup.size() > pool->liveNum) {
pool->threadExit();
}
}
return NULL;
}
template<class T>
void ThreadPool<T>::threadExit() {
//清空容器里结束的线程
//thread::id tid = this_thread::get_id();
auto it = workGroup.begin();
while (it != workGroup.end()) {
auto correntTid = (*it).native_handle();//获取线程pthread_id
int kill_rc = pthread_kill(correntTid, 0);//发送信号0,探测线程是否存活
if (kill_rc == ESRCH) {
//the specified thread did not exists or already quit
if ((*it).joinable()) {
(*it).join();//等待线程结束 清理线程存储
}
it = workGroup.erase(it);
cout << "thread " << correntTid << " exit from group" << endl;
}
else {
++it;
}
}
}
template<class T>
void ThreadPool<T>::addTask(const Task<T> &task) {
taskQueue->addTask(task);
}
template<class T>
unsigned int ThreadPool<T>::getBusyNum() {
return this->busyNum;
}
template<class T>
unsigned int ThreadPool<T>::getAliveNum() {
return this->liveNum;
}
2.5 测试程序
#include<iostream>
#include<future>
#include<thread>
#include"sly/task_queue.hpp"
#include"sly/thread_pool.hpp"
using namespace std;
void task(void *arg) {
int num = *(int *)arg;
cout << "corrent id is: " << this_thread::get_id() << " :" << num << endl;
this_thread::sleep_for(chrono::seconds(1));
}
void test1() {
cout << "start threadPool test" << endl;
ThreadPool<int> pool(2, 10);
for (int i = 0; i < 100; ++i) {
int *num = new int(i + 100);
pool.addTask(Task<int>(task, num));
}
this_thread::sleep_for(chrono::seconds(15));
for (int i = 0; i < 20; ++i) {
int* num = new int(i + 200);
pool.addTask(Task<int>(task, num));
}
this_thread::sleep_for(chrono::seconds(20));
cout << "end threadPool test" << endl;
}
int main() {
test1();
}
以上代码来自make_wheel_threadPool/TaskQueue.hpp · Dylan/造轮子 - Gitee.com博主,侵删。