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