c++中一个简单易用的线程类设计

  1. 前言

    现在大家在编写程序时,很少使用单线程的方式,往往会创建多线程。今天我们就针对创建线程来进行讨论。

在java、c#中有Thread类可以调用,提供了丰富的操作线程的接口,可以直接调用,所以这不是我们今天所讲的重点。

今天我们主要讲一讲在c++中如何创建线程(调用系统的接口也可以说是c语言,但是c语言不支持面向对象,所以这里不是重点)。

其实系统都提供了创建线程的接口。Windows中通过CreateThread_beginthreadex,linux中可以调用 pthread 接口(支持POSIX标准)来实现线程的创建。但是无论是_beginthreadex 还是 pthread 建立线程都不是那么直观,也不像java中的线程那么易于操作。对于深受面向对象思想影响的我们来说,很想将线程操作封装成类,用操作对象的方式来操作线程。

这时你会说开源库boost不是已经提供了跨平台的thread类了么,为什么还要自己写一个?的确,boost已经提供了我们能想象到的对于线程的操作,为什么我们还要自己重复造轮子呢?

  • 由于boost库很大,很多项目无法直接使用boost库,况且单独裁剪boost中thread部分也不是很简单。
  • 通过编写一个thread类,可以加强我们面向对象设计能力。

下文中的线程设计仅在Windows下通过了验证,所以并未实现跨平台(虽然原本的初衷是跨平台,但是未在linux/unix下做过尝试)。

  1. 需求

    设计原则:

  • 功能简单易用,提供基本的接口,包括如下接口:
    • 开始
    • 停止
    • 获取线程状态
    • 获取线程号
  • 满足面向对象程序设计:
    • 可以继承,子类自动获得父类线程类的功能。
    • 允许子类override线程处理函数,实现其功能。
  1. 设计与实现

    如下给出的都是在Windows平台下的实现,对于若要移植到 linux 平台,需要做一些修改。

前置定义

为了定义线程状态,我们定义一个枚举来表示,如下:

1
	enum ThreadStatus{T_STOPPED=0,T_STARTING,T_RUNNING,T_STOPPING};

如上四个值对应的线程状态说明如下:

  • T_STOPPED :缺省状态:停止。
  • T_STARTING :正在开始的状态,表示线程开始的接口正在执行,未返回。
  • T_RUNNING :运行的状态,线程运行中处于这种状态。
  • T_STOPPING :正在停止的状态,表示线程停止的接口正在执行,未返回。

数据成员

类需要定义如下成员变量:

  • handle_ : 句柄 / 或线程描述符
  • status_ : 线程状态
  • tid_ : 线程号
  • name_ : 线程名。非必选项。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
		//预编译宏
	#ifdef _WIN32
		//thread handle
		HANDLE _handle;
	#else
		unsigned int _handle;
	#endif // _WIN32

		//tid
		unsigned int _tid;

		//tname
		string _name;

		//Thread status
		ThreadStatus _status;

接口定义

  • 公有方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
	//构造函数
	explicit BaseThread(string tname);

	//析构函数
	virtual ~BaseThread();

	//线程开始,需要返回线程句柄或线程描述符。
	unsigned int Start();

	//线程结束,有可能会被阻塞。
	void Stop();

	//线程中循环执行的函数,可以被override
	virtual void Run(){};

	//获取线程状态
	inline ThreadStatus GetState()	const{	return this->_status;}

	//获取线程号
	inline unsigned int GetTid() const {return _tid;}
  • 私有方法:
1
2
3
4
5
6
7
8
	//拷贝构造函数,设为私有,使该类的对象不可被拷贝
	BaseThread(const BaseThread&);

	//赋值操作符,设为私有,使该类的对象不可被拷贝
	BaseThread& operator=(const BaseThread&);

	//线程函数的入口地址,参数为当前线程类的 this 指针
	static unsigned __stdcall StartAddress(void* para);

实现

  • 构造函数。在初始化列表中初始化成员变量,不需要做任何操作。
