目录
一、线程池的实现
1.什么是线程池
2.设计线程类
3.设计线程池类
4.运行
5.RAII加锁改造
二、利用单例模式改造线程池
1.复习
2.饿汉模式
3.懒汉模式
关于系统编程的知识我们已经学完了,最后我们需要利用之前写过的代码实现一个线程池,彻底结束系统编程部分。
线程池是一种多线程处理形式,先将任务添加到队列,创建线程后线程池会自动启动线程处理这些任务。
线程池有以下特点:
设计线程池就需要一个线程类管理线程,之前封装的classThread我们增加了一个Context类,这次我们想只使用一个类就完成线程的管理。
虽然需要重新设计,但是原来的函数有一部分也能使用。
线程类的设计思路:
最终实现的Thread.hpp
#include
#include
#include
#define NUM 64
class Thread
{
typedef std::function func_t;
public:
//构造函数创建线程
Thread()
{
//对线程进行规范化命名
char buffer[NUM];
snprintf(buffer, sizeof(buffer), "thread%d", _threadnum++);
_name = buffer;
}
//启动线程
void start(func_t func, void* args = nullptr)
{
//初始化这两个变量
_args = args;
_func = func;
//创建线程,线程的处理函数为start_routine
int n = pthread_create(&_tid, nullptr, start_routine, (void*)this);//将this指针传递到执行代码中
//断言创建成功,这里也可以换成打印错误码的代码
assert(n == 0);
}
//利用回调函数
void* callback()
{
return _func(_args);
}
//获取线程的名字
std::string threadname()
{
return std::string(_name);
}
void join()
{
int n = pthread_join(_tid, nullptr);
assert(n == 0);
}
private:
//处理函数的函数能只有一个确定的参数,所以只能定义为静态。
static void* start_routine(void* args)//用args传递this指针
{
Thread* pt = static_cast(args);
return pt->callback();
}
std::string _name;
pthread_t _tid;
func_t _func;
void* _args;
static int _threadnum;
};
int Thread::_threadnum = 1;
我们测试该类
test.cc
#include
#include
#include"Thread.hpp"
using namespace std;
void* test(void* args)
{
char* p = (char*)args;
while(1)
{
printf("%s\n", p);
sleep(1);
}
}
int main()
{
Thread t;
t.start(test, (void*)"thread running");
t.join();
return 0;
}
线程可以正常运行:
线程池类负责多线程的创建和维护,并且在基于阻塞队列的生产者消费者模型下运行。其中生产者是生成任务的线程,消费者是线程池维护的多个线程,数据结构为阻塞队列。线程池包括阻塞队列和所有消费者。
线程池类的设计思路:
此时我们再根据任务类型在main函数和handler_task函数中设置打印函数显示线程调度就可以了。我们设置主线程每一秒推送一个计算任务,计算任务直接使用之前的类。
CalTask.hpp
#include
#include
#include
#define MAX_NUM 10
//计算任务类
class CalTask
{
typedef std::function func_t;
public:
//默认构造
CalTask()
{}
//构造函数
CalTask(int a, int b, char op, func_t func)
:_a(a)
,_b(b)
,_op(op)
,_func(func)
{}
//仿函数
std::string operator()()
{
int result = _func(_a, _b, _op);
char buffer[64];
snprintf(buffer, sizeof(buffer), "%d %c %d = %d", _a, _op, _b, result);
std::string s(buffer);
return s;
}
//显示任务
std::string show_task()
{
char buffer[64];
snprintf(buffer, sizeof(buffer), "%d %c %d = ?", _a, _op, _b);
std::string s(buffer);
return s;
}
private:
func_t _func;
int _a;
int _b;
char _op;
};
Thread.hpp
#include
#include
#include
#define NUM 64
class Thread
{
typedef std::function func_t;
public:
//构造函数创建线程
Thread()
{
//对线程进行规范化命名
char buffer[NUM];
snprintf(buffer, sizeof(buffer), "thread%d", _threadnum++);
_name = buffer;
}
//启动线程
void start(func_t func, void* args = nullptr)
{
//初始化这两个变量
_args = args;
_func = func;
//创建线程,线程的处理函数为start_routine
int n = pthread_create(&_tid, nullptr, start_routine, (void*)this);//将this指针传递到执行代码中
//断言创建成功,这里也可以换成打印错误码的代码
assert(n == 0);
}
//利用回调函数
void* callback()
{
return _func(_args);
}
//获取线程的名字
std::string threadname()
{
return std::string(_name);
}
void join()
{
int n = pthread_join(_tid, nullptr);
assert(n == 0);
}
private:
//处理函数的函数能只有一个确定的参数,所以只能定义为静态。
static void* start_routine(void* args)//用args传递this指针
{
Thread* pt = static_cast(args);
return pt->callback();
}
std::string _name;
pthread_t _tid;
func_t _func;
void* _args;
static int _threadnum;
};
int Thread::_threadnum = 1;
Threadpool.hpp
#include
#include
#include
#define THREAD_NUM 10
//前面加上声明
template
class ThreadPool;
//线程数据类
template
class ThreadData
{
public:
ThreadPool* _pthreadpool;//线程池的this指针
std::string _threadname;//线程的名字
//构造函数
ThreadData(ThreadPool* tp, std::string name)
:_pthreadpool(tp)
,_threadname(name)
{}
};
//线程池
template
class ThreadPool
{
public:
//构造函数
ThreadPool(int num = THREAD_NUM)
:_num(num)
{
pthread_mutex_init(&_cmutex, nullptr);//初始化消费互斥锁
pthread_mutex_init(&_pmutex, nullptr);//初始化生产互斥锁
pthread_cond_init(&_cond, nullptr);//初始化条件变量
//创建多个线程
for(size_t i = 0; i < _num; ++i)
{
_threads.push_back(new Thread());
}
}
//析构函数
~ThreadPool()
{
pthread_mutex_destroy(&_cmutex);//销毁消费互斥锁
pthread_mutex_destroy(&_pmutex);//销毁生产互斥锁
pthread_cond_destroy(&_cond);//销毁条件变量
//销毁多个线程
for(size_t i = 0; i < _num; ++i)
{
_threads[i]->join();
delete _threads[i];
}
}
//将所有线程启动
void run()
{
for(size_t i = 0; i < _num; ++i)
{
//由于线程函数需要使用线程池类内的函数和每一个线程的名字,所以将它们合起来构造一个线程数据类传递给线程操作函数
ThreadData* p = new ThreadData(this, _threads[i]->threadname());
_threads[i]->start(handler_task, (void*)p);//这里也可以设计一个类
std::string s(p->_threadname);
s += " start...\n";
std::cout << s;
}
}
//向线程池推送任务
void push(const T& data)
{
pthread_mutex_lock(&_pmutex);
_task_queue.push(data);
pthread_cond_signal(&_cond);
pthread_mutex_unlock(&_pmutex);
}
//消费线程取任务,加锁解锁已经在消费线程处理函数里进行了,不需要注意线程安全
T pop()
{
T data = _task_queue.front();
_task_queue.pop();
return data;
}
//静态成员函数需要访问的非静态成员接口
bool isQueueEmpty() {return _task_queue.empty();}//判断任务队列是否为空
void lockQueue() {pthread_mutex_lock(&_cmutex);}//给任务队列加锁
void unlockQueue() {pthread_mutex_unlock(&_cmutex);}//给任务队列解锁
void threadWait() {pthread_cond_wait(&_cond,&_cmutex);}//将线程放入条件变量的等待队列中
private:
//消费线程的处理函数
static void* handler_task(void* args)
{
ThreadData* p = (ThreadData*)args;
while(1)
{
p->_pthreadpool->lockQueue();
//如果任务队列为空,消费者进程会被加入到条件变量的阻塞队列中
while(p->_pthreadpool->isQueueEmpty())
{
p->_pthreadpool->threadWait();
}
T data = p->_pthreadpool->pop();
p->_pthreadpool->unlockQueue();
printf("%s接受了任务%s并处理完成,结果为:%s\n", p->_threadname.c_str(),
data.show_task().c_str(), data().c_str());
}
delete p;
return nullptr;
}
int _num;//维护的线程数量
std::vector _threads;//管理多个线程对象的容器
std::queue _task_queue;//任务队列
pthread_mutex_t _cmutex;//消费者互斥锁
pthread_cond_t _cond;//条件变量
pthread_mutex_t _pmutex;//生成任务时的互斥锁
};
test.cc
#include
#include
#include"CalTask.hpp"
#include"Thread.hpp"
#include"Threadpool.hpp"
using namespace std;
//计算器函数
const string ops = "+-*/%";
int calculate(int a, int b, char op)
{
int result = 0;
switch(op)
{
case '+':
result = a + b;
break;
case '-':
result = a - b;
break;
case '*':
result = a * b;
break;
case '/':
{
if(b == 0)
cerr << "除数不能为0\n";
else
result = a / b;
}
break;
case '%':
{
if(b == 0)
cerr << "取模的数字不能为0\n";
else
result = a % b;
}
break;
default:
break;
}
return result;
}
int main()
{
srand((unsigned int)time(nullptr));
ThreadPool* tp = new ThreadPool();
tp->run();
for(;;)
{
//
int a = rand()%10;
int b = rand()%10;
char op = ops[rand()%ops.size()];
CalTask task(a, b, op, calculate);
tp->push(task);
printf("主线程推送任务:%d %c %d = ?\n", a, op, b);
sleep(1);
}
return 0;
}
运行结果:
我们之前写了一个LockGuard类,我们可以将这个类在task_handler的加锁代码中就可以用起来。
#include
class mutex
{
public:
//构造函数
mutex(pthread_mutex_t* p = nullptr)
:_pmutx(p)
{}
//加锁
void lock()
{
pthread_mutex_lock(_pmutx);
}
//解锁
void unlock()
{
pthread_mutex_unlock(_pmutx);
}
private:
pthread_mutex_t* _pmutx;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t* lock_p):_mutex(lock_p)
{
_mutex.lock();//构造函数内加锁
}
~LockGuard()
{
_mutex.unlock();//析构函数内解锁
}
private:
mutex _mutex;
};
LockGuard构造函数加锁,析构函数解锁。通过{}控制该变量的生命周控制加锁。
由于该函数是由线程执行的,锁是线程池的私有变量,所以也需要增加一个函数获取锁。
最后改造的handler_task代码如下:
代码正常运行。
饿汉模式和懒汉模式都是单例模式的实现方式,具体的介绍可以看这篇博客的第二章:C++特殊类设计及类型转换_聪明的骑士的博客-CSDN博客
现在我们要将线程池改为这两种单例模式。
饿汉模式:不管以后会不会使用单例对象,只要程序一启动,程序就会先创建一个唯一的实例对象然后再执行其他代码。
首先,增加一个静态对象保证单例的唯一性,静态变量需要在根据自己的类型在类外初始化。
然后需要将构造函数设为私有,并且不允许生成拷贝构造和赋值运算符重载。
最后,添加一个静态函数GetInstance()获取单例的地址。
此时,更改一下主线程的代码:
int main()
{
srand((unsigned int)time(nullptr));
ThreadPool::GetInstance()->run();
for(;;)
{
int a = rand()%10;
int b = rand()%10;
char op = ops[rand()%ops.size()];
CalTask task(a, b, op, calculate);
ThreadPool::GetInstance()->push(task);
printf("主线程推送任务:%d %c %d = ?\n", a, op, b);
sleep(1);
}
return 0;
}
程序正常运行。
懒汉模式:单例只有在第一次被使用到时才被建立,就像一个懒汉一样,什么事都拖到截止日才干。
首先,在原来饿汉模式的基础上将单例对象改为单例对象指针,并且设置为空。
然后,为了保证单例的建立是线程安全的,还要增加一个C++11提供的锁并在类外初始化(需要包含mutex.h头文件,这里用C++标准提供的锁是因为它操作更方便)。
最后,使用之前的双检查加锁方式重新设计一下获取单例指针的接口。
主线程代码不需要改,直接运行就可以跑起来了。
最后,所有Linux系统编程的知识就讲解完毕了,接下来我们要进行网络编程的学习。