多线程同步与互斥

来源:互联网 发布:java protectedd 编辑:程序博客网 时间:2024/04/29 06:19
         多线程编程在以后的编程中无可避免的会接触到,而如何解决线程间同步与互斥的问题是多线程编程中的重点,现在总结下几种常用的解决线程同步与互斥的问题的方法,并贴上相关简单例子的源代码。解决线程同步问题在用户模式下常用的就是关键段;本文重点是用内核对象进行线程同步。
  操作系统中有一个成为P、V原语的东西,用来处理多道环境下的同步问题。这些P、V原语都是原子操作——执行的时候不会被中断。具体如何实现的话可以google一下就知道了。实际上下面所有的线程同步的方法本质上都是“原子操作”
  关键段是一小段代码,它在执行之前需要独占对一些共享资源的访问权。

  下面一个例子没有用到任何同步的方法,因此执行的时候结果是不可预料的。

const int COUNT = 1000;int gSum = 0;DWORD WINAPI FirstThread(PVOID lParam){gSum = 0;for (int n = 1; n <= COUNT; n++)gSum += n;return gSum;}DWORD WINAPI SecondThread(PVOID lParam){gSum = 0;for (int n = 1; n <= COUNT; n++)gSum += n;return gSum;}

  这两个线程都要访问共享的资源COUNT,要解决这个问题的话可以用关键段。先介绍一下几个重要的函数和变量吧。
CRITICAL_SECTION:仅仅是一个数据结构而已。

 两个重要的函数,用来操作CRITICAL_SECTION结构体变量。