1
2
3
4
5
	//构造函数
	BaseThread::BaseThread(string tname):
	_name(tname),_tid(0),_handle(0),_status(T_STOPPED)
	{
	}
  • Start() 。使用 _beginthreadex 函数建立线程,并执行,获取线程号,设置线程状态,返回线程句柄并保存。如果调用该接口时,线程已经启动,则直接返回线程句柄。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	//线程开始
	unsigned BaseThread::Start()
	{
		if(0 != _handle)
			return (unsigned)_handle;

		_status = T_STARTING;
		printf("Before begin thread tid=%d State is %d\n",_tid,_status);
		_handle = reinterpret_cast<HANDLE>(_beginthreadex(NULL,0,StartAddress,this,0,&_tid));
		if(0 != _handle)
		{
			printf("Thread %s create succeed. thread tid=%d\n",this->_name.c_str(),this->_tid);
		}
		else
		{
			_status = T_STOPPED;
			printf("Thread create failure.errno=%d\n",errno);
		}

		return (unsigned)_handle;
	}
  • Stop() 。先将线程状态设置为 正在停止状态 ,使用 WaitForSingleObject 等待线程结束,等待条件为句柄 _handle 无效,线程结束后,将其状态设为 已经停止状态
1
2
3
4
5
6
7
8
9
10
11
12
13
	//线程停止
	void BaseThread::Stop()
	{
		if(0 != _handle)
		{
			this->_status = T_STOPPING;
			printf("Waiting For the thread stopping... tid=%d\n",_tid);
			WaitForSingleObject(_handle,INFINITE);
			CloseHandle(_handle);
			_handle = 0;
		}
		this->_status = T_STOPPED;
	}
  • StartAddress()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
	//线程入口函数
	unsigned int __stdcall BaseThread::StartAddress(void* para)
	{
		Sleep(10);
		if(NULL == para)
		{
			printf("Thread \'StartAddress\'  Param is NULL \n");
			return 0;
		}
		BaseThread* pthread = static_cast<BaseThread*>(para);
		//atomic operation
	#if defined(_MSC_VER) && _MSC_VER>1200
		//vc10
		InterlockedCompareExchange((long*)&pthread->_status,(long)T_RUNNING,(long)T_STARTING);
	#else
		//vc6
		InterlockedCompareExchange((void**)&pthread->_status,(void*)T_RUNNING,(void*)T_STARTING);
	#endif //VC6 == 1200
		while(pthread->_status == T_RUNNING)
		{
			pthread->Run();
		}

		return 0;
	}

注:InterlockedCompareExchange((long*)&pthread->_status,(long)T_RUNNING,(long)T_STARTING);,当pthread->_status 等于 T_STARTING时,其值才被改为T_RUNNING;否则其值不变。该操作整体为一个原子操作。

使用InterlockedCompareExchange可以解决在连续调用StartStop 时对 pthread->_status 值的竞争。如果不是原子操作,有一种可能,在判断完pthread->_status 等于 T_STARTING 与将其改为T_RUNNING的间隔,调用 Stop 有可能其值会被改为 T_STOPPING,随后再被改为T_RUNNING,接着线程正常执行循环,但是此时上层用户已经调用过Stop()了,所以产生了不一致的矛盾。

  • 析构函数 。调用Stop()接口即可,有可能会被阻塞。
1
2
3
4
5
6
	//析构函数
	BaseThread::~BaseThread()
	{
		//may block
		this->Stop();
	}
  1. 使用方法

    • 继承 BaseThread
    • override Run(); 实现自己的功能。
  2. TODO

    此线程类仅实现了最基本的功能。

目前的设计线程自身无法停止自身。可以采用这样的解决方法:添加具有 “信号” 含义的数据成员,使Run()函数在执行的时候根据条件设置信号的值,从而在线程内部自己停止自己。但是这样会破坏了线程的封装。所以一直没有找到好的解决方法(由于Windows并不支持abort这样的信号来停止一个线程),希望能得到大家多多的指点!

期待和大家的交流!


庄永耀 2014-03-10


写于 2014. 03. 10