1. 程式人生 > 實用技巧 >併發和Read-copy update(RCU)

併發和Read-copy update(RCU)

多執行緒解決方案及效能

程式操作的開銷: (取自<<深入理解並行程式設計>>表3.1)

操 作

開 銷(ns)

比 率

單週期指令

0.6

1.0

最好情況的CAS

37.9

63.2

最好情況的鎖

65.6

109.3

單次快取未命中

139.5

232.5

CAS快取未命中

306.0

510.0

光纖通訊

3,000

5000

全球通訊

130,000,000

216,000,000


併發和Read-copy update(RCU)


2020-06-04 06:20:48 分類專欄:java多執行緒文章標籤:java多執行緒併發程式設計RCUCOW

文章目錄

簡介

在上一篇文章中的併發和ABA問題的介紹中,我們提到了要解決ABA中的memory reclamation問題,有一個辦法就是使用RCU。

詳見ABA問題的本質及其解決辦法,今天本文將會深入的探討一下RCU是什麼,RCU和COW(Copy-On-Write)之間的關係。

RCU(Read-copy update)是一種同步機制,並在2002年被加入了Linux核心中。它的優點就是可以在更新的過程中,執行多個reader進行讀操作。

熟悉鎖的朋友應該知道,對於排它鎖,同一時間只允許一個操作進行,不管這個操作是讀還是寫。

對於讀寫鎖,可以允許同時讀,但是不能允許同時寫,並且這個寫鎖是排他的,也就是說寫的同時是不允許進行讀操作的。

RCU可以支援一個寫操作和多個讀操作同時進行。

Copy on Write和RCU

什麼是Copy on Write? 它和read copy update有什麼關係呢?

我們把Copy on Write簡寫為COW,COW是併發中經常會用到的一種演算法,java裡面就有java.util.concurrent.CopyOnWriteArrayList和java.util.concurrent.CopyOnWriteArraySet。

COW的本質就是,在併發的環境中,如果想要更新某個物件,首先將它拷貝一份,在這個拷貝的物件中進行修改,最後把指向原物件的指標指回更新好的物件。

CopyOnWriteArrayList和CopyOnWriteArraySet中的COW使用在遍歷的時候。

我們知道使用Iterator來遍歷集合的時候,是不允許在Iterator外部修改集合的資料的,只能在Iterator內部遍歷的時候修改,否則會丟擲ConcurrentModificationException。

而對於CopyOnWriteArrayList和CopyOnWriteArraySet來說,在建立Iterator的時候,就對原List進行了拷貝,Iterator的遍歷是在拷貝過後的List中進行的,這時候如果其他的執行緒修改了原List物件,程式正常執行,不會丟擲ConcurrentModificationException。

同時CopyOnWriteArrayList和CopyOnWriteArraySet中的Iterator是不支援remove,set,add方法的,因為這是拷貝過來的物件,在遍歷過後是要被丟棄的。在它上面的修改是沒有任何意義的。

在併發情況下,COW其實還有一個問題沒有處理,那就是對於拷貝出來的物件什麼時候回收的問題,是不是可以馬上將物件回收?有沒有其他的執行緒在訪問這個物件? 處理這個問題就需要用到物件生命週期的跟蹤技術,也就是RCU中的RCU-sync。

所以RCU和COW的關係就是:RCU是由RCU-sync和COW兩部分組成的。

因為java中有自動垃圾回收功能,我們並不需要考慮拷貝物件的生命週期問題,所以在java中我們一般只看到COW,看不到RCU。

RCU的流程和API

我們將RCU和排它鎖和讀寫鎖進行比較。

對於排它鎖來說,需要這兩個API:

lock()
unlock()
  • 1
  • 2

對於對寫鎖來說,需要這四個API:

read_lock()
read_unlock()
write_lock()
write_unlock()
  • 1
  • 2
  • 3
  • 4

而RCU需要下面三個API:

rcu_read_lock()
rcu_read_unlock()
synchronize_rcu()
  • 1
  • 2
  • 3

