併發不得不說的偽共享

2022-09-22 19:52:18 字數 2223 閱讀 9697

前言

可謂是一入併發深似海,看得越多,發現自己懂的越少,總感覺自己只是瞭解了其冰山一角。但是在研究的過程中越來越感受到一些框架的設計之美,很細膩的趕腳。同時也讓我get到了新的知識點。

cpu快取

truesharing

步入正題,下面是我擷取的disruptor框架中的一段原始碼:

這麼長一段**,主要是為了包裝value這個值。初始看來,也是一頭霧水,不知其所以然,一度認為這種設計還造成記憶體的浪費。後面通過查閱一些資料,才發現在併發情況下這種包裝是多麼的完美,可以大大減少快取不命中的機率。

簡單分析一下:一個long型別的值佔用8個位元組,現在大多數cpu的快取行都是64個位元組的,也就是可以存放8個long型別的單後設資料,現在採用上圖所示的方式載入value到快取行中,可以保證不會存在任意一個有效的值與value共存在同一快取行(這裡預設p1.....p15均是無效值)。

為什麼不能共存在同一快取行?

細心的朋友可能注意到了我上面有一句話:這裡前提是volatile修飾的變數,這裡還得再強調一遍,如果不是volatile修飾的變數,快取行應該是不會立即失效的,也就是還會讀到髒資料。因為cpu保證一個快取行失效並得到確認失效的返回通知相對於cpu來說也是一個很耗時的操作,會白白浪費執行權。所以這裡有個invalidate queues的知識點,cpu會將失效指令寫入到invalidate queues中,然後由使用者自行決定什麼時候執行invalidate queues中的指令。

大概意思就是無效的訊息會進入到一個無效佇列中,但不會立即被處理,因此導致實際上cpu是無法知曉該快取行是失效了的,cpu也無法主動去掃描這個無效佇列,需要記憶體屏障來幫助我們去flush失效佇列。

變數申明為volatile後便會在讀取前有一個read barrier,寫入後有個store barrier,這樣可以使store buffer 與 invalidate queues中的指令都會被重新整理。這樣可以保證所有的寫都能同步的被應用,快取行的失效也會被同步,只不過這裡會導致一些效能上的損耗,但是和正確的進行高併發比起來,這點損耗也是能夠接受的。

falsesharing

下面演示一下偽共享的可怕之處:

public final class falsesharing implements runnable 

}public falsesharing(final int arrayindex)

public static void main(final string args) throws exception

private static void runtest() throws interruptedexception

for (thread t : threads)

for (thread t : threads)

}public void run()

}public final static class volatilelong

}

上面是我分別將num_threads值改為1,2,3,4後的測試結果,每個執行緒進行了5億次迭代,可以發現在public long value = 0l情況下,有沒有填充均對結果無太大影響,最後耗費時間基本持平。但是public volatile long value情況下,填充前後耗費時間成倍增長。由此可以觀察出偽共享的情況下對效能的影響是有多大了吧。

總結要想寫出高效的**必須得對細節把控到位,雖然研究的過程是有些許枯燥,但是不停的get新知識還是很舒服的。上面也許有理解不到位的地方,大家可以一起**一下,共同進步。

end

不得不去奮鬥的原因

一 為了生存 人人都想討飯,個個都得餓死。路上坑愈坑,人間貧愈貧。滿足過去將失去現在,滿足現在將失去將來。春天不耕耘,秋天將無糧。 人家都在...

Python 爬蟲 不得不說的 清洗

為什麼要這樣說呢,因為爬蟲首先是獲得資料,清洗是把非結構化的資料轉換成結果化的資料,這個時候是最考驗人的時候。 如果是國內的 ,清洗工作相對...

一個不得不用deque的情況

過去總以為vector和deque差不多,效率方面deque和vector接近,那乾脆用效率高的vector好了。 但我忽略了另一方,一個事務存在就有它的理由,今天找到程式裡面隱藏的bug給了我不得不用deque的理由, deque和vector的結構很類似,但它是多段連續空間,如果vector空間...