12.[個人]C++線程入門到進階(12)----讀者寫者問題&讀寫鎖SRWLock

與上一篇《秒殺多線程第十篇 生產者消費者問題》的生產者消費者問題一樣,讀者寫者也是一個非常著名的同步問題。讀者寫者問題描述非常簡單,有一個寫者很多讀者,多個讀者可以同時讀文件,但寫者在寫文件時不允許有讀者在讀文件,同樣有讀者在讀文件時寫者也不去能寫文件。

上面是讀者寫者問題示意圖,類似於生產者消費者問題的分析過程,首先來找找哪些是屬於“等待”情況。

第一.寫者要等到沒有讀者時才能去寫文件。

第二.所有讀者要等待寫者完成寫文件後才能去讀文件。

找完“等待”情況後,再看看有沒有要互斥訪問的資源。由於只有一個寫者而讀者們是可以共享的讀文件,所以按題目要求並沒有需要互斥訪問的資源。類似於上一篇中美觀的彩色輸出,我們對生產者輸出代碼進行了顏色設置(在控制檯輸出顏色設置參見《VC 控制檯顏色設置》)。因此在這裏要加個互斥訪問,不然很有可能在寫者線程將控制檯顏色設置還原之前,讀者線程就已經有輸出了。所以要對輸出語句作個互斥訪問處理,修改後的讀者及寫者的輸出函數如下所示:

[cpp] view plain copy
  1. //讀者線程輸出函數  
  2. void ReaderPrintf(char *pszFormat, ...)  
  3. {  
  4.     va_list   pArgList;  
  5.     va_start(pArgList, pszFormat);  
  6.     EnterCriticalSection(&g_cs);  
  7.     vfprintf(stdout, pszFormat, pArgList);  
  8.     LeaveCriticalSection(&g_cs);  
  9.     va_end(pArgList);  
  10. }  
  11. //寫者線程輸出函數  
  12. void WriterPrintf(char *pszStr)  
  13. {  
  14.     EnterCriticalSection(&g_cs);  
  15.     SetConsoleColor(FOREGROUND_GREEN);  
  16.     printf("     %s\n", pszStr);  
  17.     SetConsoleColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);  
  18.     LeaveCriticalSection(&g_cs);  
  19. }  

讀者線程輸出函數所使用的可變參數詳見《C,C++中使用可變參數》。

       解決了互斥輸出問題,接下來再考慮如何實現同步問題。可以設置一個變量來記錄正在讀文件的讀者個數,第一個開始讀文件的讀者要負責將關閉允許寫者進入的標誌,最後一個結束讀文件的讀者要負責打開允許寫者進入的標誌。這樣第一種“等待”情況就解決了。第二種“等待”情況是有寫者進入時所以讀者不能進入,使用一個事件就可以完成這個任務了——所有讀者都要等待這個事件而寫者負責觸發事件和設置事件爲未觸發。詳細見代碼中註釋:

[cpp] view plain copy
  1. //讀者與寫者問題  
  2. #include <stdio.h>  
  3. #include <process.h>  
  4. #include <windows.h>  
  5. //設置控制檯輸出顏色  
  6. BOOL SetConsoleColor(WORD wAttributes)  
  7. {  
  8.     HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);  
  9.     if (hConsole == INVALID_HANDLE_VALUE)  
  10.         return FALSE;  
  11.       
  12.     return SetConsoleTextAttribute(hConsole, wAttributes);  
  13. }  
  14. const int READER_NUM = 5;  //讀者個數  
  15. //關鍵段和事件  
  16. CRITICAL_SECTION g_cs, g_cs_writer_count;  
  17. HANDLE g_hEventWriter, g_hEventNoReader;  
  18. int g_nReaderCount;  
  19. //讀者線程輸出函數(變參函數的實現)  
  20. void ReaderPrintf(char *pszFormat, ...)  
  21. {  
  22.     va_list   pArgList;  
  23.       
  24.     va_start(pArgList, pszFormat);  
  25.     EnterCriticalSection(&g_cs);  
  26.     vfprintf(stdout, pszFormat, pArgList);  
  27.     LeaveCriticalSection(&g_cs);  
  28.     va_end(pArgList);  
  29. }  
  30. //讀者線程函數  
  31. unsigned int __stdcall ReaderThreadFun(PVOID pM)  
  32. {  
  33.     ReaderPrintf("     編號爲%d的讀者進入等待中...\n", GetCurrentThreadId());  
  34.     //等待寫者完成  
  35.     WaitForSingleObject(g_hEventWriter, INFINITE);  
  36.   
  37.     //讀者個數增加  
  38.     EnterCriticalSection(&g_cs_writer_count);  
  39.     g_nReaderCount++;  
  40.     if (g_nReaderCount == 1)  
  41.         ResetEvent(g_hEventNoReader);  
  42.     LeaveCriticalSection(&g_cs_writer_count);  
  43.   
  44.     //讀取文件  
  45.     ReaderPrintf("編號爲%d的讀者開始讀取文件...\n", GetCurrentThreadId());  
  46.   
  47.     Sleep(rand() % 100);  
  48.   
  49.     //結束閱讀,讀者個數減小,空位增加  
  50.     ReaderPrintf(" 編號爲%d的讀者結束讀取文件\n", GetCurrentThreadId());  
  51.   
  52.     //讀者個數減少  
  53.     EnterCriticalSection(&g_cs_writer_count);  
  54.     g_nReaderCount--;  
  55.     if (g_nReaderCount == 0)  
  56.         SetEvent(g_hEventNoReader);  
  57.     LeaveCriticalSection(&g_cs_writer_count);  
  58.   
  59.     return 0;  
  60. }  
  61. //寫者線程輸出函數  
  62. void WriterPrintf(char *pszStr)  
  63. {  
  64.     EnterCriticalSection(&g_cs);  
  65.     SetConsoleColor(FOREGROUND_GREEN);  
  66.     printf("     %s\n", pszStr);  
  67.     SetConsoleColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);  
  68.     LeaveCriticalSection(&g_cs);  
  69. }  
  70. //寫者線程函數  
  71. unsigned int __stdcall WriterThreadFun(PVOID pM)  
  72. {  
  73.     WriterPrintf("寫者線程進入等待中...");  
  74.     //等待讀文件的讀者爲零  
  75.     WaitForSingleObject(g_hEventNoReader, INFINITE);  
  76.     //標記寫者正在寫文件  
  77.     ResetEvent(g_hEventWriter);  
  78.           
  79.     //寫文件  
  80.     WriterPrintf("  寫者開始寫文件.....");  
  81.     Sleep(rand() % 100);  
  82.     WriterPrintf("  寫者結束寫文件");  
  83.   
  84.     //標記寫者結束寫文件  
  85.     SetEvent(g_hEventWriter);  
  86.     return 0;  
  87. }  
  88. int main()  
  89. {  
  90.     printf("  讀者寫者問題\n");  
  91.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  92.   
  93.     //初始化事件和信號量  
  94.     InitializeCriticalSection(&g_cs);  
  95.     InitializeCriticalSection(&g_cs_writer_count);  
  96.   
  97.     //手動置位,初始已觸發  
  98.     g_hEventWriter = CreateEvent(NULL, TRUE, TRUE, NULL);  
  99.     g_hEventNoReader  = CreateEvent(NULL, FALSE, TRUE, NULL);  
  100.     g_nReaderCount = 0;  
  101.   
  102.     int i;  
  103.     HANDLE hThread[READER_NUM + 1];  
  104.     //先啓動二個讀者線程  
  105.     for (i = 1; i <= 2; i++)  
  106.         hThread[i] = (HANDLE)_beginthreadex(NULL, 0, ReaderThreadFun, NULL, 0, NULL);  
  107.     //啓動寫者線程  
  108.     hThread[0] = (HANDLE)_beginthreadex(NULL, 0, WriterThreadFun, NULL, 0, NULL);  
  109.     Sleep(50);  
  110.     //最後啓動其它讀者結程  
  111.     for ( ; i <= READER_NUM; i++)  
  112.         hThread[i] = (HANDLE)_beginthreadex(NULL, 0, ReaderThreadFun, NULL, 0, NULL);  
  113.     WaitForMultipleObjects(READER_NUM + 1, hThread, TRUE, INFINITE);  
  114.     for (i = 0; i < READER_NUM + 1; i++)  
  115.         CloseHandle(hThread[i]);  
  116.   
  117.     //銷燬事件和信號量  
  118.     CloseHandle(g_hEventWriter);  
  119.     CloseHandle(g_hEventNoReader);  
  120.     DeleteCriticalSection(&g_cs);  
  121.     DeleteCriticalSection(&g_cs_writer_count);  
  122.     return 0;  
  123. }  