InitializeCriticalSection(); //初始化关键段变量
EnterCriticalSection(); //进入关键度
LeaveCriticalSection(); //离开关键段
DeleteCriticalSection(); //删除关键度变量
用法如下:
关键就是用一个变量来保护共享资源。

  const int COUNT = 1000;int gSum = 0;CRITICAL_SECTION cs;DWORD WINAPI FirstThread(PVOID lParam){EnterCriticalSection(&cs);//........1gSum = 0;for (int n = 1; n <= COUNT; n++)gSum += n;LeaveCriticalSection(&cs);//........2return gSum;}DWORD WINAPI FirstThread(PVOID lParam){EnterCriticalSection(&cs);gSum = 0;for (int n = 1; n <= COUNT; n++)gSum += n;LeaveCriticalSection(&cs);return gSum;}

注意:EnterCriticalSection函数的内部只不过是执行了一些简单的测试,只是这些测试能够以原子方式执行。调用该函数时如果发现一个线程已经在访问资源,那么该函数就会将当前线程切换到等待状态,将不会占用CPU资源。不过需要注意的是可能该线程等待很长时间,产生“挨饿”现象,这是我们不想看到的。(实际上永远不会挨饿,因为最多会等待30天!) 如果等不及的话可以用下面这个函数代替EnterCriticalSection:
BOOL TryEnterCriticalSection()
看函数名就是到时怎么回事吧?该函数从来不会让调用线程进入等待状态。它会通过返回值来表示是否获准访问。
总结:使用关键段最大的好处就在于他们很容易使用,而且内部也使用了Interlocked函数,因此执行速度非常快。但是最大的缺点就是无法在多个进程间对线程进行同步。当然很少用到。


用内核对象进行线程同步
  线程内核对象在创建的时候总是处于未触发状态。当线程终止的时候操作系统会自动将线程对象的状态改为已触发。所谓的内核对象也就是我们所常见的进程、线程、作业、事件、信号量等等。在EOS操作系统中,这些内核对象的结构体中都包含有一个使用计数、一个等待队列。
  首先要介绍的就是WaitForSingleObject()和WaitForMultipleObjects()两个函数,这两个函数的功能都是一样的,等待一个可触发的内核对象,并将对象的状态改变过来,如将内核对象从已触发改变为未触发状态。这就是所谓的等待成功所引起的副作用。不同点就是第二个函数允许调用线程同时检查多个内核对象的触发状态。
  事件内核对象:与其他的内核对象不同的就是有一个用来表示事件是自动重置事件还是手动重置事件的bool值。 自动重置和手动重置的区别,当一个手动重置对象被触发时,正在等待该事件的所有线程都将变成可调度状态。而当一个自动重置事件被触发的时候,只有一个正在等待该事件的线程会变成可调度状态。 事件最通常的用途就是让一个线程执行初始化工作,然后当线程完成初始化工作的时候,让它执行剩余的工作。下面一个例子很典型的说明的事件内核对象的用法。


当我点击提交按钮时,在结果栏中就会显示出处理后的结果。

HANDLE g_hevtRequestSubmitted;HANDLE g_hevtResultReturned;CString m_Request;HANDLE hThreadServer;//初始化工作g_hevtRequestSubmitted = CreateEvent(NULL, FALSE, FALSE, NULL);g_hevtResultReturned = CreateEvent(NULL, FALSE, FALSE, NULL);hThreadServer = CreateThread(NULL, 0, ServerThread, NULL, 0, NULL);//处理DWORD  WINAPI ServerThread(LPVOID lParam){BOOL fShutDown = FALSE;while (!fShutDown){WaitForSingleObject(g_hevtRequestSubmitted, INFINITE);fShutDown = m_Request.IsEmpty();if (!fShutDown){ m_Request.Append(_T("_me"));}SetEvent(g_hevtResultReturned);Sleep(100);}return 0;}void CHandShakeDlg::OnBnClickedButton1(){GetDlgItemText(IDC_EDIT1,m_Request);SetFocus();SetEvent(g_hevtRequestSubmitted);WaitForSingleObject(g_hevtResultReturned, INFINITE);SetDlgItemText(IDC_EDIT2, m_Request);}

创建事件内核对象的几个重要的函数:
HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes,// pointer to security attributesBOOL bManualReset, // flag for manual-reset eventBOOL bInitialState, // flag for initial stateLPCTSTR lpName // pointer to event-object name);The SetEvent function sets the state of the specified event object to signaled.BOOL SetEvent(HANDLE hEvent);



  可等待的计时器内核对象:他们会在某个指定的时间触发,或每隔一段时间触发一次,它们通常用来在某个时间执行一些操作。直接上例子吧。

#include <Windows.h>#include <iostream>using namespace std;DWORD  WINAPI ServerThread(LPVOID lParam);HANDLEhThread;HANDLEhTime;SYSTEMTIMEst;FILETIMEftLocal, ftUTC;LARGE_INTEGERliUTC;  DWORD  WINAPI ServerThread(LPVOID lParam){BOOL fShutDown = FALSE;while (!fShutDown){WaitForSingleObject(hTime, INFINITE);cout<<"The Timer has touched!"<<endl;}return 0;}void main(){hTime = CreateWaitableTimer(NULL, FALSE, NULL);st.wYear= 2013;st.wMonth= 4;st.wDayOfWeek= 0;st.wDay= 8;st.wHour= 10;st.wMinute= 54;SystemTimeToFileTime(&st, &ftLocal);LocalFileTimeToFileTime(&ftLocal, &ftUTC);liUTC.LowPart = ftUTC.dwLowDateTime;liUTC.HighPart = ftUTC.dwHighDateTime;hThread= CreateThread(NULL, 0, ServerThread, NULL, 0, NULL);SetWaitableTimer(hTime, &liUTC, 1000*5, NULL, NULL, FALSE);Sleep(100000);}

  这段程序就是当程序到某个时刻后开始打印语句。然后每隔5s打印一次。具体见代码。 其实这个和WM_TIMER消息类似的效果,但是后者被称为“用户计时器”(SetTimer)两者最大的区别在于用户计时器需要在应用程序中使用大量的用户界面基础设施,从而消耗更多的资源。此外,可等待的计时器是内核对象,这意味着它们不仅可以在多个线程间共享,而且可以具备安全性。
  
  信号量内核对象:用来对资源进行计数。信号量可以用这个例子来理解,两个线程同时用一个缓冲区,只有当缓冲区有数据的时候,取数线程才会从缓冲区中取出数据并进行处理。这个时候就需要用到信号量了。例子源代码如下:

#include <iostream>#include <windows.h>using namespace std;int num = 0;HANDLE Sema;DWORD WINAPI GetNum(LPVOID lParam);DWORD WINAPI PushNum(LPVOID lParam);void main(){Sema = CreateSemaphore(NULL, 1, 1, NULL);HANDLE hThread1 = CreateThread(NULL, 0, GetNum, NULL, 0, NULL);HANDLE hThread2 = CreateThread(NULL, 0, PushNum, NULL, 0, NULL);CloseHandle(hThread1);CloseHandle(hThread2);Sleep(100000);}DWORD WINAPI GetNum(LPVOID lParam){while(TRUE){WaitForSingleObject(Sema, INFINITE);num = rand()% 10;ReleaseSemaphore(Sema, 1, NULL);}}DWORD WINAPI PushNum(LPVOID lParam){while(TRUE){WaitForSingleObject(Sema,INFINITE);cout<<"This time num is: "<<num<<endl;ReleaseSemaphore(Sema, 1, NULL);Sleep(400);}}

总结:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
// pointer to security attributes
LONG lInitialCount, // initial count 初始化时有当前资源数
LONG lMaximumCount, // maximum count 总的可以容纳的资源数
LPCTSTR lpName // pointer to semaphore-object name
);
在上面的程序中,初始化当前的资源数为1,即缓冲区是空的;最大的资源数也为1. 当程序运行时,两个线程会调用等待函数,这是会将当前可用资源数置为0. 而只有当当前可用资源数大于0时,信号量才会处于触发状态。 调用ReleaseSemaphore函数时才会将当前可用资源数加上指定的计数。当满足条件时线程才会继续运行。

