小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

性能優(yōu)化-使用雙buffer實現(xiàn)無鎖隊列

 程序員文庫 2022-01-17

借助本文,實現(xiàn)一種在“讀多寫一”場景下的無鎖實現(xiàn)方式

在我們的工作中,多線程編程是一件太稀松平常的事。在多線程環(huán)境下操作一個變量或者一塊緩存,如果不對其操作加以限制,輕則變量值或者緩存內(nèi)容不符合預(yù)期,重則會產(chǎn)生異常,導(dǎo)致進程崩潰。為了解決這個問題,操作系統(tǒng)提供了鎖、信號量以及條件變量等幾種線程同步機制供我們使用。如果每次操作都使用上述機制,在某些條件下(系統(tǒng)調(diào)用在很多情況下不會陷入內(nèi)核),系統(tǒng)調(diào)用會陷入內(nèi)核從而導(dǎo)致上下文切換,這樣就會對我們的程序性能造成影響。

今天,借助此文,分享一下去年引擎優(yōu)化的一個點,最終優(yōu)化結(jié)果就是在多線程環(huán)境下訪問某個變量,實現(xiàn)了無鎖(lock-free)操作。

背景

對于后端開發(fā)者來說,服務(wù)穩(wěn)定性第一,性能第二,二者相輔相成,缺一不可。

作為IT開發(fā)人員,秉承著一句話:只要程序正常運行,就不要隨便動。所以程序優(yōu)化就一直被擱置,因為沒有壓力,所以就沒有動力嘛??。在去年的時候,隨著廣告訂單數(shù)量越來越多,導(dǎo)致服務(wù)rt上漲,光報警郵件每天都能收到上百封,于是痛定思痛,決定優(yōu)化一版。

秉承小步快跑的理念,決定從各個角度逐步優(yōu)化,從簡單到困難,逐個擊破。所以在分析了代碼之后,準備從鎖這個角度入手,看看能否進行優(yōu)化。

在進行具體的問題分析以及優(yōu)化之前,先看下現(xiàn)有召回引擎的實現(xiàn)方案,后面的方案是針對現(xiàn)有方案的優(yōu)化。

  • 廣告訂單以HTTP方式推送給消息系統(tǒng)

  • 消息系統(tǒng)收到廣告訂單消息后

    • 將廣告訂單消息格式化后推送給消息隊列kafka(第1步)

    • 將廣告訂單消息持久化到DB(第2步)

  • 召回引擎訂閱kafka的topic

    • 從kafka中實時獲取廣告訂單消息,建立并實時更建立維度索引(第3步)

    • 召回引擎接收pv流量,實時計算,并返回滿足定向后的廣告候選集(第4步)

從上面圖中可以看出,召回引擎是一個多線程應(yīng)用,一方面有個線程專門從kafka中獲取最新的廣告訂單消息建立維度索引(此為寫線程),另一方面,接收線上流量,根據(jù)流量屬性,獲取廣告候選集(此為讀線程)。因為召回引擎涉及到同時讀和寫同一塊變量,因此讀寫不能同時操作。

概述

在多線程環(huán)境下,對同一個變量訪問,大致分為以下幾種情況:

  • 多個線程同時讀

  • 多個線程同時寫

  • 一個線程寫,一個線程讀

  • 一個線程寫,多個線程讀

  • 多個線程寫,一個線程讀

  • 多個線程寫,多個線程讀

在上述幾種情況中,多個線程同時讀顯然是線程安全的,而對于其他幾種情況,則需要保證其_互斥排他_性,即讀寫不能同時進行,管他幾個線程讀幾個線程寫,代碼走起。

在上述代碼中,每一個線程對共享變量的訪問,都會通過mutex來加鎖操作,這樣完全就避免了共享變量競爭的問題。

如果對于性能要求不是很高的業(yè)務(wù),上述實現(xiàn)完全滿足需求,但是對于性能要求很高的業(yè)務(wù),上述實現(xiàn)就不是很好,所以可以考慮通過其他方式來實現(xiàn)。

我們設(shè)想一個場景,假如某個業(yè)務(wù),寫操作次數(shù)遠遠小于讀操作次數(shù),例如我們的召回引擎,那么我們完全可以使用讀寫鎖來實現(xiàn)該功能,換句話說_讀寫鎖適合于讀多寫少的場景_。

