欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

淺談C++高并發(fā)場(chǎng)景下讀多寫少的優(yōu)化方案

 更新時(shí)間:2022年01月17日 12:44:41   作者:longbozhan  
本文主要介紹了淺談C++高并發(fā)場(chǎng)景下讀多寫少的優(yōu)化方案,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下

來源:https://www.cnblogs.com/longbozhan/p/15780194.html

作者:longbozhan

概述

一談到高并發(fā)的優(yōu)化方案,往往能想到模塊水平拆分、數(shù)據(jù)庫(kù)讀寫分離、分庫(kù)分表,加緩存、加mq等,這些都是從系統(tǒng)架構(gòu)上解決。單模塊作為系統(tǒng)的組成單元,其性能好壞也能很大的影響整體性能,本文從單模塊下讀多寫少的場(chǎng)景出發(fā),探討其解決方案,以其更好的實(shí)現(xiàn)高并發(fā)。
不同的業(yè)務(wù)場(chǎng)景,讀和寫的頻率各有側(cè)重,有兩種常見的業(yè)務(wù)場(chǎng)景:

  • 讀多寫少:典型場(chǎng)景如廣告檢索端、白名單更新維護(hù)、loadbalancer
  • 讀少寫多:典型場(chǎng)景如qps統(tǒng)計(jì)

本文針對(duì)讀多寫少(也稱一寫多讀)場(chǎng)景下遇到的問題進(jìn)行分析,并探討一種合適的解決方案。

分析

讀多寫少的場(chǎng)景,服務(wù)大部分情況下都是處于讀,而且要求讀的耗時(shí)不能太長(zhǎng),一般是毫秒或者更低的級(jí)別;更新的頻率就不是那么頻繁,如幾秒鐘更新一次。通過簡(jiǎn)單的加互斥鎖,騰出一片臨界區(qū),往往能到達(dá)預(yù)期的效果,保證數(shù)據(jù)更新正確。

但是,只要加了鎖,就會(huì)帶來競(jìng)爭(zhēng),即使加的是讀寫鎖,雖然讀之間不互斥,但寫一樣會(huì)影響讀,而且讀寫同時(shí)爭(zhēng)奪鎖的時(shí)候,鎖優(yōu)先分配給寫(讀寫鎖的特性)。例如,寫的時(shí)候,要求所有的讀請(qǐng)求阻塞住,等到寫線程或協(xié)程釋放鎖之后才能讀。如果寫的臨界區(qū)耗時(shí)比較大,則所有的讀請(qǐng)求都會(huì)受影響,從監(jiān)控圖上看,這時(shí)候會(huì)有一根很尖的耗時(shí)毛刺,所有的讀請(qǐng)求都在隊(duì)列中等待處理,如果在下個(gè)更新周期來之前,服務(wù)能處理完這些讀請(qǐng)求,可能情況沒那么糟糕。但極端情況下,如果下個(gè)更新周期來了,讀請(qǐng)求還沒處理完,就會(huì)形成一個(gè)惡性循環(huán),不斷的有讀請(qǐng)求在隊(duì)列中等待,最終導(dǎo)致隊(duì)列被擠滿,服務(wù)出現(xiàn)假死,情況再惡劣一點(diǎn)的話,上游服務(wù)發(fā)現(xiàn)某個(gè)節(jié)點(diǎn)假死后,由于負(fù)載均衡策略,一般會(huì)重試請(qǐng)求其他節(jié)點(diǎn),這時(shí)候其他節(jié)點(diǎn)的壓力跟著增加了,最終導(dǎo)致整個(gè)系統(tǒng)出現(xiàn)雪崩。

因此,加鎖在高并發(fā)場(chǎng)景下要盡量避免,如果避免不了,需要讓鎖的粒度盡量小,接近無鎖(lock-free)更好,簡(jiǎn)單的對(duì)一大片臨界區(qū)加鎖,在高并發(fā)場(chǎng)景下不是一種合適的解決方案

雙緩沖