rcu_read_lock和rcu_read_unlock必須是成對出現的,並且synchronize_rcu不能出現在rcu_read_lock和rcu_read_unlock之間。

雖然RCU並不提供任何排他鎖,但是RCU必須要滿足下面的兩個條件:

  1. 如果Thread1(T1)中synchronize_rcu方法在Thread2(T2)的rcu_read_lock方法之前返回,則happens before synchronize_rcu的操作一定在T2的rcu_read_lock方法之後可見。
  2. 如果T2的rcu_read_lock方法呼叫在T1的synchronize_rcu方法呼叫之前,則happens after synchronize_rcu的操作一定在T2的rcu_read_unlock方法之前不可見。

聽起來很拗口,沒關係,我們畫個圖來理解一下:

記住RCU比較的是synchronize_rcu和rcu_read_lock的順序。

Thread2和Thread3中rcu_read_lock在synchronize_rcu之前執行,則b=2在T2,T3中一定不可見。

Thread4中rcu_read_lock雖然在synchronize_rcu啟動之後才開始執行的,但是rcu_read_unlock是在synchronize_rcu返回之後才執行的,所以可以等同於看做Thread5的情況。

Thread5中,rcu_read_lock在synchronize_rcu返回之後才執行的,所以a=1一定可見。

RCU要注意的事項

RCU雖然沒有提供鎖的機制,但允許同時多個執行緒進行讀操作。注意,RCU同時只允許一個synchronize_rcu操作,所以需要我們自己來實現synchronize_rcu的排它鎖操作。

所以對於RCU來說,它是一個寫多個讀的同步機制,而不是多個寫多個讀的同步機制。

RCU的java實現

最後放上一段大神的RCU的java實現程式碼:

public class RCU {
    final static long NOT_READING = Long.MAX_VALUE;
    final static int MAX_THREADS = 128;
    final AtomicLong reclaimerVersion = new AtomicLong(0);
    final AtomicLongArray readersVersion = new AtomicLongArray(MAX_THREADS);

    public RCU() {
        for (int i=0; i < MAX_THREADS; i++) readersVersion.set(i, NOT_READING);
    }

    public static int getTID() {
        return (int)(Thread.currentThread().getId() % MAX_THREADS);
    }

    public void read_lock(final int tid) {  // rcu_read_lock()
        final long rv = reclaimerVersion.get();
        readersVersion.set(tid, rv);
        final long nrv = reclaimerVersion.get();
        if (rv != nrv) readersVersion.lazySet(tid, nrv);
    }

    public void read_unlock(final int tid) { // rcu_read_unlock()
        readersVersion.set(tid, NOT_READING);
    }

    public void synchronize_rcu() {
        final long waitForVersion = reclaimerVersion.incrementAndGet();
        for (int i=0; i < MAX_THREADS; i++) {
            while (readersVersion.get(i) < waitForVersion) { } // spin
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

簡單講解一下這個RCU的實現:

readersVersion是一個長度為128的Long陣列,裡面存放著每個reader的讀數。預設情況下reader儲存的值是NOT_READING,表示未儲存任何資料。

在RCU初始化的時候,將會初始化這些reader。

read_unlock方法會將reader的值重置為NOT_READING。

reclaimerVersion儲存的是修改的資料,它的值將會在synchronize_rcu方法中進行更新。

同時synchronize_rcu將會遍歷所有的reader,只有當所有的reader都讀取完畢才繼續執行。

最後,read_lock方法將會讀取reclaimerVersion的值。這裡會讀取兩次,如果兩次的結果不同,則會呼叫readersVersion.lazySet方法,延遲設定reader的值。

為什麼要讀取兩次呢?因為雖然reclaimerVersion和readersVersion都是原子性操作,但是在多執行緒環境中,並不能保證reclaimerVersion一定就在readersVersion之前執行,所以我們需要新增一個記憶體屏障:memory barrier來實現這個功能。

總結

本文介紹了RCU演算法和應用。希望大家能夠喜歡。