C++网络编程学习:线程退出安全优化
Posted 河边小咸鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++网络编程学习:线程退出安全优化相关的知识,希望对你有一定的参考价值。
网络编程学习记录
- 使用的语言为C/C++
- 源码支持的平台为:Windows(本文中内容使用windows平台下vs2019开发,故本文项目不完全支持linux平台)
C++网络编程学习:心跳机制与定时发送数据 点我查看之前的代码开发记录
0:本次增改方向
- 封装自己的线程类,使其可以控制线程及时关闭
- 使得程序可以按合适的顺序正常退出,避免因退出顺序问题引发崩溃
1:封装线程类相关
首先,为何要及时使得线程退出?因为我的接收、发送线程与主线程是分离的,当我在主线程中析构线程类时,线程中所使用的变量遂被释放,但此时接收、发送线程可能还未从上次循环中结束,仍然在调用已被释放的变量,此时就会出现崩溃等问题。所以我需要自己封装一个线程类,来实现对线程退出的控制,使得可以获得线程已经正常退出的信号,从而再安全的释放各种变量。
对此,该如何实现?首先可以采用本方法:即新建一个bool
类型的信号量,在主线程发出关闭线程信号后,使用while(1)
进行阻塞。当线程退出时,更改bool信号量的状态,当while(1)中检测到信号量发生变化后,则跳出循环解除阻塞,正常向下运行释放变量。伪代码如下:
主线程内:
{
_state = false;//线程是否运行
while(1)
{
if(_semaphore == true)//查看信号量状态
break;
}
释放变量等;
}
子线程内:
{
while(_state)
{
工作;
}
printf("线程已退出\\n");
_semaphore = true;
}
如上,好处是可以确保线程可以按顺序退出,使得释放变量等操作不会出错。但是坏处是这个while(1)
循环会占用大量的系统资源,影响程序效率。以及可能出现信号量未能正确变化,从而陷入死循环。首先是占用资源太多的问题,我们可以引用C++11中的condition_variable
条件变量,其中含有的wait()
可以减少资源消耗,且使用notify_one()
可以进行唤醒。伪代码如下:
#include<condition_variable>//条件变量
std::condition_variable _cv;
主线程内:
{
std::unique_lock<std::mutex> lock(_mutex);//需上锁
_state = false;//线程是否运行
_cv.wait(lock);//阻塞等待
释放变量等;
}
子线程内:
{
while(_state)
{
工作;
}
printf("线程已退出\\n");
_cv.notify_one();//唤醒
}
如上,阻塞资源消耗太大的问题已经得到了改善,接下来将着重于如何避免死循环状态。首先新建信号量类CellSemaphore
,对信号量操作进行封装,使得线程类内可以直接调用信号量相关操作。结构如下:
class CellSemaphore
{
public:
CellSemaphore();
~CellSemaphore();
//开始阻塞
void wait();
//唤醒
void wakeup();
private:
//等待数
int _wait = 0;
//唤醒数
int _wakeup = 0;
//条件变量
std::condition_variable _cv;
//锁
std::mutex _mutex;
};
如上,CellSemaphore
封装了wait
和wakeup
方法,通过调用这两个方法,即可实现阻塞与唤醒。而成员变量方面,我声明了一个等待计数器_wait
和一个唤醒计数器_wakeup
。每当成功调用wakeup
方法,都会使等待计数器和唤醒计数器加一;而每当成功调用wait
方法,都会使等待计数器和唤醒计数器减一。所以正常情况下,一组操作后,两个计数器的值都为0。而由此也可以判断不同的情况,比如当调用wait
方法时,唤醒计数器的值大于0,则说明之前已经进行了唤醒操作,则直接跳过阻塞即可。而当调用wakeup
方法时,若等待计数器数值不正常,则也直接跳过唤醒操作。相关代码如下:
//开始阻塞
void CellSemaphore::wait()
{
std::unique_lock<std::mutex> lock(_mutex);
if (--_wait < 0)
{
//阻塞开始 等待唤醒
_cv.wait(lock, [this]()->bool
{
return _wakeup > 0;
});
--_wakeup;
}
}
//唤醒
void CellSemaphore::wakeup()
{
std::lock_guard<std::mutex> lock(_mutex);
if (++_wait <= 0)
{
++_wakeup;
_cv.notify_one();
}
}
由此,信号量相关的封装完成了,接下来将进行线程类相关的封装。首先,线程类中需要有一个CellSemaphore
信号量对象以便于我们对线程的掌握。其次,线程的基础函数:启动Start()
、关闭Close()
、退出Exit()
、是否运行isRun()
需要存在。接着,是最重要的线程工作函数OnWork()
。在工作函数OnWork()
中,我预计执行三个匿名函数:_onCreate、_onRun、 _onDestory,这三个匿名函数分别执行线程创建时的操作、线程运行时的操作、线程销毁时的操作。最后还需要一个锁和一个bool变量来保证数据的正常更改以及线程运行状态的判定。线程类CellThread
结构如下:
class CellThread
{
private:
typedef std::function<void(CellThread*)> EventCall;
public:
//启动线程
void Start(EventCall onCreate = nullptr, EventCall onRun = nullptr, EventCall onDestory = nullptr);
//关闭线程
void Close();
//工作中退出
void Exit();
//是否运行中
bool isRun();
protected:
//工作函数
void OnWork();
private:
//三个事件 匿名函数
EventCall _onCreate;
EventCall _onRun;
EventCall _onDestory;
//改变数据时 需要加锁
std::mutex _mutex;
//控制线程的终止与退出
CellSemaphore _semaphore;
//线程是否启动
bool _state = false;
};
在启动线程时,需要传入三个匿名函数(默认为空);当关闭线程时,需要调用wait()
方法;而当退出线程时,由于一般都是出现错误时调用该方法,所以不需要阻塞,直接停止线程即可。
//启动线程
void CellThread::Start(EventCall onCreate, EventCall onRun, EventCall onDestory)
{
//上锁
std::lock_guard<std::mutex> lock(_mutex);
if (!_state)
{
//事件赋值
if (onCreate)
_onCreate = onCreate;
if (onRun)
_onRun = onRun;
if (onDestory)
_onDestory = onDestory;
//线程启动
_state = true;
std::thread t(std::mem_fn(&CellThread::OnWork), this);
t.detach();
}
}
//关闭线程
void CellThread::Close()
{
//上锁
std::lock_guard<std::mutex> lock(_mutex);
if (_state)
{
_state = false;
_semaphore.wait();
}
}
//退出线程
void CellThread::Exit()
{
//上锁
std::lock_guard<std::mutex> lock(_mutex);
if (_state)
{
_state = false;
//这里的目的是退出线程 没必要阻塞等信号量
}
}
//线程是否运行
bool CellThread::isRun()
{
return _state;
}
OnWork
方法内按顺序依次执行三个匿名函数,当销毁阶段函数执行后,调用信号量类的唤醒操作,来告知线程已安全退出。由此,封装线程类相关操作已经完成。我们可以通过相关方法来更精准的操作线程。
void CellThread::OnWork()
{
//开始事件
if (_onCreate)
_onCreate(this);
//运行
if (_onRun)
_onRun(this);
//销毁
if (_onDestory)
_onDestory(this);
_semaphore.wakeup();
}
2:退出顺序相关
在上文完成线程类的封装后,我对源码中的线程相关进行更换。如下,即为线程的创建与关闭操作。
//启动接收线程
_thread.Start(
//onCreate
nullptr,
//onRun
[this](CellThread*)
{
OnRun(&_thread);//工作函数OnRun
},
//onDestory
nullptr);
//关闭接收线程
_thread.Close();
接下来对程序退出的顺序进行规范,大制思路如下:
- 在TcpServer(主线程)宣布程序退出时,首先对接收线程类进行析构,进入接收线程的析构函数。
- 在接收线程的析构函数中,首先对接收线程类中配套的发送线程类进行析构,进入发送线程的析构函数。
- 在发送线程的析构函数中,调用发送线程的
Close()
操作,在线程关闭后,释放发送线程内的相关变量。 - 在发送线程的析构结束后,调用接收线程的
Close()
操作,在线程关闭后,释放接收线程内的相关变量,随后挨个释放储存的客户端连接对象。 - 在释放客户端连接对象时,会进入其析构函数,进行释放相关变量以及关闭socket连接的操作。
- 此时接收线程的析构函数完毕,若还有未释放的接收线程,则重复上述操作。
- 当子线程全部析构后,回到第一步主线程的退出函数中,此时执行关闭主机socket、清除环境、释放变量等操作。
- 至此程序安全退出。
具体更改就不一一叙述了,按照上述思路即可。下图为四条接收/发送线程情况下,程序各部分的退出日志。其中exit为线程退出日志,start/end为析构函数日志。
※ - 项目源码 (github)
提交名:v1.1 线程退出优化
github项目连接
- guguServer为服务端项目
- guguAlloc为内存池静态库项目
- guguDll为相关动态库项目
- debugLib内为debug模式的静态库文件
- lib内为release模式的静态库和动态库文件
以上是关于C++网络编程学习:线程退出安全优化的主要内容,如果未能解决你的问题,请参考以下文章