有一種數(shù)據(jù)結(jié)構(gòu)叫雙緩沖,其這種數(shù)據(jù)結(jié)構(gòu)很常見,例如顯示屏的顯示原理,顯示屏顯示的當(dāng)前幀,下一幀已經(jīng)在后臺(tái)的buffer準(zhǔn)備好,等時(shí)間周期一到,就直接替換前臺(tái)幀,這樣能做到無卡頓的刷新,其實(shí)現(xiàn)的指導(dǎo)思想是空間換時(shí)間,這種數(shù)據(jù)結(jié)構(gòu)的工作原理如下:

  • 數(shù)據(jù)分為前臺(tái)和后臺(tái)
  • 所有讀線程讀前臺(tái)數(shù)據(jù),不用加鎖,通過一個(gè)指針來指向當(dāng)前讀的前臺(tái)數(shù)據(jù)
  • 只有一個(gè)線程負(fù)責(zé)更新,更新的時(shí)候,先準(zhǔn)備好后臺(tái)數(shù)據(jù),接著直接切指針,這之后所有新進(jìn)來的讀請(qǐng)求都看到了新的前臺(tái)數(shù)據(jù)
  • 有部分讀還落在老的前臺(tái)那里處理,因?yàn)楦逻€不算完成,也就不能退出寫線程,寫線程需要等待所有落在老前臺(tái)的線程讀完成后,才能退出,在退出之前,順便再更新一遍老前臺(tái)數(shù)據(jù)(也就當(dāng)前的新后臺(tái)),可以保證前后臺(tái)數(shù)據(jù)一致,這點(diǎn)在做增量更新的時(shí)候有用

工程實(shí)現(xiàn)上需要攻克的難點(diǎn)

寫線程要怎么知道所有的讀線程在老前臺(tái)中的讀完成了呢?

一種做法是讓各個(gè)讀線程都維護(hù)一把鎖,讀的時(shí)候鎖住,這時(shí)候不會(huì)影響其他線程的讀,但會(huì)影響寫,讀完后釋放鎖(某些時(shí)候可能會(huì)有通知寫線程的開銷,但寫本身很少),寫線程只需要確認(rèn)鎖有沒有釋放了,確認(rèn)完了后馬上釋放,確認(rèn)這個(gè)動(dòng)作非??欤ㄐ∮?5ns,1s=103ms=106us=10^9ns),讀線程幾乎不會(huì)感覺到鎖的存在。

每個(gè)線程都有一把自己的鎖,需要用全局的map來做線程id和鎖的映射嗎?

不需要,而且這樣做全局map就要加全局鎖了,又回到了剛開始分析中遇到的問題了。其實(shí),每個(gè)線程可以有私有存儲(chǔ)(thread local storage,簡(jiǎn)稱TLS),如果是協(xié)程,就對(duì)應(yīng)這協(xié)程的TLS(但對(duì)于go語言,官方是不支持TLS的,想實(shí)現(xiàn)類似功能,要么就想辦法獲取到TLS,要么就不要基于協(xié)程鎖,而是用全局鎖,但盡量讓鎖粒度小,本文主要針對(duì)C++語言,暫時(shí)不深入討論其他語言的實(shí)現(xiàn))。這樣每個(gè)讀線程鎖的是自己的鎖,不會(huì)影響到其他的讀線程,鎖的目的僅僅是為了保證讀優(yōu)先。
對(duì)于線程私有存儲(chǔ),可以使用pthread_key_create, pthread_setspecific,pthread_getspecific系列函數(shù)

核心代碼實(shí)現(xiàn)

template <typename T, typename TLS>
int DoublyBufferedData<T, TLS>::Read(
    typename DoublyBufferedData<T, TLS>::ScopedPtr* ptr) { // ScopedPtr析構(gòu)的時(shí)候,會(huì)釋放鎖
    Wrapper* w = static_cast<Wrapper*>(pthread_getspecific(_wrapper_key)); //非首次讀,獲取pthread local lock
    if (BAIDU_LIKELY(w != NULL)) {
        w->BeginRead();    // 鎖住
        ptr->_data = UnsafeRead();
        ptr->_w = w;
        return 0;
    }
    w = AddWrapper();
    if (BAIDU_LIKELY(w != NULL)) {
        const int rc = pthread_setspecific(_wrapper_key, w); // 首次讀,設(shè)置pthread local lock
        if (rc == 0) {
            w->BeginRead();
            ptr->_data = UnsafeRead();
            ptr->_w = w;
            return 0;
        }
    }
    return -1;
}

