目录
2.2Critical Sections(关键代码段、关键区域、临界区域)
线程同步的必要性:
我们知道操作系统的执行最小单位是线程,而一个进程包含了很多的线程,现在已经实现了
真正的并行,如双核cpu,在每个核心里开一个进程,则双核cpu就可以开两个并行运行的进程
而在每个进程类又可以开很多的线程,这里需要强调的是在两个核跑的两个进程是实实在在的并行的, 不会互相干扰,但是在每个核的进程中又运行了很多的线程,而这些线程并不是并行的,而是串行的,即操作系统会在线程中不停的切换执行,在切换执行很快时,给我们的感觉像是并行,但是实际并不是并行,这里大家需要注意,因此在同一个进程中的线程执行是串行的,这个大家需要理解。
在一个进程中,我们定义一个全局变量,此时有很多的线程都在调用或者修改这个全局变量,那么会不会存在这样的一个情况,就是其中一个进程刚修改了这个全局变量,还没来得及使用就被切换到其他线程去了,而被切换的这个线程刚好也要修改或者使用这个全局变量,那个此时会不会出现问题呢?但是肯定的,会导致无法预估的错误,这也就是线程在操作这个变量时需要遵守一定的规则的重要性的原因,而线程同步就是解决这样类似问题的方法,因此线程同步很重要。
1.出现情况的例子
//定义一个全局变量
int g_Num = 0;
UINT __cdecl ThreadProc(LPVOID pParam)
{
//线程函数的目的是先进行累加在进行自减,程序执行完应该是g_Num = 0,对吧
//但是运行情况会是什么情况呢?
for (int idx = 0;idx<100;++idx)
{
g_Num = g_Num + 1;
CString strNum;
Sleep(5);
strNum.Format(_T("%d"), g_Num);
g_Num = g_Num - 1;
}
return 0;
}
void CThreadSynDlg::OnBnClickedThreadsynBut()
{
//连续开辟了50个线程,每个线程的线程函数都是ThreadProc
for (int idx = 1; idx<=50;++idx)
{
AfxBeginThread(ThreadProc, NULL);
}
}
void CThreadSynDlg::OnBnClickedResultBut()
{
int realNum = g_Num;
//经过调试会发现,得到的值都不是0,其原因就是线程在执行过程中可能就会被暂停
//导致结果会不一样
}
2.解决同步问题的方法
2.1原子互锁家族函数
简单来说,这种保护方法是基于,一旦一个线程去修改这个全局变量就不 允许其他线程再去修改这个全局变量。主要有一下函数:
① InterlockedIncrement 加1操作
② InterlockedDecrement 减1操作
③ InterlockedExchangeAdd加上“指定”的值,可以加上一个负数;
④ InterlockedExchange、InterlockedExchangePointer能够以原子操作的方式用第二个参数的值来取代第一个参数的值;
还有很多可以参数微软的文档及MSDN。一般情况下,在多线程中如果对于某一个变量的值进行改变的话使用以上的互锁函数比较方便,但是很多时候应用场合很复杂,比如对一个结构体进行操作,对类进行操作,对链表进行插入等等,上面的方法就无法满足了,需要引入更高级的方法及:Critical Sections(关键代码段、关键区域、临界区域)
//定义一个全局变量
int g_Num = 0;
UINT __cdecl ThreadProc(LPVOID pParam)
{
//线程函数的目的是先进行累加在进行自减,程序执行完应该是g_Num = 0,对吧
//但是运行情况会是什么情况呢?
for (int idx = 0;idx<100;++idx)
{
//g_Num = g_Num + 1;
InterlockedIncrement((LONG*)&g_Num);
CString strNum;
Sleep(5);
strNum.Format(_T("%d"), g_Num);
//g_Num = g_Num - 1;
InterlockedDecrement((LONG*)&g_Num);
}
return 0;
}
void CThreadSynDlg::OnBnClickedThreadsynBut()
{
//连续开辟了50个线程,每个线程的线程函数都是ThreadProc
for (int idx = 1; idx<=50;++idx)
{
AfxBeginThread(ThreadProc, NULL);
}
}
void CThreadSynDlg::OnBnClickedResultBut()
{
int realNum = g_Num;
//经过调试会发现,得到的值都是0,其原因就是线程在执行过程中使用了原子锁进行操作,保证了安全性
}
2.2Critical Sections(关键代码段、关键区域、临界区域)
//定义一个全局对象
CStringArray g_ArrString;
UINT __cdecl ThreadProc(LPVOID pParam)
{
//每个线程函数的目的是在自身的idx的基础上进行100次循环累加,每次
//循环都会把数字转换成字符串存储在动态数组对象中g_ArrString,
//因为每个线程都会向数组中添加100个字符串,总共50个线程,如果程序正常执行那么字符串数组的元素个数应该为5000
//但是运行结果会是什么情况呢?
int startIdx = (int)pParam;
for (int idx = startIdx;idx< startIdx+100;++idx)
{
CString str;
Sleep(1);
str.Format(_T("%d"), idx);
g_ArrString.Add(str);
}
return 0;
}
void CThreadSynDlg::OnBnClickedThreadsynBut()
{
//连续开辟了50个线程,每个线程的线程函数都是ThreadProc
//传入的参数为每个线程idx乘上10
for (int idx = 1; idx<=50;++idx)
{
AfxBeginThread(ThreadProc, (LPVOID)(idx*10));
}
//结果是虽然编译通过了,但是在执行过程中出错了,出现了异常
//主要原因是多个线程同时操作动态数组,导致数组操作异常,因此需要
//线程同步才能解决,同时使用原子方法是无法解决了,因此需要引入新的
//解决方法即Critical Sections(关键代码段、关键区域、临界区域)
}
Critical Sections(关键代码段、关键区域、临界区域)使用方法
建立一个Critical Sections对象
1.初始化: InitializeCriticalSection()
2.删除:DeleteCriticalSection()
3.进入:EnterCriticalSection() (可能造成阻塞,原因是一旦有一个线程使用了这个,其他线程在使用,只能等待之前使用
这个的离开 也就是接到5的信号,那么才能开始执行,中间会出现卡顿)
4.尝试进入:TryEnterCriticalSection() (不会造成阻塞)
5.离开:LeaveCriticalSection();
//详细的请参考微软MSDN的文档,搜索同步数据结构
//定义一个全局对象
CStringArray g_ArrString;
//定义一个Critical Sections的对象
CRITICAL_SECTION g_CS;
UINT __cdecl ThreadProc(LPVOID pParam)
{
//每个线程函数的目的是在自身的idx的基础上进行100次循环累加,每次
//循环都会把数字转换成字符串存储在动态数组对象中g_ArrString,
//因为每个线程都会向数组中添加100个字符串,总共50个线程,如果程序正常执行那么字符串数组的元素个数应该为5000
//但是运行结果会是什么情况呢?
int startIdx = (int)pParam;
for (int idx = startIdx;idx< startIdx+100;++idx)
{
CString str;
//Sleep(1);
str.Format(_T("%d"), idx);
//因为修改数组的是g_ArrString.Add(str);,所以在他前面加入即可,后面离开
EnterCriticalSection(&g_CS);
g_ArrString.Add(str);
LeaveCriticalSection(&g_CS);
}
return 0;
}
void CThreadSynDlg::OnBnClickedThreadsynBut()
{
//连续开辟了50个线程,每个线程的线程函数都是ThreadProc
//传入的参数为每个线程idx乘上10
//初始化关键代码段,同时记得删除
InitializeCriticalSection(&g_CS);
for (int idx = 1; idx<=50;++idx)
{
AfxBeginThread(ThreadProc, (LPVOID)(idx*10));
}
//结果是虽然编译通过了,但是在执行过程中出错了,出现了异常
//主要原因是多个线程同时操作动态数组,导致数组操作异常,因此需要
//线程同步才能解决,同时使用原子方法是无法解决了,因此需要引入新的
//解决方法即Critical Sections(关键代码段、关键区域、临界区域)
}
//InitializeCriticalSection()
//DeleteCriticalSection()
//EnterCriticalSection()
//TryEnterCriticalSection()
//LeaveCriticalSection();
void CThreadSynDlg::OnBnClickedResultBut()
{
//这里的主要目的是为了显示计数的个数
CString strCount;
INT_PTR nCount = g_ArrString.GetCount();
strCount.Format(_T("%d"), nCount);
MessageBox(strCount);
for (INT_PTR idx=0;idx<nCount;++idx)
{
OutputDebugString(g_ArrString.GetAt(idx));
}
//这里进行删除代码段
DeleteCriticalSection(&g_CS);
}
结果是编译通过,执行正确,结果是5000
通过Critical Sections技术,可以避免多线程间的冲突问题,程序可以顺利的执行下去
下面就总结一下,该方法进行线程同步的特点
固有特点:
1.是一个用户模式的对象,不是系统的核心对象;
2.因为不是核心对象,所以执行速度快、有效率
3.因为不是核心对象,所以不能跨进程使用
4.可以多次“进入”,但必须多次“退出”
5.最好不要同时进入或等到多个Critical Sections,容易造成死锁;
什么是死锁呢?简单来说加入开启两个Critical Sections,第一线程进入第一个Critical Sections1,第二个线程进入
Critical Sections2,但是同时呢,进入的Critical Sections1的线程又要打算进入Critical Sections2,而进入Critical Sections2的线程需要 进入
Critical Sections1,结果这两个线程都在等,这样进入死循环了,各自出不来,导致死锁,只有线程死了才能解脱所以叫死锁。
所以避免死锁的最好办法就是进来少建立Critical Sections对象,这样就避免死锁了。
6.无法检测到进入到Critical Sections里面的线程当前是否已经退出,所以在Critical Sections尽量不要执行耗时的操作,,,
那么还有没有更好的方法进行线程间同步呢?当然有下面就介绍互斥器Mutex
2.3 互斥器Mutex
互斥器和2.2的关键代码段方法不同,他是系统的核心对象,所以速度上要比关键代码段方法慢点,当然他也有它的优点,使用方法方面和关键代码段方法差不多,我们来看看:
使用方法:
1.创建一个互斥器:CreateMutex;
2.打开一个已经存在的互斥器:OpenMutex;
3.获得互斥器的拥有权:WaitForSingleObject(),WaitForMultipleObjects(),,,等一类等待的函数,但是可能造成阻塞;
4.释放互斥器拥有权:ReleaseMutex;
5.关闭互斥器:CloseHandle;
具体例子如下:
创建是在 void CThreadSynDlg::OnBnClickedThreadsynBut()函数中
销毁是在 void CThreadSynDlg::OnBnClickedResultBut()函数中
处理是在线程函数中。
//定义一个全局对象
CStringArray g_ArrString;
//定义一个互斥器Mutex的句柄
HANDLE ghMutex = NULL;
UINT __cdecl ThreadProc(LPVOID pParam)
{
//每个线程函数的目的是在自身的idx的基础上进行100次循环累加,每次
//循环都会把数字转换成字符串存储在动态数组对象中g_ArrString,
//因为每个线程都会向数组中添加100个字符串,总共50个线程,如果程序正常执行那么字符串数组的元素个数应该为5000
//但是运行结果会是什么情况呢?
int startIdx = (int)pParam;
for (int idx = startIdx;idx< startIdx+100;++idx)
{
CString str;
//Sleep(1);
str.Format(_T("%d"), idx);
//定义一个单一的等待函数,第一次参数是互斥器的对象,第二个是等待时间,这里是一直等待
//因为现在的互斥器是激活态,这线程一旦调用立刻返回,此时互斥器进入非激活态,如果这时候
//有其他线程也是用这个WaitForSingleObject函数不会立刻返回,只能等待互斥器的在此激活
DWORD dwWaitResult = WaitForSingleObject(ghMutex, INFINITE);
switch (dwWaitResult)
{
case WAIT_ABANDONED://这个其实就是没用获得互斥器的激活态,或者其他的线程正在使用
case WAIT_OBJECT_0://如果返回的是该值,则说明当前的互斥器已经被当前线程拥有了,可以进场操作了
g_ArrString.Add(str);
ReleaseMutex(ghMutex);//如果处理完,就释放一下,使互斥器激活态,以便其他线程可以使用
break;
}
}
return 0;
}
void CThreadSynDlg::OnBnClickedThreadsynBut()
{
//连续开辟了50个线程,每个线程的线程函数都是ThreadProc
//传入的参数为每个线程idx乘上10
//1.创建一个互斥器:CreateMutex;同时记得释放句柄
//一旦创建成功,则处于激活态,也就是现在没有任何一个线程在调用它,可以谁是等待线程的调用
//一旦线程线程调用则该互斥器立刻处于非激活态,其他线程在调用只有等待该互斥器的在此激活
ghMutex = CreateMutex(NULL, FALSE, NULL);
for (int idx = 1; idx<=50;++idx)
{
AfxBeginThread(ThreadProc, (LPVOID)(idx*10));
}
}
//InitializeCriticalSection()
//DeleteCriticalSection()
//EnterCriticalSection()
//TryEnterCriticalSection()
//LeaveCriticalSection();
void CThreadSynDlg::OnBnClickedResultBut()
{
//这里的主要目的是为了显示计数的个数
CString strCount;
INT_PTR nCount = g_ArrString.GetCount();
strCount.Format(_T("%d"), nCount);
MessageBox(strCount);
for (INT_PTR idx=0;idx<nCount;++idx)
{
OutputDebugString(g_ArrString.GetAt(idx));
}
//关闭互斥器
CloseHandle(ghMutex);
}
总结:
命名标准:Mutex可以跨进程使用,所以其名称对整个系统而言是全局的,所以命名不能过于普通,类似Mutex、object等
最后独一无二且易识别
互斥器的特点:
1.是一个系统核心对象,所以有安全描述指针,用完了要CloseHandle关闭句柄,这些是内核对象的共同特征;
2.因为是核心对象,所以执行速度会比Critical Sections慢几乎100倍的时间
3.因为是核心对象,而且可以命名,所以可以跨进程使用;
4.Mutex使用正确的情况下不会发生死锁;
5.在“等待”一个Mutex的时候,可以指定“结束等待”的时间长度;
6.可以检测到当前拥有互斥器所有权的线程是否已经退出!wait。。。。函数会返回:WAIT_ABANDONED
2.4 Semaphores(信号量)
和前几个的处理方法不同,我们从前面的方法中可以看到,其实本质上都同一个时间只有一个线程处理一个对象,
而信号量和他们的区别在与信号量的是同一个时间多个线程同时处理多个对象,这里大家需要理解,前面的都是
一对一,而信号量是多对多。
使用方法:
1.创建一个信号量:CreateSemaphore();
2.打开一个已经存在的信号量:OpenSemaphore();
3.获得信号量的一个占有权:WaitForSingleObject()、 WaitForMultipleObjects()等一类等待函数。。。。。可能造成阻塞
4.释放信号量的占有权:ReleaseSemaphore()
5.关闭信号量:CloseHandle()
HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName );
- lpSemaphoreAttributes
[输入]设置为NULL。 - lInitialCount
[in]指定信号量对象的初始计数。此值必须大于或等于零且小于或等于lMaximumCount。当信号量的计数大于零时,将发出信号状态;在信号量为零时,将不发出信号状态。每当等待函数释放等待信号量的线程时,计数就会减少一。通过调用ReleaseSemaphore函数将计数增加指定的数量。 - lMaximumCount
[in]指定信号量对象的最大计数。该值必须大于零。 - lpName
[in]指向以空值结尾的字符串的长指针,该字符串指定信号量对象的名称。名称限制为MAX_PATH字符,并且可以包含除反斜杠路径分隔符(\)之外的任何字符。名称比较区分大小写。
如果lpName与现有命名信号对象的名称匹配,则lInitialCount和lMaximumCount参数将被忽略,因为它们已在创建过程中设置。
每种对象类型(例如内存映射,信号量,事件,消息队列,互斥体和看门狗计时器)都有其自己单独的名称空间。空字符串(“”)被视为命名对象。在基于Windows桌面的平台上,同步对象都共享相同的名称空间。
来自 <https://docs.microsoft.com/en-us/previous-versions/aa911525(v=msdn.10)>
//定义一个全局对象
CStringArray g_ArrString;
//定义一个信号量的全局句柄
HANDLE ghSemaphore = NULL;
UINT __cdecl ThreadProc(LPVOID pParam)
{
//每个线程函数的目的是在自身的idx的基础上进行100次循环累加,每次
//循环都会把数字转换成字符串存储在动态数组对象中g_ArrString,
//因为每个线程都会向数组中添加100个字符串,总共50个线程,如果程序正常执行那么字符串数组的元素个数应该为5000
//但是运行结果会是什么情况呢?
int startIdx = (int)pParam;
CString strOut;
while (TRUE)
{
//因为现在的互斥器是激活态,这线程一旦调用立刻返回,此时互斥器进入非激活态,如果这时候
//有其他线程也是用这个WaitForSingleObject函数会立刻返回,因为这是多个线程可以同时进行操作
//这里不同的是不需要等待,因为信号量可以多个线程同时操作他
DWORD dwWaitResult = WaitForSingleObject(ghSemaphore, 0);
switch (dwWaitResult)
{
case WAIT_OBJECT_0://如果返回的是该值,则说明当前信号量已经被当前线程拥有了,可以进场操作了
strOut.Format(_T("Thred %d: wait succeeded!"), GetCurrentThreadId());
OutputDebugString(strOut);
/*
可以加入其它要干的活
*/
ReleaseSemaphore(ghSemaphore,1,NULL);//如果处理完,就释放一下
break;
case WAIT_TIMEOUT:
strOut.Format(_T("Thread %d: wait timed out!"), GetCurrentThreadId());
OutputDebugString(strOut);
break;
}
}
return 0;
}
//CreateSemaphore
//OpenSemaphore
//WaitForSingleObject() WaitForMultipleObjects()
//ReleaseSemaphore()
void CThreadSynDlg::OnBnClickedThreadsynBut()
{
//连续开辟了50个线程,每个线程的线程函数都是ThreadProc
//传入的参数为每个线程idx乘上10
//1.创建信号量ghSemaphore = CreateSemaphore(NULL, 10, 10, NULL);
//其中在创建的过程中我们需要告诉他多少个线程访问多少个对象
//第三个参数是有多少个对象可以执行
//第二个参数是初始化时有多少个对象可以执行,一般要小于等于第三个参数的值且大于等于0
ghSemaphore = CreateSemaphore(NULL, 10, 10, NULL);
for (int idx = 1; idx<=20;++idx)
{
AfxBeginThread(ThreadProc, (LPVOID)(idx*10));
}
}
//InitializeCriticalSection()
//DeleteCriticalSection()
//EnterCriticalSection()
//TryEnterCriticalSection()
//LeaveCriticalSection();
void CThreadSynDlg::OnBnClickedResultBut()
{
//这里的主要目的是为了显示计数的个数
CString strCount;
INT_PTR nCount = g_ArrString.GetCount();
strCount.Format(_T("%d"), nCount);
MessageBox(strCount);
for (INT_PTR idx=0;idx<nCount;++idx)
{
OutputDebugString(g_ArrString.GetAt(idx));
}
//关闭信号量
CloseHandle(ghSemaphore);
}
总结:
命名标准:Semaphore可以跨进程使用,所以其名称对整个系统而言是全局的,所以命名不能过于普通,类似Mutex、object等
最后独一无二且易识别
互斥器的特点:
1.是一个系统核心对象,所以有安全描述指针,用完了要CloseHandle关闭句柄,这些是内核对象的共同特征;
2.因为是核心对象,所以执行速度会比Critical Sections慢几乎100倍的时间(相对而言,现在的cpu很快了,几乎没差别了)
3.因为是核心对象,而且可以命名,所以可以跨进程使用;
4.Semaphore使用正确的情况下不会发生死锁;
5.在“等待”一个Semaphore的时候,可以指定“结束等待”的时间长度;
6.非排他性的占有,跟Critical Sections和Mutex不同,这两种而言是排他性的占有,即同一时间内只能有单一的线程获得目标并拥有操作的权利,而Semaphores则不是这样的,同一时间可以有多个线程获得目标并进行操作
如果上面程序使用信号量方式去做向CStringArray中添加节点的同步可以吗:
答案是可以的,此时只需要修改:
CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName );
的第三个参数为1即可,也就是说每次只允许一个线程对象对其操作,这样设置就和前面的互斥锁没什么两样了。
2.5 Event Objects(事件)
Event 方式是最具有弹性的同步机制,因为他的状态完全由你决定,不会像Mutex和Semaphores的状态一样会根据
waitforsingleObject----等类似的调用而改变,所以你需要精确的告诉Event对象该做什么事,以及什么时候去做.
使用方法:
1.创建一个事件对象:CreateEvent;
2.打开一个已经存在的事件对象:OpenEvent
3.获得事件的占有权:waitforsingleObject等函数,可能会阻塞
4.释放事件的占有权(设置为激发状态,以让其他等待的线程苏醒:setEvent
5.手动设置为非激发态,ResetEvent
6.关闭事件对象的句柄:closehandle
特点:
1.是一个系统核心对象,所以有安全描述指针,用完了要CloseHandle关闭句柄,这些是内核对象的共同特征;
2.因为是核心对象,所以执行速度会比Critical Sections慢几乎100倍的时间(相对而言,现在的cpu很快了,几乎没差别了)
3.因为是核心对象,而且可以命名,所以可以跨进程使用;
4.通常被用于overlapped I/O或者被用来设计某些自定义的同步对象。
https://docs.microsoft.com/zh-cn/windows/win32/sync/using-event-objects
#include <windows.h>
#include <stdio.h>
#define THREADCOUNT 4
HANDLE ghWriteEvent;
HANDLE ghThreads[THREADCOUNT];
DWORD WINAPI ThreadProc(LPVOID);
void CreateEventsAndThreads(void)
{
int i;
DWORD dwThreadID;
// Create a manual-reset event object. The write thread sets this
// object to the signaled state when it finishes writing to a
// shared buffer.
ghWriteEvent = CreateEvent(
NULL, // default security attributes
TRUE, // manual-reset event
FALSE, // initial state is nonsignaled
TEXT("WriteEvent") // object name
);
if (ghWriteEvent == NULL)
{
printf("CreateEvent failed (%d)\n", GetLastError());
return;
}
// Create multiple threads to read from the buffer.
for(i = 0; i < THREADCOUNT; i++)
{
// TODO: More complex scenarios may require use of a parameter
// to the thread procedure, such as an event per thread to
// be used for synchronization.
ghThreads[i] = CreateThread(
NULL, // default security
0, // default stack size
ThreadProc, // name of the thread function
NULL, // no thread parameters
0, // default startup flags
&dwThreadID);
if (ghThreads[i] == NULL)
{
printf("CreateThread failed (%d)\n", GetLastError());
return;
}
}
}
void WriteToBuffer(VOID)
{
// TODO: Write to the shared buffer.
printf("Main thread writing to the shared buffer...\n");
// Set ghWriteEvent to signaled
if (! SetEvent(ghWriteEvent) )
{
printf("SetEvent failed (%d)\n", GetLastError());
return;
}
}
void CloseEvents()
{
// Close all event handles (currently, only one global handle).
CloseHandle(ghWriteEvent);
}
int main( void )
{
DWORD dwWaitResult;
// TODO: Create the shared buffer
// Create events and THREADCOUNT threads to read from the buffer
CreateEventsAndThreads();
// At this point, the reader threads have started and are most
// likely waiting for the global event to be signaled. However,
// it is safe to write to the buffer because the event is a
// manual-reset event.
WriteToBuffer();
printf("Main thread waiting for threads to exit...\n");
// The handle for each thread is signaled when the thread is
// terminated.
dwWaitResult = WaitForMultipleObjects(
THREADCOUNT, // number of handles in array
ghThreads, // array of thread handles
TRUE, // wait until all are signaled
INFINITE);
switch (dwWaitResult)
{
// All thread objects were signaled
case WAIT_OBJECT_0:
printf("All threads ended, cleaning up for application exit...\n");
break;
// An error occurred
default:
printf("WaitForMultipleObjects failed (%d)\n", GetLastError());
return 1;
}
// Close the events to clean up
CloseEvents();
return 0;
}
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
// lpParam not used in this example.
UNREFERENCED_PARAMETER(lpParam);
DWORD dwWaitResult;
printf("Thread %d waiting for write event...\n", GetCurrentThreadId());
dwWaitResult = WaitForSingleObject(
ghWriteEvent, // event handle
INFINITE); // indefinite wait
switch (dwWaitResult)
{
// Event object was signaled
case WAIT_OBJECT_0:
//
// TODO: Read from the shared buffer
//
printf("Thread %d reading from buffer\n",
GetCurrentThreadId());
break;
// An error occurred
default:
printf("Wait error (%d)\n", GetLastError());
return 0;
}
// Now that we are done reading the buffer, we could use another
// event to signal that this thread is no longer reading. This
// example simply uses the thread handle for synchronization (the
// handle is signaled when the thread terminates.)
printf("Thread %d exiting\n", GetCurrentThreadId());
return 1;
}