注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

写着玩

Bob

 
 
 

日志

 
 
 
 

线程学习(1) 线程的基础知识  

2009-07-18 14:33:58|  分类: Win32 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

      对于了解任何一种事物,最容易也最常见的方法自然是先学习他的基础知识,先知其然。循序渐进,这很重要。对于线程当然也不例外。那么线程会有哪些基础的知识要了解呢?我在这里不会大篇的介绍那些API如何使用,也不会介绍一些库的使用,比如MFC啥的。要了解线程的基础知识,我想只需要回答四个问题即可:

        1.什么是线程?

        2. 什么时候应该用多线程?

        3. 什么时候又不能用多线程?

        4. 如何创建线程?

        5. 如何终止线程?

     回答了这五个问题,对于线程自然会有了基本的了解,那么你也就进入了线程的世界。首先我们来看第一个问题:

         什么是线程?

         每个进程至少包含一个线程,一个进程包括一个进程对象,一个地址空间。同样线程由两个部分组成:线程内核对象和线程栈。操作系统通过线程内核对象管理线程,同时内核对象也是系统用来存放线程统计信息的地方。线程栈用于维护线程在执行代码时需要的所有函数参数和局部变量。进程是从来不执行任何东西的,他只是线程依存的地方。线程总是在某个进程中创建,并且整个生命周期都在进程中。也就是说,线程在他的进程地址空间中执行代码,对相同的数据进行操作。当然还能共享内核对象句柄。因为内核对象句柄表是依赖于进程而不是线程。
       很多人都知道线程比进程“重”,为什么呢?因为:

        1. 进程需要更多的地址空间所以进程使用的系统资源比线程多很多。为进程创建一个虚拟地址空间需要很多系统资源。需要占用大量的内存来保留大量记录。
        2. 由于.exe或.dll文件要加载到一个弟子空间,所以需要文件资源。而线程使用的系统资源少很多。

         什么时候应该用线程?

         用一句话来回答就是:当你想一心二用的时候你就应该使用多线程!比如我现在想一边敲这些字一边和可乐就得用多线程。在你炒菜的同时又要烧水也得用多线程(并发)。。当你很用心在写一段很长的代码的时候,如果别人在这个时候叫你,你不希望听不见那么也得用多线程(防止阻塞,UI假死)。再比如分房的年代,如果是按照人头分房,你希望分的房子大一点那么你也得用多线程(可能会获得更多的CPU时间片,特别是在多核上)。如果你是一个创业者,当你的公司渐渐的长大,人越来越多,事情也越来越复杂的时候,你希望不同的人去做不同的事情,你希望把更多的资源给重要的人,而不希望(至少希望不是很多)那些不太重要的事情占用你某些宝贵的资源的时候,你也得用多线程(优先级)。因此我认为有四种情况,我们是需要使用多线程的。即:

                               1. 有多件事情,顺序执行无法满足的时候;

                               2. 在处理长时间的事情(算法)时为了防止应用界面(UI)不响应用户输入,造成UI假死的时候;比如大图像渲染、大数据处理/排序、搜索等

                               3. 为了通过获得更多的CPU时间片来提高程序效率的时候;

                               4. 需要同时处理的事情有优先级别的时候;应该使用高优先级线程管理对时间要求很急的任务,而使用低优先级线程执行被动任务或者对时间不敏感的任务。高

        什么时候不应该使用多线程?

        非常重要也是初学多线程编程很容易犯错误的一点就是不要将你的程序的界面(窗口)放到不同的线程中去。除非你能做到像explorer那么好。还有编辑/打印问题。当然都不是绝对的。关键是要用好,不能随便用。

        如何创建线程?
        线程都必须有一个入口函数,线程从这个入口开始运行。主线程的入口函数可以为:main,wmain,WinMain,wWinMain.创建的辅助线程也必须有入口函数,函数形式:

DWORD WINAPI ThreadFunc(PVOID pvParam)