template <typename T, typename TLS>
template <typename Fn>
size_t DoublyBufferedData<T, TLS>::Modify(Fn& fn) {
    BAIDU_SCOPED_LOCK(_modify_mutex); // 加鎖,保證只有一個(gè)寫
    int bg_index = !_index.load(butil::memory_order_relaxed); // 指向后臺(tái)buffer
    const size_t ret = fn(_data[bg_index]); // 修改后臺(tái)buffer
    if (!ret) {
        return 0;
    }
    // 切指針
    _index.store(bg_index, butil::memory_order_release);    
    bg_index = !bg_index;
    // 等所有讀老前臺(tái)的線程讀結(jié)束
    {
        BAIDU_SCOPED_LOCK(_wrappers_mutex);
        for (size_t i = 0; i < _wrappers.size(); ++i) {
            _wrappers[i]->WaitReadDone();
        }
    }
    // 確認(rèn)沒有讀了,直接修改新后臺(tái)數(shù)據(jù),對(duì)其新前臺(tái)
    const size_t ret2 = fn(_data[bg_index]);
    return ret2;
}

完整實(shí)現(xiàn)請(qǐng)參考brpc的DoublyBufferData

簡(jiǎn)單說說golang中雙緩沖的實(shí)現(xiàn)

普通的雙緩沖加載實(shí)現(xiàn)

基于計(jì)數(shù)器,用atomic,保證原子性,讀進(jìn)入臨界區(qū),計(jì)數(shù)器+1,退出-1,寫判斷計(jì)數(shù)器為0則切換,但計(jì)數(shù)器是全局鎖。這種方案C++也可以采取,只是計(jì)數(shù)器畢竟也是全局鎖,性能會(huì)差那么一丟丟。即使用智能指針shared_ptr,也會(huì)面臨智能指針引用計(jì)數(shù)互斥的問題。之所以用計(jì)數(shù)器,而不用TLS,是因?yàn)間o不支持TLS,對(duì)比TLS版本和計(jì)數(shù)器版本,TLS性能更優(yōu),因?yàn)闆]有搶計(jì)數(shù)器的互斥問題,但搶計(jì)數(shù)器本身很快,性能沒測(cè)試過,可以試試。

sync.Map的實(shí)現(xiàn)

也是基于計(jì)數(shù)器,只是計(jì)數(shù)器是為了讓讀前臺(tái)緩存失效的概率不要太高,有抑制和收斂的作用,實(shí)現(xiàn)了讀的無鎖,少部分情況下,前臺(tái)緩存讀不到數(shù)據(jù)的時(shí)候,會(huì)去讀后臺(tái)緩存,這時(shí)候也要加鎖,同時(shí)計(jì)數(shù)器+1。計(jì)數(shù)器數(shù)值達(dá)到一定程度(超過后臺(tái)緩存的元素個(gè)數(shù)),就執(zhí)行切換

是否適用于讀少寫多的場(chǎng)景

不合適,雙緩沖優(yōu)先保證讀的性能,寫多讀少的場(chǎng)景需要優(yōu)先保證寫的性能。

相關(guān)文獻(xiàn)

brpc對(duì)于雙buffer的描述:https://www.bookstack.cn/read/incubator-brpc/3c7745da34a1418b.md#DoublyBufferedData
go實(shí)現(xiàn)的雙buffer(但讀是互斥的,性能先對(duì)較差):http://blog.codeg.cn/2016/01/27/double-buffering/
雙buffer的三種實(shí)現(xiàn)方案:https://juejin.cn/post/6844904130989801479
一寫多讀:https://blog.csdn.net/lqt641/article/details/55058137
高并發(fā)下的系統(tǒng)設(shè)計(jì):https://www.cnblogs.com/flame540/p/12817529.html
基于計(jì)數(shù)器的實(shí)現(xiàn):https://www.cnblogs.com/gaoxingnjiagoutansuo/p/15773361.html#4998436

