對於如今不少編程語言來講,多線程已經獲得了很好的支持,數據庫
以致於咱們寫多線程程序簡單,可是一旦遇到併發產生的問題就會各類嘗試。編程
由於不是明白爲何會產生併發問題,併發問題的根本緣由是什麼。c#
接下來就讓咱們來走近一點併發產生的那些問題。數組
public class ThreadTest_V0 { public int count = 0; public void Add1() { int index = 0; while (index++ < 1000000)//100萬次 { ++count; } } public void Add2() { int index = 0; while (index++ < 1000000)//100萬次 { count++; } } }
結果是多少?緩存
static void V0() { ThreadTest_V0 testV0 = new ThreadTest_V0(); Thread th1 = new Thread(testV0.Add1); Thread th2 = new Thread(testV0.Add2); th1.Start(); th2.Start(); th1.Join(); th2.Join(); Console.WriteLine($"V0:count = {testV0.count}"); }
答案:100萬 到 200萬之間的隨機數。多線程
爲何?併發
接下來咱們去深刻了解一下爲何會這樣?編程語言
首先咱們來到 「可見性」 這個陌生的詞彙身邊。優化
經過一番交談了解到:線程
對可見性進行一下總結就是我改的東西你能同時看到。
解讀一下呢,就像下面這樣:
CPU 內存 硬盤 ,處理速度上存在很大的差距,爲了彌補這種差距,也是爲了利用CPU強大計算能力。
CPU 和內存以前加入了緩存,就是咱們常常據說的 寄存器緩存、L一、二、3級緩存。
應該的處理流程是這樣的:讀取內存數據,緩存到CPU緩存中,CPU進行計算後,從CPU緩存中寫回內存。
還有一點 咱們都知道多線程實際上是經過切換時間片來達到 「同時」 處理問題的假象。
你也發現了,對於單核來講,程序其實仍是串行開發的。
就像是 「一我的」 ,東干點,西乾點,若是切換頻率上再快點速度,比咱們的眨眼時間還短呢?那……
接下來,咱們進入了多核時代。
顧名思義,多個CPU,也就是每一個CPU核心都有本身的緩存體系,可是內存只有一份。
好比CPU就是我麼們的本地緩存,而內存至關於數據庫。
咱們每一個人的本地緩存極有多是不同的,若是咱們拿着這些緩存直接作一些業務計算,
結果可想而知,多核時代,多線程併發也會有這樣的問題 — CPU緩存的數據不同咋辦?
這是CLR 爲咱們提出的解決方案,就是在遇到可見性引起的併發問題時,使用 volatile 關鍵字。
就是告訴 CPU,我不想用你的緩存,全部的請求都直接讀寫內存。
一句話,就是禁用緩存。
看上去這樣就能解決併發問題了吧?也不全是,還有下面這種槍狀況。
字面意義就是有順序,那麼是什麼有順序呢?-- 代碼
代碼其實並非咱們所寫的那樣一五一十地執行,以C# 爲例:
代碼 --> IL --> Jit --> cpu 指令
代碼 經過編譯器的優化生成了IL
CPU也會根據本身的優化從新排列指令順序
至少兩個點會有存在調整 代碼順序/指令順序的可能。
public class VolatileTest { public int falg = 0; }
static void VolatileTest() { VolatileTest volatiler = new VolatileTest(); new Thread( p => { Thread.Sleep(1000); volatiler.falg = 255; }).Start(); while (true) { if (volatiler.falg == 255) { break; } }; Console.WriteLine("OK"); }
主線程一直自旋,直到子線程將值改變就退出,顯示 「OK」
Debug 版本,執行結果:
Release 版本,執行結果:
爲何會這樣,由於咱們的代碼會通過編譯器優化,CPU指令優化,
語句的順序會發生改變,可是這樣也是這種離奇bug產生的一種方式。
怎麼避免它?
沒錯,依然是它,不只僅是禁用cpu緩存,並且還能禁止指令和編譯優化。
至少上面的那個例子咱們能夠再試試:
public class VolatileTest { public volatile int falg = 0; }
到這裏應該就能夠了吧,volatile 真好用,一個關鍵字就搞定。
正如你所想,依然沒有結束。
咱們平時常常遇到要給一段代碼區域加上鎖,好比這樣:
lock (lockObj) { count++; }
我麼們爲何要加鎖呢?你說爲了線程同步,爲何加鎖就能保證線程同步而不是其餘方式?
說到這裏,咱們須要再瞭解一個問題:count++
咱們常常寫這樣的代碼,那麼count++ 最終轉換成cpu指令會是什麼樣子呢?
指令1: 從內存中讀取 count
指令2:將 count +1
指令3:將新計算的count值,寫回內存
咱們將這個count++ 操做和線程切換進行結合
這裏纔是真正解答了最開始爲何是 100萬到200之間的隨機數。
解決 原子性問題的方法有不少,好比鎖
加鎖這個代碼我就暫且忽略,由於lock咱們並不陌生。
可是須要明白一點,lock() 是微軟提供給咱們的語法糖,其實最終使用的是 Monitor,而且作了異常和資源處理。
CLR 鎖原理
多個線程訪問同一個實例下的共享變量,同時將同步塊索引從 -1 改爲CLR維護的同步塊數組,
用完就會將實例的同步快變成-1
上面提到了隱姓埋名的Monitor,其實咱們也能夠拋頭露面地使用Monitor
這裏也不具體細說。具體使用能夠參照上面圖片。
官方定義:原子性的簡單操做,累加值,改變值等
區區 count++ 使用lock 有點浪費,咱們使用更加輕量級的 Interlocked,
爲咱們的 count ++ 保駕護航。
public class ThreadTest_V3 { public volatile int count = 0; public void Add1() { int index = 0; while (index++ < 1000000)//100萬次 { Interlocked.Add(ref count, 1); } } public void Add2() { int index = 0; while (index++ < 1000000)//100萬次 { Interlocked.Add(ref count, 1); } } }
結果很少說,依然穩穩的 200萬。
自旋鎖結構,能夠這樣理解。
多線程訪問共享資源時,只有一個線程能夠拿到鎖,其餘線程都在原地等待,
直到這個鎖被釋放,原地等待的資源又一次進行搶佔,以此類推。
在具體使用 System.Threading.SpinLock結構 以前,咱們根據剛剛講過的 System.Threading.Interlocked,進行一下改造:
public struct Spin { private int m_lock;//0=unlock ,1=lock public void Enter() { while (System.Threading.Interlocked.Exchange(ref m_lock, 1) != 0) { //能夠限制自旋次數和時間,自動斷開退出 } } public void Exit() { System.Threading.Interlocked.Exchange(ref m_lock, 0); } }
public class ThreadTest_V4 { private Spin spin = new Spin(); public volatile int count = 0; public void Add1() { int index = 0; while (index++ < 1000000)//100萬次 { spin.Enter(); count++; spin.Exit(); } } public void Add2() { int index = 0; while (index++ < 1000000)//100萬次 { spin.Enter(); count++; spin.Exit(); } } }
Enter() , m_lock 從0到1,就是加鎖;
鎖的是共享資源 count;
其餘線程原地自旋等待(循環)
Exit(),m_lock 從1到0,就是解鎖;
System.Threading.SpinLock 結構和以上實現思想相似。
後面的內容就簡單提一下定義和應用場景,有必要的就能夠單獨細查。
提供了基於自旋等待支援。
在線程必須等待發出事件信號或知足條件時方可以使用.
授予獨佔訪問共享資源的寫做,
並容許多個線程同時訪問資源進行讀取。
cas 核心思想:
將 count 從內存讀取出來並賦值給一個局部變量,叫作 originalData;
而後這個局部變量 +1 並賦值給新值,叫作 newData;
再次從內存中將count讀取出來,若是originalData ==count,
說明沒有線程修改內存中count值,能夠將新值存儲到內存中。
反之則能夠選擇自旋或者其餘策略。
固然還有進程之間的同步,這裏就不一一展開說了。
總結一下:
併發三要素 可見性、有序性、原子性
幾種鎖原理和CAS操做