<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
當兩個或兩個以上的執行緒存取共用資料,並且嘗試同時改變它時,就發生爭用的情況。它們所依賴的那部分共用資料,叫做競爭條件。
資料爭用是競爭條件中的一種,出現競爭條件可能會導致記憶體(資料)損壞或者出現不確定性的行為。
如果有 N 個執行緒都會執行某個操作,當一個執行緒正在執行這個操作時,其它執行緒都必須依次等待,這就是執行緒同步。
多執行緒環境下出現競爭條件,通常是沒有執行正確的同步而導致的。
時間片(timeslice)是作業系統分配給每個正在執行的程序微觀上的一段 CPU 時間。
首先,核心會給每個程序分配相等的初始時間片,然後每個程序輪番地執行相應的時間,當所有程序都處於時間 片耗盡的狀態時,核心會重新為每個程序計算並分配時間片,如此往復。
請參考:https://zh.wikipedia.org/wiki/%E6%97%B6%E9%97%B4%E7%89%87
上下文切換(Context Switch),也稱做程序切換或工作切換,是指 CPU 從一個程序或執行緒切換到另一個程序或執行緒。
在接受到中斷(Interrupt)的時候,CPU 必須要進行上下文交換。進行上下文切換時,會帶來效能損失。
請參考[https://zh.wikipedia.org/wiki/上下文交換
阻塞狀態指執行緒處於等待狀態。當執行緒處於阻塞狀態時,會盡可能少佔用 CPU 時間。
當執行緒從執行狀態(Runing)變為阻塞狀態時(WaitSleepJoin),作業系統就會將此執行緒佔用的 CPU 時間片分配給別的執行緒。當執行緒恢復執行狀態時(Runing),作業系統會重新分配 CPU 時間片。
分配 CPU 時間片時,會出現上下文切換。
只有作業系統才能切換執行緒、掛起執行緒,因此阻塞執行緒是由作業系統處理的,這種方式被稱為核心模式(kernel-mode)。
Sleep()
、Join()
等,都是使用核心模式來阻塞執行緒,實現執行緒同步(等待)。
核心模式實現執行緒等待時,出現上下文切換。這適合等待時間比較長的操作,這樣會減少大量的 CPU 時間損耗。
如果執行緒只需要等待非常微小的時間,阻塞執行緒帶來的上下文切換代價會比較大,這時我們可以使用自旋,來實現執行緒同步,這一方法稱為使用者模式(user-mode)。
為多個執行緒共用的變數提供原子操作。
使用 Interlocked 類,可以在不阻塞執行緒(lock、Monitor)的情況下,避免競爭條件。
Interlocked 類是靜態類,讓我們先來看看 Interlocked 的常用方法:
方法 | 作用 |
---|---|
CompareExchange() | 比較兩個數是否相等,如果相等,則替換第一個值。 |
Decrement() | 以原子操作的形式遞減指定變數的值並儲存結果。 |
Exchange() | 以原子操作的形式,設定為指定的值並返回原始值。 |
Increment() | 以原子操作的形式遞增指定變數的值並儲存結果。 |
Add() | 對兩個數進行求和並用和替換第一個整數,上述操作作為一個原子操作完成。 |
Read() | 返回一個以原子操作形式載入的值。 |
全部方法請檢視:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.interlocked?view=netcore-3.1
問題:
C# 中賦值和一些簡單的數學運算不是原子操作,受多執行緒環境影響,可能會出現問題。
我們可以使用 lock 和 Monitor 來解決這些問題,但是還有沒有更加簡單的方法呢?
首先我們編寫以下程式碼:
private static int sum = 0; public static void AddOne() { for (int i = 0; i < 100_0000; i++) { sum += 1; } }
這個方法的工作完成後,sum 會 +100。
我們在 Main 方法中呼叫:
static void Main(string[] args) { AddOne(); AddOne(); AddOne(); AddOne(); AddOne(); Console.WriteLine("sum = " + sum); }
結果肯定是 5000000,無可爭議的。
但是這樣會慢一些,如果作死,要多執行緒同時執行呢?
好的,Main 方法改成如下:
static void Main(string[] args) { for (int i = 0; i < 5; i++) { Thread thread = new Thread(AddOne); thread.Start(); } Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine("sum = " + sum); }
筆者執行一次,出現了 sum = 2633938
我們將每次運算的結果儲存到陣列中,擷取其中一段發現:
8757 8758 8760 8760 8760 8761 8762 8763 8764 8765 8766 8766 8768 8769
多個執行緒使用同一個變數進行操作時,並不知道此變數已經在其它執行緒中發生改變,導致執行完畢後結果不符合期望。
我們可以通過下面這張圖來解釋:
因此,這裡就需要原子操作,在某個時刻,必須只有一個執行緒能夠進行某個操作。而上面的操作,指的是讀取、計算、寫入這一過程。
當然,我們可以使用 lock 或者 Monitor 來解決,但是這樣會帶來比較大的效能損失。
這時 Interlocked 就起作用了,對於一些簡單的操作運算, Interlocked 可以實現原子性的操作。
實現原子性,可以通過多種鎖來解決,目前我們學習到了 lock、Monitor,現在來學習 Interlocked ,後面會學到更加多的鎖的實現。
用於自增操作。
我們修改一下 AddOne 方法:
public static void AddOne() { for (int i = 0; i < 100_0000; i++) { Interlocked.Increment(ref sum); } }
然後執行,你會發現結果 sum = 5000000 ,這就對了。
說明 Interlocked 可以對簡單值型別進行原子操作。
Interlocked.Increment() 是遞增,而 Interlocked.Decrement() 是遞減。
Interlocked.Exchange()
實現賦值運算。
這個方法有多個過載,我們找其中一個來看看:
public static int Exchange(ref int location1, int value);
意思是將 value 賦給 location1 ,然後返回 location1 改變之前的值。
測試:
static void Main(string[] args) { int a = 1; int b = 5; // a 改變前為1 int result1 = Interlocked.Exchange(ref a, 2); Console.WriteLine($"a新的值 a = {a} | a改變前的值 result1 = {result1}"); Console.WriteLine(); // a 改變前為 2,b 為 5 int result2 = Interlocked.Exchange(ref a, b); Console.WriteLine($"a新的值 a = {a} | b不會變化的 b = {b} | a 之前的值 result2 = {result2}"); }
另外 Exchange()
也有對參照型別的過載:
Exchange<T>(T, T)
其中一個過載:
public static int CompareExchange (ref int location1, int value, int comparand)
比較兩個 32 位有符號整數是否相等,如果相等,則替換第一個值。
如果 comparand
和 location1
中的值相等,則將 value
儲存在 location1
中。 否則,不會執行任何操作。
看準了,是 location1
和 comparand
比較!
使用範例如下:
static void Main(string[] args) { int location1 = 1; int value = 2; int comparand = 3; Console.WriteLine("執行前:"); Console.WriteLine($" location1 = {location1} | value = {value} | comparand = {comparand}"); Console.WriteLine("當 location1 != comparand 時"); int result = Interlocked.CompareExchange(ref location1, value, comparand); Console.WriteLine($" location1 = {location1} | value = {value} | comparand = {comparand} | location1 改變前的值 {result}"); Console.WriteLine("當 location1 == comparand 時"); comparand = 1; result = Interlocked.CompareExchange(ref location1, value, comparand); Console.WriteLine($" location1 = {location1} | value = {value} | comparand = {comparand} | location1 改變前的值 {result}"); }
對兩個 32 位整數進行求和並用和替換第一個整數,上述操作作為一個原子操作完成。
public static int Add (ref int location1, int value);
只能對 int 或 long 有效。
回到第一小節的多執行緒求和問題,使用 Interlocked.Add()
來替換Interlocked.Increment()
。
static void Main(string[] args) { for (int i = 0; i < 5; i++) { Thread thread = new Thread(AddOne); thread.Start(); } Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine("sum = " + sum); } private static int sum = 0; public static void AddOne() { for (int i = 0; i < 100_0000; i++) { Interlocked.Add(ref sum,1); } }
返回一個以原子操作形式載入的 64 位值。
64位元系統上不需要 Read 方法,因為64位元讀取操作已是原子操作。 在32位元系統上,64位元讀取操作不是原子操作,除非使用 Read 執行。
public static long Read (ref long location);
就是說 32 位系統上才用得上。
到此這篇關於C#多執行緒系列之原子操作的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45