《深入理解并行編程》整理筆記

目錄

<a id="1."></a>1.并發編程的目標

相對于串行編程來說,并行編程有如下三個主要目標:

  • 性能 - 因為現在CPU性能已經遇到瓶頸,使用單線程編程又無法發揮多核CPU的性能
  • 生產率 - 提高創建并行軟件的生產率
  • 通用性 - 開發并行程序需要很高的成本,更加通用的并行程序能夠有效降低成本。然而通用性又會帶來更大的性能損失和生產率損失。參考一下現在流行的我所知道的并行編程環境:
    • C/C++"鎖及線程":這包含POSIX線程(pthread),Windows線程,以及很多操作系統內核環境。它們提供了優秀的性能(至少在SMP系統上是如此),也提供了良好的通用性。可惜的是生產率較低.
    • Java: 這個編程環境與生俱來就有多線程能力,它比C/C++生產率高。但性能要比它們底。
    • SQL: 結構化編程語言 SQL 非常特殊,僅僅運用于數據庫查詢。但是,它的性能非常好,生產率也很優秀,對并行編程知之甚少的人員來說,這個并行編程環境很好的允許它們使用大型并行機器。

所以,并沒有完美的環境,需要我們在性能、生產率、通用性之間進行權衡。

對于電腦或者手機的結構來講:從底層硬件、固件、操作系統核心、系統庫、中間軟件到上層應用;越往上層,生產率越來越重要。越往下層,性能和通用性越來越重要。因為在上層需要進行大量開發工作,必須考慮通用性來降低成本,而下層性能的損失極難在上層恢復。

所以,性能和通用性是底層開發主要關心的地方

<a id="2."></a>2.并行訪問控制 - 是什么使并行編程變得復雜?

(需要注意的是:并行計算的困難,有人為因素的原因與并行計算本身的技術 屬性的原因,二者給并行計算帶來的困難是差不多的。這是由于我們需要人為干 涉并行計算的過程,人和計算機間的雙向交互需要人和機器都執行同等復雜的操 作。因此,采用抽象或者數學分析將極大的限制實用性。)

  • 給定一個單線程的順序進程,單線程對所有進程的資源都有訪問權。
  • 第一個并行訪問控制問題是訪問特定的資源是否受限于資源的位置。
  • 其他的并行訪問控制問題是線程如何協調訪問資源。這種協調是由非常多的 同步機制通過 供不同的并行語言和環境來實施的,包括消息傳遞,加鎖,事務, 引用計數,顯式計時,共享原子變量,以及數據所有權。需要傳統的并行編程關 注由此引出的死鎖,活鎖,和事務回滾。這種框架如果要詳細討論需要涉及到同 步機制的比較,比如加鎖相對于事務內存,但這些討論不在這節的討論范圍內。

<a id="3."></a>3.關于硬件 - 對并行編程造成的障礙

(大多數人根據直覺就知道,在系統間傳遞消息要比在單個系統上執行簡單計 算更加耗時。不過,在共享同一塊內存的系統的線程間傳遞消息是不是也更加耗 時,這點可就不一定了。本章主要關注共享內存系統中的同步和通信的開銷,只 涉及了一些共享內存并行硬件設計的皮毛)

單線程

  1. CUP流水線

    • 過去微處理器在處理下一條指令之前,至少需要取址、解碼和執行3個周期來完成本條指令;后來的CPU可以同時處理多條指令,通過一條很長的‘流水線’來控制CPU內部的指令流。
    • 帶有長流水線的 CPU 想要達到最佳性能,需要程序給出高度可預測的控制流。在這種程序中,流水線可以一直保持在滿狀態,CPU 高速運行。
    • (如果程序中帶有許多循環,且循環計數都比較小;或者面向對象 的程序中帶有許多虛對象,每個虛對象都可以引用不同的實對象,而這些實對象 都有頻繁被調用的成員函數的不同實現,此時 CPU 很難或者完全不可能預測某 個分支的走向。這樣 CPU 要么等待控制流進行到足以知道分支走向的方向時, 要么干脆猜測——由于此時程序的控制流不可預測——CPU 常常猜錯。在這兩 種情況中,流水線都是空的,CPU 需要等待流水線被新指令填充,這將大幅降 低 CPU 的性能)
  2. 內存引用

    • 過去,微處理器從內存讀取一個值的時間比執行一條指令的時間短。現在它可以在這段時間執行上百 條甚至上千條指令
    • 雖然現代微型計算機上的大型緩存極大地減少了內存訪問延遲,但是只有高 度可預測的數據訪問模式才能讓緩存發揮最大效用。不幸的是,一般像遍歷鏈表 這樣的操作的內存訪問模式非常難以預測——畢竟如果這些模式是可預測的,我 們也就不會被指針所困擾了,是吧?內存引用常常是影響現代 CPU 性能的重要因素。

