簡體   English   中英

並行無鎖升序id生成

[英]Parallel lock-free ascending id generation

我有一個地圖,應該將字符串與ID相關聯。 id之間一定不能有間隙,它們必須是從0到N的唯一整數。

請求總是帶有兩個字符串,其中一個,兩個或沒有可能已被索引。 地圖是從ForkJoin池並行構建的,理想情況下我想避免顯式同步塊。 我正在尋找一種最佳方法來最大化吞吐量,無論是否鎖定。

我沒有看到如何使用AtomicInteger而不會為已經存在於地圖中的鍵創建序列間隙。

public class Foo {
    private final Map<String, Integer> idGenerator = new ConcurrentHashMap<>();

    // invoked from multiple threads
    public void update(String key1, String key2) {
      idGenerator.dosomething(key, ?) // should save the key and unique id
      idGenerator.dosomething(key2, ?) // should save the key2 and its unique id
      Bar bar = new Bar(idGenerator.get(key), idGenerator.get(key2));
      // ... do something with bar
   }
}

我認為size()方法結合merge()可能會解決問題,但我不能完全說服自己。 有誰能建議解決這個問題的方法?

編輯

關於重復標志,這不能通過鏈接答案中建議的AtomicInteger.incrementAndGet()來解決。 如果我盲目地為每個字符串執行此操作,則序列中將存在間隙 需要復合操作來檢查密鑰是否存在,然后才生成id。 我一直在尋找一種通過Map API實現這種復合操作的方法。

第二個提供的答案違背了我在問題中特別提出的要求。

沒有辦法按照你想要的方式完成它 - ConcurrentHashMap本身並不是無鎖的。 但是,您可以通過原子方式執行此操作,而無需使用java.util.Map.computeIfAbsent函數進行任何顯式鎖定管理。

這是一個代碼示例,其風格與您提供的內容有關。

ConcurrentHashMap<String, Integer> keyMap = new ConcurrentHashMap<>();
AtomicInteger sequence = new AtomicInteger();

public void update(String key1, String key2) {
    Integer id1 = keyMap.computeIfAbsent(key1, s -> sequence.getAndIncrement());
    Integer id2 = keyMap.computeIfAbsent(key2, s -> sequence.getAndIncrement());

    Bar bar = new Bar(id1, id2);
    // ... do something with bar
}

我不確定你能做到你想要的。 但是,您可以批量處理某些更新,或者與枚舉/添加分開進行檢查。

很多答案都假設訂單並不重要:你需要給出一個數字的所有字符串,但即使在一對中也可以重新排序,對吧? 並發可能已經導致對的重新排序,或者對於成員的成員不能獲得連續的數字,但重新排序可能導致一對中的第一個獲得更高的數字。

延遲並不重要。 該應用程序應該咀嚼大量數據並最終產生輸出。 大多數情況下,應該在地圖中進行搜索

如果大多數搜索命中,那么我們通常需要在地圖上讀取吞吐量。

單個編寫器線程可能就足夠了。

因此,不是直接添加到主映射,並發讀者可以檢查他們的輸入,如果不存在,則將它們添加到要枚舉的隊列並添加到主ConcurrentHashMap。 隊列可以是一個簡單的無鎖隊列,也可以是另一個ConCurrentHashMap,也可以從尚未添加的候選中過濾重復項。 但是可能無鎖隊列很好。

然后你不需要一個原子計數器,或者當兩個線程在它們中的任何一個可以將它添加到地圖之前看到相同的字符串時兩個線程遞增計數器有任何問題。 (因為否則這是個大問題。)

如果有一種方法可以讓編寫器鎖定ConcurrentHashMap以使一批更新更有效,那可能會很好。 但是如果預計命中率非常高,那么你真的希望其他讀者線程盡可能多地過濾重復數據,而我們正在增長而不是暫停它。


為了減少主要前端線程之間的爭用,您可以擁有多個隊列,例如每個線程可能具有單生產者/單個使用者隊列,或者在一對物理核心上運行的一組4個線程共享一個隊列。

枚舉線程從所有這些中讀取。

在讀者不與編寫者競爭的隊列中,枚舉線程沒有爭用。 但是多個隊列可以減少編寫者之間的爭用。 (編寫這些隊列的線程是以只讀方式訪問主ConcurrentHashMap的線程,如果命中率很高,則會占用大部分CPU時間。)


如果Java有這種結構,那么某種讀取 - 復制 - 更新(RCU)數據結構可能會很好 它可以讓讀者繼續全速過濾重復項,而枚舉線程構建一個新表,並完成一批插入,在構建新表時沒有爭用。


命中率達到90%時,一個編寫器線程可能會跟上10個左右的讀取器線程,這些線程會根據主表過濾新的鍵。

您可能希望設置一些隊列大小限制以允許來自單個寫入器線程的反壓。 或者,如果你有更多的內核/線程,而不是單個編寫器可以跟上,那么可能某種並發設置讓多個線程在編號之前消除重復是有幫助的。

或者真的,如果你可以等到最后給所有東西編號,那我覺得這會簡單得多。

我想過可能會嘗試在競爭條件下為錯誤編號,然后回去解決問題,但這可能並不是更好。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM