天天看點

從多核CPU Cache一緻性的應用到分布式系統一緻性的概念遷移

概述

      現代多核CPU的cache模型基本都跟下圖1所示一樣,L1 L2 cache是每個核獨占的,隻有L3是共享的,當多個cpu讀、寫同一個變量時,就需要在多個cpu的cache之間同步資料,跟分布式系統一樣,必然涉及到一緻性的問題,隻不過兩者之間共享内容的方式不一樣而已,一個通過共享記憶體來共享内容,另一個通過網絡消息傳遞來共享内容。就像wiki所提及的:

Interestingly enough, a shared-memory multiprocessor system really is a message-passing computer under the covers. This means that clusters of SMP machines that use distributed shared memory are using message passing to implement shared memory at two different levels of the system architecture.

從多核CPU Cache一緻性的應用到分布式系統一緻性的概念遷移

 圖1、現代cpu多級cache

多核一緻性與原子操作

        多核一緻性最典型的應用場景是多線程的原子操作,其在多線程開發中經常用到,比如在計數器的生成,這類情況下資料有并發的危險,但是用鎖去保護又顯得有些浪費,是以原子類型操作十分的友善。

        原子操作雖然用起來簡單,但是其背景遠比我們想象的要複雜。其主要在于現代計算系統過于的複雜:多處理器、多核處理器、處理器又有核心獨有以及核心共享的多級緩存,在這種情況下,一個核心修改了某個變量,其他核心什麼時候可見是一個十分嚴肅的問題。同時在極緻最求性能的時代,處理器和編譯器往往表現的很智能,進行極度的優化,比如什麼亂序執行、指令重排等,雖然可以在目前上下文中做到很好的優化,但是放在多核環境下常常會引出新的問題來,這時候就必須提示編譯器和處理器某種提示,告訴某些代碼的執行順序不能被優化。今天我們重點看一下處理器在多線程原子操作上的背景原理以及具體應用。

CPU Cache與記憶體屏障

     考慮下面典型的代碼:

-Thread 1-
void foo(void)
{
   a = 1;
   b = 1;
}
-Thread 2-
void bar(void)
{
   while (b == 0) continue;
   assert(a == 1);
}      

由于cpu cache的存在,thread 2在斷言處可能會失敗。具體的,由于各個CPU的cache是獨立的,是以變量在他們各自的cache裡面的順序可能跟代碼的順序是不一緻的,也就是說執行thread2的cpu可能會先看到變量b的變化,然後再看到變量a的變化,導緻斷言失敗。就是我們常見的program order與process order的不一緻的工程現象,這裡就涉及到了memory consistency model的問題(類似于分布式系統的一緻性)。

       上述的代碼如果要正确執行,則變量a、b之間需要有‘happen before’的語義來限制(這裡就可以聯想到分布式系統中因果一緻性的概念)。但是對于這個語義上的需求,硬體設計者也愛莫能助,因為CPU無法知道變量之間的關聯關系。是以硬體設計者提供了memory barrier指令,讓軟體可以通過這些指令來告訴CPU這類關系,實作program order與process order的順序一緻。類似于下面的代碼:

-Thread 1-
void foo(void)
{
    a = 1;
    memory_barrier();
    b = 1;
}      

增加memory barrier之後,就可以保證在執行b=1的時候,cpu已經處理過\'a=1\'的操作了。也就是說通過硬體提供的memory barrier語義,使得軟體能夠保證其之前的記憶體通路操作先于其後的完成。memory barrier 常用的地方包括:實作核心的鎖機制、應用層編寫無鎖代碼、原子變量等。下面我們一起看下,c++11是怎樣使用記憶體屏障來實作原子操作的。

C++11的原子操作

        在C++11标準出來之前,C++标準沒有一個明确的記憶體模型,各個C++編譯器實作者各自為政,随着多線程開發的普及解決這個問題變得越來越迫切。在标準出來之前,GCC的實作是根據Intel的開發手冊搞出的一系列的__sync原子操作函數集合,具體如下:

type __sync_fetch_and_OP (type *ptr, type value, ...)
type __sync_OP_and_fetch (type *ptr, type value, ...)
bool__sync_bool_compare_and_swap (type *ptr, type oldval, type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...)
__sync_synchronize (...)      

       在C++11新标準中規定的記憶體模型(memory model)顆粒要比上述的記憶體模型細化很多,是以軟體開發者就有很多的操作空間了,如果熟悉這些記憶體模型,在保證業務正确的同時可以将對性能的影響減弱到最低,在硬體資源吃緊的地方,這是我們優化程式的一個重要方向。

       我們以c++11的原子變量的保證來展開這些記憶體模型。原子變量的通用接口使用store()和load()方式進行存取,可以額外接受一個額外的memory order參數,這個參數就是對應了c++11的記憶體模型,根據執行線程之間對變量的同步需求強度,新标準下的記憶體模型可以分成如下幾類:

Sequentially Consistent

      該模型是最強的同步模式,參數表示為std::memory_order_seq_cst,同時也是預設的模型。

-Thread 1-
y = 1
x.store (2); 

-Thread2-
if(x.load() ==2)
assert (y ==1)      

       對于上面的例子,即使x和y是不相關的,通常情況下處理器或者編譯器可能會對其通路進行重排,但是在seq_cst模式下,x.store(2)之前的所有memory accesses都發生在store操作之前。同時,x.load()之後的所有memory accesses都發生在load()操作之後,也就是說seq_cst模式下,記憶體的限制是雙向的。

Acquire/Release Consistent

std::atomic<int> a{0};
intb =0;
-Thread 1-
b = 1;
a.store(1, memory_order_release);
-Thread 2-
while(a.load(memory_order_acquire) !=1)/*waiting*/;
std::cout<< b <<\'\n\';      

       毫無疑問,如果是memory_order_seq_cst記憶體模型,那麼上面的操作一定是成功的(列印變量b顯示為1)。

       1. memory_order_release保證在這個操作之前的memory accesses不會重排到這個操作之後去,但是這個操作之後的memory accesses可能會重排到這個操作之前去。通常這個主要是用于之前準備某些資源後,通過store+memory_order_release的方式”Release”給别的線程;

       2. memory_order_acquire保證在這個操作之後的memory accesses不會重排到這個操作之前去,但是這個操作之前的memory accesses可能會重排到這個操作之後去。通常通過load+memory_order_acquire判斷或者等待某個資源,一旦滿足某個條件後就可以安全的“Acquire”消費這些資源了。

      這個就是類似于分布式系統的因果一緻性的概念。

Relaxed Consistent

       這個是最寬松的模式,memory_order_relaxed沒有happens-before的限制,編譯器和處理器可以對memory access做任何的re-order,是以另外的線程不能對其做任何的假設,這種模式下能做的唯一保證,就是一旦線程讀到了變量var的最新值,那麼這個線程将再也見不到var修改之前的值了(這個類似于分布式系統單調讀保證的概念)。

       這種情況通常是在需要原子變量,但是不線上程間同步共享資料的時候會用,同時當relaxed存一個資料的時候,另外的線程将需要一個時間才能relaxed讀到該值(也就是最終如果變量不再更改的話,所有的線程還是可以讀取到變量最終的值的),在非緩存一緻性的構架上需要重新整理緩存。在開發的時候,如果你的上下文沒有共享的變量需要線上程間同步,選用Relaxed就可以了。

       這一點類似于分布式系統的最終一緻性概念了。

總結

      上述的過程展現的是強一緻性、因果一緻性、最終一緻性等概念在c++11原子操作的使用,以及目前技術圈非常熱門的話題分布式系統開發中分布式一緻性概念的思考與遷移。從中我們可以看出技術在發展,但是很多概念其實是一脈相承的,隻有深刻了解了概念背後的原理以及相關技術發展的背景,才能勉強跟上技術的發展浪潮。