多線程

  1. 原子操作

    • 原子操作的概念在某種意義上與CPU流水線上的一次執行一條的匯編操作沖突了。現代CPU為了提高性能讓CPU能夠亂序執行原子操作。
    • 原子操作通常只用于數據的單個元素。由于許多并行算法都需要在更新多個數據元素時,保證正確的執行順序,大多數CPU都提供了內存屏障。它也是影響性能的因素之一。
  2. 內存屏障

    • 下面是一個簡單的基于鎖的 臨界區。
    1 spin_lock(&mylock);
    2 a = a + 1;
    3 spin_unlock(&mylock);
    
    • 如果CPU沒有順序執行上訴語句,a會在沒有鎖的情況下加一,導致我們無法得到精確的值。為防止這種情況發生,鎖操作原語必須包含顯示或隱式的內存屏障。當然這樣會降低CPU性能。
  3. Cache Miss - 緩存未命中

    • 現代CPU使用大容量的高速緩存來降低由于較低的內存訪問速度帶來的性能懲罰。但是,CPU高速緩存事實上對多CPU間頻繁訪問的變量起反效果。
    • 當某個CPU想去更改變量的值時,這個變量剛被其他CPU修改過而存在他的緩存中,這將導致代價高昂的Cache Miss
  4. I/O操作

    • 其實緩存未命中可以視為CPU之間的I/O操作,當然相比于其它的I/O操作,這個代價最低廉。
    • I/O操作設計網絡、大容量存儲器,或者(更糟的)人類本身,I/O操作對性能的影響遠遠大于以上提到的各種障礙。

<a id="4."></a>4.這種障礙的實際開銷

硬件體系結構

  • 這是一個粗略的八核計算機系統概要圖。每個管芯有兩個CPU核,每個核帶有自己的高速緩存,管芯內還帶有一個互聯模塊,使管芯內的兩個核可以互 相通信。在圖中央的系統互聯模塊可以讓四個管芯相互通信,并且將管芯與主存 連接起來。
  • 數據是以“緩存線”為單位在系統中傳輸的,每個“緩存線”大小在32到256字節之間。
  • 當 CPU 從內存中讀取一個變 量到它的寄存器中時,必須首先將包含了該變量的緩存線讀取到 CPU 高速緩存。 同樣地,CPU 將寄存器中的一個值存儲到內存時,不僅必須將包含了該值的緩 存線讀到 CPU 高速緩存,還必須確保沒有其他 CPU 擁有該緩存線的拷貝。
  • 具體過程:
如果 CPU0 在對一個變量執行“比較并交換”(CAS- Compare and Swap)操作,而該變量所在的緩存線在 CPU7 的高速緩存中,就會發生以下經過簡化的事件序列:
1. CPU0 檢查本地高速緩存,沒有找到緩存線。
2. 請求被轉發到 CPU0 和 CPU1 的互聯模塊,檢查 CPU1 的本地高速緩存,
沒有找到緩存線。
3. 請求被轉發到系統互聯模塊,檢查其他三個管芯,得知緩存線被 CPU6
和 CPU7 所在的管芯持有。
4. 請求被轉發到 CPU6 和 CPU7 的互聯模塊,檢查這兩個 CPU 的高速緩存,
在 CPU7 的高速緩存中找到緩存線。
5. CPU7 將緩存線發送給所屬的互聯模塊,并且刷新自己高速緩存中的緩
存線。
6. CPU6 和 CPU7 的互聯模塊將緩存線發送給系統互聯模塊。
7. 系統互聯模塊將緩存線發送給 CPU0 和 CPU1 的互聯模塊。
8. CPU0 和 CPU1 的互聯模塊將緩存線發送給 CPU0 的高速緩存。
9. CPU0 現在可以對高速緩存中的變量執行 CAS 操作了。
關于CAS

