一、前言
在線程編程中,資源共享與保護(hù)是一個核心議題,尤其當(dāng)多個線程試圖同時訪問同一份資源時,如果不采取適當(dāng)?shù)拇胧?,就會引發(fā)一系列的問題,如數(shù)據(jù)不一致、競態(tài)條件、死鎖等。為了確保數(shù)據(jù)的一致性和線程安全,多種資源保護(hù)機(jī)制被設(shè)計出來,這些機(jī)制主要圍繞著資源的互斥訪問展開,以防止多個線程同時修改同一份數(shù)據(jù)而導(dǎo)致的錯誤。
臨界區(qū)(Critical Section)
臨界區(qū)是最基本的資源保護(hù)方式之一,它允許同一時間內(nèi)只有一個線程進(jìn)入臨界區(qū)并訪問受保護(hù)的資源。臨界區(qū)通過操作系統(tǒng)提供的原語實(shí)現(xiàn),如Windows下的EnterCriticalSection
和LeaveCriticalSection
函數(shù)。當(dāng)一個線程進(jìn)入臨界區(qū)時,其他試圖進(jìn)入同一臨界區(qū)的線程將被阻塞,直到當(dāng)前線程離開臨界區(qū)。臨界區(qū)適用于同一進(jìn)程內(nèi)的線程,因?yàn)樗鼈児蚕硐嗤牡刂房臻g,可以快速且有效地進(jìn)行同步。
互斥量(Mutex)
互斥量是一種更通用的同步機(jī)制,它不僅限于同一進(jìn)程內(nèi)的線程,還可以跨越進(jìn)程邊界?;コ饬刻峁┝吮扰R界區(qū)更強(qiáng)大的功能,如命名互斥量,這允許不同進(jìn)程中的線程可以共享同一個互斥量對象?;コ饬客ㄟ^CreateMutex
函數(shù)創(chuàng)建,并使用WaitForSingleObject
和ReleaseMutex
函數(shù)進(jìn)行鎖定和解鎖?;コ饬恐С謨?yōu)先級繼承,這有助于防止優(yōu)先級反轉(zhuǎn)問題,即高優(yōu)先級線程等待低優(yōu)先級線程釋放資源的情況。
信號量(Semaphore)
信號量用于控制對有限數(shù)量資源的訪問,例如控制并發(fā)訪問數(shù)據(jù)庫連接的數(shù)量。信號量維護(hù)一個計數(shù)器,當(dāng)計數(shù)器大于零時,線程可以獲取信號量并減少計數(shù)器的值,從而獲得訪問資源的許可。當(dāng)線程釋放信號量時,計數(shù)器增加,允許其他等待的線程獲取信號量。信號量分為二進(jìn)制信號量和計數(shù)信號量,前者只能在0和1之間切換,常用于實(shí)現(xiàn)互斥訪問;后者可以有任意非負(fù)值,用于控制資源的數(shù)量。
自旋鎖(Spin Lock)
自旋鎖是一種非阻塞的同步機(jī)制,主要用于短時間的鎖定,尤其是在高負(fù)載、高頻率的訪問場景中。當(dāng)一個線程嘗試獲取一個已被占用的自旋鎖時,它不會被阻塞,而是循環(huán)檢查鎖的狀態(tài),直到鎖被釋放。自旋鎖避免了線程上下文切換帶來的開銷,但在鎖長時間被占用的情況下,可能會消耗大量的CPU資源。
讀寫鎖(Reader-Writer Lock)
讀寫鎖允許多個讀線程同時訪問資源,但只允許一個寫線程訪問資源,這在讀操作遠(yuǎn)多于寫操作的場景中非常有效。讀寫鎖優(yōu)化了讀取性能,因?yàn)槎鄠€讀線程可以同時持有讀鎖,而寫操作則需要獨(dú)占鎖才能進(jìn)行,以防止數(shù)據(jù)的不一致性。
條件變量(Condition Variable)
條件變量通常與互斥量結(jié)合使用,用于實(shí)現(xiàn)線程間的高級同步。當(dāng)線程需要等待某個條件變?yōu)檎鏁r,它可以釋放互斥量并調(diào)用條件變量的Wait
函數(shù)。當(dāng)條件滿足時,線程可以被喚醒并重新獲取互斥量,繼續(xù)執(zhí)行。條件變量是實(shí)現(xiàn)生產(chǎn)者-消費(fèi)者模式、讀者-寫者模式等復(fù)雜同步策略的基礎(chǔ)。
在多線程編程中,正確選擇和使用這些同步機(jī)制對于保證程序的正確性和性能至關(guān)重要。開發(fā)人員必須仔細(xì)分析線程間的交互,識別出可能引起競態(tài)條件的資源,并采取適當(dāng)?shù)谋Wo(hù)措施,以確保數(shù)據(jù)的一致性和線程的安全運(yùn)行。同時,過度的同步也可能導(dǎo)致性能瓶頸,因此在設(shè)計時還需平衡同步的必要性和程序的效率。
二、實(shí)操代碼
2.1 互斥量案例-消費(fèi)者與生產(chǎn)者模型
開發(fā)環(huán)境:在Windows下安裝一個VS即可。我當(dāng)前采用的版本是VS2020。
創(chuàng)建一個基于互斥量(mutex)的火車票售賣模型,可以很好地展示消費(fèi)者與生產(chǎn)者關(guān)系中資源保護(hù)的重要性。在這個模型中,“生產(chǎn)者”可以視為負(fù)責(zé)初始化火車票數(shù)量的角色,而“消費(fèi)者”則是購買火車票的線程。為了確保在多線程環(huán)境中票數(shù)的正確性和一致性,需要使用互斥量來保護(hù)對票數(shù)的訪問和修改。
下面是一個使用C語言和Windows API實(shí)現(xiàn)的火車票售賣模型的示例代碼:
#include <windows.h>
#include <stdio.h>
#define TICKET_COUNT 10
// 定義互斥量
CRITICAL_SECTION ticketMutex;
int ticketsAvailable = TICKET_COUNT;
// 消費(fèi)者線程函數(shù)
DWORD WINAPI ConsumerThread(LPVOID lpParameter)
{
int id = (int)lpParameter;
while (ticketsAvailable > 0)
{
// 進(jìn)入臨界區(qū)
EnterCriticalSection(&ticketMutex);
if (ticketsAvailable > 0)
{
ticketsAvailable--;
printf("Consumer %d bought a ticket. Tickets left: %dn", id, ticketsAvailable);
}
// 離開臨界區(qū)
LeaveCriticalSection(&ticketMutex);
}
return 0;
}
int main()
{
HANDLE consumerThreads[TICKET_COUNT * 2]; // 假設(shè)有兩倍于票數(shù)的消費(fèi)者
DWORD threadIDs[TICKET_COUNT * 2];
// 初始化臨界區(qū)
InitializeCriticalSection(&ticketMutex);
// 創(chuàng)建消費(fèi)者線程
for (int i = 0; i < TICKET_COUNT * 2; i++)
{
consumerThreads[i] = CreateThread(
NULL, // 默認(rèn)安全屬性
0, // 使用默認(rèn)堆棧大小
ConsumerThread, // 線程函數(shù)
(LPVOID)(i + 1), // 傳遞給線程函數(shù)的參數(shù)
0, // 創(chuàng)建標(biāo)志,0表示立即啟動
&threadIDs[i]); // 返回線程ID
if (consumerThreads[i] == NULL)
{
printf("Failed to create thread %d.n", i);
return 1;
}
}
// 等待所有線程結(jié)束
for (int i = 0; i < TICKET_COUNT * 2; i++)
{
WaitForSingleObject(consumerThreads[i], INFINITE);
}
// 刪除臨界區(qū)
DeleteCriticalSection(&ticketMutex);
// 關(guān)閉所有線程句柄
for (int i = 0; i < TICKET_COUNT * 2; i++)
{
CloseHandle(consumerThreads[i]);
}
return 0;
}
在這個示例中,定義了一個CRITICAL_SECTION
類型的ticketMutex
互斥量來保護(hù)對ticketsAvailable
變量的訪問。在ConsumerThread
函數(shù)中,每個線程在嘗試購買一張票之前,都需要先通過EnterCriticalSection
函數(shù)進(jìn)入臨界區(qū),以確保在任何時刻只有一個線程可以修改票數(shù)。購買完成后,通過LeaveCriticalSection
函數(shù)離開臨界區(qū),允許其他線程有機(jī)會進(jìn)入臨界區(qū)并嘗試購票。
雖然創(chuàng)建了兩倍于票數(shù)的消費(fèi)者線程,但由于互斥量的存在,最多只會有一張票在同一時刻被售出,從而避免了資源競爭和數(shù)據(jù)不一致的問題。
此代碼演示了如何在多線程環(huán)境中使用互斥量來保護(hù)共享資源,確保數(shù)據(jù)的一致性和線程安全。在實(shí)際應(yīng)用中,互斥量是處理多線程并發(fā)訪問問題的重要工具,尤其是在涉及到資源有限且需要嚴(yán)格控制訪問順序的場景下。
2.2 使用臨界區(qū)保護(hù)共享資源
開發(fā)環(huán)境:在Windows下安裝一個VS即可。我當(dāng)前采用的版本是VS2020。
使用臨界區(qū)(Critical Section)來保護(hù)共享資源,如火車票數(shù)量,在多線程環(huán)境中確保數(shù)據(jù)一致性。
下面是一個使用C語言和Windows API實(shí)現(xiàn)的火車票售賣模型,其中包含了生產(chǎn)者初始化票數(shù)和多個消費(fèi)者線程購買票的過程。這個模型將展示如何使用臨界區(qū)來避免競態(tài)條件,確保所有線程安全地訪問和修改票數(shù)。
#include <windows.h>
#include <stdio.h>
#define TICKET_COUNT 10
// 定義臨界區(qū)
CRITICAL_SECTION ticketMutex;
int ticketsAvailable = TICKET_COUNT;
// 消費(fèi)者線程函數(shù)
DWORD WINAPI ConsumerThread(LPVOID lpParameter)
{
int id = (int)lpParameter;
while (1)
{
// 進(jìn)入臨界區(qū)
EnterCriticalSection(&ticketMutex);
// 檢查是否有票
if (ticketsAvailable > 0)
{
ticketsAvailable--;
printf("Consumer %d bought a ticket. Tickets left: %dn", id, ticketsAvailable);
}
else
{
// 如果沒有票了,退出循環(huán)
LeaveCriticalSection(&ticketMutex);
break;
}
// 離開臨界區(qū)
LeaveCriticalSection(&ticketMutex);
}
return 0;
}
int main()
{
HANDLE consumerThreads[TICKET_COUNT * 2]; // 假設(shè)有兩倍于票數(shù)的消費(fèi)者
DWORD threadIDs[TICKET_COUNT * 2];
// 初始化臨界區(qū)
InitializeCriticalSection(&ticketMutex);
// 創(chuàng)建消費(fèi)者線程
for (int i = 0; i < TICKET_COUNT * 2; i++)
{
consumerThreads[i] = CreateThread(
NULL, // 默認(rèn)安全屬性
0, // 使用默認(rèn)堆棧大小
ConsumerThread, // 線程函數(shù)
(LPVOID)(i + 1), // 傳遞給線程函數(shù)的參數(shù)
0, // 創(chuàng)建標(biāo)志,0表示立即啟動
&threadIDs[i]); // 返回線程ID
if (consumerThreads[i] == NULL)
{
printf("Failed to create thread %d.n", i);
return 1;
}
}
// 等待所有線程結(jié)束
for (int i = 0; i < TICKET_COUNT * 2; i++)
{
WaitForSingleObject(consumerThreads[i], INFINITE);
}
// 刪除臨界區(qū)
DeleteCriticalSection(&ticketMutex);
// 關(guān)閉所有線程句柄
for (int i = 0; i < TICKET_COUNT * 2; i++)
{
CloseHandle(consumerThreads[i]);
}
return 0;
}
在這個代碼示例中,使用InitializeCriticalSection
函數(shù)初始化臨界區(qū)ticketMutex
,并在每個線程的ConsumerThread
函數(shù)中使用EnterCriticalSection
和LeaveCriticalSection
函數(shù)來保護(hù)對ticketsAvailable
變量的訪問。這意味著在任何時候,只有一個線程能夠修改ticketsAvailable
的值,從而避免了多線程并發(fā)訪問時可能出現(xiàn)的數(shù)據(jù)不一致問題。
每個線程在進(jìn)入臨界區(qū)檢查是否有剩余票之前,都要調(diào)用EnterCriticalSection
,而在完成票的購買之后,調(diào)用LeaveCriticalSection
來釋放臨界區(qū),允許其他線程有機(jī)會進(jìn)入并購買票。當(dāng)票賣完后,線程會退出循環(huán)并結(jié)束。
通過這種方式,臨界區(qū)確保了即使在高并發(fā)的環(huán)境中,火車票的銷售過程也能有序進(jìn)行,每張票只被出售一次,且所有消費(fèi)者線程都能正確地跟蹤剩余票數(shù)。