C++网络编程学习:线程退出安全优化

Posted 河边小咸鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++网络编程学习:线程退出安全优化相关的知识,希望对你有一定的参考价值。

网络编程学习记录

  • 使用的语言为C/C++
  • 源码支持的平台为:Windows(本文中内容使用windows平台下vs2019开发,故本文项目不完全支持linux平台)

C++网络编程学习:心跳机制与定时发送数据  点我查看之前的代码开发记录


0:本次增改方向

  1. 封装自己的线程类,使其可以控制线程及时关闭
  2. 使得程序可以按合适的顺序正常退出,避免因退出顺序问题引发崩溃

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封装了waitwakeup方法,通过调用这两个方法,即可实现阻塞与唤醒。而成员变量方面,我声明了一个等待计数器_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();

  接下来对程序退出的顺序进行规范,大制思路如下:

  1. 在TcpServer(主线程)宣布程序退出时,首先对接收线程类进行析构,进入接收线程的析构函数。
  2. 接收线程的析构函数中,首先对接收线程类中配套的发送线程类进行析构,进入发送线程的析构函数。
  3. 发送线程的析构函数中,调用发送线程的Close()操作,在线程关闭后,释放发送线程内的相关变量。
  4. 发送线程的析构结束后,调用接收线程的Close()操作,在线程关闭后,释放接收线程内的相关变量,随后挨个释放储存的客户端连接对象。
  5. 在释放客户端连接对象时,会进入其析构函数,进行释放相关变量以及关闭socket连接的操作。
  6. 此时接收线程的析构函数完毕,若还有未释放的接收线程,则重复上述操作。
  7. 子线程全部析构后,回到第一步主线程的退出函数中,此时执行关闭主机socket、清除环境、释放变量等操作。
  8. 至此程序安全退出。

  具体更改就不一一叙述了,按照上述思路即可。下图为四条接收/发送线程情况下,程序各部分的退出日志。其中exit为线程退出日志,start/end为析构函数日志。
在这里插入图片描述

※ - 项目源码 (github)

提交名:v1.1 线程退出优化
github项目连接
在这里插入图片描述

  • guguServer为服务端项目
  • guguAlloc为内存池静态库项目
  • guguDll为相关动态库项目
  • debugLib内为debug模式的静态库文件
  • lib内为release模式的静态库和动态库文件

以上是关于C++网络编程学习:线程退出安全优化的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程学习:线程安全与锁优化

C++ Windows 线程不会退出

《C++多线程编程》学习笔记

使用 C++ 反转句子中的每个单词需要对我的代码片段进行代码优化

C++笔记--Linux编程(13)-守护进程-线程

JVM学习记录-线程安全与锁优化