如何写优雅的代码(4)——简单有效地玩转线程

//========================================================================
//TITLE:
//    如何写优雅的代码(4)——简单有效地玩转线程
//AUTHOR:
//    norains
//DATE:
//    Monday 23- November-2009
//Environment:
//    WINDOWS CE 5.0
//========================================================================
    线程的使用,说复杂吧,却又是只有那几个函数,无非就是通过CreateThread创建线程,然后再通过CloseHanle关闭句柄,大不了再加一个SetThreadPriority来设置优先级;说它简单吧,如何正常退出线程,如何有效地使用线程,却又往往让初学者头疼。

 

    本文主题是如何简单却又有效地使用线程,但不涉及复杂的线程间数据交换。

 

    首先,我们先来了解如何创建线程。很简单,调用CreateThread函数即可。该函数的原型如下:

 

    lpsa形参好办,不用我们担心,只要直接设置为NULL;cbStack也容易,一般使用的话,我们也很少使用自定义的堆栈,所以这个也可以直接这只为NULL。lpStartAddr是最重要的一个,指向我们线程的处理函数。lpvThreadParam是传递给处理函数的形参,如果线程处理函数是封装于类中,那么这玩意可万万不能忽略。后面的fdwCreate和lpIDThread,如果没有特殊的用途,也可以一并设置为NULL。

 

    所以,最简单的的线程创建函数的调用将可以如此:

    如果后续不需要对该线程进行设置,比如更改优先级之类,那么创建完毕后,我们可以调用CloseHandle关闭句柄:

 

    需要注意的是,这里的关闭句柄,并不意味着是关闭线程处理函数,而只是将句柄对象从系统中删除而已。简单但又不失严谨来说,对于系统,有一个列表,是用来记录创建的线程对象;当该对象不再使用时,我们必须将其关闭,以避免句柄泄漏。

 

    接下来,我们再看看线程处理函数的原型:

 

    没什么特别的,返回值为DWORD,形参只有一个,为lpParameter。这个lpParameter的数值,就是CreateThread的第四个形参。

我们简单地说说这形参是怎么传递的。

 

    如果我们代码是这么创建线程的:

 

    那么我们的线程可以这么获取数值:

 

    此时ThreadProc中的数值即为123。

 

    似乎这形参并没有多大的作用,如果仅仅是为了传递一个DWORD类型的数值,我们完全可以采用全局变量的方式。那我们现在将话题往前推一点,看看在类中封装线程处理函数的情形。这时候,这个看似没多大用的形参,却是我们访问成员变量或函数的唯一桥梁。

 

    在CreateThread的描述中,很清楚知道,我们不能将对象函数的地址作为参数传递,而只能传递类函数。通俗点来说,只有用了static修饰的函数才能作为形参。

 

如:

 

    上面的的ThreadProc是无法作为函数形参的。但下面的这个,就能作为形参:

 

    虽然增加static修饰是可以作为形参传递,但我们不可避免会遇到一个问题,就是在ThreadProc中无法访问对象成员或对象函数。解决这个问题也是很简单,我们只需要将this指针作为参数传递给ThreadProc函数,然后再转换为对象指针,就能正常访问对象成员了。

 

    创建线程时:

 

    然后是线程处理函数:

 

    在类中封装线程函数就是这么简单,关键只在于传递this指针而已。线程的基础差不多就说到这里,如果需要更详细的说明,可以查阅相关文档。只不过,到目前为止的介绍,对于接下来的说明已经足够了。

 

    为了方便,接下来的讨论,我们都假设所有的操作都封装在类里。

 

    之前我们有讨论过,CloseHandle并不是关闭线程,只是将线程的句柄从系统的列表中删除,那么,我们应该如何关闭线程呢?

普遍的,也是最受推荐的,就是让线程自己返回。

 

    比如:

 

    也许有人会问,API不是有TerminateThread函数么,调用该函数为什么不可以?当然可以,只不过非常不好。

 

    加入我们有一个线程函数的代码如下:

 

    如果在线程函数还在执行的时候,就调用TerminateThread,那么,最终g_iFlag会是什么数值?

 

    如果执行到LABEL1,刚好调用TerminateThread,那么g_iFlag等于原值;如果是刚好执行到LABEL2,那么g_iFlag会设置MUTEX_CHECK位;如果再往下执行到LABEL3,那么就又和之前的完全不同。

 

    更为重要的是,多线程,你在调用Terminate时,根本无法知道ThreadProc究竟执行到了哪一步。换句话说,这程序,每次实行,都可能会和上一次不一样,这难道不是一个灾难么?

 

    所以,还是老老实实,线程该咋样就咋样,该自己退出就让它自生自灭吧!

 

    线程的使用多种多样,本文无法一一列举,因此接下来的讨论,我们将范围缩小,局限于线程是不停地循环接收事件。

 

    根据该要求我们很简单地罗列出相应的代码:

 

    不过这段代码确实是有问题,因为我们无法让线程自己退出。那么,我们先采用一个最简单的方式,设置一个标志位,当该标志位为TRUE时,我们让线程跳出循环,然后直接线程返回。

 

    线程部分代码更改如下:

 

    但这样的修改,其实在效率上还是有点问题的。因为我们需要判断m_ExitProc的数值,所以我们对于WaitForSingleObject需要每隔一段时间就从等待中返回,然后再判断标志位。在这间隔性的返回当中,我们白白耗费了不少CPU时间。

 

    为了避免这种无谓的损耗,我们应该改用WaitForMultipleObjects函数,同时等待两个事件。其中一个事件当然是我们之前所需要的,另外一个新的事件我们称其为唤醒事件,当接收到该事件时,我们就直接退出线程。

 

根据这个思想,那么我们代码又可以改装如下:

 

    嗯,这一下子,效率是提上去了。如果啥事情都没有呢,这线程就乖乖地在休息;如果有事情呢,它就会立马苏醒,然后再看看外面的世界。

 

    只不过,工作正常了,并不代表优雅。简单地说,如果我们想知道当前这线程究竟是在运行,还是不在运行呢?

 

    这个也非常简单,我们再给线程函数添加一个变量,当进入的时候设置TRUE,退出的时候设置FALSE。是不是也很简单呢?

 

    代码如下:

 

    看到这里也许有人会觉得奇怪,因为对于m_bThrdRunning变量来说,也只有在线程里才会变更其数值,为什么还要祭出InterlockedExchange呢?对,没错,如果外部只需要读取其数值,而不用更改,那么只要简单地调用等号就好了。那为什么我们还要这么弄呢?主要是考虑到关闭线程的函数。

 

    简单点来说,我们关闭线程的函数应该分为两种模式。一种模式为同步,另一种为异步。换句话来说,当其为异步模式时,我们只需要像线程发送个事件就好了;如果为同步模式,那么发送事件完毕后,我们还要判断退出标识。这时候,InterlockedExchange就派上用场了,我们可以采用它做一个自旋判断,直到其为FALSE,我们才退出关闭函数。

 

    如上所言,则关闭函数如下:

posted @ 2009-11-23 18:49  我的一天  阅读(242)  评论(0编辑  收藏  举报