順序性,一致性,原子性:現代多核體系結構與原子操作·CAS與自旋鎖·自旋鎖與併發編程的原語·語句原子性和編程邏輯的原子性·行鎖與數據庫事務原子性·binlog與數據庫同

順序性:

亂序執行·邏輯正確性 

現代體系結構的每一個核的指令流水是亂序執行的,但是他能夠保證其執行效果正確,即等同於順序執行。

不過這帶來的問題是對於一個核在主觀上它的執行狀態最終保證正確,但是對於別的核,如果在某一箇中間時間點需要觀察它呢?看到的是一個不正確的中間狀態對應的數據:

亂序中間態:

core1: 

asm write a=x(沒提交)

asm write b=y(已提交)

core2:

asm if(b==y)   

           assert(a==x) // 出錯了,因爲core1亂序提交!

一般情況下,我們可以容忍這類問題發生。

但是當 write b=y 是一個非常重要的多核控制原語的時候,這類問題就無法容忍了。

局部順序性與局部正確中間態:

杜絕這種問題的關鍵是讓 write b=y操作滿足局部順序性,從而在該操作上得到局部正確中間態即該操作一旦執行成功,則前面的操作都執行成功。該操作如果沒有執行,那麼後面的操作也都沒有執行。  也即任何時刻只要b==y,那麼a==x。


一致性:

各核及線程緩存·volatile

各核緩存及線程緩存不一致是影響併發並行計算正確性的一大問題。

如果上層編程邏輯需要使他們可見的值保持一致,則可以引入volatile。


原子性:

1 多核體系結構與多核原子操作

http://my.oschina.net/jcseg/blog/316726

 一. 何謂"原子操作":
原子操作就是: 不可中斷的一個或者一系列操作, 也就是不會被線程調度機制打斷的操作, 運行期間不會有任何的上下文切換(context switch).

多核原子操作:不可打斷(原子),不可干擾(互斥=》串行=》最高隔離)。在原子操作基礎上,不被其他核上運行的指令干擾的指令操作。如何不被其他核指令干擾?內存是多核共用的,所以當本核訪問內存的時候,其他核都不能訪問。下文會講到總線鎖。

二. 爲什麼關注原子操作?
1. 如果確定某個操作是原子的, 就不用爲了去保護這個操作而加上會耗費昂貴性能開銷的鎖. - (巧妙的利用原子操作和實現無鎖編程)
2. 藉助原子操作可以實現互斥鎖(mutex). (linux中的mutex_lock_t)
3. 藉助互斥鎖, 可以實現讓更多的操作變成原子操作. 

三. 單核CPU的原子操作:
在單核CPU中, 能夠在一個指令中完成的操作都可以看作爲原子操作, 因爲中斷只發生在指令間.

四. 多核CPU的原子操作:
在多核CPU的時代(確實moore定律有些過時了,我們需要更多的CPU,而不是更快的CPU,無法處理快速CPU中的熱量散發問題), 體系中運行着多個獨立的CPU, 即使是可以在單個指令中完成的操作也可能會被幹擾. 典型的例子就是decl指令(遞減指令), 它細分爲三個過程: "讀->改->寫", 涉及兩次內存操作. 如果多個CPU運行的多個進程在同時對同一塊內存執行這個指令, 那情況是無法預測的

五. 硬件支持 & 多核原子操作:
軟件級別的原子操作是依賴於硬件支持的. 在x86體系中, CPU提供了HLOCK pin引線, 允許CPU在執行某一個指令(僅僅是一個指令)時拉低HLOCK pin引線的電位, 直到這個指令執行完畢才放開.  從而鎖住了總線, 如此在同一總線的CPU就暫時無法通過總線訪問內存了, 這樣就保證了多核處理器的原子性(個人理解:另外使得cpu強制串行性,該條指令不能和任何其他指令之間發生亂序提交). (想想這機制對性能影響挺大的).  

關於爲什麼本文所講的“多核原子操作”要鎖總線

內存屏障的結果,是在操作原子性基礎上實現核間高度隔離以及局部順序性
1. 核間高度隔離:鎖對其他核上的內存操作(不管R/W)互斥,從而爲本操作提供最高級別隔離性。
2. 局部順序性:如上文,加總線鎖之後還會使得本核的指令流水在此串行化,防止本指令相對之前和之後的其他指令發生亂序提交,提供局部狀態順序性。
http://blog.codingnow.com/2007/12/fence_in_multi_core.html


六. 哪些操作可以確定爲原子操作了?
對於非long和double基本數據類型的"簡單操作"都可以看作是原子的. 例如: 賦值和返回. 大多數體系中long和double都佔據8個字節, 操作系統或者JVM很可能會將寫入和讀取操作分離爲兩個單獨的32位的操作來執行, 這就產生了在一個讀取和寫入過程中一個上下文切換(context switch), 從而導致了不同任務線程看到不正確結果的的可能性.

遞增, 遞減不是原子操作: i++反彙編的彙編指令: (需要三條指令操作, 和兩個內存訪問, 一次寄存器修改)
?
1
2
3
movl i, %eax                            //內存訪問, 讀取i變量的值到cpu的eax寄存器
addl $1, %eax                         //增加寄存器中的值
movl %eax, i                            //寫入寄存器中的值到內存


七. 如何實現++i和i++的原子性: 
1. 單CPU, 使用鎖或則禁止多線程調度, 因爲本身單核CPU的併發就是僞併發. (在單核CPU中, 在沒有阻塞的程序中使用多線程是沒必要的).
2. 多核CPU, 就需要藉助上面說道的CPU提供的Lock, 鎖住總線. 防止在"讀取, 修改, 寫入"整個過程期間其他CPU訪問內存. (那麼“讀寫,修改,寫入”這個操作會不會在在單核中發生線程的切換呢?)

八. Linux提供的兩個原子操作接口:
1. 原子整數操作
針對整數的原子操作只能對atomic_t類型的數據處理。這裏沒有使用C語言的int類型,主要是因爲:
1) 讓原子函數只接受atomic_t類型操作數,可以確保原子操作只與這種特殊類型數據一起使用.
2) 使用atomic_t類型確保編譯器不對相應的值進行訪問優化. (原理爲: 變量被volatile修飾了)
3) 使用atomic_t類型可以屏蔽不同體系結構上的數據類型的差異。儘管Linux支持的所有機器上的整型數據都是32位,但是使用atomic_t的代碼只能將該類型的數據當作24位來使用。這個限制完全是因爲在SPARC體系結構上,原子操作的實現不同於其它體系結構:32位int類型的低8位嵌入了一個鎖,因爲SPARC體系結構對原子操作缺乏指令級的支持,所以只能利用該鎖來避免對原子類型數據的併發訪問。

原子整數操作最常見的用途就是實現計數器。原子整數操作列表在中定義。原子操作通常是內斂函數,往往通過內嵌彙編指令來實現。如果某個函數本來就是原子的,那麼它往往會被定義成一個宏。

在編寫內核時,操作demo如下:

?
1
2
3
4
atomic_t cnt;
atomic_set(&cnt, 2);
atomic_add(4, &cnt);
atomic_inc(cnt);


2. 原子位操作:
原子位操作定義在文件中。令人感到奇怪的是位操作函數是對普通的內存地址進行操作的。原子位操作在多數情況下是對一個字節長的內存(注1)訪問,因而位號該位於0-31之間(在64位機器上是0-63之間),但是對位號的範圍沒有限制。

注1:操作系統可以確保,在同一時刻,只有一個CPU的一個進程訪問特定的某個字節,再加上單核中的原子性(基本數據類型的簡單操作),所以單字節內存的簡單操作是具有天生的多核原子性的。 

編寫內核代碼,把要操作的數據的指針給操作函數,就可以進行位操作了:

?
1
2
3
4
5
unsigned long var = 0;
set_bit(0, &var);           /*set the 0th bit*/
set_bit(1, &var);           /*set the 1th bit*/
clear_bit(1, &var);         /*clear the 1th bit*/
change_bit(0, &var);        /*change the 1th bit*/


九. spinlock CPU同步: 
spin lock必須基於CPU的數據總線鎖定, 它通過讀取一個內存單元(spinlock_t)來判斷這個spinlock是否已經被別的CPU鎖住. 如果否, 它寫進一個特定值, 表示鎖定了總線, 然後返回. 如果是, 它會重複以上操作直到成功, 或者spin次數超過一個設定值. 記住上面提及到的: 鎖定數據總線的指令只能保證一個指令操作期間CPU獨佔數據總線. (spinlock在鎖定的時侯, 不會睡眠而是會持續的嘗試).