{
     DWORD dwResult = 0;
    ......
    return dwResult ;
}
在线程函数中必须返回一个值,他将成为该进程的代码。而且应该尽可能使用参数和局部变量。当使用静态变量和全局变量时,多个线程可以同时访问这些变量,就可能会破坏变量的内存。但是参数和局部变量是在线程栈中创建的,所以不太可能别其他线程破坏。
       创建线程使用CreateThread函数。这是系统提供的唯一一个创建线程的函数。这个函数首先创建一个线程内核对象,如何从进程的地址空间分配内存供线程使用。线程可以访问进程的内核对象的所有句柄、进程中的所有内存以及同进程的其他线程的栈。因此同进程中多个线程能够非常容易相互通信。虽然CreateThread是系统提供的唯一一个创建线程的函数,但是如果你是使用C/C++编写多线程,却不能使用这个函数,而是应该使用编译器提供的替代函数。比如VC提供的是_beginthreadex函数。可能你会问为什么。要回答这个问题,得从c运行库说起。C运行库是1970年问世的。那时候还没有任何线程的应用,因此,C运行库自然是不支持多线程的。存在问题的函数包括errno,_doserrno,strtok,_wcstok,strerror,_strerror,tmpnam,tmpfile,asctime,_wasctime,gmtime,_ecvt和_fcvt等。存在问题是因为这些内容都是活类似全局变量,线程直接是相互覆盖的,也就是不是线程安全的。要解决也很容易。只要创建一个数据结构保存这些内容,并将他和线程关联就可以了。但是问题也来了,系统又不知道你不安全,甚至连你是不是C/C++程序都不知道。_beginthreadex函数是vc的crt函数。幸好MS提供了源代码。看看源代码我们就知道一切了。下面是从VC8的crt中的threadex.c文件中拷贝而来。


_MCRTIMP uintptr_t __cdecl _beginthreadex (

        void *security,

        unsigned stacksize,

        unsigned (__CLR_OR_STD_CALL * initialcode) (void *),

        void * argument,

        unsigned createflag,

        unsigned *thrdaddr

        )