操作的開銷

一些在并行程序中很重要的常見操作開銷如下圖所示。該系統的時鐘周期為0.6ns。在表格的第三列,操作被標準化到了整個時鐘周期,稱作“比率”。

Operation Cost (ns) Ratio
Clock period 0. 1.0
Best-case CAS 37.9 63.2
Best-case lock 65.6 109.3
Single cache miss 139.5 232.5
CAS cache miss 306.0 510.0
Comms Fabric 3,000 5,000
Global Comms 130,000,000 216,000,000

(最好情況下的CAS操作消耗大概40納秒,超過60個時鐘周期。這里的“最 好情況”是指對某一個變量執行CAS操作的CPU正好是最后一個操作該變量的 CPU,所以對應的緩存線已經在 CPU 的高速緩存中了,類似地,最好情況下的 鎖操作(一個“round trip 對”包括獲取鎖和隨后的釋放鎖)消耗超過 60 納秒, 超過 100 個時鐘周期。這里的“最好情況”意味著用于表示鎖的數據結構已經在 獲取和釋放鎖的 CPU 所屬的高速緩存中了。鎖操作比 CAS 操作更加耗時,是因
深入理解并行編程
為鎖操作的數據結構中需要兩個原子操作。
緩存未命中消耗大概 140 納秒,超過 200 個時鐘周期。需要在存儲新值時查
詢變量的舊值的 CAS 操作,消耗大概 300 納秒,超過 500 個時鐘周期。想想這 個,在執行一次 CAS 操作的時間里,CPU 可以執行 500 條普通指令。這表明了 細粒度鎖的局限性。)

<a id="5."></a>5.并行編程領域的基本工具

<a id="5.1"></a>5.1 腳本語言

Linux shell 腳本語言用一種簡單而又有效的方法處理并行化。比如,假設你 有一個叫做 compute_it 的程序,你需要用不同的參數運行兩次。那么只需要這樣 寫:

1 compute_it 1 > compute_it.1.out &
2 compute_it 2 > compute_it.2.out &
3 wait
4 cat compute_it.1.out
5 cat compute_it.2.out

小問題:可是這個愚蠢透頂的 shell 腳本并不是真正的并行程序!這些垃 圾有什么用??

<a id="5.2"></a> 5.2 POSIX API - 支持多進程虛擬化和POSIX線程

(本節淺入淺出地介紹了 POSIX 環境,包括廣泛應用的 pthreads[Ope97]。3.2.1 節介紹了 POSIX 的 fork()和相關原語,3.2.2 節介紹了線程創建和撤銷,3.2.3 節 介紹了 POSIX locking 機制,最后的 3.4 節展示了 Linux 內核中的類似操作。)

  • POSIX進程創建和撤銷
    • 進程通過 fork()原語創建,使用 kill()原語撤銷,也可以用 exit()原語自我撤 銷。執行 fork()的進程被稱為新創建進程的“父進程”。父進程可以通過 wait()原 語等待子進程的執行完畢。
