C++:多线程类库的设计与实现(五)

来源:互联网 发布:数据信息知识的关系 编辑:程序博客网 时间:2024/05/25 21:33
原文地址:http://yuyunwu.blog.sohu.com/67137268.html

设计线程类库的接口


笔者按:因为工作忙,很长一段时间没有更新文章了。闲暇时间太少是个大问题,可惜短期内没有改善的趋向,只好能做多少是多少了

因为对类库的接口作了一些修改,相应地本篇文章也作若干修正


这一篇文章中我们着手设计C++多线程类库的接口。

 

根据我们前几篇文章的分析,我们希望线程类Thread至少包含下面的方法。

class Thread {

public:

    void Start();

    bool WaitForEnd()

    void Terminate();

}; // class Thread

 

Start用于启动线程,WaitForEnd用于等待线程结束,Terminate用于强制结束线程。

线程启动时失败怎么办?我们可以把Start定义为bool型,不过,从操作系统角度来看,线程创建失败可以被视为一种小概率的重大错误,我们可以用异常来描述这种错误。为此,我们需要为我们的线程库定义一个异常类,姑且把这个类定义为ThreadException,并且假设它是从一个更为一般的异常类Exception派生而来。

然后我们再为定义细化的异常类。这里我们首先定义ThreadCreateException,它是从ThreadException派生而来

另外我们还希望在启动线程时可以指定线程的初始栈空间的大小---这对许多内存有限的系统来说是非常必要的,这样,ThreadStart方法应该为:

 

void Start(size_t cbStackSize) throw (ThreadCreateException);

 

类似的,让等待线程结束方法WaitForEnd可以设置超时参数,在超时的情况下返回false:

 

bool WaitForEnd(int iTimeoutMilliseconds);

 

我们还希望可以判断线程的运行状态。从操作系统来看,一个线程可以拥有等待、就绪、运行、僵死等多种调度状态,在这里我们只区分非僵死与僵死(包括无效---未被创建等)状态,因为从用户观点来看,线程的就绪与运行状态是用户不可控制的,而对于等待状态信息的需求也不是很高,对于这些状态的区分可以留给将来的功能扩展。

这样我们还应该提供判断线程死活的方法:

bool IsAlive();

到此为止,我们还没有考虑用户如何使用这个线程类。C的做法是用户定义一个例程函数,然后将函数地址作为参数传递给线程创建函数。当然我们也可以沿用这个方法,不过这个方法不够“OO”,于是我们采用让用户重载需函数的方法。这样我们再提供下面这个需函数:

 
virtual void Run();

 
我们怎么识别线程呢?当然每个Thread对象具有不同的实例,也就有不同的内存映像。我们也可以为Thread定义名字,这样又有了:

 
string GetName();

 
另外,我们考虑为Thread对象定义ID。这对于Windows平台来说比较好解决,直接返回线程的系统ID就可以了,但是对于Linux稍微有些麻烦,因为在POSIX标准(ISO/IEC 9945)虽然引入了线程ID的概念,亦即thread_t类型变量,但实际上很多Linux系统的具体实现都把这个变量等同于内部某个线程数据结构的内存地址,这样就与我们一般理解的“ID”的语义有些出入。一般来说,ID,亦即所谓的“标识符”,如果取为整数型,则应该为从一个较小的自然数开始,具有逐渐递增分配的自然数值域的变量类型。而指针是不具有这种特性的(通常虚拟地址都是一个很大的无符号“数”)。另外,Windows系统的线程ID是系统域唯一的,而进程内的虚拟地址空间并不能保证地址的唯一性。

 
越往深考虑就越麻烦了。我们不妨这样约定,保证至少在进程内部线程ID是唯一的,然后要提醒用户不要对这个ID的值域有任何假定。

 
unsigned int GetId();

 
我们再提供一个线程睡眠函数,因为与具体对象属性无关,这个函数应该是静态成员函数:

 
static void Sleep(int iMilliseconds);

 