{

        _ptiddata ptd;                  /* pointer to per-thread data */

        uintptr_t thdl;                 /* thread handle */

        unsigned long err = 0L;     /* Return from GetLastError() */

        unsigned dummyid;               /* dummy returned thread ID */


        /* validation section */

        _VALIDATE_RETURN(initialcode != NULL, EINVAL, 0);


        /* Initialize FlsGetValue function pointer */

        __set_flsgetvalue();


        /*

         * Allocate and initialize a per-thread data structure for the to-

         * be-created thread.

         */

        if ( (ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL )

                goto error_return;


        /*

         * Initialize the per-thread data

         */


        _initptd(ptd, _getptd()->ptlocinfo);


        ptd->_initaddr = (void *) initialcode;

        ptd->_initarg = argument;

        ptd->_thandle = (uintptr_t)(-1);


#if defined (_M_CEE) || defined (MRTDLL)

        if(!_getdomain(&(ptd->__initDomain)))

        {

            goto error_return;

        }

#endif  /* defined (_M_CEE) || defined (MRTDLL) */


        /*

         * Make sure non-NULL thrdaddr is passed to CreateThread

         */

        if ( thrdaddr == NULL )

                thrdaddr = &dummyid;


        /*

         * Create the new thread using the parameters supplied by the caller.

         */

        if ( (thdl = (uintptr_t)

              CreateThread( (LPSECURITY_ATTRIBUTES)security,

                            stacksize,

                            _threadstartex,

                            (LPVOID)ptd,

                            createflag,

                            (LPDWORD)thrdaddr))

             == (uintptr_t)0 )

        {

                err = GetLastError();

                goto error_return;

        }


        /*

         * Good return

         */

        return(thdl);


        /*

         * Error return

         */

error_return:

        /*

         * Either ptd is NULL, or it points to the no-longer-necessary block

         * calloc-ed for the _tiddata struct which should now be freed up.

         */

        _free_crt(ptd);


        /*

         * Map the error, if necessary.

         *

         * Note: this routine returns 0 for failure, just like the Win32

         * API CreateThread, but _beginthread() returns -1 for failure.

         */

        if ( err != 0L )

                _dosmaperr(err);


        return( (uintptr_t)0 );

}
从代码中清楚的看到每个线程都有一个属于自己的tiddata数据结构。传进来的参数和现场函数地址都保存在这里。当然他正如我们预料的一样最终调用了CreateThread函数。只是没有把传进来的线程函数和参数传给CreateThread。而是_threadstartex和tiddata。下面我们接下来看一下这两个东西。tiddata位于mtdll.h,_threadstartex和_beginthread在同一个文件。

/* Structure for each thread's data */


struct _tiddata {

    unsigned long   _tid;       /* thread ID */



    uintptr_t _thandle;         /* thread handle */


    int     _terrno;            /* errno value */

    unsigned long   _tdoserrno; /* _doserrno value */

    unsigned int    _fpds;      /* Floating Point data segment */

    unsigned long   _holdrand;  /* rand() seed value */

    char *      _token;         /* ptr to strtok() token */

    wchar_t *   _wtoken;        /* ptr to wcstok() token */

    unsigned char * _mtoken;    /* ptr to _mbstok() token */


    /* following pointers get malloc'd at runtime */

    char *      _errmsg;        /* ptr to strerror()/_strerror() buff */

    wchar_t *   _werrmsg;       /* ptr to _wcserror()/__wcserror() buff */

    char *      _namebuf0;      /* ptr to tmpnam() buffer */

    wchar_t *   _wnamebuf0;     /* ptr to _wtmpnam() buffer */

    char *      _namebuf1;      /* ptr to tmpfile() buffer */

    wchar_t *   _wnamebuf1;     /* ptr to _wtmpfile() buffer */

    char *      _asctimebuf;    /* ptr to asctime() buffer */

    wchar_t *   _wasctimebuf;   /* ptr to _wasctime() buffer */

    void *      _gmtimebuf;     /* ptr to gmtime() structure */

    char *      _cvtbuf;        /* ptr to ecvt()/fcvt buffer */

    unsigned char _con_ch_buf[MB_LEN_MAX];

                                /* ptr to putch() buffer */

    unsigned short _ch_buf_used;   /* if the _con_ch_buf is used */


    /* following fields are needed by _beginthread code */

    void *      _initaddr;      /* initial user thread address */

    void *      _initarg;       /* initial user thread argument */


    /* following three fields are needed to support signal handling and

     * runtime errors */

    void *      _pxcptacttab;   /* ptr to exception-action table */

    void *      _tpxcptinfoptrs; /* ptr to exception info pointers */

    int         _tfpecode;      /* float point exception code */


    /* pointer to the copy of the multibyte character information used by

     * the thread */

    pthreadmbcinfo  ptmbcinfo;


    /* pointer to the copy of the locale informaton used by the thead */

    pthreadlocinfo  ptlocinfo;

    int         _ownlocale;     /* if 1, this thread owns its own locale */


    /* following field is needed by NLG routines */

    unsigned long   _NLG_dwCode;


    /*

     * Per-Thread data needed by C++ Exception Handling

     */

    void *      _terminate;     /* terminate() routine */

    void *      _unexpected;    /* unexpected() routine */

    void *      _translator;    /* S.E. translator */

    void *      _purecall;      /* called when pure virtual happens */

    void *      _curexception;  /* current exception */

    void *      _curcontext;    /* current exception context */

    int         _ProcessingThrow; /* for uncaught_exception */

    void *              _curexcspec;    /* for handling exceptions thrown from std::unexpected */

#if defined (_M_IA64) || defined (_M_AMD64)

    void *      _pExitContext;

    void *      _pUnwindContext;

    void *      _pFrameInfoChain;

    unsigned __int64    _ImageBase;

#if defined (_M_IA64)

    unsigned __int64    _TargetGp;

#endif  /* defined (_M_IA64) */

    unsigned __int64    _ThrowImageBase;

    void *      _pForeignException;

#elif defined (_M_IX86)

    void *      _pFrameInfoChain;

#endif  /* defined (_M_IX86) */

    _setloc_struct _setloc_data;


    void *      _encode_ptr;    /* EncodePointer() routine */

    void *      _decode_ptr;    /* DecodePointer() routine */


    void *      _reserved1;     /* nothing */

    void *      _reserved2;     /* nothing */

    void *      _reserved3;     /* nothing */


    int _cxxReThrow;        /* Set to True if it's a rethrown C++ Exception */


    unsigned long __initDomain;     /* initial domain used by _beginthread[ex] for managed function */

};


typedef struct _tiddata * _ptiddata;

从上面的代码可以看出很多内容就是前面我们提到的C运行库不安全的内容。


/***
*_threadstartex() - New thread begins here
*
*Purpose:
*       The new thread begins execution here.  This routine, in turn,
*       passes control to the user's code.
*
*Entry:
*       void *ptd       = pointer to _tiddata structure for this thread
*
*Exit:
*       Never returns - terminates thread!
*
*Exceptions:
*
*******************************************************************************/

static unsigned long WINAPI _threadstartex (
        void * ptd
        )
{
        _ptiddata _ptd;                  /* pointer to per-thread data */

        /* Initialize FlsGetValue function pointer */
        __set_flsgetvalue();

        /*
         * Check if ptd is initialised during THREAD_ATTACH call to dll mains
         */
        if ( ( _ptd = (_ptiddata)__fls_getvalue(__get_flsindex())) == NULL)
        {
            /*
             * Stash the pointer to the per-thread data stucture in TLS
             */
            if ( !__fls_setvalue(__get_flsindex(), ptd) )
                ExitThread(GetLastError());
            /*
             * Set the thread ID field -- parent thread cannot set it after
             * CreateThread() returns since the child thread might have run
             * to completion and already freed its per-thread data block!
             */
            ((_ptiddata) ptd)->_tid = GetCurrentThreadId();
        }
        else
        {
            _ptd->_initaddr = ((_ptiddata) ptd)->_initaddr;
            _ptd->_initarg =  ((_ptiddata) ptd)->_initarg;
            _ptd->_thandle =  ((_ptiddata) ptd)->_thandle;
#if defined (_M_CEE) || defined (MRTDLL)
            _ptd->__initDomain=((_ptiddata) ptd)->__initDomain;
#endif  /* defined (_M_CEE) || defined (MRTDLL) */
            _freefls(ptd);
            ptd = _ptd;
        }


        /*
         * Call fp initialization, if necessary
         */
#ifndef MRTDLL
#ifdef CRTDLL
        _fpclear();
#else  /* CRTDLL */
        if (_FPmtinit != NULL &&
            _IsNonwritableInCurrentImage((PBYTE)&_FPmtinit))
        {
            (*_FPmtinit)();
        }
#endif  /* CRTDLL */
#endif  /* MRTDLL */

#if defined (_M_CEE) || defined (MRTDLL)
        DWORD domain=0;
        if(!_getdomain(&domain))
        {
            ExitThread(0);
        }
        if(domain!=_ptd->__initDomain)
        {
            /* need to transition to caller's domain and startup there*/
            ::msclr::call_in_appdomain(_ptd->__initDomain, _callthreadstartex);

            return 0L;
        }
#endif  /* defined (_M_CEE) || defined (MRTDLL) */

        _callthreadstartex();

        /*
         * Never executed!
         */
        return(0L);
}
这个函数的主要功能就是使用线程本地存储将数据结构tiddata和线程关联起来。以及一个SEH。至此,我们知道了为什么C/C++运行库的函数需要为为他创建的每个函数设置单独的内存块,同时也了解了如何通过调用_beginthreadex函数来分配内存块以及初始化,并且还和线程关联起来。
        下面我们来看一下如果应用程序直接调用CreateThread会如何。首先他肯定没有创建与线程关联的内存块,但是在运行的过程中很有可能有其他的C++运行库的函数企图去获得线程内存块(TlsGetValue)。结果当然是返回NULL。这是C++运行库会自动为线程创建一个内存块。看起来很完美,系统想得真是周到。但是仍然有两个问题:1. 没有SEH。程序可能直接挂掉;2.系统自动创建的内存块谁负责释放呢?
         C++运行库还提供了另外一个函数创建线程:_beginthread。这个函数能用吗?能用!但是不提倡。因为:1.无法创建带有安全属性的线程;2.无法创建暂停的线程;3.无法返回线程ID。
         如何销毁终止线程
          终止线程运行有几种方法:
1. 线程函数返回结束;
       这是线程结束的唯一的正确方法。线程函数必须设计成需要线程结束时就能返回的形式。通过这种方式结束线程可以保证:a。所有C++对象正确的被销毁;b。线程正确的释放线程栈使用的内存;c。系统将线程函数的返回值设置成线程的推出码;d。系统递减线程的计数器;
2.调用ExitThread;
        用这种方式终止线程能让系统自动清除所有线程使用的所有系统资源。但是无法清除C++资源。
3.其他线程调用TerminateThread;
        这种方法无法实现1中的a和b两项。而且这个函数是异步函数。
4.创建线程的进程终止;
        这种方法最野蛮,问题也最多。1中的四项都无法保证。而且很有可能是线程要访问的资源不存在了,但是线程还在,这样必然导致弹出出错对话框。而且内存数据也不会存入硬盘。
线程终止过程:
1. 线程所有的用户对象将被释放,但是窗口和钩子比较特殊,当线程终止时,所有窗口将被销毁,同时所有的钩子将被卸载。其他资源在进程终止时才被销毁。
2. 线程的退出代码从STILL_ACTIVE改为ExitThread或TerminateThread的代码。
3. 线程内核对象的状态变成已经通知。
4. 如果线程是进程的最后一个线程,那么该进程也被视为已经终止运行。
5.线程内核对象的使用计数递减1.一个线程终止运行时,在线程的内核对象所有相关联的引用都关闭之前,该内核对象不会被自动释放。
前面线程的创建中我们介绍了不用直接使用CreateThread而应该使用C++运行库提供的函数,VC提供的是_beginthreadex,也不要使用_beginthread.那么线程结束也应该使用C++运行库提供的函数。VC提供了两个函数_endthreadex和_endthread.
void __cdecl _endthreadex(unsigned retcode)
{
_ptiddata ptd;
pid = _getptd();
__freeptd(ptd);
ExitThread(retcode)
}
从该函数的实现上看,我们知道他做了两个事情,一个是释放内存块。然后调用系统的ExitThread,真正推出线程。现在大家知道为什么前面我们说直接调用ExitThread会引起内存块的泄漏。因此如果想要强制终止线程可以调用_endthreadex而不是调用ExitThread。但是一般不要调用。
        我们来看一下_endthread函数。他和_endthreadex有点不同。最大的区别在于它在调用ExitThread之前调用了CloseHandle,如果这时候线程的计数器已经为0,那么内核对象将被释放。此后所有其他线程调用此内核对象都将失败。
        最后我们来看一下,线程的标识。标识有两个,一个是句柄,一个是ID。句柄用得比较多。可以通过GetCurrentThread获得当前线程句柄。但是要注意的是该句柄是一个伪句柄。线程伪句柄是一个特别的数(0xfffffffe),只是代表当前的线程句柄(假如将某个线程的伪句柄拿到另一个线程去使用,那么这个伪句柄实际操作另一个线程),因此它不会影响线程的引用计数。所以不需要调用CloseHandle。当然调用了不会有问题,CloseHandle会忽略。线程的真实句柄:每个进程有一张内核对象表,这个表里放置进程内打开的所有内核对象,并给每个对象分配一个序号,线程句柄实际上就是 内核对象表中对应线程对象的序号。因此句柄与进程相关。在这个进程中的句柄在不能随意拿到另一个进程中使用,可以通过DuplicateHandle进行句柄拷贝。打开一个句柄,会使线程对象的引用技术加一,CloseHandle会使线程对象引用计数减一,所以使用完句柄后需要进行关闭。
  评论这张
 
阅读(731)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017