并發(fā)編程 - 線程同步(五)之原子操作Interlocked詳解二
上一章我們學(xué)習(xí)了原子操作Interlocked類的幾個常用方法,今天我們將繼續(xù)學(xué)習(xí)該類的其他方法。

01、Exchange方法
該方法用于原子的將變量的值設(shè)置為新值,并返回變量的原始值。該方法共有14個重載方法,其中13個為常見的數(shù)據(jù)類型,有1個為泛型版本。
我們可以根據(jù)該方法可以原子更新一個變量并且可以同時獲取該變量舊值這一特性,設(shè)計一個簡單的鎖機制,大致思路如下:
1.定義一個標志位,如果該標志位舊值為0表示當(dāng)前線程獲取鎖,否則表示當(dāng)前線程無法獲取鎖;
2.當(dāng)線程獲取鎖后,可以進行業(yè)務(wù)處理,安全的處理非線程安全資源訪問的代碼;
3.當(dāng)線程處理完業(yè)務(wù)后,釋放鎖,即更新標志位值為0,當(dāng)前線程則退出鎖,其他線程可以繼續(xù)獲取鎖;
實現(xiàn)代碼如下;
//0 表示未鎖定,1 表示鎖定
private static long _exchangeValue = 0;
public static void ExchangeRun()
{
var rnd = new Random();
//啟動10個線程
var threads = new Thread[10];
for (var j = 0; j < threads.Length; j++)
{
threads[j] = new Thread(ModifyExchangeValue);
threads[j].Start();
//等待一段隨機的時間后再開始啟動另一個線程
Thread.Sleep(rnd.Next(0, 100));
}
}
static void ModifyExchangeValue()
{
//更新_exchangeValue為1,同時獲取_exchangeValue舊值
var oldExchangeValue = Interlocked.Exchange(ref _exchangeValue, 1);
//如果舊值為0,表示該邏輯未被其他線程占用
if (0 == oldExchangeValue)
{
//當(dāng)前線程開始鎖定該代碼塊,其他線程無法進入
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} 線程 進入鎖");
//模擬一些工作
//這里可以實現(xiàn)安全的處理非線程安全資源訪問的代碼
Thread.Sleep(100);
//釋放鎖
Interlocked.Exchange(ref _exchangeValue, 0);
//當(dāng)前線程釋放完鎖,其他線程可以進入該代碼塊
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} 線程 退出鎖");
}
else
{
Console.WriteLine($" {Thread.CurrentThread.ManagedThreadId} 線程 無法進入鎖");
}
}
我們可以看看執(zhí)行結(jié)果:

可以發(fā)現(xiàn)當(dāng)一個線程獲取鎖后,其他線程則無法再獲取該鎖,只有當(dāng)前線程處理完成業(yè)務(wù)退出改鎖后,其他線程才可以繼續(xù)獲取該鎖。
02、Exchange方法
該方法是Exchange方法的泛型版本,具體使用可以參考上一節(jié)。
03、CompareExchange方法
該方法用于原子的比較第一個變量和第三個變量是否相等,如果相等,則將第一個變量替換為第二個變量值,并返回第一個變量的原始值;該方法也有14個重載方法,其中13個為常見的數(shù)據(jù)類型,有1個為泛型版本。
可以理解為該方法比Exchange方法多了一個條件判斷。因此該方法可以應(yīng)用于更復(fù)雜的業(yè)務(wù)場景。
下面我們就使用該方法實現(xiàn)CAS(Compare and Swap)算法,并實現(xiàn)一個簡單的版本控制功能。
所謂版本控制就是指使用一個版本號來表示對象的狀態(tài),每次更新該對象時,我們都希望確保只有當(dāng)當(dāng)前版本號與我們預(yù)期的版本號一致時才能執(zhí)行更新操作。否則,說明在這期間有其他線程更新了該對象,我們需要放棄當(dāng)前操作或者重試。
首先我們需要構(gòu)建一個版本化數(shù)據(jù)類,該類中有兩個字段用于分別用于存儲數(shù)據(jù)和版本號;并提供兩個方法,一個方法用于獲取當(dāng)前數(shù)據(jù)和版本號,一個方法用于通過版本號更新數(shù)據(jù)。具體實現(xiàn)代碼如下:
//版本化數(shù)據(jù)
public class VersionedData<T>(T data)
{
private T _data = data;
private long _version = 0;
//獲取當(dāng)前數(shù)據(jù)和版本號
public (T Data, long Version) GetData()
{
return (_data, _version);
}
//基于版本號嘗試更新數(shù)據(jù)
public bool TryUpdateData(T data, long expectedVersion)
{
//如果_version與預(yù)期版本號相同,
//則對預(yù)期版本號加1后再替換為_version,
//同時返回_version舊值
var oldVersion = Interlocked.CompareExchange(ref _version, expectedVersion + 1, expectedVersion);
//如果_version舊值與預(yù)期版本號相同
if (oldVersion == expectedVersion)
{
//則版本號匹配,更新數(shù)據(jù)
_data = data;
return true;
}
//否則版本號不匹配,更新失敗
return false;
}
}
完成版本化類設(shè)計后,就可以使用了,我們模擬兩個線程,同時獲取當(dāng)前版本化數(shù)據(jù)和版本號,然后同時再更新數(shù)據(jù),具體代碼如下:
public static void CompareExchangeRun()
{
var versionedData = new VersionedData<string>("初始化數(shù)據(jù)");
//線程 1 嘗試更新數(shù)據(jù)
var thread1 = new Thread(ModifyCompareExchangeValue);
//線程 2 嘗試更新數(shù)據(jù)
var thread2 = new Thread(ModifyCompareExchangeValue);
thread1.Start(versionedData);
thread2.Start(versionedData);
thread1.Join();
thread2.Join();
//最終結(jié)果
var (finalData, finalVersion) = versionedData.GetData();
Console.WriteLine($"最終數(shù)據(jù)為 [{finalData}], 最終版本號為 [{finalVersion}]");
}
static void ModifyCompareExchangeValue(object param)
{
var threadId = Thread.CurrentThread.ManagedThreadId;
var versionedData = (VersionedData<string>)param;
var (data, version) = versionedData.GetData();
Console.WriteLine($"線程 {threadId} : 當(dāng)前數(shù)據(jù)為 [{data}], 當(dāng)前版本號為 [{version}]");
Console.WriteLine("---------------------------------------------------");
var newData = $"線程 {threadId} 數(shù)據(jù)";
var success = versionedData.TryUpdateData(newData, version);
Console.WriteLine($"線程 {threadId} 更新數(shù)據(jù): [{(success ? "成功" : "失敗")}]");
Console.WriteLine($" 數(shù)據(jù)預(yù)期更新為:[{newData}]");
Console.WriteLine($" 版本號預(yù)期更新為:[{version + 1}]");
Console.WriteLine("---------------------------------------------------");
}
我們來看看執(zhí)行結(jié)果:

通過結(jié)果可以發(fā)現(xiàn)只有1個線程更新成功了。
04、CompareExchange方法
該方法是CompareExchange方法的泛型版本,具體使用可以參考上一節(jié)。
05、And方法
該方法用于原子的對兩個變量進行按位與操作,將第一個變量替換為操作結(jié)果,并返回第一個變量的原始值;該方法同樣有4個重載方法,分別為long、ulong、int和uint四種數(shù)據(jù)類型;
主要還是用于多線程環(huán)境下,對共享變量進行安全的原子性按位與操作,避免并發(fā)修改時可能出現(xiàn)的數(shù)據(jù)不一致問題。
下面看一個簡單的例子:
public static void AndRun()
{
var a = 10; // 二進制: 1010
var b = 5; // 二進制: 0101
var oldA = Interlocked.And(ref a, b);
//1010 & 0101 = 0000 = 0
Console.WriteLine("操作后的值: " + a);
Console.WriteLine("返回的結(jié)果: " + oldA);
}
看看執(zhí)行結(jié)果;

可以理解為就是兩個數(shù)進行按位與運算,并且可以原子的更新原值,并返回原始值。
06、Or方法
該方法用于原子的對兩個變量進行按位或操作,將第一個變量替換為操作結(jié)果,并返回第一個變量的原始值;該方法也有4個重載方法,分別為long、ulong、int和uint四種數(shù)據(jù)類型;
具體使用可以參考And方法。
07、MemoryBarrier方法
該方法用于強制執(zhí)行內(nèi)存屏障,作用范圍當(dāng)前線程,無返回值。后面有機會我們再詳細講解。
08、MemoryBarrierProcessWide方法
該方法用于提供進程范圍的內(nèi)存屏障,確保任何 CPU 的讀取和寫入無法跨屏障移動。后面有機會我們再詳細講解。
注:測試方法代碼以及示例源碼都已經(jīng)上傳至代碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Planner

浙公網(wǎng)安備 33010602011771號