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