CreateThread與_beginthreadex本質區別

来源:http://www.cnblogs.com/candl/archive/2016/04/25/5429919.html
-Advertisement-
Play Games

原文地址:http://blog.csdn.net/morewindows/article/details/7421759 使用多線程其實是非常容易的,下麵這個程式的主線程會創建了一個子線程並等待其運行完畢,子線程就輸出它的線程ID號然後輸出一句經典名言——Hello World。整個程式的代碼非常 ...


 原文地址:http://blog.csdn.net/morewindows/article/details/7421759

 

   使用多線程其實是非常容易的,下麵這個程式的主線程會創建了一個子線程並等待其運行完畢,子線程就輸出它的線程ID號然後輸出一句經典名言——Hello World。整個程式的代碼非常簡短,只有區區幾行。

[cpp] view plain copy  
  1. //最簡單的創建多線程實例  
  2. #include <stdio.h>  
  3. #include <windows.h>  
  4. //子線程函數  
  5. DWORD WINAPI ThreadFun(LPVOID pM)  
  6. {  
  7.     printf("子線程的線程ID號為:%d\n子線程輸出Hello World\n", GetCurrentThreadId());  
  8.     return 0;  
  9. }  
  10. //主函數,所謂主函數其實就是主線程執行的函數。  
  11. int main()  
  12. {  
  13.     printf("     最簡單的創建多線程實例\n");  
  14.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  15.   
  16.     HANDLE handle = CreateThread(NULL, 0, ThreadFun, NULL, 0, NULL);  
  17.     WaitForSingleObject(handle, INFINITE);  
  18.     return 0;  
  19. }  

運行結果如下所示:

 

下麵來細講下代碼中的一些函數

第一個 CreateThread

函數功能:創建線程

函數原型:

HANDLEWINAPICreateThread(

  LPSECURITY_ATTRIBUTESlpThreadAttributes,

  SIZE_TdwStackSize,

  LPTHREAD_START_ROUTINElpStartAddress,

  LPVOIDlpParameter,

  DWORDdwCreationFlags,

  LPDWORDlpThreadId

);

函數說明:

第一個參數表示線程內核對象的安全屬性,一般傳入NULL表示使用預設設置。

第二個參數表示線程棧空間大小。傳入0表示使用預設大小(1MB)。

第三個參數表示新線程所執行的線程函數地址,多個線程可以使用同一個函數地址。

第四個參數是傳給線程函數的參數。

第五個參數指定額外的標誌來控制線程的創建,為0表示線程創建之後立即就可以進行調度,如果為CREATE_SUSPENDED則表示線程創建後暫停運行,這樣它就無法調度,直到調用ResumeThread()。

第六個參數將返回線程的ID號,傳入NULL表示不需要返回該線程ID號。

函數返回值:

成功返回新線程的句柄,失敗返回NULL。 

 

第二個 WaitForSingleObject

函數功能:等待函數 – 使線程進入等待狀態,直到指定的內核對象被觸發。

函數原形:

DWORDWINAPIWaitForSingleObject(

  HANDLEhHandle,

  DWORDdwMilliseconds

);

函數說明:

第一個參數為要等待的內核對象。

第二個參數為最長等待的時間,以毫秒為單位,如傳入5000就表示5秒,傳入0就立即返回,傳入INFINITE表示無限等待。

因為線程的句柄線上程運行時是未觸發的,線程結束運行,句柄處於觸髮狀態。所以可以用WaitForSingleObject()來等待一個線程結束運行。

函數返回值:

在指定的時間內對象被觸發,函數返回WAIT_OBJECT_0。超過最長等待時間對象仍未被觸發返回WAIT_TIMEOUT。傳入參數有錯誤將返回WAIT_FAILED

 

CreateThread()函數是Windows提供的API介面,在C/C++語言另有一個創建線程的函數_beginthreadex(),在很多書上(包括《Windows核心編程》)提到過儘量使用_beginthreadex()來代替使用CreateThread(),這是為什麼了?下麵就來探索與發現它們的區別吧。

 

       首先要從標準C運行庫與多線程的矛盾說起,標準C運行庫在1970年被實現了,由於當時沒任何一個操作系統提供對多線程的支持。因此編寫標準C運行庫的程式員根本沒考慮多線程程式使用標準C運行庫的情況。比如標準C運行庫的全局變數errno。很多運行庫中的函數在出錯時會將錯誤代號賦值給這個全局變數,這樣可以方便調試。但如果有這樣的一個代碼片段:

[cpp] view plain copy  
  1. if (system("notepad.exe readme.txt") == -1)  
  2. {  
  3.     switch(errno)  
  4.     {  
  5.         ...//錯誤處理代碼  
  6.     }  
  7. }  

假設某個線程A在執行上面的代碼,該線程在調用system()之後且尚未調用switch()語句時另外一個線程B啟動了,這個線程B也調用了標準C運行庫的函數,不幸的是這個函數執行出錯了並將錯誤代號寫入全局變數errno中。這樣線程A一旦開始執行switch()語句時,它將訪問一個被B線程改動了的errno。這種情況必須要加以避免!因為不單單是這一個變數會出問題,其它像strerror()、strtok()、tmpnam()、gmtime()、asctime()等函數也會遇到這種由多個線程訪問修改導致的數據覆蓋問題。

 

為瞭解決這個問題,Windows操作系統提供了這樣的一種解決方案——每個線程都將擁有自己專用的一塊記憶體區域來供標準C運行庫中所有有需要的函數使用。而且這塊記憶體區域的創建就是由C/C++運行庫函數_beginthreadex()來負責的。下麵列出_beginthreadex()函數的源代碼(我在這份代碼中增加了一些註釋)以便讀者更好的理解_beginthreadex()函數與CreateThread()函數的區別。

