與上一篇的生產者消費者問題一樣,讀者寫者也是一個非常著名的同步問題。讀者寫者問題描述非常簡單,有一個寫者很多讀者,多個讀者可以同時讀文件,但寫者在寫文件時不允許有讀者在讀文件,同樣有讀者在讀文件時寫者也不去能寫文件。
上面是讀者寫者問題示意圖,類似於生產者消費者問題的分析過程,首先來找找哪些是屬於“等待”情況。
第一.寫者要等到沒有讀者時才能去寫文件。
第二.所有讀者要等待寫者完成寫文件后才能去讀文件。
找完“等待”情況后,再看看有沒有要互斥訪問的資源。由於只有一個寫者而讀者們是可以共享的讀文件,所以按題目要求並沒有需要互斥訪問的資源。類似於上一篇中美觀的彩色輸出,我們對生產者輸出代碼進行了顏色設置(在控制台輸出顏色設置參見《VC 控制台顏色設置》)。因此在這里要加個互斥訪問,不然很有可能在寫者線程將控制台顏色設置還原之前,讀者線程就已經有輸出了。所以要對輸出語句作個互斥訪問處理,修改后的讀者及寫者的輸出函數如下所示:
- //讀者線程輸出函數
- void ReaderPrintf(char *pszFormat, ...)
- {
- va_list pArgList;
- va_start(pArgList, pszFormat);
- EnterCriticalSection(&g_cs);
- vfprintf(stdout, pszFormat, pArgList);
- LeaveCriticalSection(&g_cs);
- va_end(pArgList);
- }
- //寫者線程輸出函數
- void WriterPrintf(char *pszStr)
- {
- EnterCriticalSection(&g_cs);
- SetConsoleColor(FOREGROUND_GREEN);
- printf(" %s\n", pszStr);
- SetConsoleColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
- LeaveCriticalSection(&g_cs);
- }
讀者線程輸出函數所使用的可變參數詳見《C,C++中使用可變參數》。
解決了互斥輸出問題,接下來再考慮如何實現同步問題。可以設置一個變量來記錄正在讀文件的讀者個數,第一個開始讀文件的讀者要負責將關閉允許寫者進入的標志,最后一個結束讀文件的讀者要負責打開允許寫者進入的標志。這樣第一種“等待”情況就解決了。第二種“等待”情況是有寫者進入時所以讀者不能進入,使用一個事件就可以完成這個任務了——所有讀者都要等待這個事件而寫者負責觸發事件和設置事件為未觸發。詳細見代碼中注釋:
- //讀者與寫者問題
- #include <stdio.h>
- #include <process.h>
- #include <windows.h>
- //設置控制台輸出顏色
- BOOL SetConsoleColor(WORD wAttributes)
- {
- HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
- if (hConsole == INVALID_HANDLE_VALUE)
- return FALSE;
- return SetConsoleTextAttribute(hConsole, wAttributes);
- }
- const int READER_NUM = 5; //讀者個數
- //關鍵段和事件
- CRITICAL_SECTION g_cs, g_cs_writer_count;
- HANDLE g_hEventWriter, g_hEventNoReader;
- int g_nReaderCount;
- //讀者線程輸出函數(變參函數的實現)
- void ReaderPrintf(char *pszFormat, ...)
- {
- va_list pArgList;
- va_start(pArgList, pszFormat);
- EnterCriticalSection(&g_cs);
- vfprintf(stdout, pszFormat, pArgList);
- LeaveCriticalSection(&g_cs);
- va_end(pArgList);
- }
- //讀者線程函數
- unsigned int __stdcall ReaderThreadFun(PVOID pM)
- {
- ReaderPrintf(" 編號為%d的讀者進入等待中...\n", GetCurrentThreadId());
- //等待寫者完成
- WaitForSingleObject(g_hEventWriter, INFINITE);
- //讀者個數增加
- EnterCriticalSection(&g_cs_writer_count);
- g_nReaderCount++;
- if (g_nReaderCount == 1)
- ResetEvent(g_hEventNoReader);
- LeaveCriticalSection(&g_cs_writer_count);
- //讀取文件
- ReaderPrintf("編號為%d的讀者開始讀取文件...\n", GetCurrentThreadId());
- Sleep(rand() % 100);
- //結束閱讀,讀者個數減小,空位增加
- ReaderPrintf(" 編號為%d的讀者結束讀取文件\n", GetCurrentThreadId());
- //讀者個數減少
- EnterCriticalSection(&g_cs_writer_count);
- g_nReaderCount--;
- if (g_nReaderCount == 0)
- SetEvent(g_hEventNoReader);
- LeaveCriticalSection(&g_cs_writer_count);
- return 0;
- }
- //寫者線程輸出函數
- void WriterPrintf(char *pszStr)
- {
- EnterCriticalSection(&g_cs);
- SetConsoleColor(FOREGROUND_GREEN);
- printf(" %s\n", pszStr);
- SetConsoleColor(FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
- LeaveCriticalSection(&g_cs);
- }
- //寫者線程函數
- unsigned int __stdcall WriterThreadFun(PVOID pM)
- {
- WriterPrintf("寫者線程進入等待中...");
- //等待讀文件的讀者為零
- WaitForSingleObject(g_hEventNoReader, INFINITE);
- //標記寫者正在寫文件
- ResetEvent(g_hEventWriter);
- //寫文件
- WriterPrintf(" 寫者開始寫文件.....");
- Sleep(rand() % 100);
- WriterPrintf(" 寫者結束寫文件");
- //標記寫者結束寫文件
- SetEvent(g_hEventWriter);
- return 0;
- }
- int main()
- {
- printf(" 讀者寫者問題\n");
- printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
- //初始化事件和信號量
- InitializeCriticalSection(&g_cs);
- InitializeCriticalSection(&g_cs_writer_count);
- //手動置位,初始已觸發
- g_hEventWriter = CreateEvent(NULL, TRUE, TRUE, NULL);
- g_hEventNoReader = CreateEvent(NULL, FALSE, TRUE, NULL);
- g_nReaderCount = 0;
- int i;
- HANDLE hThread[READER_NUM + 1];
- //先啟動二個讀者線程
- for (i = 1; i <= 2; i++)
- hThread[i] = (HANDLE)_beginthreadex(NULL, 0, ReaderThreadFun, NULL, 0, NULL);
- //啟動寫者線程
- hThread[0] = (HANDLE)_beginthreadex(NULL, 0, WriterThreadFun, NULL, 0, NULL);
- Sleep(50);
- //最后啟動其它讀者結程
- for ( ; i <= READER_NUM; i++)
- hThread[i] = (HANDLE)_beginthreadex(NULL, 0, ReaderThreadFun, NULL, 0, NULL);
- WaitForMultipleObjects(READER_NUM + 1, hThread, TRUE, INFINITE);
- for (i = 0; i < READER_NUM + 1; i++)
- CloseHandle(hThread[i]);
- //銷毀事件和信號量
- CloseHandle(g_hEventWriter);
- CloseHandle(g_hEventNoReader);
- DeleteCriticalSection(&g_cs);
- DeleteCriticalSection(&g_cs_writer_count);
- return 0;
- }
運行結果如下所示:
根據結果可以看出當有讀者在讀文件時,寫者線程會進入等待狀態中。當寫者線程在寫文件時,讀者線程也會排隊等待,說明讀者和寫者已經完成了同步。
本系列通過經典線程同步問題來列舉線程同步手段的關鍵段、事件、互斥量、信號量,並作對這四種方法進行了總結。然后又通過二個著名的線程同步實例——生產者消費者問題和讀者寫者問題來強化對多線程同步互斥的理解與運用。希望讀者們能夠熟練掌握,從而在筆試面試中能夠順利的“秒殺”多線程的相關試題,獲得自己滿意的offer。
從第十篇和第十一篇可以得出多線程問題的關鍵在於找到所有“等待”情況和判斷有無需要互斥訪問的資源。那么如何從實際問題中更好更快更全面的找出這些了?請看第十二、十三這二篇以加強解決多線程同步問題的“內功”。
另外,讀者寫者問題可以用讀寫鎖SRWLock來解決,請看第十四篇。