2. 原子操作CAS與自旋鎖

spinlock又稱自旋鎖,線程通過busy-wait-loop的方式來獲取鎖,任時刻只有一個線程能夠獲得鎖,其他線程忙等待直到獲得鎖。spinlock在多處理器多線程環境的場景中有很廣泛的使用,一般要求使用spinlock的臨界區儘量簡短,這樣獲取的鎖可以儘快釋放,以滿足其他忙等的線程。Spinlock和mutex不同,spinlock不會導致線程的狀態切換(用戶態->內核態),但是spinlock使用不當(如臨界區執行時間過長)會導致cpu busy飆高。
spinlock與mutex對比

 優缺點比較

  spinlock不會使線程狀態發生切換,mutex在獲取不到鎖的時候會選擇sleep。
  mutex獲取鎖分爲兩階段,第一階段在用戶態採用spinlock鎖總線的方式獲取一次鎖,如果成功立即返回;否則進入第二階段,調用系統的futex鎖去sleep,當鎖可用後被喚醒,繼續競爭鎖。
  Spinlock優點:沒有昂貴的系統調用,一直處於用戶態,執行速度快。
  Spinlock缺點:一直佔用cpu,而且在執行過程中還會鎖bus總線,鎖總線時其他處理器不能使用總線。
  Mutex優點:不會忙等,得不到鎖會sleep。
  Mutex缺點:sleep時會陷入到內核態,需要昂貴的系統調用。

  Spinlock使用準則:臨界區儘量簡短,控制在100行代碼以內,不要有顯式或者隱式的系統調用,調用的函數也儘量簡短。例如,不要在臨界區中調用read,write,open等會產生系統調用的函數,也不要去sleep;strcpy,memcpy等函數慎用,依賴於數據的大小。

spinlock系統實現

  spinlock的實現方式有多種,但是思想都是差不多的,glibc-2.9中的實現方法:
int pthread_spin_lock (lock) pthread_spinlock_t *lock;
{
asm ("\n"
"1:\t" LOCK_PREFIX "decl %0\n\t"
"jne 2f\n\t"
".subsection 1\n\t"
".align 16\n"
"2:\trep; nop\n\t"
"cmpl $0, %0\n\t"
"jg 1b\n\t"
"jmp 2b\n\t"
".previous"
: "=m" (*lock)
: "m" (*lock));
return 0;
}
  執行過程:
  1,lock_prefix 即 lock。lock decl %0,鎖總線將%0(即lock變量)減一。Lock可以保證接下來一條指令的原子性。
  2, 如果lock=1,decl的執行結果爲lock=0,ZF標誌位爲1,直接跳到return 0;否則跳到標籤2。也許要問,爲啥能直接跳到return 0呢?因爲subsection和previous之間的代碼被編譯到別的段中,因此jne之後緊接着的代碼就是 return 0 (leaveq;retq)。Rep nop在經過編譯器編譯之後被編譯成 pause。
  3, 如果跳到標籤2,說明獲取鎖不成功,循環等待lock重新變成1,如果lock爲1跳到標籤1重新競爭鎖。
  該實現採用的是AT&T的彙編語法,更詳細的執行流程解釋可以參考“五竹”大牛的文檔。
  3.2,系統自帶(glibc-2.3.4)spinlock反彙編代碼:
  系統環境:
2.6.9-89.ELsmp #1 SMP x86_64 x86_64 x86_64 GNU/Linux
(gdb) disas pthread_spin_lock
Dump of assembler code for function pthread_spin_lock:
//eax寄存器清零,做返回值
0x0000003056a092f0 <pthread_spin_lock+0>: xor %eax,%eax
//rdi存的是lock鎖地址,原子減一
0x0000003056a092f2 <pthread_spin_lock+2>: lock decl (%rdi)
//杯了個催的,加鎖不成功,跳轉,開始busy wait
0x0000003056a092f5 <pthread_spin_lock+5>: jne 0x3056a09300 <pthread_spin_lock+16>
//終於夾上了…加鎖成功,返回
0x0000003056a092f7 <pthread_spin_lock+7>: retq
……………………………………….省略若干nop……………………………………….
0x0000003056a092ff <pthread_spin_lock+15>: nop
//pause指令降低CPU功耗
0x0000003056a09300 <pthread_spin_lock+16>: pause
//檢查鎖是否可用
0x0000003056a09302 <pthread_spin_lock+18>: cmpl $0×0,(%rdi)
//回跳,重新鎖總線獲取鎖
0x0000003056a09305 <pthread_spin_lock+21>: jg 0x3056a092f2 <pthread_spin_lock+2>
//長夜漫漫,愛上一個不回家的人,繼續等~
0x0000003056a09307 <pthread_spin_lock+23>: jmp 0x3056a09300 <pthread_spin_lock+16>
0x0000003056a09309 <pthread_spin_lock+25>: nop
……………………………………….省略若干nop……………………………………….
End of assembler dump.
Glibc的彙編代碼還是很簡潔的,沒有多餘的代碼。