基本上由这些都够了。不过,既然是C++类设计,就不得不考虑异常处理。我们知道C++异常是具有线程执行域的,也就是子线程的异常必须在子线程中处理。为此,我们再为用户提供一个异常处理回调函数:

 

void OnException(const Exception *);

 

这个函数包含一个指向异常对象的指针。如果异常不可识别,则该指针为NULL


另外,我们有必要考虑一下所谓“线程泄漏”问题。线程泄漏是指,当线程类对象被销毁时,线程实体仍然在运行的情况。我们的线程类不具有引用计数功能,所以本来线程类对象被销毁时按照正常逻辑线程实体应该已经终止运行,否则此线程对象一经销毁,我们在程序中就失去有关该线程的所有信息,而这个线程实体也就成了脱缰野马,不再受我们控制。线程泄漏属于一种异常情况,而这种异常情况大多数是由于程序设计不良而导致的,当然也不排除某些极端的情况(比如OS或系统线程库本身的BUG,一般而言概率极低)。对于线程类来说,至少给用户提供一个检测和报告这种异常的机会,我们定义线程泄漏回调函数,并在在线程的析构函数中进行线程实体结束检测。

virtual void OnLeaking() throw ();

 
我们说过要考虑后台线程模型,这类线程通常在某一个特定时间点去做某一件事。我们可以在基本线程类的基础上派生后台线程类,这些内容我们放到另一篇文章中详细讨论。

基本的考虑点都已完成了,这样我们就可以编写这个线程类了。进行类设计的时候,参考Andrew Koening<<Ruminations On C++>>提供的类检查表(Class Check List)是非常有帮助的。我们只取其中我们感兴趣的一部分进行检查。

 

*类需要一个构造函数吗?

需要。我们需要初始化相关的一些变量,并且允许用户在创建Thread对象的时候为其指定名称。

 

*成员是私有的吗?

成员的设计我们放到下一篇文章中讨论。基本方针是成员变量都是私有的。

 

*需要无参的构造函数吗?

需要。无参时线程对象的名字为空字符串。

 

*类需要析构函数吗?

需要,因为我们要检测线程是否泄漏,具体设计我们留到下一篇文章详细分析。

 

*类需要复制构造函数吗?

不需要。而且我们禁止复制构造函数(将其定义为私有)。因为“复制”的语义对线程来说是难以界定的,我们既不能用内存拷贝为正在执行中的线程复制一个副本,也不能简单地把两个Thread对象指向同一个线程---我们的Thread类不具有引用计数功能(关于对象的引用计数以及与之相关的“计数指针”类的设计,我们将在别的文章中进行讨论)。

 

*类需要赋值操作符吗?

不需要。而且我们禁止它。理由和方法同上。

 

至此我们Thread类的经设计已经基本完成了,细节性的问题我们留到下一篇讨论。Thread的类代码(尚未完成)如下。

 

// file : Thread.h

 
#ifndef THREAD_H

#define THREAD_H

 

class ThreadException;

 

class ThreadCreateException;

 

class Thread 

{

   

protected:

 

    /**

     * The name of the thread, or "" for unnamed.

     */

    string m_strThreadName;

 

public:

    explicit Thread(const string & strThreadName);

   

    virtual ~Thread();

 

private:

    virtual void Run();

    virtual void OnException(const Exception * pEx);

    virtual void OnLeaking() throw ();

public:

 

    virtual void Start(size_t cbStackSize);

 

    bool WaitForEnd(int iTimeout);

 

    virtual void Terminate(int iExitCode);

 

    virtual bool IsAlive() const;

 

    static void Sleep(int iMilliseconds);

   

    string GetName () const;

 

    int GetId() const;

   

private:

 
    // Prevent copy constructing.

    Thread(const Thread &) throw();

    void operator=(const Thread &);

   

}; // class Thread

 

#endif // #ifndef THREAD_H

 

//--EOF--