讀寫鎖其實還是一種鎖,是給一段臨界區(qū)代碼加鎖,但是此加鎖是在進行寫操作的時候才會互斥,而在進行讀的時候是可以共享的進行訪問臨界區(qū)的,其本質(zhì)上是一種自旋鎖。

代碼實現(xiàn)也比較簡單,如下:

在此,說下讀寫鎖的特性:

  • 讀和讀指針沒有競爭關(guān)系

  • 寫和寫之間是互斥關(guān)系

  • 讀和寫之間是同步互斥關(guān)系(這里的同步指的是寫優(yōu)先,即讀寫都在競爭鎖的時候,寫優(yōu)先獲得鎖)

那么,對于一寫多讀的場景,還有沒有可能進行再次優(yōu)化呢?

答案是:有的。

下面,我們將針對一寫多讀,讀多寫少的場景,進行優(yōu)化。

方案

在上一節(jié)中,我們提到對于多線程訪問,可以使用mutex對共享變量進行加鎖訪問。對于一寫多讀的場景,使用讀寫鎖進行優(yōu)化,使用讀寫鎖,在讀的時候,是不進行加鎖操作的,但是當(dāng)有寫操作的時候,就需要加鎖,這樣難免也會產(chǎn)生性能上的影響,在本節(jié),我們提供終極優(yōu)化版本,目的是在寫少讀多的場景下實現(xiàn)lock-free。

如何在讀寫都存在的場景下實現(xiàn)lock-free呢?假設(shè)如果有兩個共享變量,一個變量用來專供寫線程來寫,一個共享變量用來專供讀線程來讀,這樣就不存在讀寫同步的問題了,如下所示:

在上節(jié)中,我們有提到,多個線程對一個變量同時進行讀操作,是線程安全的。一個線程對一個變量進行寫操作也是線程安全的(這不廢話么,都沒人跟它競爭),那么結(jié)合上述兩點,上圖就是線程安全的(多個線程讀一個資源,一個線程寫另外一個資源)。

好了,截止到現(xiàn)在,我們lock-free的雛形已經(jīng)出來了,就是_使用雙變量_來實現(xiàn)lock-free的目標。那么reader線程是如何第一時間能夠訪問writer更新后的數(shù)據(jù)呢?

假設(shè)有兩個共享資源A和B,當(dāng)前情況下,讀線程正在讀資源A。突然在某一個時刻,寫線程需要更新資源,寫線程發(fā)現(xiàn)資源A正在被訪問,那么其更新資源B,更新完資源B后,進行切換,讓讀線程讀資源B,然后寫線程繼續(xù)寫資源A,這樣就能完全實現(xiàn)了lock-free的目標,此種方案也可以成為雙buffer方式。

實現(xiàn)

在上節(jié)中,我們提出了使用雙buffer來實現(xiàn)lock-free的目標,那么如何實現(xiàn)讀寫buffer無損切換呢?

指針互換

假設(shè)有兩個資源,其指針分別為ptrA和ptrB,在某一時刻,ptrA所指向的資源正在被多個線程讀,而ptrB所指向的資源則作為備份資源,此時,如果有寫線程進行寫操作,按照我們之前的思路,寫完之后,馬上啟用ptrA作為讀資源,然后寫線程繼續(xù)寫ptrB所指向的資源,這樣會有什么問題呢?

我們就以std::vector為例,如下圖所示:

在上圖左半部分,假設(shè)ptr指向讀對象的指針,也就是說讀操作只能訪問ptr所指向的對象。

某一時刻,需要對對象進行寫操作(刪除對象Obj4),因為此時ptr = ptrA,因此寫操作只能操作ptrB所指向的對象,在寫操作執(zhí)行完后,將ptr賦值為ptrB(保證后面所有的讀操作都是在ptrB上),即保證當(dāng)前ptr所指向的對象永遠為最新操作,然后寫操作去刪除ptrA中的Obj4,但是此時,有個線程正在訪問ptrA的Obj4,自然而然會輕則當(dāng)前線程獲取的數(shù)據(jù)為非法數(shù)據(jù),重則程序崩潰。