運行結果如下所示:

根據結果可以看出當有讀者在讀文件時,寫者線程會進入等待狀態中。當寫者線程在寫文件時,讀者線程也會排隊等待,說明讀者和寫者已經完成了同步。

 

本系列通過經典線程同步問題來列舉線程同步手段的關鍵段事件互斥量信號量,並作對這四種方法進行了總結。然後又通過二個著名的線程同步實例——生產者消費者問題讀者寫者問題來強化對多線程同步互斥的理解與運用。希望讀者們能夠熟練掌握,從而在筆試面試中能夠順利的“秒殺”多線程的相關試題,獲得自己滿意的offer

 

從《秒殺多線程第十篇生產者消費者問題》到《秒殺多線程第十一篇讀者寫者問題》可以得出多線程問題的關鍵在於找到所有“等待”情況和判斷有無需要互斥訪問的資源。那麼如何從實際問題中更好更快更全面的找出這些了?請看《秒殺多線程第十二篇多線程同步內功心法——PV操作上》和《秒殺多線程第十三篇多線程同步內功心法——PV操作下》這二篇以加強解決多線程同步問題的“內功”。  

---------------------------------------------------------------------------------------------------------------

    在《秒殺多線程第十一篇讀者寫者問題》文章中我們使用事件和一個記錄讀者個數的變量來解決讀者寫者問題。問題雖然得到了解決,但代碼有點複雜。本篇將介紹一種新方法——讀寫鎖SRWLock來解決這一問題。讀寫鎖在對資源進行保護的同時,還能區分想要讀取資源值的線程(讀取者線程)和想要更新資源的線程(寫入者線程)。對於讀取者線程,讀寫鎖會允許他們併發的執行。當有寫入者線程在佔有資源時,讀寫鎖會讓其它寫入者線程和讀取者線程等待。因此用讀寫鎖來解決讀者寫者問題會使代碼非常清晰和簡潔。

 

    下面就來看看如何使用讀寫鎖,要注意編譯讀寫鎖程序需要VS2008,運行讀寫鎖程序要在VistaWindows Server2008系統(比這兩個更高級的系統也可以)。讀寫鎖的主要函數就五個,分爲初始化函數,寫入者線程申請和釋放函數,讀取者線程申請和釋放函數,以下是詳細的函數使用說明:

第一個 InitializeSRWLock

函數功能:初始化讀寫鎖

函數原型:VOID InitializeSRWLock(PSRWLOCK SRWLock);

函數說明:初始化(沒有刪除或銷燬SRWLOCK的函數,系統會自動清理)

 

第二個 AcquireSRWLockExclusive

函數功能:寫入者線程申請寫資源。

函數原型:VOID AcquireSRWLockExclusive(PSRWLOCK SRWLock);

 

第三個 ReleaseSRWLockExclusive

函數功能:寫入者線程寫資源完畢,釋放對資源的佔用。

函數原型:VOID ReleaseSRWLockExclusive(PSRWLOCK SRWLock);

 

第四個 AcquireSRWLockShared

函數功能:讀取者線程申請讀資源。

函數原型:VOID AcquireSRWLockShared(PSRWLOCK SRWLock);

 

第五個 ReleaseSRWLockShared

函數功能:讀取者線程結束讀取資源,釋放對資源的佔用。

函數原型:VOID ReleaseSRWLockShared(PSRWLOCK SRWLock);

 

注意一個線程僅能鎖定資源一次,不能多次鎖定資源。

 

使用讀寫鎖精簡後的代碼如下(代碼中變參函數的實現請參閱《C,C++中使用可變參數》,控制檯顏色設置請參閱《VC 控制檯顏色設置》):

