日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

深入浅出Win32多线程程序设计之线程通信

發(fā)布時間:2023/12/19 编程问答 52 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入浅出Win32多线程程序设计之线程通信 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

簡介

  線程之間通信的兩個基本問題是互斥和同步。

  線程同步是指線程之間所具有的一種制約關(guān)系,一個線程的執(zhí)行依賴另一個線程的消息,當(dāng)它沒有得到另一個線程的消息時應(yīng)等待,直到消息到達時才被喚醒。

  線程互斥是指對于共享的操作系統(tǒng)資源(指的是廣義的"資源",而不是Windows的.res文件,譬如全局變量就是一種共享資源),在各線程訪問時的排它性。當(dāng)有若干個線程都要使用某一共享資源時,任何時刻最多只允許一個線程去使用,其它要使用該資源的線程必須等待,直到占用資源者釋放該資源。

  線程互斥是一種特殊的線程同步。

  實際上,互斥和同步對應(yīng)著線程間通信發(fā)生的兩種情況:

  (1)當(dāng)有多個線程訪問共享資源而不使資源被破壞時;

  (2)當(dāng)一個線程需要將某個任務(wù)已經(jīng)完成的情況通知另外一個或多個線程時。

  在WIN32中,同步機制主要有以下幾種:

  (1)事件(Event);

  (2)信號量(semaphore);

  (3)互斥量(mutex);

  (4)臨界區(qū)(Critical section)。

  全局變量

  因為進程中的所有線程均可以訪問所有的全局變量,因而全局變量成為Win32多線程通信的最簡單方式。例如:

int var; //全局變量
UINT ThreadFunction(LPVOIDpParam)
{
 var = 0;
 while (var < MaxValue)
 {
  //線程處理
  ::InterlockedIncrement(long*) &var);
 }
 return 0;
}
請看下列程序:
int globalFlag = false;
DWORD WINAPI ThreadFunc(LPVOID n)
{
 Sleep(2000);
 globalFlag = true;

 return 0;
}

int main()
{
 HANDLE hThrd;
 DWORD threadId;

 hThrd = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &threadId);
 if (hThrd)
 {
  printf("Thread launched\n");
  CloseHandle(hThrd);
 }

 while (!globalFlag)
 ;
 printf("exit\n");
}


  上述程序中使用全局變量和while循環(huán)查詢進行線程間同步,實際上,這是一種應(yīng)該避免的方法,因為:

  (1)當(dāng)主線程必須使自己與ThreadFunc函數(shù)的完成運行實現(xiàn)同步時,它并沒有使自己進入睡眠狀態(tài)。由于主線程沒有進入睡眠狀態(tài),因此操作系統(tǒng)繼續(xù)為它調(diào)度C P U時間,這就要占用其他線程的寶貴時間周期;

  (2)當(dāng)主線程的優(yōu)先級高于執(zhí)行ThreadFunc函數(shù)的線程時,就會發(fā)生globalFlag永遠不能被賦值為true的情況。因為在這種情況下,系統(tǒng)決不會將任何時間片分配給ThreadFunc線程。

  事件

  事件(Event)是WIN32提供的最靈活的線程間同步方式,事件可以處于激發(fā)狀態(tài)(signaled or true)或未激發(fā)狀態(tài)(unsignal or false)。根據(jù)狀態(tài)變遷方式的不同,事件可分為兩類:

  (1)手動設(shè)置:這種對象只可能用程序手動設(shè)置,在需要該事件或者事件發(fā)生時,采用SetEvent及ResetEvent來進行設(shè)置。

  (2)自動恢復(fù):一旦事件發(fā)生并被處理后,自動恢復(fù)到?jīng)]有事件狀態(tài),不需要再次設(shè)置。

  創(chuàng)建事件的函數(shù)原型為:

