对于一个大型的复杂项目。或者多人协作的项目,如何处理同时运行的线程,如何在线程之间传递消息,处理同步是首要问题。
经历过C++技术面的同学应该深有体会,在面试中,多线程/观察者模式/数据总线/事件总线都是经常被问到的重灾区。最近在工作中遇到的此类场景比较多,因此对这部分知识做一个梳理。
一.UI线程
首先提一个问题:
UI线程是主线程吗?
以典型的C++(QT)窗口应用程序为例,
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
一个窗口应用程序有一个main.cpp,里边有一个main函数。编译器在执行预编译后,会找到main函数作为全局程序的主函数开始启动。而main函数里的Mainwindow(根据你自己的UI名定义)创建了一个UI,并为之分配了空间。这条在main函数里执行,创建了主UI的线程是这个基本窗口应用程序的主线程。
话说到这里,好像有点似是而非的感觉。说了主线程,说了UI线程,看起来像是一个东西,可是为什么呢?
对窗口应用程序而言,主线程就是UI线程,UI线程就是主线程。我们在学习编程的过程中,被无数次的告知,不要在子线程里更新UI。 GUI为了性能(不知道GUI的可以自己查一下),故意让你只能在一个线程里面操作UI。多线程操作同一个UI时,很容易导致,或者极其容易导致反向加锁和死锁问题。
简单地讲,两个线程不能同时paint,否则屏幕会花;不能同时insert map,否则内存会花;不能同时write buffer,否则文件会花。需要互斥,比如锁。结果就是同一时刻只有一个线程可以做UI。那么当两个线程互斥几率较大时,或者保证互斥的代码复杂时,选择其中一个做为主线程操作UI,其他线程发送消息给它,再由它完成UI的刷新,这是一种比较成熟的结局方案。
因此,主线程是UI线程,操作界面,子线程操作消息,处理运算,这是一个典型窗口应用程序的方式。
实际在应用中,还有更复杂更标准的线程设计模式。
二.多线程设计处理
“主线程操作UI,而更加耗时的计算操作交给子线程去做”。
这句话相信大家也已经很熟悉了,实际在设计过程中,除了复杂耗时多的计算操作,还有别的复杂事务也需要交给子线程去完成。通常的UI界面运行在主线程,系统的其它部分可能运行在不同的线程。多线程交互的难点在于将事件派发到开发者期望的线程,因此涌现了大量的技术,比如信号量、并发队列、Window消息、轮询等技术,这也使得系统中对于跨线程事件派发出现多种多样的形式,导致系统的可维护大打折扣。
1.事件总线与观察者模式
一个成熟的系统应该设计一条事件总线(EventBus)进行事件派发,用于处理跨线程事件派发。它和普通的事件派发器最大的差别在于事件派发的方式,普通事件派发器在事件发送的时候向事件监听(订阅)者进行派发,事件总线将事件先派发到各个线程,再由各个 线程的派发器进行事件派发。
提到监听,不得不提到设计模式中的观察者模式:
观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被完成业务的更新。
观察者模式属于行为模式,一个对象(被观察者)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。它的主要成员就是观察者和被观察者。
-
被观察者(Observerable):目标对象,状态发生变化时,将通知所有的观察者。
-
观察者(observer):接受被观察者的状态变化通知,执行预先定义的业务。
使用场景: 完成某件事情后,异步通知场景。如,登陆成功,发个IM消息等等。经典的订阅模式就是基于观察者模式设计实现。
要实现一个基本的观察者模式,首先要定义一个被观察者类。而“观察”行为本身是多对一的,即一个被观察者会被多个观察者观察,每个观察者针对被观察者的变动消息有着自己的独立处理逻辑。
class Observerable {
private:
List<Observer> observers = new ArrayList<Observer>();//观察者集合
int state;//状态
public:
int getState() {
return state;
}//获取被观察者状态
void setState(int state) {
notifyAllObservers();
}//设置被观察者状态
//添加观察者
void addServer(Observer observer){
observers.add(observer);
}
//移除观察者
void removeServer(Observer observer){
observers.remove(observer);
}
//通知
void notifyAllObservers(int state){
if(state!=1){
cout << "不是通知的状态" << endl;
return ;
}
for (Observer observer : observers) {
observer.doEvent();
}
}
}
在复杂的系统中,所有可以被观察的被观察者往往会被进行封装,而所有需要进行观察的观察者不会直接与被观察者连接,而是通过一个中间消息处理类去中转处理逻辑。这样可以很好的保护系统底层数据安全,防止线程锁冲突。观察者通过中间类获取被观察者的状态消息,这样的方式称为订阅。
class EventBusCenter {
//一个基本的事件总线
private:
static EventBus eventBus = new EventBus();
//先定义一个静态的事件总线变量
EventBusCenter() {
}
public:
static EventBus getInstance() {
return eventBus;
}
//获取句柄
//添加观察者
static void register(Object obj) {
eventBus.register(obj);
}
//移除观察者
static void unregister(Object obj) {
eventBus.unregister(obj);
}
//把消息推给观察者
static void post(Object obj) {
eventBus.post(obj);
}
}
以上是事件总线的基本模板,接下来需要定义观察者,设计响应函数。
class EventListener {
//观察者,事件监听类处理消息
public:
void handle(NotifyEvent notifyEvent) {
cout<<("发送IM消息" + notifyEvent.getInfo())<<endl;
}
}
//通知事件类
class NotifyEvent {
private:
String info;
public:
NotifyEvent(String info) {
this.info = info;
}
}
最后进行测试:
class EventBusDemoTest {
public:
void main(String args) {
EventListener eventListener = new EventListener();
EventBusCenter.register(eventListener);
EventBusCenter.post(new NotifyEvent("info"));
}
}
2.复杂的事件总线设计
不同的项目因为架构的不同,对于事件的处理也有不同的方式。
所以接下来的内容是以我目前接手的项目为例,介绍一下这种设计方法。
1.订阅
主线程工作者 (Main_Thread_Worker())事件线程工作者(GetEventWorker())数据线程工作者 (Current_Worker())异步线程工作者(Async_Worker())阻塞线程工作者(Null)
2.发布
三.线程池
熟悉QT的朋友应该知道线程池的概念QThreadPool,它是QT自定义的线程池类,用于管理多线程并发业务场景。
在前文中也提到,线程池也可以用于进行异步派发事件,触发多条线程的消息响应函数。
在应用中经常会遇到这样一种场景,我不需要进行复杂的操作,也不需要搞复杂的订阅发布,我只是单纯的把一个简单操作重复很多次。
这个时候就需要用到线程池来对并发操作进行管理,理论上你当然也可以不用线程池,每当需要进行一次操作的时候新建一条线程,代价是功耗飙升CPU占用率+++++。
所以还是需要这样一个pool,来帮助快速降低和减少性能损耗。
那么,一个典型的线程池应该是怎么样的呢。
1.定义
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。
如果某个线程在托管代码中空闲(如正在等待某个事件), 则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
简单解析这段话,线程池应该是由这样几个东西组成:
- 任务队列
需要进行的任务将被添加到队列,线程被创建后会启动并执行任务。
队列的长度有限,超出任务队列处理能力的任务会被挂机排队,等到之前执行任务的线程完成任务释放资源后才能进行新任务的处理。同理,已执行的任务会从任务队列里删除。
- 工作线程
就像食堂打饭一样,任务队列里充满了任务,而工作的线程们不断读取任务(打饭)进行执行(吃饭),执行完毕后释放资源,然后重新获取任务(打饭)。
没有任务待执行怎么办?工作线程会被阻塞挂起。(等阿姨上菜)
当任务队列里添加任务之后,工作线程会解除阻塞,重新开始工作(打饭吃饭)。
- 管理线程
相当于食堂主管,线程池创建时,创建工作线程组合。当线程工作时,管理调度线程与任务。当挂起线程数目过多时,删除一部分线程以释放资源。
// 任务
typedef struct Task
{
void (*function)(void* arg);
void* arg;
}Task;
// 线程池结构体
struct ThreadPool
{
// 任务队列
Task* myTask;
int queueCapacity; // 容量
int queueSize; // 当前任务个数
int queueFront; // 队头 -> 取数据
int queueRear; // 队尾 -> 放数据
pthread_t managerID; // 管理者线程ID
pthread_t *threadIDs; // 工作的线程ID
int minNum; // 最小线程数量
int maxNum; // 最大线程数量
int busyNum; // 忙的线程的个数
int liveNum; // 存活的线程的个数
int exitNum; // 要销毁的线程个数
pthread_mutex_t mutexPool; // 锁整个的线程池
pthread_mutex_t mutexBusy; // 锁busyNum变量
pthread_cond_t notFull; // 任务队列是不是满了
pthread_cond_t notEmpty; // 任务队列是不是空了
bull shutdown; // 是不是要销毁线程池, 销毁为true, 不销毁为false
};
2.实现
首先,我们需要写一个自定义线程池的头文件。
#ifndef _THREADPOOL_H
#define _THREADPOOL_H
//刚刚已经定义了线程池结构体
typedef struct ThreadPool ThreadPool;
// 创建线程池并初始化
ThreadPool *threadPoolCreate(int min, int max, int queueSize);
// 销毁线程池
int threadPoolDestroy(ThreadPool* pool);
// 给线程池添加任务
void threadPoolAdd(ThreadPool* pool, void(*func)(void*), void* arg);
// 获取线程池中工作的线程的个数
int threadPoolBusyNum(ThreadPool* pool);
// 获取线程池中活着的线程的个数
int threadPoolAliveNum(ThreadPool* pool);
//
// 工作的线程(消费者线程)任务函数
void* worker(void* arg);
// 管理者线程任务函数
void* manager(void* arg);
// 单个线程退出
void threadExit(ThreadPool* pool);
#endif // _THREADPOOL_H
然后在CPP里进行实现。
#ifndef _THREADPOOL_H
#define _THREADPOOL_H
//刚刚已经定义了线程池结构体
typedef struct ThreadPool ThreadPool;
// 创建线程池并初始化
ThreadPool *threadPoolCreate(int min, int max, int queueSize);
// 销毁线程池
int threadPoolDestroy(ThreadPool* pool);
// 给线程池添加任务
void threadPoolAdd(ThreadPool* pool, void(*func)(void*), void* arg);
// 获取线程池中工作的线程的个数
int threadPoolBusyNum(ThreadPool* pool);
// 获取线程池中活着的线程的个数
int threadPoolAliveNum(ThreadPool* pool);
//
// 工作的线程(消费者线程)任务函数
void* worker(void* arg);
// 管理者线程任务函数
void* manager(void* arg);
// 单个线程退出
void threadExit(ThreadPool* pool);
#endif // _THREADPOOL_H
ThreadPool* threadPoolCreate(int min, int max, int queueSize)
{
//创建一个线程池并分配空间
ThreadPool* pool = (ThreadPool*)malloc(sizeof(ThreadPool));
do
{
if (pool == NULL)
{
//分配内存失败则直接退出
printf("malloc threadpool fail...\n");
break;
}
pool->threadIDs = (pthread_t*)malloc(sizeof(pthread_t) * max);
if (pool->threadIDs == NULL)
{
//分配线程内存失败则直接退出
printf("malloc threadIDs fail...\n");
break;
}
memset(pool->threadIDs, 0, sizeof(pthread_t) * max);
pool->minNum = min;
pool->maxNum = max;
pool->busyNum = 0;
pool->liveNum = min; // 和最小个数相等
pool->exitNum = 0;
if (pthread_mutex_init(&pool->mutexPool, NULL) != 0 ||
pthread_mutex_init(&pool->mutexBusy, NULL) != 0 ||
pthread_cond_init(&pool->notEmpty, NULL) != 0 ||
pthread_cond_init(&pool->notFull, NULL) != 0)
{
//获取线程权限失败则退出
printf("mutex or condition init fail...\n");
break;
}
// 任务队列
pool->myTask = (Task*)malloc(sizeof(Task) * queueSize);
pool->queueCapacity = queueSize;
pool->queueSize = 0;
pool->queueFront = 0;
pool->queueRear = 0;
pool->shutdown = false;
// 创建线程
pthread_create(&pool->managerID, NULL, manager, pool);
for (int i = 0; i < min; ++i)
{
pthread_create(&pool->threadIDs[i], NULL, worker, pool);
}
return pool;
} while (0);
// 释放资源
if (pool && pool->threadIDs) free(pool->threadIDs);
if (pool && pool->taskQ) free(pool->taskQ);
if (pool) free(pool);
return NULL;
}
int threadPoolDestroy(ThreadPool* pool)
{
if (pool == NULL)
{
return -1;
}
// 关闭线程池
pool->shutdown = 1;
// 阻塞回收管理者线程
pthread_join(pool->managerID, NULL);
// 唤醒阻塞的消费者线程
for (int i = 0; i < pool->liveNum; ++i)
{
pthread_cond_signal(&pool->notEmpty);
}
// 释放堆内存
if (pool->taskQ)
{
free(pool->taskQ);
}
if (pool->threadIDs)
{
free(pool->threadIDs);
}
pthread_mutex_destroy(&pool->mutexPool);
pthread_mutex_destroy(&pool->mutexBusy);
pthread_cond_destroy(&pool->notEmpty);
pthread_cond_destroy(&pool->notFull);
free(pool);
pool = NULL;
return 0;
}
void threadPoolAdd(ThreadPool* pool, void(*func)(void*), void* arg)
{
pthread_mutex_lock(&pool->mutexPool);
while (pool->queueSize == pool->queueCapacity && !pool->shutdown)
{
// 阻塞生产者线程
pthread_cond_wait(&pool->notFull, &pool->mutexPool);
}
if (pool->shutdown)
{
pthread_mutex_unlock(&pool->mutexPool);
return;
}
// 添加任务
pool->taskQ[pool->queueRear].function = func;
pool->taskQ[pool->queueRear].arg = arg;
pool->queueRear = (pool->queueRear + 1) % pool->queueCapacity;
pool->queueSize++;
pthread_cond_signal(&pool->notEmpty);
pthread_mutex_unlock(&pool->mutexPool);
}
int threadPoolBusyNum(ThreadPool* pool)
{
pthread_mutex_lock(&pool->mutexBusy);
int busyNum = pool->busyNum;
pthread_mutex_unlock(&pool->mutexBusy);
return busyNum;
}
int threadPoolAliveNum(ThreadPool* pool)
{
pthread_mutex_lock(&pool->mutexPool);
int aliveNum = pool->liveNum;
pthread_mutex_unlock(&pool->mutexPool);
return aliveNum;
}
void* worker(void* arg)
{
ThreadPool* pool = (ThreadPool*)arg;
while (1)
{
pthread_mutex_lock(&pool->mutexPool);
// 当前任务队列是否为空
while (pool->queueSize == 0 && !pool->shutdown)
{
// 阻塞工作线程
pthread_cond_wait(&pool->notEmpty, &pool->mutexPool);
// 判断是不是要销毁线程
if (pool->exitNum > 0)
{
pool->exitNum--;
if (pool->liveNum > pool->minNum)
{
pool->liveNum--;
pthread_mutex_unlock(&pool->mutexPool);
threadExit(pool);
}
}
}
// 判断线程池是否被关闭了
if (pool->shutdown)
{
pthread_mutex_unlock(&pool->mutexPool);
threadExit(pool);
}
// 从任务队列中取出一个任务
Task task;
task.function = pool->taskQ[pool->queueFront].function;
task.arg = pool->taskQ[pool->queueFront].arg;
// 移动头结点
pool->queueFront = (pool->queueFront + 1) % pool->queueCapacity;
pool->queueSize--;
// 解锁
pthread_cond_signal(&pool->notFull);
pthread_mutex_unlock(&pool->mutexPool);
printf("thread %ld start working...\n", pthread_self());
pthread_mutex_lock(&pool->mutexBusy);
pool->busyNum++;
pthread_mutex_unlock(&pool->mutexBusy);
task.function(task.arg);
free(task.arg);
task.arg = NULL;
printf("thread %ld end working...\n", pthread_self());
pthread_mutex_lock(&pool->mutexBusy);
pool->busyNum--;
pthread_mutex_unlock(&pool->mutexBusy);
}
return NULL;
}
void* manager(void* arg)
{
ThreadPool* pool = (ThreadPool*)arg;
while (!pool->shutdown)
{
// 每隔3s检测一次
sleep(3);
// 取出线程池中任务的数量和当前线程的数量
pthread_mutex_lock(&pool->mutexPool);
int queueSize = pool->queueSize;
int liveNum = pool->liveNum;
pthread_mutex_unlock(&pool->mutexPool);
// 取出忙的线程的数量
pthread_mutex_lock(&pool->mutexBusy);
int busyNum = pool->busyNum;
pthread_mutex_unlock(&pool->mutexBusy);
// 添加线程
// 任务的个数>存活的线程个数 && 存活的线程数<最大线程数
if (queueSize > liveNum && liveNum < pool->maxNum)
{
pthread_mutex_lock(&pool->mutexPool);
int counter = 0;
for (int i = 0; i < pool->maxNum && counter < NUMBER
&& pool->liveNum < pool->maxNum; ++i)
{
if (pool->threadIDs[i] == 0)
{
pthread_create(&pool->threadIDs[i], NULL, worker, pool);
counter++;
pool->liveNum++;
}
}
pthread_mutex_unlock(&pool->mutexPool);
}
// 销毁线程
// 忙的线程*2 < 存活的线程数 && 存活的线程>最小线程数
if (busyNum * 2 < liveNum && liveNum > pool->minNum)
{
pthread_mutex_lock(&pool->mutexPool);
pool->exitNum = NUMBER;
pthread_mutex_unlock(&pool->mutexPool);
// 让工作的线程自杀
for (int i = 0; i < NUMBER; ++i)
{
pthread_cond_signal(&pool->notEmpty);
}
}
}
return NULL;
}
void threadExit(ThreadPool* pool)
{
pthread_t tid = pthread_self();
for (int i = 0; i < pool->maxNum; ++i)
{
if (pool->threadIDs[i] == tid)
{
pool->threadIDs[i] = 0;
printf("threadExit() called, %ld exiting...\n", tid);
break;
}
}
pthread_exit(NULL);
}
总结
思绪比较发散,因为是两天写出来的东西。
总之,本文主要是两个方向的内容,一块介绍了观察者模式和常见的多线程同步异步处理设计架构,一块介绍了线程池的底层原理及自我实现。
在实际的生产过程中,根据自己的业务需求,对于多线程的操作可以继承语言本身提供的多线程管理类EventBus或者ThreadPool,也可以自己手写去进行实现。在了解了底层原理后,对于实际应用中的常见场景和问题,会处理得更得心应手。
多线程是操作系统里的核心部分,想要学好操作系统,手撕源码是必经之路。