免费视频淫片aa毛片_日韩高清在线亚洲专区vr_日韩大片免费观看视频播放_亚洲欧美国产精品完整版

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
線程學(xué)習(xí)

     對于了解任何一種事物,最容易也最常見的方法自然是先學(xué)習(xí)他的基礎(chǔ)知識,先知其然。循序漸進(jìn),這很重要。對于線程當(dāng)然也不例外。那么線程會有哪些基礎(chǔ)的知識要了解呢?我在這里不會大篇的介紹那些API如何使用,也不會介紹一些庫的使用,比如MFC啥的。要了解線程的基礎(chǔ)知識,我想只需要回答四個問題即可:

        1.什么是線程?

        2. 什么時候應(yīng)該用多線程?

        3. 什么時候又不能用多線程?

        4. 如何創(chuàng)建線程?

        5. 如何終止線程?

     回答了這五個問題,對于線程自然會有了基本的了解,那么你也就進(jìn)入了線程的世界。首先我們來看第一個問題:

         什么是線程?

        每個進(jìn)程至少包含一個線程,一個進(jìn)程包括一個進(jìn)程對象,一個地址空間。同樣線程由兩個部分組成:線程內(nèi)核對象和線程棧。操作系統(tǒng)通過線程內(nèi)核對象管理線程,同時內(nèi)核對象也是系統(tǒng)用來存放線程統(tǒng)計(jì)信息的地方。線程棧用于維護(hù)線程在執(zhí)行代碼時需要的所有函數(shù)參數(shù)和局部變量。進(jìn)程是從來不執(zhí)行任何東西的,他只是線程依存的地方。線程總是在某個進(jìn)程中創(chuàng)建,并且整個生命周期都在進(jìn)程中。也就是說,線程在他的進(jìn)程地址空間中執(zhí)行代碼,對相同的數(shù)據(jù)進(jìn)行操作。當(dāng)然還能共享內(nèi)核對象句柄。因?yàn)閮?nèi)核對象句柄表是依賴于進(jìn)程而不是線程。
       很多人都知道線程比進(jìn)程“重”,為什么呢?因?yàn)椋?/p>

        1. 進(jìn)程需要更多的地址空間所以進(jìn)程使用的系統(tǒng)資源比線程多很多。為進(jìn)程創(chuàng)建一個虛擬地址空間需要很多系統(tǒng)資源。需要占用大量的內(nèi)存來保留大量記錄。
        2. 由于.exe或.dll文件要加載到一個弟子空間,所以需要文件資源。而線程使用的系統(tǒng)資源少很多。

         什么時候應(yīng)該用線程?

        用一句話來回答就是:當(dāng)你想一心二用的時候你就應(yīng)該使用多線程!比如我現(xiàn)在想一邊敲這些字一邊和可樂就得用多線程。在你炒菜的同時又要燒水也得用多線程(并發(fā))。。當(dāng)你很用心在寫一段很長的代碼的時候,如果別人在這個時候叫你,你不希望聽不見那么也得用多線程(防止阻塞,UI假死)。再比如分房的年代,如果是按照人頭分房,你希望分的房子大一點(diǎn)那么你也得用多線程(可能會獲得更多的CPU時間片,特別是在多核上)。如果你是一個創(chuàng)業(yè)者,當(dāng)你的公司漸漸的長大,人越來越多,事情也越來越復(fù)雜的時候,你希望不同的人去做不同的事情,你希望把更多的資源給重要的人,而不希望(至少希望不是很多)那些不太重要的事情占用你某些寶貴的資源的時候,你也得用多線程(優(yōu)先級)。因此我認(rèn)為有四種情況,我們是需要使用多線程的。即:

                               1. 有多件事情,順序執(zhí)行無法滿足的時候;

                               2. 在處理長時間的事情(算法)時為了防止應(yīng)用界面(UI)不響應(yīng)用戶輸入,造成UI假死的時候;比如大圖像渲染、大數(shù)據(jù)處理/排序、搜索等

                               3. 為了通過獲得更多的CPU時間片來提高程序效率的時候;

                               4. 需要同時處理的事情有優(yōu)先級別的時候;應(yīng)該使用高優(yōu)先級線程管理對時間要求很急的任務(wù),而使用低優(yōu)先級線程執(zhí)行被動任務(wù)或者對時間不敏感的任務(wù)。高

        什么時候不應(yīng)該使用多線程?

        非常重要也是初學(xué)多線程編程很容易犯錯誤的一點(diǎn)就是不要將你的程序的界面(窗口)放到不同的線程中去。除非你能做到像explorer那么好。還有編輯/打印問題。當(dāng)然都不是絕對的。關(guān)鍵是要用好,不能隨便用。

        如何創(chuàng)建線程?
        線程都必須有一個入口函數(shù),線程從這個入口開始運(yùn)行。主線程的入口函數(shù)可以為:main,wmain,WinMain,wWinMain.創(chuàng)建的輔助線程也必須有入口函數(shù),函數(shù)形式:

DWORD WINAPI ThreadFunc(PVOID pvParam)

{
     DWORD dwResult = 0;
    ……
    return dwResult ;
}
在線程函數(shù)中必須返回一個值,他將成為該進(jìn)程的代碼。而且應(yīng)該盡可能使用參數(shù)和局部變量。當(dāng)使用靜態(tài)變量和全局變量時,多個線程可以同時訪問這些變量,就可能會破壞變量的內(nèi)存。但是參數(shù)和局部變量是在線程棧中創(chuàng)建的,所以不太可能別其他線程破壞。
      創(chuàng)建線程使用CreateThread函數(shù)。這是系統(tǒng)提供的唯一一個創(chuàng)建線程的函數(shù)。這個函數(shù)首先創(chuàng)建一個線程內(nèi)核對象,如何從進(jìn)程的地址空間分配內(nèi)存供線程使用。線程可以訪問進(jìn)程的內(nèi)核對象的所有句柄、進(jìn)程中的所有內(nèi)存以及同進(jìn)程的其他線程的棧。因此同進(jìn)程中多個線程能夠非常容易相互通信。雖然CreateThread是系統(tǒng)提供的唯一一個創(chuàng)建線程的函數(shù),但是如果你是使用C/C++編寫多線程,卻不能使用這個函數(shù),而是應(yīng)該使用編譯器提供的替代函數(shù)。比如VC提供的是_beginthreadex函數(shù)??赡苣銜枮槭裁础R卮疬@個問題,得從c運(yùn)行庫說起。C運(yùn)行庫是1970年問世的。那時候還沒有任何線程的應(yīng)用,因此,C運(yùn)行庫自然是不支持多線程的。存在問題的函數(shù)包括errno,_doserrno,strtok,_wcstok,strerror,_strerror,tmpnam,tmpfile,asctime,_wasctime,gmtime,_ecvt和_fcvt等。存在問題是因?yàn)檫@些內(nèi)容都是活類似全局變量,線程直接是相互覆蓋的,也就是不是線程安全的。要解決也很容易。只要創(chuàng)建一個數(shù)據(jù)結(jié)構(gòu)保存這些內(nèi)容,并將他和線程關(guān)聯(lián)就可以了。但是問題也來了,系統(tǒng)又不知道你不安全,甚至連你是不是C/C++程序都不知道。_beginthreadex函數(shù)是vc的crt函數(shù)。幸好MS提供了源代碼??纯丛创a我們就知道一切了。下面是從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數(shù)據(jù)結(jié)構(gòu)。傳進(jìn)來的參數(shù)和現(xiàn)場函數(shù)地址都保存在這里。當(dāng)然他正如我們預(yù)料的一樣最終調(diào)用了CreateThread函數(shù)。只是沒有把傳進(jìn)來的線程函數(shù)和參數(shù)傳給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;