[cpp] view plain copy
  1. //讀者與寫者問題繼 讀寫鎖SRWLock  
  2. #include <stdio.h>  
  3. #include <process.h>  
  4. #include <windows.h>  
  5. //設置控制檯輸出顏色  
  6. BOOL SetConsoleColor(WORD wAttributes)  
  7. {  
  8.     HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);  
  9.     if (hConsole == INVALID_HANDLE_VALUE)  
  10.         return FALSE;  
  11.     return SetConsoleTextAttribute(hConsole, wAttributes);  
  12. }  
  13. const int READER_NUM = 5;  //讀者個數  
  14. //關鍵段和事件  
  15. CRITICAL_SECTION g_cs;  
  16. SRWLOCK          g_srwLock;   
  17. //讀者線程輸出函數(變參函數的實現)  
  18. void ReaderPrintf(char *pszFormat, ...)  
  19. {  
  20.     va_list   pArgList;  
  21.     va_start(pArgList, pszFormat);  
  22.     EnterCriticalSection(&g_cs);  
  23.     vfprintf(stdout, pszFormat, pArgList);  
  24.     LeaveCriticalSection(&g_cs);  
  25.     va_end(pArgList);  
  26. }  
  27. //讀者線程函數  
  28. unsigned int __stdcall ReaderThreadFun(PVOID pM)  
  29. {  
  30.     ReaderPrintf("     編號爲%d的讀者進入等待中...\n", GetCurrentThreadId());  
  31.     //讀者申請讀取文件  
  32.     AcquireSRWLockShared(&g_srwLock);  
  33.   
  34.     //讀取文件  
  35.     ReaderPrintf("編號爲%d的讀者開始讀取文件...\n", GetCurrentThreadId());  
  36.     Sleep(rand() % 100);  
  37.     ReaderPrintf(" 編號爲%d的讀者結束讀取文件\n", GetCurrentThreadId());  
  38.   
  39.     //讀者結束讀取文件  
  40.     ReleaseSRWLockShared(&g_srwLock);  
  41.     return 0;  
  42. }  
  43. //寫者線程輸出函數  
  44. void WriterPrintf(char *pszStr)  
  45. {  
  46.     EnterCriticalSection(&g_cs);  
  47.     SetConsoleColor(FOREGROUND_GREEN);  
  48.     printf("     %s\n", pszStr);  
  49.     SetConsoleColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);  
  50.     LeaveCriticalSection(&g_cs);  
  51. }  
  52. //寫者線程函數  
  53. unsigned int __stdcall WriterThreadFun(PVOID pM)  
  54. {  
  55.     WriterPrintf("寫者線程進入等待中...");  
  56.     //寫者申請寫文件  
  57.     AcquireSRWLockExclusive(&g_srwLock);  
  58.           
  59.     //寫文件  
  60.     WriterPrintf("  寫者開始寫文件.....");  
  61.     Sleep(rand() % 100);  
  62.     WriterPrintf("  寫者結束寫文件");  
  63.   
  64.     //標記寫者結束寫文件  
  65.     ReleaseSRWLockExclusive(&g_srwLock);  
  66.     return 0;  
  67. }  
  68. int main()  
  69. {  
  70.     printf("  讀者寫者問題繼 讀寫鎖SRWLock\n");  
  71.     printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");  
  72.   
  73.     //初始化讀寫鎖和關鍵段  
  74.     InitializeCriticalSection(&g_cs);  
  75.     InitializeSRWLock(&g_srwLock);  
  76.   
  77.     HANDLE hThread[READER_NUM + 1];  
  78.     int i;  
  79.     //先啓動二個讀者線程  
  80.     for (i = 1; i <= 2; i++)  
  81.         hThread[i] = (HANDLE)_beginthreadex(NULL, 0, ReaderThreadFun, NULL, 0, NULL);  
  82.     //啓動寫者線程  
  83.     hThread[0] = (HANDLE)_beginthreadex(NULL, 0, WriterThreadFun, NULL, 0, NULL);  
  84.     Sleep(50);  
  85.     //最後啓動其它讀者結程  
  86.     for ( ; i <= READER_NUM; i++)  
  87.         hThread[i] = (HANDLE)_beginthreadex(NULL, 0, ReaderThreadFun, NULL, 0, NULL);  
  88.     WaitForMultipleObjects(READER_NUM + 1, hThread, TRUE, INFINITE);  
  89.     for (i = 0; i < READER_NUM + 1; i++)  
  90.         CloseHandle(hThread[i]);  
  91.   
  92.     //銷燬關鍵段  
  93.     DeleteCriticalSection(&g_cs);  
  94.     return 0;  
  95. }  

對比下《秒殺多線程第十一篇讀者寫者問題》中的代碼就可以發現這份代碼確實清爽許多了。這個程序用VS2008編譯可以通過,但在XP系統下運行會導致報錯。

Win7系統下能夠正確的運行,結果如圖所示:

 

 

最後總結一下讀寫鎖SRWLock

1.讀寫鎖聲明後要初始化,但不用銷燬,系統會自動清理讀寫鎖。

2.讀取者和寫入者分別調用不同的申請函數和釋放函數。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章