【多线程】学习10
预备知识:
上面是读者写者问题示意图,类似于生产者消费者问题的分析过程,首先来找找哪些是属于“等待”情况。
第一.写者要等到没有读者时才能去写文件。
第二.所有读者要等待写者完成写文件后才能去读文件。
找完“等待”情况后,再看看有没有要互斥访问的资源。由于只有一个写者而读者们是可以共享的读文件,所以按题目要求并没有需要互斥访问的资源。类似于上一篇中美观的彩色输出,我们对生产者输出代码进行了颜色设置(在控制台输出颜色设置参见《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); }
解决了互斥输出问题,接下来再考虑如何实现同步问题。可以设置一个变量来记录正在读文件的读者个数,第一个开始读文件的读者要负责将关闭允许写者进入的标志,最后一个结束读文件的读者要负责打开允许写者进入的标志。这样第一种“等待”情况就解决了。第二种“等待”情况是有写者进入时所以读者不能进入,使用一个事件就可以完成这个任务了——所有读者都要等待这个事件而写者负责触发事件和设置事件为未触发。
#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("写者线程进入等待中..."); //等待读文件的读者为0 WaitForSingleObject(g_hEventNoReader, INFINITE); //标记写者正在写文件 ResetEvent(g_hEventWriter); //写文件 WriterPrintf(" 写者开始写文件..."); Sleep(rand()%100); WriterPrintf(" 写者结束写文件"); //标记写文件结束 SetEvent(g_hEventWriter); return 0; } int main() { //初始化事件和关键段 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; }
根据结果可以看出当有读者在读文件时,写者线程会进入等待状态中。当写者线程在写文件时,读者线程也会排队等待,说明读者和写者已经完成了同步。
有用评论,代码有bug
我按照评论里的在读者线程函数中加了个Sleep(10)
unsigned int __stdcall ReaderThreadFun(PVOID pM) { ReaderPrintf(" 编号为%d的读者进入等待中...\n", GetCurrentThreadId()); //等待写者完成 WaitForSingleObject(g_hEventWriter, INFINITE); Sleep(10); //读者个数增加 EnterCriticalSection(&g_cs_writer_count);
然后输出结果变成了
确实不同步了,看了一下是因为两个事件初始化时
g_hEventWriter = CreateEvent(NULL, TRUE, TRUE, NULL);
g_hEventNoReader = CreateEvent(NULL, FALSE, TRUE, NULL);
都是初始化为已经触发,这样最开始两个读者线程运行时WaitForSingleObject(g_hEventWriter, INFINITE);可以顺利运行,在Sleep(10)时写者线程启动,但是读者线程还没有ResetEvent(g_hEventNoReader)所以写者线程的WaitForSingleObject(g_hEventNoReader, INFINITE)也可以成功通过,然后写者线程ResetEvent(g_hEventWriter)使得其他读者线程阻塞,但是最开始的两个读者线程由于已经通过了WaitForSingleObject(g_hEventWriter, INFINITE)所以可以读取,写者线程也同时在写,导致不同步。
又修改了一下,在有Sleep(10)的时候,把没有读者的事件初始化为未触发
g_hEventNoReader = CreateEvent(NULL, FALSE, FALSE, NULL);
结果还是不同步
分析一下,开始写线程由于事件g_hEventNoReader未触发处于阻塞状态,两个读者线程正常工作,但是在第3个和第4个读者线程启动后,在Sleep(10)的时候,第1个和第2个读者线程完成了,并将g_nReaderCount减到了1,并且触发了g_hEventNoReader,使得写者线程可以向下运行了,可此时第3个和第4个线程也已经通过了等待函数,这是就出现了写线程和两个读线程同时运行的情况。
----------------------------------------------------------------------------------------------------------------------------------
我自己的理解:为了避免这种不同步,必须读线程的WaitForSingleObject(g_hEventWriter, INFINITE)通过和g_nReaderCount++ 和 ResetEvent(g_hEventNoReader)没有延迟。
-------------------------------------------------------------------------------------------------------------
正确的解决方案:
从原文32楼评论得到启发的。
使用互斥量, 即锁,来表示文件的所有权即可解决这个问题。
不管读者还是写者,在处理文件前都要先获取这把锁。读者只要第一个人获取这把锁就可以了。只有有锁的才可以处理文件。由于锁不是归读者就是归写者,所以可以保证读者和写者不交错。
当第一个读者想读取文件时,先测试这把锁,如果写者占用了锁,则等待。
当读者数量归0时,释放锁。这样,接下来读者和写者都可以公平竞争这把锁。
当写者试图写文件时,也先测试这把锁,如果读者占用了锁,等待。
当写者写完文件后,释放锁。这样,接下来读者和写者又都可以公平竞争这把锁。
代码:
#include<stdio.h> #include<Windows.h> #include<process.h> const int g_ReaderNum = 5; HANDLE g_hMutex; //互斥量 即一个锁 表示当前文件归读者还是写者所有 CRITICAL_SECTION g_cs; int g_ReaderCount; unsigned int __stdcall ReaderFun(LPVOID pM) { printf(" 读者%d开始等待......\n", GetCurrentThreadId()); //读者数量增加 EnterCriticalSection(&g_cs); g_ReaderCount++; //当有第一个读者出现时,对文件加锁,表示文件归读者所有。如果这时有写者,则加锁会失败,在这里等待。 if(g_ReaderCount == 1) WaitForSingleObject(g_hMutex, INFINITE); LeaveCriticalSection(&g_cs); printf(" 读者%d开始读取数据\n", GetCurrentThreadId()); Sleep(50); printf(" 读者%d数据读取结束\n", GetCurrentThreadId()); //读者数量减少 EnterCriticalSection(&g_cs); g_ReaderCount--; //没有读者时 对文件解锁,此时文件可被读者或者写者公平争取 if(g_ReaderCount == 0) { ReleaseMutex(g_hMutex); } LeaveCriticalSection(&g_cs); return 0; } unsigned int __stdcall WriterFun(LPVOID pM) { printf("写者%d开始等待.....\n", GetCurrentThreadId()); //写者对文件加锁,表示文件归写者所有。如果当前有读者在读取文件,则加锁不会成功,会停在这里等待。 WaitForSingleObject(g_hMutex, INFINITE); printf("写者%d开始写.....\n", GetCurrentThreadId()); Sleep(70); printf("写者%d写完成.....\n", GetCurrentThreadId()); //写完后,释放锁 ReleaseMutex(g_hMutex); return 0; } int main() { InitializeCriticalSection(&g_cs); g_hMutex = CreateMutex(NULL, FALSE, NULL); HANDLE hThread[g_ReaderNum + 1]; g_ReaderCount = 0; int i; for(i = 1; i <= 2; i++) hThread[i] = (HANDLE)_beginthreadex(NULL, 0, ReaderFun, NULL, 0, NULL); Sleep(50); hThread[0] = (HANDLE)_beginthreadex(NULL, 0, WriterFun, NULL, 0, NULL); Sleep(50); for ( ; i <= g_ReaderNum; i++) hThread[i] = (HANDLE)_beginthreadex(NULL, 0, ReaderFun, NULL, 0, NULL); WaitForMultipleObjects(g_ReaderNum + 1, hThread, TRUE, INFINITE); for(i = 0; i < g_ReaderNum + 1; i++) CloseHandle(hThread[i]); //销毁互斥量和信号量 CloseHandle(g_hMutex); DeleteCriticalSection(&g_cs); return 0; }