到此這篇關(guān)于淺談C++高并發(fā)場(chǎng)景下讀多寫少的優(yōu)化方案的文章就介紹到這了,更多相關(guān)淺談C++高并發(fā)場(chǎng)景下讀多寫少的優(yōu)化方案內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • C++中vector的用法實(shí)例解析

    C++中vector的用法實(shí)例解析

    這篇文章主要介紹了C++中vector的用法,詳細(xì)描述了vector的各種常見的用法及注意事項(xiàng),需要的朋友可以參考下
    2014-08-08
  • STL各個(gè)容器性能詳細(xì)比較

    STL各個(gè)容器性能詳細(xì)比較

    從下面表中的數(shù)據(jù)來看寫入用時(shí)vector和deque很快,因?yàn)樗麄儍?nèi)存分配次數(shù)少,關(guān)聯(lián)容器和list都是一個(gè)一個(gè)分配的,一個(gè)一個(gè)分配也會(huì)造成內(nèi)存碎片,內(nèi)存利用率低
    2013-09-09
  • 純C語言:貪心Prim算法生成樹問題源碼分享

    純C語言:貪心Prim算法生成樹問題源碼分享

    這篇文章主要介紹了貪心Prim算法生成樹問題源碼,有需要的朋友可以參考一下
    2014-01-01
  • 關(guān)于STL的erase()陷阱-迭代器失效問題的總結(jié)

    關(guān)于STL的erase()陷阱-迭代器失效問題的總結(jié)

    下面小編就為大家?guī)硪黄P(guān)于STL的erase()陷阱-迭代器失效問題的總結(jié)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2016-12-12
  • C++11中delete和default的用法詳解

    C++11中delete和default的用法詳解

    這篇文章主要為大家詳細(xì)介紹了C++11中delete和default的具體用法,文中的示例代碼簡(jiǎn)潔易懂,具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以了解下
    2023-08-08
  • C++實(shí)現(xiàn)LeetCode(187.求重復(fù)的DNA序列)

    C++實(shí)現(xiàn)LeetCode(187.求重復(fù)的DNA序列)

    這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(187.求重復(fù)的DNA序列),本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-07-07
  • QT6中QTextcodec頭文件找不到的解決方法

    QT6中QTextcodec頭文件找不到的解決方法

    本文主要介紹了QT6中QTextcodec頭文件找不到的解決方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-02-02
  • C++中std::count函數(shù)介紹和使用場(chǎng)景

    C++中std::count函數(shù)介紹和使用場(chǎng)景

    std::count函數(shù)是一個(gè)非常實(shí)用的算法,它可以幫助我們快速統(tǒng)計(jì)給定值在指定范圍內(nèi)的出現(xiàn)次數(shù),本文主要介紹了C++中std::count函數(shù)介紹和使用場(chǎng)景,感興趣的可以了解一下
    2024-02-02
  • C語言實(shí)現(xiàn)學(xué)生管理系統(tǒng)的源碼分享

    C語言實(shí)現(xiàn)學(xué)生管理系統(tǒng)的源碼分享

    這篇文章主要為大家詳細(xì)介紹了如何利用C語言實(shí)現(xiàn)學(xué)生管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-07-07
  • C++文件輸入輸出fstream使用方法

    C++文件輸入輸出fstream使用方法

    C++標(biāo)準(zhǔn)庫(kù)提供了<fstream>頭文件,其中包含了用于文件輸入輸出的相關(guān)類和函數(shù),本文將詳細(xì)介紹<fstream>頭文件的使用方法,包括函數(shù)原型、打開文件、讀取和寫入文件、以及錯(cuò)誤處理等注意事項(xiàng),感興趣的朋友跟隨小編一起看看吧
    2023-10-10

最新評(píng)論