此方案不可行,主要是因為在寫操作的時候,沒有判斷當(dāng)前是否還有讀操作。

原子性

在上述方案中,簡單的變量交換,最終仍然可能存在讀寫同一個變量,進而導(dǎo)致崩潰。那么如果保證在寫的時候,沒有讀是不是就能解決上述問題了呢?如果是的話,那么應(yīng)該如何做呢?

顯然,此問題就轉(zhuǎn)換成如何判斷一個對象上存在線程讀操作。

在上述代碼中

  • 首先創(chuàng)建一個vector,其內(nèi)有兩個Obj的智能指針,這倆智能指針所指向的Obj對象一個供讀線程進行讀操作,一個供寫線程進行寫操作

  • curr_idx代表當(dāng)前可供讀操作對象在obj_buffers的索引,即obj_buffers[curr_idx.load()]所指對象供讀線程進行讀操作

  • 那么相應(yīng)的,obj_buffers[1- curr_idx.load()]所指對象供寫線程進行寫操作

  • 在讀線程中

    • 通過auto tmp = obj_buffers[curr_idx.load()];獲取一個拷貝,由于obj_buffers中存儲的是shared_ptr那么,該對象的引用計數(shù)+1

    • 在tmp上進行讀操作

  • 在寫線程中

    • prepare = 1 - curr_idx.load();在上面我有提到curr_idx指向可讀對象在obj_buffers的索引,換句話說,1 - curr_idx.load()就是另外一個對象即可寫對象在obj_buffers中的索引

    • 通過while循環(huán)判斷另外一個對象的引用計數(shù)是否大于1(如果大于1證明還有讀線程正在進行讀操作)

好了,截止到此,lock-free的實現(xiàn)目標基本已經(jīng)完成。實現(xiàn)原理也也相對來說比較簡單,重點是要保證_寫的時候沒有讀操作_即可。

![image-20211212162535172](/Users/lijun/Library/Application Support/typora-user-images/image-20211212162535172.png)

上圖是召回引擎做了lock-free優(yōu)化后的效果圖,從圖上來看,效果還是很明顯的。

擴展

雙buffer方案在“一寫多讀”的場景下能夠?qū)崿F(xiàn)lock-free的目標,那么對于“多寫一讀”或者“多寫多讀”場景,是否也能夠滿足呢?

答案是不太適合,主要是以下兩個原因:

  • 在多寫的場景下,多個寫之間需要通過鎖來進行同步,雖然避免了對讀寫互斥情況加鎖,但是多線程寫時通常對數(shù)據(jù)的實時性要求較高,如果使用雙buffer,所有新數(shù)據(jù)必須要等到索引切換時候才能使用,很可能達不到實時性要求

  • 多線程寫時若用雙buffer模式,則在索引切換時候也需要給對應(yīng)的對象加鎖,并且也要用類似于上面的while循環(huán)保證沒有現(xiàn)成在執(zhí)行寫入操作時才能進行指針切換,而且此時也要等待讀操作完成才能進行切換,這時候就對備用對象的鎖定時間過長,在數(shù)據(jù)更新頻繁的情況下是不合適的。

缺點

通過前面的章節(jié),我們知道通過雙buffer方式可以實現(xiàn)在一寫多讀場景下的lock-free,該方式要求兩個對象或者buffer最終持有的數(shù)據(jù)是完全一致的,也就是說在單buffer情況下,只需要一個buffer持有數(shù)據(jù)就行,但是雙buffer情況下,需要持有兩份數(shù)據(jù),所以存在內(nèi)存浪費的情況。

其實說白了,雙buffer實現(xiàn)lock-free,就是采用的空間換時間的方式。

結(jié)語

雙buffer方案在多線程環(huán)境下能較好的解決 “一寫多讀” 時的數(shù)據(jù)更新問題,特別是適用于數(shù)據(jù)需要定期更新,且一次更新數(shù)據(jù)量較大的情形。

性能優(yōu)化是一個漫長的不斷自我提升的過程,項目中的一點點優(yōu)化往往就可以使得性能得到質(zhì)的提升。

好了,今天的文章就到這,我們下期見。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多