[cpp] view plain copy  
  1. //_beginthreadex源碼整理By MoreWindows( http://blog.csdn.net/MoreWindows )  
  2. _MCRTIMP uintptr_t __cdecl _beginthreadex(  
  3.     void *security,  
  4.     unsigned stacksize,  
  5.     unsigned (__CLR_OR_STD_CALL * initialcode) (void *),  
  6.     void * argument,  
  7.     unsigned createflag,  
  8.     unsigned *thrdaddr  
  9. )  
  10. {  
  11.     _ptiddata ptd;          //pointer to per-thread data 見註1  
  12.     uintptr_t thdl;         //thread handle 線程句柄  
  13.     unsigned long err = 0L; //Return from GetLastError()  
  14.     unsigned dummyid;    //dummy returned thread ID 線程ID號  
  15.       
  16.     // validation section 檢查initialcode是否為NULL  
  17.     _VALIDATE_RETURN(initialcode != NULL, EINVAL, 0);  
  18.   
  19.     //Initialize FlsGetValue function pointer  
  20.     __set_flsgetvalue();  
  21.       
  22.     //Allocate and initialize a per-thread data structure for the to-be-created thread.  
  23.     //相當於new一個_tiddata結構,並賦給_ptiddata指針。  
  24.     if ( (ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL )  
  25.         goto error_return;  
  26.   
  27.     // Initialize the per-thread data  
  28.     //初始化線程的_tiddata塊即CRT數據區域 見註2  
  29.     _initptd(ptd, _getptd()->ptlocinfo);  
  30.       
  31.     //設置_tiddata結構中的其它數據,這樣這塊_tiddata塊就與線程聯繫在一起了。  
  32.     ptd->_initaddr = (void *) initialcode; //線程函數地址  
  33.     ptd->_initarg = argument;              //傳入的線程參數  
  34.     ptd->_thandle = (uintptr_t)(-1);  
  35.       
  36. #if defined (_M_CEE) || defined (MRTDLL)  
  37.     if(!_getdomain(&(ptd->__initDomain))) //見註3  
  38.     {  
  39.         goto error_return;  
  40.     }  
  41. #endif  // defined (_M_CEE) || defined (MRTDLL)  
  42.       
  43.     // Make sure non-NULL thrdaddr is passed to CreateThread  
  44.     if ( thrdaddr == NULL )//判斷是否需要返回線程ID號  
  45.         thrdaddr = &dummyid;  
  46.   
  47.     // Create the new thread using the parameters supplied by the caller.  
  48.     //_beginthreadex()最終還是會調用CreateThread()來向系統申請創建線程  
  49.     if ( (thdl = (uintptr_t)CreateThread(  
  50.                     (LPSECURITY_ATTRIBUTES)security,  
  51.                     stacksize,  
  52.                     _threadstartex,  
  53.                     (LPVOID)ptd,  
  54.                     createflag,  
  55.                     (LPDWORD)thrdaddr))  
  56.         == (uintptr_t)0 )  
  57.     {  
  58.         err = GetLastError();  
  59.         goto error_return;  
  60.     }  
  61.   
  62.     //Good return  
  63.     return(thdl); //線程創建成功,返回新線程的句柄.  
  64.       
  65.     //Error return  
  66. error_return:  
  67.     //Either ptd is NULL, or it points to the no-longer-necessary block  
  68.     //calloc-ed for the _tiddata struct which should now be freed up.  
  69.     //回收由_calloc_crt()申請的_tiddata塊  
  70.     _free_crt(ptd);  
  71.     // Map the error, if necessary.  
  72.     // Note: this routine returns 0 for failure, just like the Win32  
  73.     // API CreateThread, but _beginthread() returns -1 for failure.  
  74.     //校正錯誤代號(可以調用GetLastError()得到錯誤代號)  
  75.     if ( err != 0L )  
  76.         _dosmaperr(err);  
  77.     return( (uintptr_t)0 ); //返回值為NULL的效句柄  
  78. }  

講解下部分代碼:

註1._ptiddataptd;中的_ptiddata是個結構體指針。在mtdll.h文件被定義:

      typedefstruct_tiddata * _ptiddata

微軟對它的註釋為Structure for each thread's data。這是一個非常大的結構體,有很多成員。本文由於篇幅所限就不列出來了。

 

註2._initptd(ptd, _getptd()->ptlocinfo);微軟對這一句代碼中的getptd()的說明為:

      /* return address of per-thread CRT data */

      _ptiddata __cdecl_getptd(void);

對_initptd()說明如下:

      /* initialize a per-thread CRT data block */

      void__cdecl_initptd(_Inout_ _ptiddata _Ptd,_In_opt_ pthreadlocinfo _Locale);

註釋中的CRT (C Runtime Library)即標準C運行庫。

 

註3.if(!_getdomain(&(ptd->__initDomain)))中的_getdomain()函數代碼可以在thread.c文件中找到,其主要功能是初始化COM環境。

 

由上面的源代碼可知,_beginthreadex()函數在創建新線程時會分配並初始化一個_tiddata塊。這個_tiddata塊自然是用來存放一些需要線程獨享的數據。事實上新線程運行時會首先將_tiddata塊與自己進一步關聯起來。然後新線程調用標準C運行庫函數如strtok()時就會先取得_tiddata塊的地址再將需要保護的數據存入_tiddata塊中。這樣每個線程就只會訪問和修改自己的數據而不會去篡改其它線程的數據了。因此,如果在代碼中有使用標準C運行庫中的函數時,儘量使用_beginthreadex()來代替CreateThread()相信閱讀到這裡時,你會對這句簡短的話有個非常深刻的印象,如果有面試官問起,你也可以流暢準確的回答了^_^。

 

接下來,類似於上面的程式用CreateThread()創建輸出“Hello World”的子線程,下麵使用_beginthreadex()來創建多個子線程:

[cpp] view plain copy  
  1. //創建多子個線程實例  
  2. #include <stdio.h>  
  3. #include <process.h>  
  4. #include <windows.h>  
  5. //子線程函數  
  6. unsigned int __stdcall ThreadFun(PVOID pM)  
  7. {  
  8.     printf("線程ID號為%4d的子線程說:Hello World\n", GetCurrentThreadId());  
  9.     return 0;  
  10. }  
  11. //主函數,所謂主函數其實就是主線程執行的函數。  
  12. int main()  
  13. {  
  14.     printf("     創建多個子線程實例 \n");  
  15.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  16.       
  17.     const int THREAD_NUM = 5;  
  18.     HANDLE handle[THREAD_NUM];  
  19.     for (int i = 0; i < THREAD_NUM; i++)  
  20.         handle[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL);  
  21.     WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);  
  22.     return 0;  
  23. }  

運行結果如下:

圖中每個子線程說的都是同一句話,不太好看。能不能來一個線程報數功能,即第一個子線程輸出1,第二個子線程輸出2,第三個子線程輸出3,……。要實現這個功能似乎非常簡單——每個子線程對一個全局變數進行遞增並輸出就可以了。代碼如下:

[cpp] view plain copy  
  1. //子線程報數  
  2. #include <stdio.h>  
  3. #include <process.h>  
  4. #include <windows.h>  
  5. int g_nCount;  
  6. //子線程函數  
  7. unsigned int __stdcall ThreadFun(PVOID pM)  
  8. {  
  9.     g_nCount++;  
  10.     printf("線程ID號為%4d的子線程報數%d\n", GetCurrentThreadId(), g_nCount);  
  11.     return 0;  
  12. }  
  13. //主函數,所謂主函數其實就是主線程執行的函數。  
  14. int main()  
  15. {  
  16.     printf("     子線程報數 \n");  
  17.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  18.       
  19.     const int THREAD_NUM = 10;  
  20.     HANDLE handle[THREAD_NUM];  
  21.   
  22.     g_nCount = 0;  
  23.     for (int i = 0; i < THREAD_NUM; i++)  
  24.         handle[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL);  
  25.     WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);  
  26.     return 0;  
  27. }  

對一次運行結果截圖如下:

 

顯示結果從1數到10,看起來好象沒有問題。

       答案是不對的,雖然這種做法在邏輯上是正確的,但在多線程環境下這樣做是會產生嚴重的問題.


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Struts2.0已經成為了一個高度成熟的框架,不管是穩定性還是可靠性都得到了廣泛的證明。 擁有豐富的開發人群,幾乎已經成為了事實上的工業標準。因此,學習MVC框架,struts2.0幾乎是必須熟悉的。 本課程從基本的struts2.0使用開始,深入原理講解,授人以漁!同時,將工作中容易出現的細節問 ...
  • 作者:白狼 出處:http://www.manks.top/article/yii2_captcha本文版權歸作者,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。 本來以為yii2框架驗證碼這塊很全面,嘗試百度google了一下,大多數教程寫 ...
  • 一、 Comparable<T>: Comparable是類內部的比較器,用於創建類的時候實現此介面,同時實現比較方法;對於不能修改源碼的類則無法應用此方式進行比較排序等。 源碼為: 1 public interface Comparable<T> { 2 public int compareTo( ...
  • 這是一個NB的安全認證機制。 1、這是一個安全認證機制 2、可以防止黑客截獲到客戶端發送的請求消息,避免了黑客冒充客戶端向伺服器發送操作的請求。 原理與步驟: 1、客戶端與伺服器端都會放著一份驗證用的token欄位,這欄位無論通過什麼方式前提是不能被黑客提前拿到。 2、客戶端在本地把時間戳和toke ...
  • 亂碼 上節說到亂碼出現的主要原因,即在進行編碼轉換的時候,如果將原來的編碼識別錯了,併進行了轉換,就會發生亂碼,而且這時候無論怎麼切換查看編碼的方式,都是不行的。 我們來看一個這種錯誤轉換後的亂碼,還是用上節的例子,二進位是(16進位表示):C3 80 C3 8F C3 82 C3 AD,無論按哪種 ...
  • 在騷擾了PayPal的技術支持好幾天之後終於成功對接了PayPal支付,非常感謝PayPal的技術支持人員,沒有她估計一周都搞不定。記錄一下這個過程。 接到這個任務聯繫了PayPal的技術之後,第一件事就是向她要了一些文檔。PayPal提供了一個demo商店https://demo.paypal.c ...
  • 前言:當我們進行大的項目書寫的時候或者我們選擇維護程式的時候,想知道幾點幾時我們錄入的數據有bug是那麼我們就採用 》log4j記錄日誌的信息 一、日誌及其分類 1、軟體運行的過程中離不開日誌。日誌主要用來記錄系統運行過程中的一些重要操作信息,便於監視系統運行的情況,幫助用戶避免和發現可能出現的問題 ...
  • 1、接收用戶輸入: input:接收用戶輸入的是合法的python表達式,比如字元串。 raw_input:把所有的輸入當做原始數據(raw data)。 除非對input有特別的需要,否則儘可能使用raw_input函數。 2、長字元串和原始字元串 長字元串常利用'\'經行轉義,例如: 這句話會打 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...