亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频

蟲蟲首頁| 資源下載| 資源專輯| 精品軟件
登錄| 注冊

您現在的位置是:首頁 > 技術閱讀 >  【性能優化】lock-free在召回引擎中的實現

【性能優化】lock-free在召回引擎中的實現

時間:2024-02-10
大家好!

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

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

背景

對于后端開發者來說,服務穩定性第一,性能第二,二者相輔相成,缺一不可。

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

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

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

  • 廣告訂單以HTTP方式推送給消息系統
  • 消息系統收到廣告訂單消息后
    • 將廣告訂單消息格式化后推送給消息隊列kafka(第1步)
    • 將廣告訂單消息持久化到DB(第2步)
  • 召回引擎訂閱kafka的topic
    • 從kafka中實時獲取廣告訂單消息,建立并實時更建立維度索引(第3步)
    • 召回引擎接收pv流量,實時計算,并返回滿足定向后的廣告候選集(第4步)

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

概述

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

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

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

thread1
{
  std::lock_guard<std::mutex> lock(mtx);
  // do sth(read or write)
}

thread2
{
  std::lock_guard<std::mutex> lock(mtx);
  // do sth(read or write)
}

threadN
{
  std::lock_guard<std::mutex> lock(mtx);
  // do sth(read or write)
}

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

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

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

?

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

?

代碼實現也比較簡單,如下:

writer thread {
 pthread_rwlock_wrlock(&rwlock);
  // do write operation
  pthread_rwlock_unlock(&rwlock);
}

reader thread2 {
  pthread_rwlock_rdlock(&rwlock);
  // do read operation
  pthread_rwlock_unlock(&rwlock);
}

reader threadN {
  pthread_rwlock_rdlock(&rwlock)
  // do read operation
  pthread_rwlock_unlock(&rwlock);
}

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

  • 讀和讀指針沒有競爭關系
  • 寫和寫之間是互斥關系
  • 讀和寫之間是同步互斥關系(這里的同步指的是寫優先,即讀寫都在競爭鎖的時候,寫優先獲得鎖)

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

答案是:有的。

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

方案

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

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

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

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

?

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

?

實現

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

指針互換

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

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

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

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

?

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

?

原子性

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

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

用過std::shared_ptr的都知道,其內部有個成員函數use_count()來判斷當前智能指針所指向變量的訪問個數,代碼如下:

long
      _M_get_use_count() const noexcept
      {
        // No memory barrier is used here so there is no synchronization
        // with other threads.
        return __atomic_load_n(&_M_use_count, __ATOMIC_RELAXED);
      }

那么,我們可以考慮采用智能指針的方案,代碼也比較簡單,如下:

std::atomic_size_t curr_idx = 0;

std::vector<std::shared_ptr<Obj>> obj_buffers;
obj_buffers.emplace_back(std::make_shared<Obj>(...));
obj_buffers.emplace_back(std::make_shared<Obj>(...));

// write thread 

 size_t prepare = 1 - curr_idx.load(); 
 while (obj_buffers[prepare].use_count() > 1) { 
  continue
 } 
 
 obj_buffers[prepare]->load(); 
 curr_idx = prepare; 


 // read thread 
 { 
  auto tmp = obj_buffers[curr_idx.load()]; 
  // do sth
 } 

在上述代碼中

  • 首先創建一個vector,其內有兩個Obj的智能指針,這倆智能指針所指向的Obj對象一個供讀線程進行讀操作,一個供寫線程進行寫操作
  • curr_idx代表當前可供讀操作對象在obj_buffers的索引,即obj_buffers[curr_idx.load()]所指對象供讀線程進行讀操作
  • 那么相應的,obj_buffers[1- curr_idx.load()]所指對象供寫線程進行寫操作
  • 在讀線程中
    • 通過auto tmp = obj_buffers[curr_idx.load()];獲取一個拷貝,由于obj_buffers中存儲的是shared_ptr那么,該對象的引用計數+1
    • 在tmp上進行讀操作
  • 在寫線程中
    • prepare = 1 - curr_idx.load();在上面我有提到curr_idx指向可讀對象在obj_buffers的索引,換句話說,1 - curr_idx.load()就是另外一個對象即可寫對象在obj_buffers中的索引
    • 通過while循環判斷另外一個對象的引用計數是否大于1(如果大于1證明還有讀線程正在進行讀操作)

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

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

擴展

雙buffer方案在“一寫多讀”的場景下能夠實現lock-free的目標,那么對于“多寫一讀”或者“多寫多讀”場景,是否也能夠滿足呢?

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

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

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

缺點

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

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

結語

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

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

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


往期推薦



SDK開發的一些思考

Linux中對【庫函數】的調用進行跟蹤的 3 種【插樁】技巧

【線上問題】P1級公司故障,年終獎不保

探索CPU的調度原理

C++的全鏈路追蹤方案,稍微有點高端

喵哥吐血整理:軟件開發的51條建議


亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频
久久精品国产一区二区电影| 久久免费视频观看| 欧美一区二区三区电影在线观看| 欧美国产日韩一二三区| 一区二区三区精品视频| 国产精品久久久久9999| 免费一级欧美在线大片| 亚洲日本乱码在线观看| 国产精品亚洲а∨天堂免在线| 在线观看日韩专区| 久久综合九色综合欧美就去吻| 国产精品乱子乱xxxx| 亚洲视频电影图片偷拍一区| 欧美日韩在线播放一区二区| 亚洲午夜精品在线| 好吊日精品视频| 蜜臀av国产精品久久久久| 日韩视频免费观看高清在线视频| 欧美精品久久99| 欧美在线视频播放| 亚洲第一在线综合网站| 欧美日韩精品国产| 久久大香伊蕉在人线观看热2| 在线免费观看成人网| 欧美激情视频一区二区三区免费 | 欧美日韩日本网| 亚洲人成在线免费观看| 国产精品二区影院| 亚洲欧美影音先锋| 亚洲肉体裸体xxxx137| 国产婷婷色综合av蜜臀av| 欧美大片在线观看一区| 日韩视频精品在线| 激情综合色综合久久| 国产精品久久久久77777| 欧美va日韩va| 久久激情五月丁香伊人| 亚洲欧美日韩国产一区| 亚洲一区久久久| 亚洲黄色毛片| 怡红院精品视频在线观看极品| 国产女优一区| 国产一区二区三区成人欧美日韩在线观看 | 欧美色综合网| 欧美成人午夜影院| 另类图片国产| 欧美成人免费在线视频| 欧美国产日韩在线| 国产精品乱子久久久久| 欧美久久九九| 欧美午夜不卡影院在线观看完整版免费 | 老鸭窝91久久精品色噜噜导演| 欧美亚洲三级| 亚洲中字黄色| 久久国产精品72免费观看| 久久久亚洲国产美女国产盗摄| 久久亚洲电影| 欧美成人黄色小视频| 欧美成人黄色小视频| 欧美国产在线观看| 欧美日韩精品一区视频| 国产精品美女久久久浪潮软件| 国产精品久久久一本精品| 国产日韩综合| 亚洲欧美在线另类| 国产视频不卡| 亚洲日本久久| 亚洲午夜精品17c| 韩日精品中文字幕| 久久久亚洲高清| 亚洲成人在线观看视频| 欧美色区777第一页| 欧美激情91| 亚洲国产老妈| 久久久久久久性| 国产综合欧美| 欧美激情精品久久久久久免费印度| 国产精品尤物| 亚洲无玛一区| 一区二区欧美亚洲| 亚洲一区二区精品在线| 日韩午夜在线| 亚洲国产精品第一区二区| 国产一区二区三区黄视频| 欧美连裤袜在线视频| 欧美寡妇偷汉性猛交| 欧美激情一区二区在线 | 国产亚洲精品久久久久久| 黄色一区二区三区| 久久精品国产视频| 亚洲第一页中文字幕| 欧美激情精品久久久久久蜜臀| 日韩一区二区免费看| 国产精品午夜国产小视频| 久久av资源网| 亚洲美女视频| 国产欧美一区二区精品婷婷| 久久婷婷国产综合精品青草| 亚洲区中文字幕| 国产麻豆精品theporn| 久久午夜电影网| 日韩午夜激情| 韩国一区电影| 国产精品日韩久久久久| 欧美国产精品一区| 亚洲社区在线观看| 久久精品成人| 国产精品久久久久av免费| 国产一区二区毛片| 亚洲免费高清视频| 久久综合狠狠综合久久激情| 欧美日韩四区| 国产精品看片你懂得| 国产乱码精品一区二区三区忘忧草| 在线午夜精品| 欧美日韩国产首页| 国产综合精品| 欧美电影免费观看大全| 激情欧美亚洲| 老牛国产精品一区的观看方式| 亚洲人精品午夜在线观看| 国外成人在线| 亚洲激情欧美激情| 欧美日韩精品一区二区在线播放| 国产精品久久久一区麻豆最新章节| 欧美午夜精品久久久| 亚洲精选91| 欧美中文字幕第一页| 欧美在线看片| 国产精品一级二级三级| 日韩视频在线一区| 欧美日韩99| 日韩一区二区久久| 欧美另类在线观看| 亚洲一区免费观看| 欧美日韩精品免费看| 在线午夜精品| 亚洲人线精品午夜| 一区二区激情视频| 欧美一级播放| 久久精品一本| 欧美国产视频在线| 欧美丝袜一区二区三区| 国产免费一区二区三区香蕉精| 国产亚洲制服色| 亚洲精品视频中文字幕| 亚洲一区国产视频| 欧美日韩一区二区在线| 欧美色中文字幕| 国产日韩欧美亚洲| 亚洲国产精品尤物yw在线观看 | 国产亚洲欧美日韩精品| 精品成人国产| 亚洲午夜精品| 亚洲女同同性videoxma| 免费国产自线拍一欧美视频| 国产精品国产a| 亚洲精品中文字| 欧美在线免费看| 欧美特黄一区| 亚洲精品欧美日韩| 久久久亚洲影院你懂的| 欧美日韩中文在线| 亚洲激情不卡| 欧美成人精品影院| 国产欧美日韩一区二区三区在线观看 | 欧美在线三区| 欧美人与性禽动交情品| 亚洲电影专区| 久久午夜国产精品| 在线观看亚洲一区| 免费国产自线拍一欧美视频| 在线观看日韩欧美| 久久青草久久| 亚洲国产老妈| 欧美激情成人在线| 99精品欧美一区二区三区综合在线| 欧美不卡在线视频| 亚洲另类视频| 欧美三级在线| 欧美一区二区三区男人的天堂| 国产精品网站视频| 久久人体大胆视频| 亚洲性夜色噜噜噜7777| 国产精品视频男人的天堂| 欧美亚洲综合网| 亚洲激情在线观看| 欧美午夜片在线观看| 欧美一区综合| 亚洲精品久久7777| 国产视频在线观看一区二区三区 | 亚洲一级片在线观看| 国产精品综合网站| 欧美v日韩v国产v| 亚洲欧美日本精品| 在线成人h网| 国产精品美女久久久久aⅴ国产馆| 欧美一区国产在线| 亚洲视屏在线播放| 亚洲精品乱码久久久久久日本蜜臀|