HANDLE CreateEvent(
 LPSECURITY_ATTRIBUTES lpEventAttributes,
 // SECURITY_ATTRIBUTES結(jié)構(gòu)指針,可為NULL
 BOOL bManualReset,
 // 手動/自動
 // TRUE:在WaitForSingleObject后必須手動調(diào)用ResetEvent清除信號
 // FALSE:在WaitForSingleObject后,系統(tǒng)自動清除事件信號
 BOOL bInitialState, //初始狀態(tài)
 LPCTSTR lpName //事件的名稱
);


  使用"事件"機制應(yīng)注意以下事項:

  (1)如果跨進程訪問事件,必須對事件命名,在對事件命名的時候,要注意不要與系統(tǒng)命名空間中的其它全局命名對象沖突;

  (2)事件是否要自動恢復(fù);

  (3)事件的初始狀態(tài)設(shè)置。

  由于event對象屬于內(nèi)核對象,故進程B可以調(diào)用OpenEvent函數(shù)通過對象的名字獲得進程A中event對象的句柄,然后將這個句柄用于ResetEvent、SetEvent和WaitForMultipleObjects等函數(shù)中。此法可以實現(xiàn)一個進程的線程控制另一進程中線程的運行,例如:

HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent");
ResetEvent(hEvent);


臨界區(qū)

  定義臨界區(qū)變量

CRITICAL_SECTION gCriticalSection;


  通常情況下,CRITICAL_SECTION結(jié)構(gòu)體應(yīng)該被定義為全局變量,以便于進程中的所有線程方便地按照變量名來引用該結(jié)構(gòu)體。

  初始化臨界區(qū)

VOID WINAPI InitializeCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向程序員定義的CRITICAL_SECTION變量
);


  該函數(shù)用于對pcs所指的CRITICAL_SECTION結(jié)構(gòu)體進行初始化。該函數(shù)只是設(shè)置了一些成員變量,它的運行一般不會失敗,因此它采用了VOID類型的返回值。該函數(shù)必須在任何線程調(diào)用EnterCriticalSection函數(shù)之前被調(diào)用,如果一個線程試圖進入一個未初始化的CRTICAL_SECTION,那么結(jié)果將是很難預(yù)計的。

  刪除臨界區(qū)

VOID WINAPI DeleteCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向一個不再需要的CRITICAL_SECTION變量
);


  進入臨界區(qū)

VOID WINAPI EnterCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向一個你即將鎖定的CRITICAL_SECTION變量
);


  離開臨界區(qū)

VOID WINAPI LeaveCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向一個你即將離開的CRITICAL_SECTION變量
);


  使用臨界區(qū)編程的一般方法是:

void UpdateData()
{
 EnterCriticalSection(&gCriticalSection);
 ...//do something
 LeaveCriticalSection(&gCriticalSection);
}


  關(guān)于臨界區(qū)的使用,有下列注意點:

  (1)每個共享資源使用一個CRITICAL_SECTION變量;

  (2)不要長時間運行關(guān)鍵代碼段,當(dāng)一個關(guān)鍵代碼段長時間運行時,其他線程就會進入等待狀態(tài),這會降低應(yīng)用程序的運行性能;

  (3)如果需要同時訪問多個資源,則可能連續(xù)調(diào)用EnterCriticalSection;

  (4)Critical Section不是OS核心對象,如果進入臨界區(qū)的線程"掛"了,將無法釋放臨界資源。這個缺點在Mutex中得到了彌補。

  互斥

  互斥量的作用是保證每次只能有一個線程獲得互斥量而得以繼續(xù)執(zhí)行,使用CreateMutex函數(shù)創(chuàng)建:

HANDLE CreateMutex(
 LPSECURITY_ATTRIBUTES lpMutexAttributes,
 // 安全屬性結(jié)構(gòu)指針,可為NULL
 BOOL bInitialOwner,
 //是否占有該互斥量,TRUE:占有,FALSE:不占有
 LPCTSTR lpName
 //信號量的名稱
);


  Mutex是核心對象,可以跨進程訪問,下面的代碼給出了從另一進程訪問命名Mutex的例子:

HANDLE hMutex;
hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, L"mutexName");
if (hMutex){
 …

else{
 …
}


  相關(guān)API:

BOOL WINAPI ReleaseMutex(
 HANDLE hMutex
);


  使用互斥編程的一般方法是:

void UpdateResource()
{
 WaitForSingleObject(hMutex,…);
 ...//do something
 ReleaseMutex(hMutex);
}


  互斥(mutex)內(nèi)核對象能夠確保線程擁有對單個資源的互斥訪問權(quán)。互斥對象的行為特性與臨界區(qū)相同,但是互斥對象屬于內(nèi)核對象,而臨界區(qū)則屬于用戶方式對象,因此這導(dǎo)致mutex與Critical Section的如下不同:

  (1) 互斥對象的運行速度比關(guān)鍵代碼段要慢;

  (2) 不同進程中的多個線程能夠訪問單個互斥對象;

  (3) 線程在等待訪問資源時可以設(shè)定一個超時值。

  下圖更詳細地列出了互斥與臨界區(qū)的不同:


信號量

  信號量是維護0到指定最大值之間的同步對象。信號量狀態(tài)在其計數(shù)大于0時是有信號的,而其計數(shù)是0時是無信號的。信號量對象在控制上可以支持有限數(shù)量共享資源的訪問。

  信號量的特點和用途可用下列幾句話定義:

  (1)如果當(dāng)前資源的數(shù)量大于0,則信號量有效;

  (2)如果當(dāng)前資源數(shù)量是0,則信號量無效;

  (3)系統(tǒng)決不允許當(dāng)前資源的數(shù)量為負值;

  (4)當(dāng)前資源數(shù)量決不能大于最大資源數(shù)量。

  創(chuàng)建信號量

HANDLE CreateSemaphore (
 PSECURITY_ATTRIBUTE psa,
 LONG lInitialCount, //開始時可供使用的資源數(shù)
 LONG lMaximumCount, //最大資源數(shù)
PCTSTR pszName);


  釋放信號量

  通過調(diào)用ReleaseSemaphore函數(shù),線程就能夠?qū)π艠?biāo)的當(dāng)前資源數(shù)量進行遞增,該函數(shù)原型為:

BOOL WINAPI ReleaseSemaphore(
 HANDLE hSemaphore,
 LONG lReleaseCount, //信號量的當(dāng)前資源數(shù)增加lReleaseCount
 LPLONG lpPreviousCount
);


  打開信號量

  和其他核心對象一樣,信號量也可以通過名字跨進程訪問,打開信號量的API為:

HANDLE OpenSemaphore (
 DWORD fdwAccess,
 BOOL bInherithandle,
 PCTSTR pszName
);


  互鎖訪問

  當(dāng)必須以原子操作方式來修改單個值時,互鎖訪問函數(shù)是相當(dāng)有用的。所謂原子訪問,是指線程在訪問資源時能夠確保所有其他線程都不在同一時間內(nèi)訪問相同的資源。

  請看下列代碼:

int globalVar = 0;

DWORD WINAPI ThreadFunc1(LPVOID n)
{
 globalVar++;
 return 0;
}
DWORD WINAPI ThreadFunc2(LPVOID n)
{
 globalVar++;
 return 0;
}


  運行ThreadFunc1和ThreadFunc2線程,結(jié)果是不可預(yù)料的,因為globalVar++并不對應(yīng)著一條機器指令,我們看看globalVar++的反匯編代碼:

00401038 mov eax,[globalVar (0042d3f0)]
0040103D add eax,1
00401040 mov [globalVar (0042d3f0)],eax


  在"mov eax,[globalVar (0042d3f0)]" 指令與"add eax,1" 指令以及"add eax,1" 指令與"mov [globalVar (0042d3f0)],eax"指令之間都可能發(fā)生線程切換,使得程序的執(zhí)行后globalVar的結(jié)果不能確定。我們可以使用InterlockedExchangeAdd函數(shù)解決這個問題:

int globalVar = 0;

DWORD WINAPI ThreadFunc1(LPVOID n)
{
 InterlockedExchangeAdd(&globalVar,1);
 return 0;
}
DWORD WINAPI ThreadFunc2(LPVOID n)
{
 InterlockedExchangeAdd(&globalVar,1);
 return 0;
}


  InterlockedExchangeAdd保證對變量globalVar的訪問具有"原子性"。互鎖訪問的控制速度非常快,調(diào)用一個互鎖函數(shù)的CPU周期通常小于50,不需要進行用戶方式與內(nèi)核方式的切換(該切換通常需要運行1000個CPU周期)。

  互鎖訪問函數(shù)的缺點在于其只能對單一變量進行原子訪問,如果要訪問的資源比較復(fù)雜,仍要使用臨界區(qū)或互斥。

  可等待定時器

  可等待定時器是在某個時間或按規(guī)定的間隔時間發(fā)出自己的信號通知的內(nèi)核對象。它們通常用來在某個時間執(zhí)行某個操作。

  創(chuàng)建可等待定時器