互斥量内核对象:用来确保一个线程独占对一个资源的访问。
所用到的函数如下:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,// pointer to security attributes
BOOL bInitialOwner, // flag for initial ownership
LPCTSTR lpName // pointer to mutex-object name
);

BOOL ReleaseMutex(
HANDLE hMutex // handle to mutex object
);
  直接上源码例子,这是一个购票的程序。有两个售票窗口进行售票,总的票数有限。要保证两个人不能买同一张票。


#include <iostream>#include <Windows.h>using namespace std;int tickets = 20;HANDLE hMutex;DWORD WINAPI Thread1(LPVOID lParam);DWORD WINAPI Thread2(LPVOID lParam);DWORD WINAPI Thread1(LPVOID lParam){while (TRUE){WaitForSingleObject(hMutex, INFINITE);if (tickets <= 0) break;cout<<"Jack buy ticket : "<<tickets--<<endl;ReleaseMutex(hMutex);Sleep(100);}return 0;}DWORD WINAPI Thread2(LPVOID lParam){while (TRUE){WaitForSingleObject(hMutex, INFINITE);if (tickets <= 0) break;cout<<"Marry buy ticket : "<<tickets--<<endl;ReleaseMutex(hMutex);Sleep(100);}return 0;}void main(){hMutex = CreateMutex(NULL,FALSE, NULL);HANDLE thread1 = CreateThread(NULL, 0, Thread1, NULL, 0, NULL);HANDLE thread2 = CreateThread(NULL, 0, Thread2, NULL, 0, NULL);CloseHandle(Thread1);CloseHandle(Thread2);Sleep(10000);}

效果图:

总结:

        最后顺便提一下内核对象的“遗弃问题”,所谓的“遗弃问题”就是指如果占用信号量的线程在释放互斥量之前终止,系统就会认为互斥量被遗弃了,而由于占用互斥量的线程已经终止了,因此也无法释放它。这种情况是使用ExitThread,TerminateThread,ExitProcess 或 TerminateProcess函数时发生的。