總結

CAS是最常見的保證原子性的WAR(write after read)指令。如上文所述:指令原子性意味着即使在多核cpu上,通過鎖總線的方式,能夠保證該指令執行過程中不會有其他衝突的R/W指令並行執行。
自旋鎖一般實現方法是: 

SpinLock:

= while(true){ CAS(&volatile t) }
查看上文glibc源碼,這個函數是用嵌入彙編實現的,可能看不到有CAS指令,但有同義的原子操作,如單核的關搶佔,多核的鎖總線指令。
 //https://github.com/wh5a/jos/commit/8223e70a9e8c9942f2fd02b6d4e046c7e6da34ed
 +spinlock_acquire(struct spinlock *lk)
 +{
 +	if(spinlock_holding(lk))
 +		panic("recursive spinlock_acquire");
 +
 +	// The xchg is atomic.
 +	// It also serializes,
 +	// so that reads after acquire are not reordered before it. 
 +	while(xchg(&lk->locked, 1) != 0)
 +		pause();	// let CPU know we're in a spin loop
 +
 +	// Record info about lock acquisition for debugging.
 +	lk->cpu = cpu_cur();
 +	debug_trace(read_ebp(), lk->eips);
 +}

 // Atomically set *addr to newval and return the old value of *addr.
 +static inline uint32_t
 +xchg(volatile uint32_t *addr, uint32_t newval)
 +{
 +<span style="white-space:pre">	</span>uint32_t result;
 +
 +<span style="white-space:pre">	</span>// The + in "+m" denotes a read-modify-write operand.
 +<span style="white-space:pre">	</span>asm volatile("lock; xchgl %0, %1" :
 +<span style="white-space:pre">	</span>       "+m" (*addr), "=a" (result) :
 +<span style="white-space:pre">	</span>       "1" (newval) :
 +<span style="white-space:pre">	</span>       "cc");
 +<span style="white-space:pre">	</span>return result;
 +}
 +
 +/* While a spinlock will work if you just do nothing in the loop,
 +   Intel has defined a special instruction called PAUSE that notifies
 +   the processor that a spin loop is in progress and can improve
 +   system performance in such cases, especially on "hyper-threaded"
 +   processors that multiplex a single execution unit among multiple
 +   virtual CPUs.
 +*/
 +static inline void
 +pause(void)
 +{
 +<span style="white-space:pre">	</span>asm volatile("pause" : : : "memory");
 +}

SpinUnlock:

= t<=xx

3 自旋鎖與併發編程原語

通過while(true){CAS}/自旋鎖的加鎖解鎖可以實現“對多線程/多進程 保持原子性的臨界區代碼”,這些臨界區代碼通常可以是併發編程庫裏最關鍵的併發原語(原語:原子性statement),如加鎖、去鎖、睡眠、喚醒、操作信號量,這些原語用於實現更高層的併發機制如互斥鎖、信號量,以及併發數據結構。

4 語句原子性和編程邏輯的原子性

考慮 這樣一個WAR的編程邏輯:
if(read(Vector[x])==xx) write(vector[x])
vector號稱是多線程安全的,也即每一條vector的讀寫語句read(),write()都是多線程互斥的(即原子的)。
但當這些語句組合成編程邏輯的時候,整個邏輯並不是對多線程原子的,中間可能被打斷或被幹擾。所以需要對整個邏輯加鎖,實現編程邏輯原子性。

5 鎖與數據庫事務原子性

數據庫事務:

事務是指對系統進行的一組操作,爲了保證系統的完整性,事務需要具有ACID特性,具體如下:

1. 原子性(Atomic)

     一個事務包含多個操作,這些操作要麼全部執行,要麼全都不執行。實現事務的原子性,要支持回滾操作,在某個操作失敗後,回滾到事務執行之前的狀態。
     回滾實際上是一個比較高層抽象的概念,大多數DB在實現事務時,是在事務操作的數據快照上進行的(比如,MVCC),並不修改實際的數據,如果有錯並不會提交,所以很自然的支持回滾。
     而在其他支持簡單事務的系統中,不會在快照上更新,而直接操作實際數據。可以先預演一邊所有要執行的操作,如果失敗則這些操作不會被執行,通過這種方式很簡單的實現了原子性。

2.隔離性(Isolation)

     併發事務之間互相影響的程度,比如一個事務會不會讀取到另一個未提交的事務修改的數據。在事務併發操作時,可能出現的問題有:
     髒讀:事務A修改了一個數據,但未提交,事務B讀到了事務A未提交的更新結果,如果事務A提交失敗,事務B讀到的就是髒數據。
     不可重複讀:在同一個事務中,對於同一份數據讀取到的結果不一致。比如,事務B在事務A提交前讀到的結果,和提交後讀到的結果可能不同。不可重複讀出現的原因就是事務併發修改記錄,要避免這種情況,最簡單的方法就是對要修改的記錄加鎖,這回導致鎖競爭加劇,影響性能。另一種方法是通過MVCC可以在無鎖的情況下,避免不可重複讀。
     幻讀:在同一個事務中,同一個查詢多次返回的結果不一致。事務A新增了一條記錄,事務B在事務A提交前後各執行了一次查詢操作,發現後一次比前一次多了一條記錄。幻讀是由於併發事務增加記錄導致的,這個不能像不可重複讀通過記錄加鎖解決,因爲對於新增的記錄根本無法加鎖。需要將事務串行化,才能避免幻讀。
     事務的隔離級別從低到高有:
     Read Uncommitted:最低的隔離級別,什麼都不需要做,一個事務可以讀到另一個事務未提交的結果。所有的併發事務問題都會發生。
     Read Committed:只有在事務提交後,其更新結果纔會被其他事務看見。可以解決髒讀問題
     Repeated Read:在一個事務中,對於同一份數據的讀取結果總是相同的,無論是否有其他事務對這份數據進行操作,以及這個事務是否提交。可以解決髒讀、不可重複讀
     Serialization:事務串行化執行,隔離級別最高,犧牲了系統的併發性。可以解決併發事務的所有問題
     通常,在工程實踐中,爲了性能的考慮會對隔離性進行折中。

3. 一致性(Consistency)

     一致性是指事務使得系統從一個一致的狀態轉換到另一個一致狀態。事務的一致性決定了一個系統設計和實現的複雜度。事務可以不同程度的一致性:
     強一致性:讀操作可以立即讀到提交的更新操作。
     弱一致性:提交的更新操作,不一定立即會被讀操作讀到,此種情況會存在一個不一致窗口,指的是讀操作可以讀到最新值的一段時間。
     最終一致性:是弱一致性的特例。事務更新一份數據,最終一致性保證在沒有其他事務更新同樣的值的話,最終所有的事務都會讀到之前事務更新的最新值。如果沒有錯誤發生,不一致窗口的大小依賴於:通信延遲,系統負載等。
     其他一致性變體還有:
     單調一致性:如果一個進程已經讀到一個值,那麼後續不會讀到更早的值。
     會話一致性:保證客戶端和服務器交互的會話過程中,讀操作可以讀到更新操作後的最新值。
  
      SQL的R/W普通語句構成了數據庫事務。通過加鎖,保證了語句原子性:不可干擾。

6 數據庫事務原子性與binlog、主從同步讀寫分離

通過加鎖(表鎖、行鎖),使得數據庫操作變成一條條原子性的事務和普通語句。
這些原子操作可能是亂序並行執行的,但運行效果卻可以保證是等同於串行提交的。
binlog按照提交順序(通過提交時的時間戳)記錄這些原子操作,能夠保證重放binlog即可復原當前數據庫。
因此binlog是數據庫復原、主從同步的最主要依據。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章