HANDLE CreateWaitableTimer(
 PSECURITY_ATTRISUTES psa,
 BOOL fManualReset,//人工重置或自動重置定時器
PCTSTR pszName);


  設(shè)置可等待定時器

  可等待定時器對象在非激活狀態(tài)下被創(chuàng)建,程序員應(yīng)調(diào)用 SetWaitableTimer函數(shù)來界定定時器在何時被激活:

BOOL SetWaitableTimer(
 HANDLE hTimer, //要設(shè)置的定時器
 const LARGE_INTEGER *pDueTime, //指明定時器第一次激活的時間
 LONG lPeriod, //指明此后定時器應(yīng)該間隔多長時間激活一次
 PTIMERAPCROUTINE pfnCompletionRoutine,
 PVOID PvArgToCompletionRoutine,
BOOL fResume);


  取消可等待定時器

BOOl Cancel WaitableTimer(
 HANDLE hTimer //要取消的定時器
);


  打開可等待定時器

  作為一種內(nèi)核對象,WaitableTimer也可以被其他進程以名字打開:

HANDLE OpenWaitableTimer (
 DWORD fdwAccess,
 BOOL bInherithandle,
 PCTSTR pszName
);


  實例

  下面給出的一個程序可能發(fā)生死鎖現(xiàn)象:

#include <windows.h>
#include <stdio.h>
CRITICAL_SECTION cs1, cs2;
long WINAPI ThreadFn(long);
main()
{
 long iThreadID;
 InitializeCriticalSection(&cs1);
 InitializeCriticalSection(&cs2);
 CloseHandle(CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadFn, NULL, 0,&iThreadID));
 while (TRUE)
 {
  EnterCriticalSection(&cs1);
  printf("\n線程1占用臨界區(qū)1");
  EnterCriticalSection(&cs2);
  printf("\n線程1占用臨界區(qū)2");

  printf("\n線程1占用兩個臨界區(qū)");

  LeaveCriticalSection(&cs2);
  LeaveCriticalSection(&cs1);

  printf("\n線程1釋放兩個臨界區(qū)");
  Sleep(20);
 };
 return (0);
}

long WINAPI ThreadFn(long lParam)
{
 while (TRUE)
 {
  EnterCriticalSection(&cs2);
  printf("\n線程2占用臨界區(qū)2");
  EnterCriticalSection(&cs1);
  printf("\n線程2占用臨界區(qū)1");

  printf("\n線程2占用兩個臨界區(qū)");

  LeaveCriticalSection(&cs1);
  LeaveCriticalSection(&cs2);

  printf("\n線程2釋放兩個臨界區(qū)");
  Sleep(20);
 };
}


  運行這個程序,在中途一旦發(fā)生這樣的輸出:

  線程1占用臨界區(qū)1

  線程2占用臨界區(qū)2

  或

  線程2占用臨界區(qū)2

  線程1占用臨界區(qū)1

  或

  線程1占用臨界區(qū)2

  線程2占用臨界區(qū)1

  或

  線程2占用臨界區(qū)1

  線程1占用臨界區(qū)2

  程序就"死"掉了,再也運行不下去。因為這樣的輸出,意味著兩個線程相互等待對方釋放臨界區(qū),也即出現(xiàn)了死鎖。

  如果我們將線程2的控制函數(shù)改為:

long WINAPI ThreadFn(long lParam)
{
 while (TRUE)
 {
  EnterCriticalSection(&cs1);
  printf("\n線程2占用臨界區(qū)1");
  EnterCriticalSection(&cs2);
  printf("\n線程2占用臨界區(qū)2");

  printf("\n線程2占用兩個臨界區(qū)");

  LeaveCriticalSection(&cs1);
  LeaveCriticalSection(&cs2);

  printf("\n線程2釋放兩個臨界區(qū)");
  Sleep(20);
 };
}


  再次運行程序,死鎖被消除,程序不再擋掉。這是因為我們改變了線程2中獲得臨界區(qū)1、2的順序,消除了線程1、2相互等待資源的可能性。

  由此我們得出結(jié)論,在使用線程間的同步機制時,要特別留心死鎖的發(fā)生。

總結(jié)

以上是生活随笔為你收集整理的深入浅出Win32多线程程序设计之线程通信的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。