從上面的代碼可以看出很多內(nèi)容就是前面我們提到的C運(yùn)行庫不安全的內(nèi)容。
/***
*_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);
}
這個函數(shù)的主要功能就是使用線程本地存儲將數(shù)據(jù)結(jié)構(gòu)tiddata和線程關(guān)聯(lián)起來。以及一個SEH。至此,我們知道了為什么C/C++運(yùn)行庫的函數(shù)需要為為他創(chuàng)建的每個函數(shù)設(shè)置單獨(dú)的內(nèi)存塊,同時也了解了如何通過調(diào)用_beginthreadex函數(shù)來分配內(nèi)存塊以及初始化,并且還和線程關(guān)聯(lián)起來。
       下面我們來看一下如果應(yīng)用程序直接調(diào)用CreateThread會如何。首先他肯定沒有創(chuàng)建與線程關(guān)聯(lián)的內(nèi)存塊,但是在運(yùn)行的過程中很有可能有其他的C++運(yùn)行庫的函數(shù)企圖去獲得線程內(nèi)存塊(TlsGetValue)。結(jié)果當(dāng)然是返回NULL。這是C++運(yùn)行庫會自動為線程創(chuàng)建一個內(nèi)存塊??雌饋砗芡昝溃到y(tǒng)想得真是周到。但是仍然有兩個問題:1. 沒有SEH。程序可能直接掛掉;2.系統(tǒng)自動創(chuàng)建的內(nèi)存塊誰負(fù)責(zé)釋放呢?
         C++運(yùn)行庫還提供了另外一個函數(shù)創(chuàng)建線程:_beginthread。這個函數(shù)能用嗎?能用!但是不提倡。因?yàn)椋?.無法創(chuàng)建帶有安全屬性的線程;2.無法創(chuàng)建暫停的線程;3.無法返回線程ID。
         如何銷毀終止線程
          終止線程運(yùn)行有幾種方法:
1. 線程函數(shù)返回結(jié)束;
       這是線程結(jié)束的唯一的正確方法。線程函數(shù)必須設(shè)計(jì)成需要線程結(jié)束時就能返回的形式。通過這種方式結(jié)束線程可以保證:a。所有C++對象正確的被銷毀;b。線程正確的釋放線程棧使用的內(nèi)存;c。系統(tǒng)將線程函數(shù)的返回值設(shè)置成線程的推出碼;d。系統(tǒng)遞減線程的計(jì)數(shù)器;
2.調(diào)用ExitThread;
        用這種方式終止線程能讓系統(tǒng)自動清除所有線程使用的所有系統(tǒng)資源。但是無法清除C++資源。
3.其他線程調(diào)用TerminateThread;
        這種方法無法實(shí)現(xiàn)1中的a和b兩項(xiàng)。而且這個函數(shù)是異步函數(shù)。
4.創(chuàng)建線程的進(jìn)程終止;
        這種方法最野蠻,問題也最多。1中的四項(xiàng)都無法保證。而且很有可能是線程要訪問的資源不存在了,但是線程還在,這樣必然導(dǎo)致彈出出錯對話框。而且內(nèi)存數(shù)據(jù)也不會存入硬盤。
線程終止過程:
1. 線程所有的用戶對象將被釋放,但是窗口和鉤子比較特殊,當(dāng)線程終止時,所有窗口將被銷毀,同時所有的鉤子將被卸載。其他資源在進(jìn)程終止時才被銷毀。
2. 線程的退出代碼從STILL_ACTIVE改為ExitThread或TerminateThread的代碼。
3. 線程內(nèi)核對象的狀態(tài)變成已經(jīng)通知。
4. 如果線程是進(jìn)程的最后一個線程,那么該進(jìn)程也被視為已經(jīng)終止運(yùn)行。
5.線程內(nèi)核對象的使用計(jì)數(shù)遞減1.一個線程終止運(yùn)行時,在線程的內(nèi)核對象所有相關(guān)聯(lián)的引用都關(guān)閉之前,該內(nèi)核對象不會被自動釋放。
前面線程的創(chuàng)建中我們介紹了不用直接使用CreateThread而應(yīng)該使用C++運(yùn)行庫提供的函數(shù),VC提供的是_beginthreadex,也不要使用_beginthread.那么線程結(jié)束也應(yīng)該使用C++運(yùn)行庫提供的函數(shù)。VC提供了兩個函數(shù)_endthreadex和_endthread.
void __cdecl _endthreadex(unsigned retcode)
{
_ptiddata ptd;
pid = _getptd();
__freeptd(ptd);
ExitThread(retcode)
}
從該函數(shù)的實(shí)現(xiàn)上看,我們知道他做了兩個事情,一個是釋放內(nèi)存塊。然后調(diào)用系統(tǒng)的ExitThread,真正推出線程?,F(xiàn)在大家知道為什么前面我們說直接調(diào)用ExitThread會引起內(nèi)存塊的泄漏。因此如果想要強(qiáng)制終止線程可以調(diào)用_endthreadex而不是調(diào)用ExitThread。但是一般不要調(diào)用。
       我們來看一下_endthread函數(shù)。他和_endthreadex有點(diǎn)不同。最大的區(qū)別在于它在調(diào)用ExitThread之前調(diào)用了CloseHandle,如果這時候線程的計(jì)數(shù)器已經(jīng)為0,那么內(nèi)核對象將被釋放。此后所有其他線程調(diào)用此內(nèi)核對象都將失敗。
       最后我們來看一下,線程的標(biāo)識。標(biāo)識有兩個,一個是句柄,一個是ID。句柄用得比較多??梢酝ㄟ^GetCurrentThread獲得當(dāng)前線程句柄。但是要注意的是該句柄是一個偽句柄。線程偽句柄是一個特別的數(shù)(0xfffffffe),只是代表當(dāng)前的線程句柄(假如將某個線程的偽句柄拿到另一個線程去使用,那么這個偽句柄實(shí)際操作另一個線程),因此它不會影響線程的引用計(jì)數(shù)。所以不需要調(diào)用CloseHandle。當(dāng)然調(diào)用了不會有問題,CloseHandle會忽略。線程的真實(shí)句柄:每個進(jìn)程有一張內(nèi)核對象表,這個表里放置進(jìn)程內(nèi)打開的所有內(nèi)核對象,并給每個對象分配一個序號,線程句柄實(shí)際上就是內(nèi)核對象表中對應(yīng)線程對象的序號。因此句柄與進(jìn)程相關(guān)。在這個進(jìn)程中的句柄在不能隨意拿到另一個進(jìn)程中使用,可以通過DuplicateHandle進(jìn)行句柄拷貝。打開一個句柄,會使線程對象的引用技術(shù)加一,CloseHandle會使線程對象引用計(jì)數(shù)減一,所以使用完句柄后需要進(jìn)行關(guān)閉。

    關(guān)于線程的實(shí)現(xiàn)細(xì)節(jié),我們這里只討論兩個問題。即線程的內(nèi)核對象和線程棧以及線程的創(chuàng)建過程。上一章我們提到,線程是由線程內(nèi)核對象以及線程棧兩個部分組成的。系統(tǒng)通過線程的內(nèi)核對象管理線程。線程棧則維護(hù)參數(shù)和局部變量。

1. 線程的內(nèi)核對象和線程棧
         下表很清楚的表明了內(nèi)核對象和線程棧的內(nèi)容以及直接的管理。

調(diào)用了CreateThread之后,系統(tǒng)會創(chuàng)建線程的內(nèi)核對象。內(nèi)核對象包括一個上下文(用于恢復(fù)現(xiàn)場)。從上圖可以看出。SP指向了線程棧的首地址。IP指向了系統(tǒng)提供的一個函數(shù)。引用計(jì)數(shù)是表示當(dāng)前內(nèi)核對象的引用次數(shù),當(dāng)遞減到0,內(nèi)核對象將被刪除。掛起計(jì)數(shù)表示此內(nèi)核對象被掛起的計(jì)數(shù),當(dāng)減到0時,線程成為可調(diào)度狀態(tài)。剛剛創(chuàng)建的線程退出碼都是STILL_ACTIVE。都是沒有信號狀態(tài)。也許你問一個問題:為什么初始引用計(jì)數(shù)要等于2呢?因?yàn)榫€程創(chuàng)建的時候要返回一個句柄,他擁有了一個計(jì)數(shù),另一個計(jì)數(shù)是被創(chuàng)建的新線程自己擁有的。在線程函數(shù)返回時遞減。弄清楚了這個我們也就清楚了在線程函數(shù)最后返回前是否需要調(diào)用_endthread或_endthreadex或調(diào)用CloseHandle關(guān)閉自己。線程內(nèi)核對象只有在計(jì)數(shù)器減到0時才會被系統(tǒng)銷毀。銷毀之后再調(diào)用與線程句柄相關(guān)的函數(shù)都可能出錯。比如CloseHandle,當(dāng)然如果傳入的是偽句柄不一定,但是可能遞減了其他線程的計(jì)數(shù)器。

      線程的內(nèi)核對象一旦創(chuàng)建完畢,系統(tǒng)就分配用于線程棧的內(nèi)存。內(nèi)存是用進(jìn)程的地址空間分配來的。然后,系統(tǒng)將兩個值寫入棧的上端。第一個值是傳給CreateThread的參數(shù)(最后傳入線程函數(shù)),另一個是線程函數(shù)的地址。從上圖可以看到,內(nèi)核對象初始化后,IP指向了一個系統(tǒng)的函數(shù)BaseThreadStart而不是用戶的線程函數(shù)。否則系統(tǒng)就無法在線程函數(shù)返回時調(diào)用ExitThread函數(shù)了。

1. 線程的創(chuàng)建過程

           a.建立線程內(nèi)核對象
           b.設(shè)置內(nèi)核對象初始值:比如計(jì)數(shù)器=2等
           c.分配內(nèi)存給線程棧
           d.初始化線程棧:將線程函數(shù)和參數(shù)push
           e.初始化上下文內(nèi)容:比如IP和SP以及其他CPU寄存器
            f. 調(diào)用BaseThreadStart函數(shù)
            g.調(diào)用用戶線程函數(shù)


終于開始寫到3了,最近一直很忙,似乎還有點(diǎn)累。上班時間是不可能有空寫的,只有回到家等女兒睡著了才能坐下來。每每這個時候也不早了,1:23,現(xiàn)在。后面還有好多,還得加油。

       說到線程的調(diào)度,我覺得還是得從線程的優(yōu)先級講起。在Windows中,每個線程都被賦予了優(yōu)先級的概念,線程總共有32個優(yōu)先級數(shù)從0(最低)到31(最高)。從前面的章節(jié)我們知道,線程是從屬于某個進(jìn)程的,那么對于某個具體進(jìn)程的某個線程的優(yōu)先級是怎么確定的呢?這得從幾個優(yōu)先級概念說起。一個線程的優(yōu)先級是由進(jìn)程的基本優(yōu)先級(或者叫優(yōu)先級類)和線程的相對優(yōu)先級決定的。因?yàn)槲覀儫o法直接設(shè)置一個線程的優(yōu)先級數(shù),比如2或29。
        進(jìn)程的基本優(yōu)先級:他是通過調(diào)用系統(tǒng)API—SetPriorityClass來設(shè)置,運(yùn)行中的進(jìn)程的優(yōu)先級可以通過任務(wù)管理器查看。基本優(yōu)先級是針對進(jìn)程來講的,基本優(yōu)先級有8個類別,分別是:ABOVE_NORMAL_PRIORITY_CLASS,
BELOW_NORMAL_PRIORITY_CLASS,
HIGH_PRIORITY_CLASS,IDLE_PRIORITY_CLASS,
NORMAL_PRIORITY_CLASS,
PROCESS_MODE_BACKGROUND_BEGIN,
PROCESS_MODE_BACKGROUND_END,
REALTIME_PRIORITY_CLASS.
具體的含義可以查看MSDN。如果程序沒有進(jìn)行特別設(shè)置,那么進(jìn)程的基本優(yōu)先級將會設(shè)置為NORMAL_PRIORITY_CLASS這個級別。先對來說,使用高于正常的優(yōu)先級需要特別小心,特別是REALTIME_PRIORITY_CLASS.他表示進(jìn)程中線程必須立即對事件作出響應(yīng),以便執(zhí)行關(guān)鍵時間的任務(wù).該進(jìn)程中的線程還會搶先于操作系統(tǒng)組件之前運(yùn)行.使用本優(yōu)先級類時必須極端小心.
線程相對優(yōu)先級:相對優(yōu)先級是針對線程的,他是通過調(diào)用SetThreadPriority函數(shù)設(shè)置的。具體參數(shù)可以參考MSDN.那么線程的優(yōu)先級到底是怎么確定的呢?基本優(yōu)先級和相對優(yōu)先級如何結(jié)合的呢?下表就是他們的對應(yīng)關(guān)系,是從MSDN上copy過來的。不過M$也說過他們不承諾任何關(guān)于優(yōu)先級級數(shù)的保證。因?yàn)檫@個未來可能會變的。所以寫程序千萬不要直接依賴于有限級數(shù)。
具體的含義可以查看MSDN。如果程序沒有進(jìn)行特別設(shè)置,那么進(jìn)程的基本優(yōu)先級將會設(shè)置為NORMAL_PRIORITY_CLASS這個級別。先對來說,使用高于正常的優(yōu)先級需要特別小心,特別是REALTIME_PRIORITY_CLASS.他表示進(jìn)程中線程必須立即對事件作出響應(yīng),以便執(zhí)行關(guān)鍵時間的任務(wù).該進(jìn)程中的線程還會搶先于操作系統(tǒng)組件之前運(yùn)行.使用本優(yōu)先級類時必須極端小心.
線程相對優(yōu)先級:相對優(yōu)先級是針對線程的,他是通過調(diào)用SetThreadPriority函數(shù)設(shè)置的。具體參數(shù)可以參考MSDN.那么線程的優(yōu)先級到底是怎么確定的呢?基本優(yōu)先級和相對優(yōu)先級如何結(jié)合的呢?下表就是他們的對應(yīng)關(guān)系,是從MSDN上copy過來的。不過M$也說過他們不承諾任何關(guān)于優(yōu)先級級數(shù)的保證。因?yàn)檫@個未來可能會變的。所以寫程序千萬不要直接依賴于有限級數(shù)。

Process priority classThread priority levelBase priority
IDLE_PRIORITY_CLASSTHREAD_PRIORITY_IDLE1
THREAD_PRIORITY_LOWEST2
THREAD_PRIORITY_BELOW_NORMAL3
THREAD_PRIORITY_NORMAL4
THREAD_PRIORITY_ABOVE_NORMAL5
THREAD_PRIORITY_HIGHEST6
THREAD_PRIORITY_TIME_CRITICAL15
BELOW_NORMAL_PRIORITY_CLASSTHREAD_PRIORITY_IDLE1
THREAD_PRIORITY_LOWEST4
THREAD_PRIORITY_BELOW_NORMAL5
THREAD_PRIORITY_NORMAL6
THREAD_PRIORITY_ABOVE_NORMAL7
THREAD_PRIORITY_HIGHEST8
THREAD_PRIORITY_TIME_CRITICAL15
NORMAL_PRIORITY_CLASSTHREAD_PRIORITY_IDLE1
THREAD_PRIORITY_LOWEST6
THREAD_PRIORITY_BELOW_NORMAL7
THREAD_PRIORITY_NORMAL8
THREAD_PRIORITY_ABOVE_NORMAL9
THREAD_PRIORITY_HIGHEST10
THREAD_PRIORITY_TIME_CRITICAL15
ABOVE_NORMAL_PRIORITY_CLASSTHREAD_PRIORITY_IDLE1
THREAD_PRIORITY_LOWEST8
THREAD_PRIORITY_BELOW_NORMAL9
THREAD_PRIORITY_NORMAL10
THREAD_PRIORITY_ABOVE_NORMAL11
THREAD_PRIORITY_HIGHEST12
THREAD_PRIORITY_TIME_CRITICAL15
HIGH_PRIORITY_CLASSTHREAD_PRIORITY_IDLE1
THREAD_PRIORITY_LOWEST11
THREAD_PRIORITY_BELOW_NORMAL12
THREAD_PRIORITY_NORMAL13
THREAD_PRIORITY_ABOVE_NORMAL14
THREAD_PRIORITY_HIGHEST15
THREAD_PRIORITY_TIME_CRITICAL15
REALTIME_PRIORITY_CLASSTHREAD_PRIORITY_IDLE16
THREAD_PRIORITY_LOWEST22
THREAD_PRIORITY_BELOW_NORMAL23
THREAD_PRIORITY_NORMAL24
THREAD_PRIORITY_ABOVE_NORMAL25
THREAD_PRIORITY_HIGHEST26
THREAD_PRIORITY_TIME_CRITICAL31

比如兩個優(yōu)先級都是正常的級別,那么線程的優(yōu)先級就是8。再如基本優(yōu)先級是REALTIME_PRIORITY_CLASS ,|
相對優(yōu)先級是THREAD_PRIORITY_TIME_CRITICAL,那么線程的優(yōu)先級將是31。
      如果你夠細(xì)心的話,你也許發(fā)現(xiàn)了一個問題,就是前面說到線程的優(yōu)先級是從0到31的,但是從表中來看并沒有那種組合的結(jié)果優(yōu)先級是0.沒錯,真的沒有0,確實(shí)是組合不錯來的。因?yàn)楫?dāng)系統(tǒng)引導(dǎo)的時候,它會創(chuàng)建一個特殊的線程,稱為0頁線程.該線程被賦予優(yōu)先級0,它是整個系統(tǒng)中唯一的一個在優(yōu)先級0上運(yùn)行的線程 。當(dāng)系統(tǒng)中沒有任何線程需要執(zhí)行的時候,0頁線程負(fù)責(zé)將系統(tǒng)中的所有空閑的RAM頁面置0.除了0還有其他一些數(shù)字在上面表里也找不到,比如17,18,19,20,21,27,28,29,30都沒有。17,18,19,20,21,27,28,29,30。如果編寫一個以內(nèi)核方式運(yùn)行的設(shè)備驅(qū)動程序,可以獲得這些優(yōu)先級的等級,而用戶方式的應(yīng)用程序則不能.
       說到了線程優(yōu)先級,不得不說一下硬件中斷。我們知道不同的處理器的中斷機(jī)制是不一樣的,雖然中斷控制器做了中斷級別的優(yōu)化工作,Windows還是強(qiáng)制使用自己的中斷優(yōu)先級方案,即IRQLs。內(nèi)核在內(nèi)部以數(shù)字表示IRQLs,x86上是從0到31,x64和IA64上是從0到15,數(shù)字越高則中斷的優(yōu)先級別越高。內(nèi)核為軟件中斷定義了一系列標(biāo)準(zhǔn)的IRQLs,HAL也將硬件診斷號映射到IRQLs。

      中斷是根據(jù)優(yōu)先級被響應(yīng)的,一個高優(yōu)先級的中斷會比一個低優(yōu)先級的中斷先被響應(yīng)。當(dāng)有一個高優(yōu)先級的中斷產(chǎn)生,處理器保存被中斷線程的狀態(tài)并呼叫與之相關(guān)的陷阱調(diào)度器(trapdispatchers)。陷阱調(diào)度器提升IRQL并調(diào)用該中斷的服務(wù)例程。在中斷例程結(jié)束之后,陷阱調(diào)度器降低處理器的IRQL到中斷發(fā)生前的級別,接著載入之前存儲的機(jī)器狀態(tài)。被中斷的線程從被中斷的點(diǎn)重新開始執(zhí)行。當(dāng)內(nèi)核降低IRQL后,之前被屏蔽的優(yōu)先級較低的中斷就可能被接收。如果順利的話,內(nèi)核會重復(fù)這一過程來處理新的中斷。
  每個處理器的IRQL設(shè)置決定了該處理器可接收的中斷。IRQLs也被用來同步訪問內(nèi)核模式的數(shù)據(jù)結(jié)構(gòu)。當(dāng)一個線程在內(nèi)核模式下運(yùn)行,它既可以直接調(diào)用KeRaiseIrql和KeLowerIrql來提升或降低處理器的IRQL,也可以間接地通過調(diào)用函數(shù)獲得內(nèi)核同步物件以提升或降低處理器的IRQL。從擁有高于當(dāng)前IRQL的中斷源發(fā)出的中斷可以中斷處理器,而從有著小于或等于當(dāng)前IRQL的中斷源發(fā)出的中斷會被屏蔽,直到有執(zhí)行線程降低該IRQL。
      一個內(nèi)核模式的線程提升或降低運(yùn)行它的處理器的IRQL取決于它的任務(wù)。例如,如果一個中斷發(fā)生,陷阱處理程序(或者是處理器)會提升處理器的IRQL到為該中斷源指定的IRQL。這個高度屏蔽了所有小于或等于該IRQL的中斷(只作用于該處理器),保證了在處理器響應(yīng)該中斷的過程中不會被同級或級別較低的中斷打斷。被屏蔽的中斷要么被交由其它的處理器處理,要么就要等到IRQL降下來。因此,系統(tǒng)中的所有組件,包括內(nèi)核和設(shè)備驅(qū)動都希望IRQL能夠待在無源級別(也叫作低級別)。原因是如果IRQL不被長時間的置于較高的狀態(tài),設(shè)備驅(qū)動就可以及時地響應(yīng)硬件中斷。
      IRQL的優(yōu)先級與線程調(diào)度的優(yōu)先級完全不同。后者是線程的一個屬性,而IRQL是中斷源,比如鍵盤、鼠標(biāo)的一個屬性。此外,每個處理器有一個IRQL設(shè)置,該設(shè)置在執(zhí)行系統(tǒng)代碼時發(fā)生變化。所有的線程都運(yùn)行在中斷優(yōu)先級0和1上。內(nèi)核態(tài)的異步調(diào)用運(yùn)行在1上,用戶線程運(yùn)行在0上,內(nèi)核線程可以中斷用戶線程。而且只有內(nèi)核線程才能提高自己的終端優(yōu)先級。用戶線程雖然提高優(yōu)先級可以阻塞系統(tǒng)線程,但是用戶線程優(yōu)先級的提高并不會阻塞硬件中斷。而線程調(diào)度代碼運(yùn)行在DPC/線程調(diào)度中斷優(yōu)先級(2級)。這樣可防止調(diào)度器代碼與線程在訪問調(diào)度器數(shù)據(jù)結(jié)構(gòu)時發(fā)生沖突。
        在了解了什么是優(yōu)先級以及如何設(shè)置優(yōu)先級和中斷之后,我們來了解另外一個概念—–時間配額。
        時間配額是一個線程從進(jìn)入運(yùn)行狀態(tài)到Windows檢查是否有其他優(yōu)先級相同的線程需要開始運(yùn)行之間的時間總和。每個線程都有一個代表本次運(yùn)行最大時間長度的時間配額。時間配額不是一個時間長度值,而一個稱為配額單位(quantum unit)的整數(shù)。 缺省時,在Windows2000專業(yè)版中線程時間配額為6;而在Windows2000服務(wù)器中線程時間配額為36。注冊表項(xiàng):HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\PriorityControl\Win32PrioritySeparationWin32PrioritySeparation 可以修改時間配額。
線程狀態(tài)

        線程在不同的時間有不同的線程狀態(tài),在NT內(nèi)核的windows中線程總共有7種狀態(tài):

線程狀態(tài)

說明

Ready(就緒)

此狀態(tài)下的線程正在等待執(zhí)行,當(dāng)調(diào)度程序需要找一個線程來執(zhí)行時,它僅考慮就緒狀態(tài)下的線程池。

Standby(備用)

已經(jīng)被選中(當(dāng)前活動線程的后繼),當(dāng)條件合適時,調(diào)度程序?qū)@個線程執(zhí)行一個上下文轉(zhuǎn)換,備用線程將被切換到某個特定的處理器上運(yùn)行。對于系統(tǒng)中的每一個處理器,只能有一個線程處于備用狀態(tài)。

Running(運(yùn)行)

一旦調(diào)度程序?qū)h(huán)境切換到某個(備用)線程,這個線程就進(jìn)入運(yùn)行狀態(tài)并開始執(zhí)行。線程一直執(zhí)行,直到內(nèi)核將其搶占去運(yùn)行一個更高優(yōu)先級的線程,或者它的時間片到結(jié)束運(yùn)行或自動進(jìn)入等待狀態(tài)。

Waiting(等待)

一個線程可能因?yàn)橐韵聨讉€原因而進(jìn)入等待狀態(tài):(1)自動等待一個對象以便同步它的執(zhí)行。(2)操作系統(tǒng)可以代替該進(jìn)程進(jìn)入等待(如為了解決換頁I/O)。(3)環(huán)境子系統(tǒng)引導(dǎo)線程掛起。

線程等待狀態(tài)結(jié)束后,根據(jù)其優(yōu)先級,開始執(zhí)行,或者進(jìn)入就緒狀態(tài)。

Transition (轉(zhuǎn)變)

當(dāng)一個線程已經(jīng)準(zhǔn)備好執(zhí)行,但它的內(nèi)核棧被換出了內(nèi)存,這時線程就進(jìn)入轉(zhuǎn)變狀態(tài)。一旦它的內(nèi)核棧被換入內(nèi)存,線程就進(jìn)入就緒狀態(tài)。

Terminated (終止)

當(dāng)一個線程完成執(zhí)行,它就進(jìn)入終止?fàn)顟B(tài)。終止后,線程對象可能被刪除,也可能不被刪除,這將取決于對象管理器什么時候刪除對象的策略。如果執(zhí)行體中有一個指針指向線程對象,執(zhí)行體可以對線程對象重新初始化并再次使用它。

Initialized (初始)

當(dāng)一個線程被創(chuàng)建時的狀態(tài)。(內(nèi)部使用)

下面這個圖是各個狀態(tài)之間的轉(zhuǎn)換關(guān)系:

處理器的親合性
        按照系統(tǒng)默認(rèn)設(shè)置,當(dāng)系統(tǒng)將CPU分配給線程時,如果其他因素都相同的話,那么系統(tǒng)將設(shè)法在線程上次運(yùn)行的那個處理器上再次運(yùn)行線程。這樣是為了重復(fù)使用CPU的高速緩存。這就是系統(tǒng)的軟親合性。但是為了適應(yīng)某些特殊情況,系統(tǒng)將允許用戶設(shè)置線程或進(jìn)程的親合性。也就是允許哪個CPU能允許哪些線程。這就是處理器的硬親合性。API為:SetProcessAffinityMask/SetThreadAffinityMask。
線程調(diào)度
       Windows是一個基于優(yōu)先級的搶占式多處理器調(diào)度系統(tǒng)。調(diào)度系統(tǒng)總是運(yùn)行優(yōu)先級最高的就緒線程。Windows的沒有單獨(dú)的調(diào)度模塊或程序,調(diào)度的代碼是在內(nèi)核中實(shí)現(xiàn)的,廣泛分布在內(nèi)核中那些與調(diào)度相關(guān)的事件發(fā)生的地方。這些負(fù)責(zé)調(diào)度的程序被總稱為“內(nèi)核的調(diào)度器”。線程調(diào)度發(fā)生在DPC/Dispatch級別。  當(dāng)一個線程進(jìn)入運(yùn)行狀態(tài)時,每次時鐘中斷都會從時間配額中減少一個固定值(一般是3)。當(dāng)配額用完后,系統(tǒng)中斷線程,看是否需要降低線程優(yōu)先級,并查找是否有其他高優(yōu)先級或相同優(yōu)先級的線程在等待運(yùn)行。因?yàn)閣indows是一個搶占式的操作系統(tǒng),,因?yàn)橐粋€線程可能會在自己時間配額還沒用完的時候就被其他已經(jīng)就緒的高優(yōu)先級的線程給搶先了。而且用戶線程可以搶先內(nèi)核線程。搶先時只與優(yōu)先級有關(guān)而不關(guān)心是用戶線程還是內(nèi)核線程。處于實(shí)時優(yōu)先級的線程被搶先時,時間配額被重置為一個完整的時間片,而處于動態(tài)優(yōu)先級的線程被搶先時,時間配額不變。當(dāng)一個線程出現(xiàn)等待事件時,時間配額會被減1,當(dāng)線程優(yōu)先級大于14時,優(yōu)先級被重置。有四種情況會引起線程的調(diào)度: 

  • 變成就緒狀態(tài)的線程。例如:一個新創(chuàng)建的線程,或者從等待狀態(tài)釋放出來的線程。

  • 因其時間配額用完而離開運(yùn)行狀態(tài)的線程,它或者結(jié)束了,或者進(jìn)入等待狀態(tài)。

  • 線程的優(yōu)先級改變了,是因?yàn)橄到y(tǒng)調(diào)用,或者是Windows自己改變了優(yōu)先級。

  • 正在運(yùn)行的線程的處理器親合性改變了。

在每一個上述情況的銜接點(diǎn),Windows必須決定下一個運(yùn)行的線程是哪一個。一旦選擇了一個新的線程運(yùn)行,Windows將對其執(zhí)行一個上下文轉(zhuǎn)換的操作,即保存正在運(yùn)行的線程的相關(guān)的機(jī)器狀態(tài),裝載另一個線程的狀態(tài),開始新線程的執(zhí)行。
動態(tài)提高優(yōu)先級
        windows(2k/xp)在下面5種情況會自動提高線程的優(yōu)先級:

  • I/O操作完成后的線程優(yōu)先級提升    1. 在完成I/O操作后,Windows 2000將臨時提升等待該操作線程的優(yōu)先級,以保證等待I/O操作的線程能有更多的機(jī)會立即開始處理得到的結(jié)果 
        2. 為了避免I/O操作導(dǎo)致對某些線程的不公平偏好,在I/O操作完成后喚醒等待線程時將把該線程的時間配額減1
        3.線程優(yōu)先級的實(shí)際提升值是由設(shè)備驅(qū)動程序決定的。與I/O操作相關(guān)的線程優(yōu)先級提升建議值在文件“Wdm.h”或“Ntddk.h”中。設(shè)備驅(qū)動程序在完成I/O請求時通過內(nèi)核函數(shù)IoCompleteRequest來指定優(yōu)先級提升的幅度。
        4. 線程優(yōu)先級的提升幅度與I/O請求的響應(yīng)時間要求是一致的,響應(yīng)時間要求越高,優(yōu)先級提升幅度越大

  • 等待事件和信號量后的線程優(yōu)先級提升。    1. 當(dāng)一個等待執(zhí)行事件對象或信號量對象的線程完成等待后,它的優(yōu)先級將提升一個優(yōu)先級。 
        2. 阻塞于事件或信號量的線程得到的處理機(jī)時間比處理機(jī)繁忙型線程要少,這種提升可減少這種不平衡帶來的影響。
        3. SetEvent、PulseEvent或ReleaseSemaphore函數(shù)調(diào)用可導(dǎo)致事件對象或信號量對象等待的結(jié)束。
         4.提升是以線程的基本優(yōu)先級為基點(diǎn)的,而不是線程的當(dāng)前優(yōu)先級。提升后的優(yōu)先級永遠(yuǎn)不會超過15。在等待結(jié)束時,線程的時間配額被減1,并在提升后的優(yōu)先級上執(zhí)行完剩余的時間配額;隨后降低1個優(yōu)先級,運(yùn)行一個新的時間配額,直到優(yōu)先級降低到初始的基本優(yōu)先級。

  • 前臺線程在等待結(jié)束后的優(yōu)先級提升
         1. 對于前臺進(jìn)程中的線程,一個內(nèi)核對象上的等待操作完成時,內(nèi)核函數(shù)KiUnwaitThread會提升線程的當(dāng)前優(yōu)先級(不是線程的基本優(yōu)先級),提升幅度為變量PsPrioritySeparation的值。

    2. 在前臺應(yīng)用完成它的等待操作時小幅提升它的優(yōu)先級,以使它更有可能馬上進(jìn)入運(yùn)行狀態(tài),有效改進(jìn)前臺應(yīng)用的響應(yīng)時間特征。

    3. 用戶不能禁止這種優(yōu)先級提升,甚至是在用戶已利用Win32的函數(shù)SetThreadPriorityBoost禁止了其他的優(yōu)先級提升策略時,也是如此。

  • 圖形用戶接口線程被喚醒后的優(yōu)先級提升。
           1. 擁有窗口的線程在被窗口活動喚醒(如收到窗口消息)時將得到一個幅度為2的額外優(yōu)先級提升。

     2. 窗口系統(tǒng)(Win32k.sys)在調(diào)用函數(shù)KeSetEvent時實(shí)施這種優(yōu)先級提升,KeSetEvent函數(shù)調(diào)用設(shè)置一個事件,用于喚醒一個圖形用戶接口線程。
           3. 這種優(yōu)先級提升的原因是改進(jìn)交互應(yīng)用的響應(yīng)時間。

  • 對處理機(jī)饑餓線程的優(yōu)先級提升
           1. 系統(tǒng)線程“平衡集管理器(balance set manager)” 會每秒鐘檢查一次就緒隊(duì)列,是否存在一直在就緒隊(duì)列中排隊(duì)超過300個時鐘中斷間隔的線程。

     2. 如果找到這樣的線程,平衡集管理器將把該線程的優(yōu)先級提升到15,并分配給它一個長度為正常值兩倍的時間配額;

    3. 當(dāng)被提升線程用完它的時間配額后,該線程的優(yōu)先級立即衰減到它原來的基本優(yōu)先級。

    Sleep 函數(shù)

    系統(tǒng)將在大約的指定毫秒數(shù)內(nèi)使線程不可調(diào)度。Windows不是個實(shí)時操作系統(tǒng)。雖然線程可能在規(guī)定的時間被喚醒,但是它能否做到,取決于系統(tǒng)中還有什么操作正在進(jìn)行??梢哉{(diào)用Sleep,并且為dwMilliseconds參數(shù)傳遞INFINITE。這將告訴系統(tǒng)永遠(yuǎn)不要調(diào)度該線程。這不是一件值得去做的事情。最好是讓線程退出,并還原它的堆棧和內(nèi)核對象??梢詫?傳遞給Sleep。這將告訴系統(tǒng),調(diào)用線程將釋放剩余的時間片,并迫使系統(tǒng)調(diào)度另一個線程。但是,系統(tǒng)可以對剛剛調(diào)用Sleep的線程重新調(diào)度。如果不存在多個擁有相同優(yōu)先級的可調(diào)度線程,就會出現(xiàn)這種情況。那么我們?nèi)绾巫龅秸嬲袚Q到另外一個線程呢?答案是可以調(diào)用SwitchtoThread函數(shù)。

    SwitchtoThread函數(shù)

    系統(tǒng)提供了SwitchToThread函數(shù)。當(dāng)調(diào)用這個函數(shù)的時候,系統(tǒng)要查看是否存在一個迫切需要CPU時間的線程。如果沒有線程迫切需要CPU時間,SwitchToThread就會立即返回。如果存在一個迫切需要CPU時間的線程,SwitchToThread就對該線程進(jìn)行調(diào)度(該線程的優(yōu)先級可能低于調(diào)用SwitchToThread的線程)。這個迫切需要CPU時間的線程可以運(yùn)行一個時間段,然后系統(tǒng)調(diào)度程序照常運(yùn)行。該函數(shù)允許一個需要資源的線程強(qiáng)制另一個優(yōu)先級較低、而目前卻擁有該資源的線程放棄該資源。如果調(diào)用SwitchToThread函數(shù)時沒有其他線程能夠運(yùn)行,那么該函數(shù)返回FALSE,否則返回一個非0值。調(diào)用SwitchToThread與調(diào)用Sleep是相似的。差別是SwitchToThread允許優(yōu)先級較低的線程運(yùn)行;而即使有低優(yōu)先級線程迫切需要CPU時間,Sleep也能夠立即對調(diào)用線程重新進(jìn)行調(diào)度。

    對稱多處理機(jī)系統(tǒng)上Windows 2000的線程調(diào)度

    每個線程在對應(yīng)的內(nèi)核線程控制塊中都保存著兩個處理器標(biāo)識:
    首選處理器:線程運(yùn)行時的偏好處理器
    第二處理器:線程運(yùn)行的第二選擇處理器
    首選處理器是基于進(jìn)程控制塊的索引值來隨機(jī)選擇的。索引值在創(chuàng)建每個線程時遞增。線程一旦創(chuàng)建后,系統(tǒng)就不會修改線程的首選處理器設(shè)置,但是用戶可以通過SetThreadIdleProcessor來修改.

    當(dāng)線程進(jìn)入運(yùn)行狀態(tài)時,Windows首先試圖調(diào)度該線程到一個空閑處理機(jī)上運(yùn)行。如果有多個空閑處理機(jī),線程調(diào)度器的調(diào)度順序?yàn)椋?/p>

    –線程的首選處理機(jī)

    –線程的第二處理機(jī)

    –當(dāng)前執(zhí)行處理機(jī)(即正在執(zhí)行調(diào)度器代碼的處理機(jī))。

    –如果這些處理機(jī)都不是空閑的,Windows將依據(jù)處理機(jī)標(biāo)識從高到低掃描系統(tǒng)中的空閑處理機(jī)狀態(tài),選擇找到的第一個空閑處理機(jī)。

    如果線程進(jìn)入就緒狀態(tài)時,所有處理機(jī)都處于繁忙狀態(tài),Windows將檢查一個處于運(yùn)行狀態(tài)或備用狀態(tài)的線程,判斷它是否可搶先。檢查的順序如下:

    –線程的首選處理機(jī)

    –線程的第二處理機(jī)

    –如果這兩個處理機(jī)都不在線程的親合掩碼中,Windows將依據(jù)活動處理機(jī)掩碼選擇線程可運(yùn)行的編號最大的處理機(jī)。

    Windows并不檢查所有處理機(jī)上的運(yùn)行線程和備用線程的優(yōu)先級,而僅僅檢查一個被選中處理機(jī)上的運(yùn)行線程和備用線程的優(yōu)先級。

    如果在被選中的處理機(jī)上沒有線程可被搶先,則新線程放入相應(yīng)優(yōu)先級的就緒隊(duì)列,并等待調(diào)度執(zhí)行。為特定的處理機(jī)調(diào)度線程

    在多處理機(jī)系統(tǒng),Windows不能簡單地從就緒隊(duì)列中取第一個線程,它要在親合掩碼限制下尋找一個滿足下列條件之一的線程。

    –線程的上一次運(yùn)行是在該處理機(jī)上;

    –線程的首選處理機(jī)是該處理機(jī);

    –處于就緒狀態(tài)的時間超過2個時間配額;

    –優(yōu)先級大于等于24;

    如果Windows不能找到滿足要求的線程,它將從就緒隊(duì)列的隊(duì)首取第一個線程進(jìn)入運(yùn)行狀態(tài)。最高優(yōu)先級就緒線程可能不處于運(yùn)行狀態(tài).有可能出現(xiàn)這種情況,一個比當(dāng)前正在運(yùn)行線程優(yōu)先級更高的線程處于就緒狀態(tài),但不能立即搶先當(dāng)前線程,進(jìn)入運(yùn)行狀態(tài)。

    空閑線程

    如果在一個處理機(jī)上沒有可運(yùn)行的線程,Windows會調(diào)度相應(yīng)處理機(jī)對應(yīng)的空閑線程。由于在多處理機(jī)系統(tǒng)中可能兩個處理機(jī)同時運(yùn)行空閑線程,所以系統(tǒng)中的每個處理機(jī)都有一個對應(yīng)的空閑線程。Windows給空閑線程指定的線程優(yōu)先級為0,該空閑線程只在沒有其他線程要運(yùn)行時才運(yùn)行。

    對于多線程編程來說,最難得有兩點(diǎn):1. 線程的生命周期的管理;2.線程同步。在前面幾節(jié)中我們討論了第一個問題,現(xiàn)在我們來討論第二個問題。到目前為止,我們創(chuàng)建線程都是相互獨(dú)立的。線程之間沒有任何瓜葛。因此他們都運(yùn)行得很好。沒有任何問題。性能也很高。這一切看起來都非常的美好。比起單線程來說簡直好得太多了。然而不幸的是在多線程編程中,多個線程都是相互獨(dú)立的事情太少了。更多的是雖然一個線程處理一些事情,另一個線程處理另外一些事情,但是其中一些線程需要了解其他線程的處理結(jié)果或者需要在其處理完成之后才能進(jìn)行。最簡單的情況比如:一個線程通過計(jì)算,將計(jì)算的結(jié)果寫入一個全局變量,另外一個線程則讀取全局變量進(jìn)行顯示。如果不采取任何同步措施,那么就無法保證讀數(shù)據(jù)的線程讀到的數(shù)據(jù)時寫數(shù)據(jù)線程寫入之后的結(jié)果。為了讓線程了解其他線程結(jié)束之后的結(jié)果或者需要訪問相同的資源并改變資源從而使得同一進(jìn)程中的多個線程協(xié)調(diào)的工作就叫線程的同步。因此也就有兩種基本情況是需要使用線程同步的:1. 當(dāng)多個線程會訪問同一個資源并且會改變資源;2.一個線程需要了解其他一個或多個線程何時結(jié)束(或者說是需要通知其他線程);打個比方說也就是“兄弟,這東西我且用著那,您得等會?!薄按蟾?,我活干完啦,你上吧?!币簿瓦@么個意味。

          要使用好多線程,線程同步是一定要過關(guān)的。線程同步是一個不小的話題,從大方面來說可以分為兩大類,一類是用戶模式同步對象,一類是內(nèi)核模式同步對象。前面章節(jié)我們說過,使用內(nèi)核對象是需要從用戶態(tài)轉(zhuǎn)入內(nèi)核態(tài)的,而這個切換是非?;ㄙM(fèi)時間的,大約是1000個時鐘周期。所以用戶模式的同步對象最大的特點(diǎn)是效率高。但是也有缺點(diǎn):不能跨進(jìn)程。而內(nèi)核模式同步對象大概正好相反。內(nèi)核對象可是系統(tǒng)級的東東,所謂錢多好辦事,自然功能也就強(qiáng)多了。
     用戶模式同步對象包括原子訪問和臨界區(qū);內(nèi)核模式同步對象包括事件,等待定時器,信號量,互斥量。下面我們來學(xué)習(xí)每個對象的具體用法。

           1. 原子訪問
            一直都不是很理解為啥叫原子訪問。ATOM(阿童木),也許和那個認(rèn)為原子是最小物質(zhì)的年代的理論有關(guān)吧。
    既然是最小的物質(zhì)了,自然是不可以再分了。所以可以理解為原子訪問就是不可以再分割的操作。也就是不會被其他更高優(yōu)先級中斷搶先得操作。說得通俗點(diǎn)就是系統(tǒng)能保證這個操作是一次性搞定的,中途絕不能休息?;ユi函數(shù)的家族十分的龐大,可以查看msdn(http://msdn.microsoft.com/en-us/library/ms683597(VS.85).aspx)以InterLocked開始的函數(shù)都是戶數(shù)函數(shù)。使用互鎖函數(shù)的優(yōu)點(diǎn)是:他的速度要比其他的CriticalSection,Mutex,Event,Semaphore快很多。通常少于50個時鐘周期

    一般的互鎖函數(shù):

    Interlocked functionDescription
    InterlockedAddPerforms an atomic addition operation on the specified LONG values.
    InterlockedAdd64Performs an atomic addition operation on the specified LONGLONG values.
    InterlockedAddAcquirePerforms an atomic addition operation on the specified LONG values. The operation is performed with acquire memory access semantics.
    InterlockedAddAcquire64Performs an atomic addition operation on the specified LONGLONG values. The operation is performed with acquire memory access semantics.
    InterlockedAddReleasePerforms an atomic addition operation on the specified LONG values. The operation is performed with release memory access semantics.
    InterlockedAddRelease64Performs an atomic addition operation on the specified LONGLONG values. The operation is performed with release memory access semantics.
    InterlockedAndPerforms an atomic AND operation on the specified LONG values.
    InterlockedAndAcquirePerforms an atomic AND operation on the specified LONG values. The operation is performed with acquire memory access semantics.
    InterlockedAndReleasePerforms an atomic AND operation on the specified LONG values. The operation is performed with release memory access semantics.
    InterlockedAnd8Performs an atomic AND operation on the specified char values.
    InterlockedAnd8AcquirePerforms an atomic AND operation on the specified char values. The operation is performed with acquire memory access semantics.
    InterlockedAnd8ReleasePerforms an atomic AND operation on the specified char values. The operation is performed with release memory access semantics.
    InterlockedAnd16Performs an atomic AND operation on the specified SHORT values.
    InterlockedAnd16AcquirePerforms an atomic AND operation on the specified SHORT values. The operation is performed with acquire memory access semantics.
    InterlockedAnd16ReleasePerforms an atomic AND operation on the specified SHORT values. The operation is performed with release memory access semantics.
    InterlockedAnd64Performs an atomic AND operation on the specified LONGLONG values.
    InterlockedAnd64AcquirePerforms an atomic AND operation on the specified LONGLONG values. The operation is performed with acquire memory access semantics.
    InterlockedAnd64ReleasePerforms an atomic AND operation on the specified LONGLONG values. The operation is performed with release memory access semantics.
    InterlockedBitTestAndResetTests the specified bit of the specified LONG value and sets it to 0.
    InterlockedBitTestAndReset64Tests the specified bit of the specified LONG64 value and sets it to 0.
    InterlockedBitTestAndSetTests the specified bit of the specified LONG value and sets it to 1.
    InterlockedBitTestAndSet64Tests the specified bit of the specified LONG64 value and sets it to 1.
    InterlockedCompare64Exchange128Performs an atomic compare-and-exchange operation on the specified values. The function compares the specified 64-bit values and exchanges with the specified 128-bit value based on the outcome of the comparison.
    InterlockedCompare64ExchangeAcquire128Performs an atomic compare-and-exchange operation on the specified values. The function compares the specified 64-bit values and exchanges with the specified 128-bit value based on the outcome of the comparison. The operation is performed with acquire memory access semantics.
    InterlockedCompare64ExchangeRelease128Performs an atomic compare-and-exchange operation on the specified values. The function compares the specified 64-bit values and exchanges with the specified 128-bit value based on the outcome of the comparison. The operation is performed with release memory access semantics.
    InterlockedCompareExchangePerforms an atomic compare-and-exchange operation on the specified values. The function compares two specified 32-bit values and exchanges with another 32-bit value based on the outcome of the comparison.
    InterlockedCompareExchange64Performs an atomic compare-and-exchange operation on the specified values. The function compares two specified 64-bit values and exchanges with another 64-bit value based on the outcome of the comparison.
    InterlockedCompareExchangeAcquirePerforms an atomic compare-and-exchange operation on the specified values. The function compares two specified 32-bit values and exchanges with another 32-bit value based on the outcome of the comparison. The operation is performed with acquire memory access semantics.
    InterlockedCompareExchangeAcquire64Performs an atomic compare-and-exchange operation on the specified values. The function compares two specified 64-bit values and exchanges with another 64-bit value based on the outcome of the comparison. The exchange is performed with acquire memory access semantics.
    InterlockedCompareExchangePointerPerforms an atomic compare-and-exchange operation on the specified pointer values. The function compares two specified pointer values and exchanges with another pointer value based on the outcome of the comparison.
    InterlockedCompareExchangePointerAcquirePerforms an atomic compare-and-exchange operation on the specified pointer values. The function compares two specified pointer values and exchanges with another pointer value based on the outcome of the comparison. The operation is performed with acquire memory access semantics.
    InterlockedCompareExchangePointerReleasePerforms an atomic compare-and-exchange operation on the specified pointer values. The function compares two specified pointer values and exchanges with another pointer value based on the outcome of the comparison. The operation is performed with release memory access semantics.
    InterlockedCompareExchangeReleasePerforms an atomic compare-and-exchange operation on the specified values. The function compares two specified 32-bit values and exchanges with another 32-bit value based on the outcome of the comparison. The exchange is performed with release memory access semantics.
    InterlockedCompareExchangeRelease64Performs an atomic compare-and-exchange operation on the specified values. The function compares two specified 64-bit values and exchanges with another 64-bit value based on the outcome of the comparison. The exchange is performed with release memory access semantics.
    InterlockedDecrementDecrements (decreases by one) the value of the specified 32-bit variable as an atomic operation.
    InterlockedDecrement64Decrements (decreases by one) the value of the specified 64-bit variable as an atomic operation.
    InterlockedDecrementAcquireDecrements (decreases by one) the value of the specified 32-bit variable as an atomic operation. The operation is performed with acquire memory access semantics.
    InterlockedDecrementAcquire64Decrements (decreases by one) the value of the specified 64-bit variable as an atomic operation. The operation is performed with acquire memory access semantics.
    InterlockedDecrementReleaseDecrements (decreases by one) the value of the specified 32-bit variable as an atomic operation. The operation is performed with release memory access semantics.
    InterlockedDecrementRelease64Decrements (decreases by one) the value of the specified 64-bit variable as an atomic operation. The operation is performed with release memory access semantics.
    InterlockedExchangeSets a 32-bit variable to the specified value as an atomic operation.
    InterlockedExchange64Sets a 64-bit variable to the specified value as an atomic operation.
    InterlockedExchangeAcquireSets a 32-bit variable to the specified value as an atomic operation. The operation is performed with acquire memory access semantics.
    InterlockedExchangeAcquire64Sets a 32-bit variable to the specified value as an atomic operation. The operation is performed with acquire memory access semantics.
    InterlockedExchangeAddPerforms an atomic addition of two 32-bit values.
    InterlockedExchangeAdd64Performs an atomic addition of two 64-bit values.
    InterlockedExchangeAddAcquirePerforms an atomic addition of two 32-bit values. The operation is performed with acquire memory access semantics.
    InterlockedExchangeAddAcquire64Performs an atomic addition of two 64-bit values. The operation is performed with acquire memory access semantics.
    InterlockedExchangeAddReleasePerforms an atomic addition of two 32-bit values. The operation is performed with release memory access semantics.
    InterlockedExchangeAddRelease64Performs an atomic addition of two 64-bit values. The operation is performed with release memory access semantics.
    InterlockedExchangePointerAtomically exchanges a pair of pointer values.
    InterlockedExchangePointerAcquireAtomically exchanges a pair of pointer values. The operation is performed with acquire memory access semantics.
    InterlockedIncrementIncrements (increases by one) the value of the specified 32-bit variable as an atomic operation.
    InterlockedIncrement64Increments (increases by one) the value of the specified 64-bit variable as an atomic operation.
    InterlockedIncrementAcquireIncrements (increases by one) the value of the specified 32-bit variable as an atomic operation. The operation is performed using acquire memory access semantics.
    InterlockedIncrementAcquire64Increments (increases by one) the value of the specified 64-bit variable as an atomic operation. The operation is performed using acquire memory access semantics.
    InterlockedIncrementReleaseIncrements (increases by one) the value of the specified 32-bit variable as an atomic operation. The operation is performed using release memory access semantics.
    InterlockedIncrementRelease64Increments (increases by one) the value of the specified 64-bit variable as an atomic operation. The operation is performed using release memory access semantics.
    InterlockedOrPerforms an atomic OR operation on the specified LONG values.
    InterlockedOrAcquirePerforms an atomic OR operation on the specified LONG values. The operation is performed with acquire memory access semantics.
    InterlockedOrReleasePerforms an atomic OR operation on the specified LONG values. The operation is performed with release memory access semantics.
    InterlockedOr8Performs an atomic OR operation on the specified char values.
    InterlockedOr8AcquirePerforms an atomic OR operation on the specified char values. The operation is performed with acquire memory access semantics.
    InterlockedOr8ReleasePerforms an atomic OR operation on the specified char values. The operation is performed with release memory access semantics.
    InterlockedOr16Performs an atomic OR operation on the specified SHORT values.
    InterlockedOr16AcquirePerforms an atomic OR operation on the specified SHORT values. The operation is performed with acquire memory access semantics.
    InterlockedOr16ReleasePerforms an atomic OR operation on the specified SHORT values. The operation is performed with release memory access semantics.
    InterlockedOr64Performs an atomic OR operation on the specified LONGLONG values.
    InterlockedOr64AcquirePerforms an atomic OR operation on the specified LONGLONG values. The operation is performed with acquire memory access semantics.
    InterlockedOr64ReleasePerforms an atomic OR operation on the specified LONGLONG values. The operation is performed with release memory access semantics.
    InterlockedXorPerforms an atomic XOR operation on the specified LONG values.
    InterlockedXorAcquirePerforms an atomic XOR operation on the specified LONG values. The operation is performed with acquire memory access semantics.
    InterlockedXorReleasePerforms an atomic XOR operation on the specified LONG values. The operation is performed with release memory access semantics.
    InterlockedXor8Performs an atomic XOR operation on the specified char values.
    InterlockedXor8AcquirePerforms an atomic XOR operation on the specified char values. The operation is performed with acquire memory access semantics.
    InterlockedXor8ReleasePerforms an atomic XOR operation on the specified char values. The operation is performed with release memory access semantics.
    InterlockedXor16Performs an atomic XOR operation on the specified SHORT values.
    InterlockedXor16AcquirePerforms an atomic XOR operation on the specified SHORT values. The operation is performed with acquire memory access semantics.
    InterlockedXor16ReleasePerforms an atomic XOR operation on the specified SHORT values. The operation is performed with release memory access semantics.
    InterlockedXor64Performs an atomic XOR operation on the specified LONGLONG values.
    InterlockedXor64AcquirePerforms an atomic XOR operation on the specified LONGLONG values. The operation is performed with acquire memory access semantics.
    InterlockedXor64ReleasePerforms an atomic XOR operation on the specified LONGLONG values. The operation is performed with release memory access semantics.

    鏈表的互鎖函數(shù):

    Singly-linked list functionDescription
    InitializeSListHeadInitializes the head of a singly linked list.
    InterlockedFlushSListFlushes the entire list of items in a singly linked list.
    InterlockedPopEntrySListRemoves an item from the front of a singly linked list.
    InterlockedPushEntrySListInserts an item at the front of a singly linked list.
    QueryDepthSListRetrieves the number of entries in the specified singly linked list.
    RtlFirstEntrySListRetrieves the first entry in a singly linked list.
    RtlInitializeSListHeadInitializes the head of a singly linked list. Applications should call InitializeSListHead instead.
    RtlInterlockedFlushSListFlushes the entire list of items in a singly linked list. Applications should call InterlockedFlushSList instead.
    RtlInterlockedPopEntrySListRemoves an item from the front of a singly linked list. Applications should call InterlockedPopEntrySList instead.
    RtlInterlockedPushEntrySListInserts an item at the front of a singly linked list. Applications should call InterlockedPushEntrySList instead.
    RtlQueryDepthSListRetrieves the number of entries in the specified singly linked list. Applications should call QueryDepthSList instead.

    具體用法可以查看MSDN。在眾多的原子操作函數(shù)中,最常見的有以下幾個:
    InterlockedExhangeAdd,InterlockedExchange,InterlockedCompareExchange,InterlockedIncrement和InterlockedDecrement。最后兩個在COM中應(yīng)該很熟悉。

    2. 臨界區(qū)
    臨界區(qū)是指一小段代碼,在執(zhí)行前必須獨(dú)占對某些資源的訪問權(quán)。也就是以原子操作的方式來訪問資源。換句話說就是你用了別人就沒法用了。要用怎么辦?等唄。等多久?先占得人說了算!中國有句古話叫“占著茅坑不拉屎”。在這里用太合適了。茅坑就是我們說的資源,想用茅坑的人就是線程啦。一個家伙進(jìn)去了,同時把牌牌給翻成紅色,這時候再有人要進(jìn)去就得等了。只有里面那個家伙出來并且把牌牌翻成綠色的才能進(jìn)去。所以用臨界區(qū)得非常小心才行。如果出來那家伙人走了,可是忘記把牌牌翻成綠色的話,雖然人走了,不過坑就算是廢啦。可是可是你還在傻傻的等。如果有個家伙進(jìn)去了,可是忘記把牌牌翻成紅色的,這時你到了門口一看是綠牌牌自然就進(jìn)去了。。。oh,My God!所以各位千萬記得使用臨界區(qū)時一定記得進(jìn)去出來都得“翻牌牌”。切記切記!
             臨界區(qū)是用戶態(tài)的同步對象,因此只能在進(jìn)程內(nèi)使用,他無法跨進(jìn)程。在使用的過程中也無法移動或拷貝或修改。對于線程的獲取順序沒有定義,因此不能假定哪個線程將會首先獲得臨界區(qū)。使用完臨界區(qū)必須執(zhí)行刪除操作。而且一個臨界區(qū)是不允許進(jìn)行重復(fù)初始化的。否則將產(chǎn)生不可預(yù)料的結(jié)果。
           臨界區(qū)的使用也很簡單,主要有四個函數(shù):

       void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);初始化臨界區(qū)。這個函數(shù)雖然沒有返回值,但是在低內(nèi)存時會拋出STATUS_NO_MEMORY 的異常.

本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊舉報(bào)。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
java 線程優(yōu)先級與yield()
Java線程優(yōu)先級設(shè)置
淺議Visual C++多線程設(shè)計(jì)
多線程編程基礎(chǔ)
Delphi線程基礎(chǔ)知識
QT線程(一):線程類
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服