1 int x = 0;
2 int pid;
3
4 pid = fork();
5 if (pid == 0) { /* child */
6 x = 1;
7 printf("Child process set x=1\n");
8 exit(0);
9}
10 if (pid < 0) { /* parent, upon error */ 11 perror("fork");
12 exit(-1);
13 }
14 waitall();
15 printf("Parent process sees x=%d\n", x);
for (;;) {
   pid = wait(&status);
if
} }
(pid == -1) {
if (errno == ECHILD)
break;
perror("wait");
exit(-1);
  • POSIX 線程的創建和撤銷
    • 在一個已有的進程中創建線程,需要調用 pthread_create()
    pthread_t tid;
    if (pthread_create(&tid, NULL, mythread, NULL) != 0) {
    perror("pthread_create"); // perror(); 是錯誤輸出函數。 用來輸出當前的錯誤信息,如果沒有錯誤就顯示ERROR 0。
    exit(-1);
    }
    if (pthread_join(tid, &vp) != 0) {
    perror("pthread_join");
    exit(-1);
    }

// pthread_create()的第一個參數是指向 pthread_t 類型
的指針,用于存放將要創建線程的線程 ID 號,第二個 NULL 參數是一個可選的 指向 pthread_attr_t 結構的指針,第三個參數是新線程將要調用的函數(在本例 中是 mythread()),最后一個 NULL 參數是傳遞給 mythread()的參數。

  • POSIX 鎖 - 互斥鎖pthread_mutex_lock() +現在OSSpinLock已經替換成這個了

    • POSIX 規范允許程序員使用“POSIX 鎖”來避免 data race。POSIX 鎖包括 幾個原語,其中最基礎的是 pthread_mutex_lock()和 pthread_mutex_unlock()。這 些原語操作類型為 pthread_mutex_t 的鎖。該鎖的靜態聲明和初始化由 PTHREAD_MUTEX_INITIALIZER 完成,或者由 pthread_mutex_init()原語來動態 分配并初始化。本節的示例代碼將采用前者。
    • pthread_mutex_lock()原語“獲取”一個指定的鎖,pthread_mutex_unlock()原 語“釋放”一個指定的鎖。如果一對線程 嘗試同時獲取同一把鎖,那么其中一個線程會先“獲準”持有該鎖,另一個線程 只能等待第一個線程釋放該鎖。
    • 如果我想讓多個線程同時獲取同一把鎖會發生什么?
1 pthread_mutex_t lock_a = PTHREAD_MUTEX_INITIALIZER; 
2 pthread_mutex_t lock_b = PTHREAD_MUTEX_INITIALIZER; 
3 int x = 0;
4
5 void *lock_reader(void *arg)
6{
7   int i;
8   int newx = -1;
9   int oldx = -1;
10  pthread_mutex_t *pmlp = (pthread_mutex_t *)arg;
11
12  if (pthread_mutex_lock(pmlp) != 0) {
13      perror("lock_reader:pthread_mutex_lock");
14      exit(-1);
15  }
16  for (i = 0; i < 100; i++) {
17      newx = ACCESS_ONCE(x);
18      if (newx != oldx) {
19          printf("lock_reader(): x = %d\n", newx);
20      }
21      oldx = newx;
22      poll(NULL, 0, 1);
23  }
24  if (pthread_mutex_unlock(pmlp) != 0) {
25      perror("lock_reader:pthread_mutex_unlock");
26      exit(-1);
27  }
28  return NULL;
29}
30
31 void *lock_writer(void *arg)
32 {
33      int i;
34      pthread_mutex_t *pmlp = (pthread_mutex_t *)arg;
35
36      if (pthread_mutex_lock(pmlp) != 0) {
37          perror("lock_reader:pthread_mutex_lock");
38          exit(-1);
39      }
40      for (i = 0; i < 3; i++) {
41          ACCESS_ONCE(x)++;
42          poll(NULL, 0, 5);
43      }
44      if (pthread_mutex_unlock(pmlp) != 0) {
45          perror("lock_reader:pthread_mutex_unlock");
46          exit(-1);
47      }
48      return NULL;
49 }
  • POSIX 讀寫鎖 - 自旋鎖pthread_rwlock_t
    • POSIX API 供了一種讀寫鎖,用 pthread_rwlock_t 類型來表示。和 pthread_mutex_t 一樣,pthread_rwlock_t 也可以由 PTHREAD_RWLOCK_INITILIZER 靜態初始化,或者由 pthread_rwlock_init()原 語動態初始化。pthread_rwlock_rdlock()原語獲取 pthread_rwlock_t 的讀鎖, pthread_rwlock_wrlock()獲取它的寫鎖,pthread_rwlock_unlock()原語負責釋放鎖。 在任意時刻只能有一個線程持有給定 pthread_rwlock_t 的寫鎖,但同時可以有多 個線程持有給定 pthread_rwlock_t 的讀鎖,至少在沒有線程持有寫鎖時是如此。
    • 正如讀者期望的那樣,讀寫鎖是專門為大多數讀的情況設計的。在這種情況 中,讀寫鎖可以 供比互斥鎖大得多的擴展性,因為互斥鎖從定義上已經限制了 任意時刻只能有一個線程持有鎖,而讀寫鎖允許任意多數目的讀者線程同時持有 讀鎖。不過我們需要知道讀寫鎖到底增加了多少可擴展性。

<a id="5.3"></a> 5.3 原子操作

* 讀寫鎖在臨界區最小時開銷最大,考慮到這一點,那么最好 能有其他手段來保護極其短小的臨界區。
* gcc 編譯器 供了許多附加的原子操作,包括__sync_fetch_and_sub()、 __sync_fetch_and_or()、__sync_fetch_and_and()、__sync_fetch_and_xor()和 __sync_fetch_and_nand()原語,這些操作都返回參數的原值。如果你一定需要變 量的新值,可以使用__sync_add_and_fetch()、__sync_sub_and_fetch()、 __sync_or_and_fetch()、__sync_and_and_fetch()、__sync_xor_and_fetch()和 __sync_nand_and_fetch()原語。
* 有一對原語 供了經典的“比較并交換”操作,__sync_bool_compare_and_swap()和__sync_val_compare_and_swap()。
* __sync_synchronize()原語是一個“內存屏障”,它限制編譯器和 CPU 對指令 亂序執行的優化。在某些情況下,只限制編譯器對指令 的優化就足夠了,CPU 的優化可以保留,此時就需要使用 barrier()原語

<a id="5.4"></a>5.4 Linux 內核中類似 POSIX 的操作

(不幸的是,遠在各種標準委員會出現之前,線程操作,加鎖、解鎖原語和原 子操作就已經存在了。因此,這些操作有很多種變體。用匯編語言實現這些操作 也十分常見,不僅因為歷史原因,還因為可以在某些特定場合獲得更好的性能。 比如,gcc 的_sync族原語都是 供 memory-ordering 的語義,這激勵許多程序 員實現自己的函數,來滿足許多不需要 memory ordering 語義的場景。)

POSIX 原語與 Linux 內核函數對應表

Category POSIX Linux Kernel
Thread Management pthread_t struct task_struct
pthread_create() kthread_create
pthread_exit() kthread_should_stop() (rough)
pthread_join() kthread_stop() (rough)
poll(NULL, 0, 5) schedule_timeout_interruptible()
POSIX Locking pthread_mutex_t spinlock_t (rough)
struct mutex
PTHREAD_MUTEX_INITIALIZER DEFINE_SPINLOCK()
DEFINE_MUTEX()
pthread_mutex_lock() spin_lock() (and friends)
mutex_lock() (and friends)
pthread_mutex_unlock() spin_unlock() (and friends)
mutex_unlock()
POSIX Reader-Writer pthread_rwlock_t rwlock_t (rough)
Locking struct rw_semaphore
PTHREAD_RWLOCK_INITIALIZER DEFINE_RWLOCK()
DECLARE_RWSEM()
pthread_rwlock_rdlock() read_lock() (and friends)
down_read() (and friends)
pthread_rwlock_unlock() read_unlock() (and friends)
up_read()
thread_rwlock_wrlock() write_lock() (and friends)
down_write() (and friends)
pthread_rwlock_unlock() write_unlock() (and friends)
up_write()
Atomic Operations C Scalar Types atomic_t
atomic64_t
__sync_fetch_and_add() atomic_add_return()
atomic64_add_return()
__sync_fetch_and_sub() atomic_sub_return()
atomic64_sub_return()
__sync_val_compare_and_swap() cmpxchg()
__sync_lock_test_and_set() xchg() (rough)
__sync_synchronize() smp_mb()

比較 POSIX、gcc 原語和 Linux 內核中使用的版本。精準的對應關系很難給出,因為 Linux 內核有各種各樣的加 鎖、解鎖原語,gcc 則有很多 Linux 內核中不能直接使用的原子操作。當然,一 方面,用戶態的代碼不需要 Linux 內核中各種類型的加鎖、解鎖原語,同時另一 方面,gcc 的原子操作也可以直接用 cmpxchg()來模擬。

<a id="6."></a>6.計數

(計算機能做的事情里,計數也許是最簡單也是最自然的了。不過在一臺大型 的共享內存的多處理器系統上高效并且 scalably(可擴展性) 的計數,仍然具有相當的挑戰性。 更進一步,計數背后隱含概念的簡單性使得我們可以探索并發中的基本問題,而 無需被繁復的數據結構或者復雜的同步原語干擾。因此,計數是并行編程的極佳 切入對象。)

把一個數從1自增加到10億(使用多線程來提高效率)

  1. 不加鎖,非原子訪問,最終可能只得到5億左右。這與CUP讀寫原理有關,讀的時候從主存復制一份到緩存,寫入時更新主存
  2. 加鎖-互斥鎖,能夠得到正確值,但性能太差。

    圖 4.4 是另一種全局原子自增的視角。為了讓每個 CPU 得到機會增加一個指定全局變量,包含變量的緩存線需要在所有 CPU 間傳播,如圖中紅箭頭所示。這種傳播相當耗時,從而導致了糟糕性能。
  3. 統計計數器,給每個線程一個計數器,那么總的計數值就是所有線程計數器值的簡單相加。

    這種做法不再需 要代價昂貴的跨越整個計算機系統的通信。但是這種在“更新”上擴展極佳的方 法,在存在大量線程時,會帶來“讀取”上的巨大代價。
    如何能在保留“更新”側擴展性的同時,減少“讀取”側產生的代價?
  4. 結果一致的實現,之前每次讀取前,都要更新所有線程的計數器以保證數據一致性。現在使用一種弱讀取方式,只是在最終計算完成之后才更新得到正確的值。

近似上限計數器-另一種設計

  • 給每個線程分配固定計算上限(因為之前的做法可能會導致上限溢出,也就是說最終結果超過10億),比如1000。

可是對于由某一個線程創建,但由另一個線程釋放就無法處理了。

那么。。。

<a id="7."></a>7.RCU(Read-Copy Update) 基礎

  • 概述:讀——拷貝——更新(RCU)是一種同步機制,2002 年 10 月引入 Linux 內 核。RCU 允許讀操作可以與更新操作并發執行,這一點 升了程序的可擴展性。 常規的互斥鎖讓并發線程互斥執行,并不關心該線程是讀者還是寫者,而讀寫鎖 在沒有寫者時允許并發的讀者,相比于這些常規鎖操作,RCU 在維護對象的多 個版本時確保讀操作保持一致,同時保證只有所有當前讀端臨界區都執行完畢后 才釋放對象。RCU 定義并使用了高效并且易于擴展的機制,用來發布和讀取對 象的新版本,還用于延后舊版本對象的垃圾收集工作。這些機制恰當地在讀端和 更新端分布工作,讓讀端非常快速。在某些場合下(比如非搶占式內核里),RCU 讀端的函數完全是零開銷。

<a id="7.1"></a>RCU概念

  • RCU 實現必須遵從以下規則:如果 RCU 讀臨界區中的任何語句在一個 grace period 之前,那么 RCU 讀臨界區中的所有語句都必須在 grace period 結束 前完成。



    紅色邊框 "Reader" 框是一個錯誤例子:任何這 樣的 RCU 實現是錯誤的,它允許一個讀臨界區完全覆蓋一個 grace period, 因為 這樣的話,在讀者仍然在使用內存的時候,內存可能已經被釋放。

所以說,會有這樣的 RCU 出現這樣的情況嗎?


  • 必須擴展 grace period,可能如圖 D.59 所示。簡單的說,RCU 實現必須確 保任何開始于特定 grace period 的 RCU 讀臨界區必須在 grace period 允許完成前 全部結束。這個事實可以允許 RCU 驗證專注于如下一點:簡單的證明任何開始 于 grace period 的 RCU 讀臨界區必須在 grace period 結束前結束。這需要充足的 屏障來防止編譯器和 CPU 破壞 RCU 的作用。

<a id="7.2"></a>1.Linux中RCU機制的原理

  • RCU機制是Linux2.6之后提供的一種數據一致性訪問的機制,從RCU(read-copy-update)的名稱上看,我們就能對他的實現機制有一個大概的了解,在修改數據的時候,首先需要讀取數據,然后生成一個副本,對副本進行修改,修改完成之后再將老數據update成新的數據,此所謂RCU。

  • 在操作系統中,數據一致性訪問是一個非常重要的部分,通常我們可以采用鎖機制實現數據的一致性訪問。例如,semaphore、spinlock機制,在訪問共享數據時,首先訪問鎖資源,在獲取鎖資源的前提下才能實現數據的訪問。這種原理很簡單,根本的思想就是在訪問臨界資源時,首先訪問一個全局的變量(鎖),通過全局變量的狀態來控制線程對臨界資源的訪問。但是,這種思想是需要硬件支持的,硬件需要配合實現全局變量(鎖)的讀-修改-寫,現代CPU都會提供這樣的原子化指令。采用鎖機制實現數據訪問的一致性存在如下兩個問題:

  1. 效率問題。鎖機制的實現需要對內存的原子化訪問,這種訪問操作會破壞流水線操作,降低了流水線效率。這是影響性能的一個因素。另外,在采用讀寫鎖機制的情況下,寫鎖是排他鎖,無法實現寫鎖與讀鎖的并發操作,在某些應用下回降低性能。

  2. 擴展性問題。當系統中CPU數量增多的時候,采用鎖機制實現數據的同步訪問效率偏低。并且隨著CPU數量的增多,效率降低,由此可見鎖機制實現的數據一致性訪問擴展性差。

為了解決上述問題,Linux中引進了RCU機制。該機制在多CPU的平臺上比較適用,對于讀多寫少的應用尤其適用。RCU的思路實際上很簡單,下面對其進行描述:

  1. 對于讀操作,可以直接對共享資源進行訪問,但是前提是需要CPU支持訪存操作的原子化,現代CPU對這一點都做了保證。但是RCU的讀操作上下文是不可搶占的(這一點在下面解釋),所以讀訪問共享資源時可以采用read_rcu_lock(),該函數的工作是停止搶占。

  2. 對于寫操作,其需要將原來的老數據作一次備份(copy),然后對備份數據進行修改,修改完畢之后再用新數據更新老數據,更新老數據時采用了rcu_assign_pointer()宏,在該函數中首先屏障一下memory,然后修改老數據。這個操作完成之后,需要進行老數據資源的回收。操作線程向系統注冊回收方法,等待回收。采用數據備份的方法可以實現讀者與寫者之間的并發操作,但是不能解決多個寫著之間的同步,所以當存在多個寫者時,需要通過鎖機制對其進行互斥,也就是在同一時刻只能存在一個寫者。

  3. 在RCU機制中存在一個垃圾回收的daemon(后臺駐留程序),當共享資源被update之后,可以采用該daemon實現老數據資源的回收。回收時間點就是在update之前的所有的讀者全部退出。由此可見寫者在update之后是需要睡眠等待的,需要等待讀者完成操作,如果在這個時刻讀者被搶占或者睡眠,那么很可能會導致系統死鎖。因為此時寫者在等待讀者,讀者被搶占或者睡眠,如果正在運行的線程需要訪問讀者和寫者已經占用的資源,那么死鎖的條件就很有可能形成了。

<a id="7.3"></a>可睡眠 RCU 實現

  • 經典 RCU 要求讀臨界區遵從與自旋鎖臨界區相同的規則:任何類型的阻塞 或者睡眠都是嚴格禁止的。這常常阻礙了 RCU 的使用,Paul 已經收到大量“可 睡眠 RCU”的請求,以允許在 RCU 讀臨界區中可以任意睡眠。以前 Paul 以難 以實現的理由拒絕了所有這些請求,這導致在 grace period 結束時,大量的內存 等待釋放。最終會導致不幸的后果,如圖 D.1 所示,幾乎所有的不幸都是由于內存耗盡而將系統掛起.
  • 在內核中使用 RCU,還是強烈的要求“RCU 讀臨界區永不阻塞”。

從上述分析來看,RCU思想是比較簡單的,其核心內容緊緊圍繞“寫時拷貝”,采用RCU機制,能夠保證在讀寫操作共享資源時,基本不需要取鎖操作,能夠在一定程度上提升性能。但是該機制的應用是有條件的,對于讀多寫少的應用,機制的開銷比較小,性能會大幅度提升,但是如果寫操作較多時,開銷將會增大,性能不一定會有所提升。總體來說,RCU機制是對rw_lock的一種優化。




<a id="smp"></a>SMP對稱多處理結構:

  • SMP的全稱是"對稱多處理"(Symmetrical Multi-Processing)技術,是指在一個計算機上匯集了一組處理器(多CPU),各CPU之間共享內存子系統以及總線結構。
  • 要組建SMP系統,首先最關鍵的一點就是需要合適的CPU相配合
    • 1、CPU內部必須內置APIC(Advanced Programmable Interrupt Controllers)單元。
    • 2、相同的產品型號,同樣類型的CPU核心。
    • 3、完全相同的運行頻率。
    • 4、盡可能保持相同的產品序列編號。
  • 對稱多處理 (SMP) 廣泛應用于 PC 領域,能夠顯著提升臺式計算機的性能。SMP 能使單芯片上多個相同的處理子系統運行相同的指令集,而且都對存儲器、I/O 和外部中斷具有同等的訪問權限。操作系統 (OS) 的單份拷貝就能控制所有內核,使任何處理器都能運行所有的線程,而無需考慮內核、應用或中斷服務的區分
  • SMP 可為各個層面的軟件大幅提高性能。對于不支持 SMP 的軟件,我們可使用操作系統任務管理器在每個內核上啟動進程來實現并行工作。并行進程執行自然會提升性能,雖然其效率不如線程級處理那么高,但也不會對應用開發人員造成更多的設計麻煩。
  • SMP 可在軟件的進程與線程層面上提高性能隨著移動設備性能的不斷提升,用戶應用的復雜性也在不斷增加,在此情況下,應用程序應更多地以并行方式進行編寫(如采用線程方式),因此,我們便可充分發揮 SMP 的真正優勢與增益。線程構成進程,不必反復返回操作系統尋求資源。應用開發人員不但要采用并行方式進行軟件設計,而且還必須注意進程中線程的互動方式。 某些應用本身就是多線程的,從而使 SMP 能夠實現更高的性能,更快的響應時間以及更出色的整體用戶體驗。如 Google 的 Chrome 等 web 瀏覽器就采用了多線程技術,因此能夠與 SMP 技術實現高度互補。預計這些 PC web 瀏覽器所使用的這種技術也將用于移動領域。 Symbian 和 Linux 移動操作系統均全面支持 SMP。這種支持針對移動環境進行了專門優化,將使所有處理器內核的單一操作系統內核映像以及調度器中的負載平衡能夠幫助確定在哪個內核上運行哪個任務或線程。 在處理原有軟件時,我們必須注意正確的任務同步,以避免系統鎖死。在 SMP 系統中,操作系統可在安排低優先級任務運行在一個不同內核上的同時,讓一個具有較高優先級的任務運行在另一個內核上。如果軟件包含不明確的同步,則會產生導致鎖死情況的錯誤判斷。通過正確使用信號量、互斥量以及自旋鎖等軟件技術,SMP 內核的編程軟件將可實現 SMP 的全部優勢。 SMP 系統上的開發與調試工具至關重要。設計人員需要進一步了解芯片情況才能緊跟軟件處理技術。在多個內核上同時運行多個線程的情況下,功能強大的新型工具將可幫助制造商快速向市場推出令人驚奇的全新產品。

<a id="why"></a>為何蘋果雙核性能比安卓4核要高

  • 為何蘋果雙核性能比安卓4核要高,實際上它們內部的晶體管數量幾乎相等;因為蘋果雙核處理器的邏輯核心只有兩個,所以它的多線程性能比安卓4核弱。
  • 蘋果的設計思路選擇了消耗兩倍ARM資源去實現一個單線程好于ARM(Cortex A15)50%-70%的設計。這樣的選擇是基于IOS系統的需要——iOS是一個對于后臺任務限制很嚴格的系統,同時也是一個高度私有化、高度優化的封閉體系,在這樣的體系里蘋果認為單線程性能更加重要一些,因此作出了這樣的選擇。
  • 這讓我回想起之前看過的一篇介紹使用GCD和NSOperation來進行并性編程的建議:就是在主線程獲取數據后,在子線程計算后回調給主線程,更新UI或者做其他的事。

<a id="java"></a>Java

  1. Java是一門面向對象編程語言,不僅吸收了C++語言的各種優點,還摒棄了C++里難以理解的多繼承、指針等概念,因此Java語言具有功能強大和簡單易用兩個特征。Java語言作為靜態面向對象編程語言的代表,極好地實現了面向對象理論,允許程序員以優雅的思維方式進行復雜的編程。
  2. Java具有簡單性、面向對象、分布式、健壯性、安全性、平臺獨立與可移植性、多線程、動態性等特點。
  3. Java可以編寫桌面應用程序、Web應用程序、分布式系統和嵌入式系統應用程序等。
  4. 應用:
    • Android應用
    • 在金融業應用的服務器程序
    • 網站
    • 嵌入式領域
    • 大數據技術。。。

<a id="tpc"></a>數據庫常用壓測工具

Tpcc-mysql

TPC(Tracsactin Processing Performance Council)事務處理性能協會是一個評價大型數據庫系統軟硬件性能的非盈利的組織,TPC-C是TPC協會制定的,用來測試典型的復雜OLTP系統的性能

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,662評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內容