并發(fā)編程 - 線程同步(四)之原子操作Interlocked詳解一
上一章我們了解了原子操作Interlocked類的設(shè)計(jì)原理及簡(jiǎn)單介紹,今天我們將對(duì)Interlocked的使用進(jìn)行詳細(xì)講解。

在此之前我們先學(xué)習(xí)一個(gè)概念——原子操作。
01、Read方法
該方法用于原子的讀取64位值,有分別針對(duì)long類型和ulong類型的兩個(gè)重載方法;
對(duì)于64位系統(tǒng),64位數(shù)據(jù)類型的讀取本身就是原子操作;而對(duì)于32位系統(tǒng),64位數(shù)據(jù)類型的讀取需要至少兩個(gè)原子指令,因此在32位系統(tǒng)可以通過Read方法對(duì)64位數(shù)據(jù)類型進(jìn)行原子讀取。
用法也很簡(jiǎn)單,示例如下:
private static long _readValue = 0;
public static void ReadRun()
{
var thread = new Thread(ModifyReadValue);
thread.Start();
Thread.Sleep(100);
var value = Interlocked.Read(ref _readValue);
Console.WriteLine("原子讀取long類型變量: " + value);
}
static void ModifyReadValue()
{
_readValue = 88;
Console.WriteLine("修改long類型變量: " + _readValue);
}
運(yùn)行結(jié)果如下:

因?yàn)橄到y(tǒng)環(huán)境原因無法模擬出32位系統(tǒng)效果,因此這里只是給了個(gè)簡(jiǎn)單使用示例。
02、Increment方法
該方法用于原子的遞增指定的變量,并返回遞增后的新值。該方法有4個(gè)重載方法,分別為long、ulong、int和uint四種數(shù)據(jù)類型;該方法適用于多線程環(huán)境中需要安全遞增變量的場(chǎng)景,如計(jì)數(shù)器、資源管理等。
對(duì)于加法操作,無論是i+1,還是i++或++i,都不是線程安全的,最終可能會(huì)生成3條CPU指令,整個(gè)操作過程大致如下:
1.將 i 的值加載到寄存器,即從內(nèi)存中讀取i;
2.將寄存器中值加1,即i值加1;
3.最后將寄存器中值回寫到i,即完成i值的變更;
而在這編碼層面為1行代碼,而CPU層面為3行指令的操作中,隨時(shí)都有可能被線程調(diào)度器打斷,而導(dǎo)致其他線程同時(shí)對(duì)i進(jìn)行操作,最終導(dǎo)致競(jìng)爭(zhēng)條件,最后數(shù)據(jù)錯(cuò)亂。
下面我們來舉個(gè)例子,啟動(dòng)100個(gè)線程,分別對(duì)一個(gè)共享變量進(jìn)行1000次遞增1,最后打印出共享變量,運(yùn)行這個(gè)示例9次觀察每次運(yùn)行結(jié)果,代碼如下:
private static long _incrementValue = 0;
public static void IncrementRun()
{
//運(yùn)行9次測(cè)試,觀察每次結(jié)果
for (var i = 1; i < 10; i++)
{
//啟動(dòng)100個(gè)線程,對(duì)變量進(jìn)行遞增
var threads = new Thread[100];
for (var j = 0; j < threads.Length; j++)
{
threads[j] = new Thread(ModifyIncrementValue);
threads[j].Start();
}
//等待所有線程執(zhí)行完成
foreach (var thread in threads)
{
thread.Join();
}
//最后打印結(jié)果
Console.WriteLine($"第 {i} 運(yùn)行結(jié)果: {_incrementValue}");
_incrementValue = 0;
}
}
static void ModifyIncrementValue()
{
for (var i = 0; i < 1000; i++)
{
++_incrementValue;
}
}
先看下執(zhí)行結(jié)果:

可以發(fā)現(xiàn)每次的運(yùn)行結(jié)果都不相同,并且結(jié)果也不對(duì)。這就是因?yàn)?+i操作并不是原子操作,是線程不安全的。
只需要把上面代碼:
++_incrementValue;
改為:
Interlocked.Increment(ref _incrementValue);
即可解決上面的問題,修改過后,我們?cè)賮砜纯磮?zhí)行結(jié)果:

03、Decrement方法
該方法用于原子的遞減指定的變量,并返回遞減后的新值。該方法同樣有4個(gè)重載方法,分別為long、ulong、int和uint四種數(shù)據(jù)類型;
該方法和Increment方法基本一樣,區(qū)別就是一個(gè)是遞增一個(gè)是遞減,因此用法可以直接參考Increment方法,這里就不做詳細(xì)講解了。
04、Add方法
該方法用于原子的對(duì)兩個(gè)變量求和,將第一個(gè)變量替換為兩者和,并返回操作后第一個(gè)變量的新值。該方法同樣有4個(gè)重載方法,分別為long、ulong、int和uint四種數(shù)據(jù)類型;
雖然這個(gè)方法叫求和是加法,但是只需要把第2個(gè)參數(shù)變?yōu)樨?fù)數(shù),既可以實(shí)現(xiàn)減法。簡(jiǎn)單來說該方法可以實(shí)現(xiàn)原子的對(duì)兩個(gè)變量求和與求差。
上面Increment方法和Decrement方法,只能對(duì)變量每次進(jìn)行遞增遞減1,而能隨意加減,可以通過Add方法實(shí)現(xiàn)兩個(gè)變量進(jìn)行加減。
下面我們用代碼實(shí)現(xiàn)累加和累減示例用來說明Add使用方法,就不展示線程安全差異了,可以參考Increment方法中的示例,自己寫一個(gè)線程不安全的示例。
private static long _addValue = 0;
public static void AddRun()
{
for (var j = 0; j < 1000; j++)
{
//_addValue =_ addValue + j;
Interlocked.Add(ref _addValue, j);
}
Console.WriteLine($"累加結(jié)果: {_addValue}");
_addValue = 0;
for (var j = 0; j < 1000; j++)
{
//_addValue =_ addValue - j;
Interlocked.Add(ref _addValue, -j);
}
Console.WriteLine($"累減結(jié)果: {_addValue}");
}
執(zhí)行結(jié)果如下:

注:測(cè)試方法代碼以及示例源碼都已經(jīng)上傳至代碼庫,有興趣的可以看看。https://gitee.com/hugogoos/Planner

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