日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

muduo网络库:09---多线程服务器之(单线程、多线程服务器的适用场合)

發布時間:2024/3/13 编程问答 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 muduo网络库:09---多线程服务器之(单线程、多线程服务器的适用场合) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
  • 本文內容銜接于前一篇文章(進程間通信只用TCP):https://blog.csdn.net/qq_41453285/article/details/104997453

一、服務器開發概述

“服務器開發”包羅萬象,用一句話形容是:跑在多核機器上的Linux用戶態的沒有用戶界面的長期運行(例如wget是不長期運行,httpd是長期運行的)的網絡應用程序,通常是分布式系統的組成部件

并發處理

  • 開發服務端程序的一個基本任務是處理并發連接,現在服務端網絡編程處理并發連接主要有兩種方式:
    • 當“線程”很廉價時,一臺機器上可以創建遠高于CPU數目的“線程”。這時一個線程只處理一個TCP連接(甚至半個),通常使用阻塞 IO(至少看起來如此)。例如,Python gevent、Go goroutine、Erlang actor。這里的“線程”由語言的runtime自行調度,與操作系統線程不是一 回事
    • 當線程很寶貴時,一臺機器上只能創建與CPU數目相當的線程。 這時一個線程要處理多個TCP連接上的IO,通常使用非阻塞IO和IO multiplexing。例如,libevent、muduo、Netty。這是原生線程,能被操 作系統的任務調度器看見
  • 在處理并發連接的同時,也要充分發揮硬件資源的作用,不能讓CPU資源閑置:
    • 以上列出的庫不是每個都能做到這一點
    • 既然討論的是C++編程,那么只考慮后一種方式,這是在Linux下使用native語言編寫用戶態高性能網絡程序的最成熟的模式
    • 本節主要討論的是這些“線程”應該屬于一個進程(以下模式2),還是分屬多個進程(模式3)
  • 本文的“進程”指的是fork系統調用的產物。“線程”指的是pthread_create()的產物,因此是寶貴的那種原生線程。而且我指的Pthreads是NPTL的,每個線程由clone產生,對應一個內核的task_struct

相關模式

  • 首先,一個由多臺機器組成的分布式系統必然是多進程的(字面意義上),因為進程不能跨OS邊界。在這個前提下,我們把目光集中到 一臺機器,一臺擁有至少4個核的普通服務器。如果要在一臺多核機器上提供一種服務或執行一個任務,可用的模式有(這里的“模式”不是pattern,而是model)
    • 1.運行一個單線程的進程
    • 2.運行一個多線程的進程
    • 3.運行多個單線程的進程
    • 4.運行多個多線程的進程
  • 這些模式之間的比較已經是老生常談,簡單地總結如下:
    • 模式1是不可伸縮的(scalable),不能發揮多核機器的計算能力
    • 模式3是目前公認的主流模式。它有以下兩種子模式:
      • 3a簡單地把模式1中的進程運行多份(如果能用多個TCP port對外提供服務的話)
      • 3b主進程+woker進程,如果必須綁定到一個TCP port,比如 httpd+fastcgi
    • 模式2是被很多人所鄙視的,認為多線程程序難寫,而且與模式3 相比并沒有什么優勢
    • 模式4更是千夫所指,它不但沒有結合2和3的優點,反而匯聚了二者的缺點
  • 本文主要想討論的是模式2和模式3b的優劣,即:什么時候一個服務器程序應該是多線程的:
    • 從功能上講,沒有什么是多線程能做到而單 線程做不到的,反之亦然,都是狀態機嘛(我很高興看到反例)
    • 從性能上講,無論是IO bound還是CPU bound的服務,多線程都沒有什么優勢
  • Paul E. McKenney在《Is Parallel Programming Hard, And, If So, What Can You Do About It?》第3.5節指出,“As a rough rule of thumb, use the simplest tool that will get the job done.”。比方說,使用速率為50MB/s的數據壓縮庫、在進程創建銷毀的開銷是800μs、線程創建銷毀的開銷是 50μs的前提下,考慮如何執行壓縮任務:
    • 如果要偶爾壓縮1GB的文本文件,預計運行時間是20s,那么起一個進程去做是合理的,因為進程啟動和銷毀的開銷遠遠小于實際任務的 耗時
    • 如果要經常壓縮500kB的文本數據,預計運行時間是10ms,那么每次都起進程似乎有點浪費了,可以每次單獨起一個線程去做
    • 如果要頻繁壓縮10kB的文本數據,預計運行時間是200μs,那么每次起線程似乎也很浪費,不如直接在當前線程搞定。也可以用一個線程池,每次把壓縮任務交給線程池,避免阻塞當前線程(特別要避免阻塞IO線程)
  • 由此可見,多線程并不是萬靈丹,它有適用的場合。那么究竟什么時候該用多線程?在回答這個問題之前,我先談談必須用單線程的場合

二、必須用單線程的場合

  • 據我所知,有兩種場合必須使用單線程:
    • 1.程序可能會fork
    • 2.限制程序的CPU占用率

①只有單線程程序能fork

  • 根據后面“多線程與fork()”文章的分析,一個設計為可能調用fork的程序必須是單線程的,比如后面“多線程與fork()”文章中提到的“看門狗進程”
  • 多線程程序不是不能調用fork,而是這么做會遇到很多麻煩, 我想不出做的理由
  • 一個程序fork之后一般有兩種行為:
    • 1.立刻執行exec(),變身為另一個程序。例如shell和inetd;又比如 lighttpd fork()出子進程,然后運行fastcgi程序。或者集群中運行在計算 節點上的負責啟動job的守護進程(即我所謂的“看門狗進程”)
    • 2.不調用exec(),繼續運行當前程序。要么通過共享的文件描述符與父進程通信,協同完成任務;要么接過父進程傳來的文件描述符,獨 立完成工作,例如20世紀80年代的Web服務器NCSA httpd
  • 這些行為中,我認為只有“看門狗進程”必須堅持單線程,其他的均可替換為多線程程序(從功能上講)

②單線程程序能限制程序的CPU占用率

  • 單線程程序能限制程序的CPU占用率這個很容易理解
  • 比如在一個8核的服務器上,一個單線程程序即便發生busy-wait(無論是因為bug,還是因為overload),占滿1個core,其CPU使用率也只有12.5%。在這種最壞的情況下,系統還是有87.5%的計算資源可供其他服務進程使用
  • 因此對于一些輔助性的程序,如果它必須和主要服務進程運行在同一臺機器的話(比如它要監控其他服務進程的狀態),那么做成單線程的能避免過分搶奪系統的計算資源,比方說:
    • 如果要把生產服務器上的日志文件壓縮后備份到NFS上,那么應該使用普通單線程壓縮工具 (gzip/bzip2)。它們對系統造成的影響較小,在8核服務器上最多占滿1個core
    • 如果有人為了“提高速度”,開啟了多線程壓縮或者同時起多個進程來壓縮多個日志文件,有可能造成的結果是非關鍵任務耗盡了CPU資源,正常客戶的請求響應變慢。這是我們不愿意看到的

三、單線程程序的優缺點

  • 從編程的角度,單線程程序的優勢無須贅言:簡單
  • 單線程程序的結構:
    • 是一個基于IO multiplexing的event loop。event loop的典型代碼框架參閱前文:https://blog.csdn.net/qq_41453285/article/details/104954338
    • 或者如云風所言,直接用阻塞IO(參閱:http://blog.codingnow.com/2006/04/iocp_kqueue_epoll.html)
  • Event loop有一個明顯的缺點,它是非搶占的:
    • 假設事件a的優先級高于事件b,處理事件a需要1ms,處理事件b需要 10ms。如果事件b稍早于a發生,那么當事件a到來時,程序已經離開了 poll(2)調用,并開始處理事件b。事件a要等上10ms才有機會被處理,總的響應時間為11ms
    • 這等于發生了優先級反轉。這個缺點可以用多線程來克服,這也是多線程的主要優勢

多線程程序有性能優勢嗎

  • 前面說過,無論是IO bound還是CPU bound的服務,多線程都沒有什么絕對意義上的性能優勢。這句話是說,如果用很少的CPU負載就能讓IO跑滿,或者用很少的IO流量就能讓CPU跑滿,那么多線程沒啥用處
  • 舉例來說:
    • 對于靜態Web服務器,或者FTP服務器,CPU的負載較輕,主要瓶頸在磁盤IO和網絡IO方面。這時候往往一個單線程的程序(模式1)就能撐滿IO。用多線程并不能提高吞吐量,因為IO硬件容量已經飽和了。 同理,這時增加CPU數目也不能提高吞吐量
    • CPU跑滿的情況比較少見,這里我只好虛構一個例子。假設有一個服務,它的輸入是n個整數,問能否從中選出m個整數,使其和為 0(這里n<100, m>0)。這是著名的subset sum問題,是NP-Complete 的。對于這樣一個“服務”,哪怕很小的n值也會讓CPU算死。比如n= 30,一次的輸入不過200字節(32-bit整數),CPU的運算時間卻能長達幾分鐘。對于這種應用,模式3a是最適合的,能發揮多核的優勢,程序也簡單
  • 也就是說,無論任何一方早早地先到達瓶頸,多線程程序都沒啥優勢

四、適合多線程程序的場景

  • 我認為多線程的適用場景是:提高響應速度,讓IO和“計算”相互重疊,降低latency(延遲)。雖然多線程不能提高絕對性能,但能提高平均響應性能
  • 一個程序要做成多線程的,大致要滿足:
    • 有多個CPU可用。單核機器上多線程沒有性能優勢(但或許能簡 化并發業務邏輯的實現)
    • 線程間有共享數據,即內存中的全局狀態。如果沒有共享數據, 用模型3b就行。雖然我們應該把線程間的共享數據降到最低,但不代表沒有
    • 共享的數據是可以修改的,而不是靜態的常量表。如果數據不能修改,那么可以在進程間用shared memory,模式3就能勝任
    • 提供非均質的服務。即,事件的響應有優先級差異,我們可以用專門的線程來處理優先級高的事件。防止優先級反轉
    • latency和throughput同樣重要,不是邏輯簡單的IO bound或CPU bound程序。換言之,程序要有相當的計算量
    • 利用異步操作。比如logging。無論往磁盤寫log file,還是往log server發送消息都不應該阻塞critical path
    • 能scale up(按比例增加)。一個好的多線程程序應該能享受增加CPU數目帶來的 好處,目前主流是8核,很快就會用到16核的機器了
    • 具有可預測的性能。隨著負載增加,性能緩慢下降,超過某個臨界點之后會急速下降。線程數目一般不隨負載變化
    • 多線程能有效地劃分責任與功能,讓每個線程的邏輯比較簡單, 任務單一,便于編碼。而不是把所有邏輯都塞到一個event loop里,不同類別的事件之間相互影響
  • 這些條件比較抽象,下面舉兩個具體的(雖然是虛構的)例子

例子①

  • 假設要管理一個Linux服務器機群,這個機群里有8個計算節點,1 個控制節點。機器的配置都是一樣的,雙路四核CPU,千兆網互聯
  • 現在需要編寫一個簡單的機群管理軟件(參考LLNL的SLURM20),這個軟件由3個程序組成:
    • 1.運行在控制節點上的master這個程序監視并控制整個機群的狀態
    • 2.運行在每個計算節點上的slave,負責啟動和終止job,并監控本機的資源
    • 3.供最終用戶使用的client命令行工具,用于提交job
  • 根據前面的分析:
    • slave是個“看門狗進程”,它會啟動別的job進程,因此必須是個單線程程序。另外它不應該占用太多的CPU資源,這也適合單線程模型
    • master應該是個模式2的多線程程序:
      • 它獨占一臺8核的機器,如果用模型1,等于浪費了87.5%的CPU資源
      • 整個機群的狀態應該能完全放在內存中,這些狀態是共享且可變 的。如果用模式3,那么進程之間的狀態同步會成大問題。而如果大量 使用共享內存,則等于是掩耳盜鈴,是披著多進程外衣的多線程程序。 因為一個進程一旦在臨界區內阻塞或crash,其他進程會全部死鎖
      • master的主要性能指標不是throughput,而是latency,即盡快地響 應各種事件。它幾乎不會出現把IO或CPU跑滿的情況
      • master監控的事件有優先級區別,一個程序正常運行結束和異常崩 潰的處理優先級不同,計算節點的磁盤滿了和機箱溫度過高這兩種報警 條件的優先級也不同。如果用單線程,則可能會出現優先級反轉
      • 假設master和每個slave之間用一個TCP連接,那么master采用2個或 4個IO線程來處理8個TCP connections能有效地降低延遲
      • master要異步地往本地硬盤寫log,這要求logging library有自己的 IO線程
      • master有可能要讀寫數據庫,那么數據庫連接這個第三方library可 能有自己的線程,并回調master的代碼
      • master要服務于多個clients,用多線程也能降低客戶響應時間。也 就是說它可以再用2個IO線程專門處理和clients的通信
      • master還可以提供一個monitor接口,用來廣播推送(pushing)機 群的狀態,這樣用戶不用主動輪詢(polling)。這個功能如果用單獨的 線程來做,會比較容易實現,不會搞亂其他主要功能
      • master一共開了10個線程:
        • ?4個用于和slaves通信的IO線程
        • ?1個logging線程
        • ?1個數據庫IO線程
        • ?2個和clients通信的IO線程
        • ?1個主線程,用于做些背景工作,比如job調度
        • ?1個pushing線程,用于主動廣播機群的狀態
      • 雖然線程數目略多于core數目,但是這些線程很多時候都是空閑 的,可以依賴OS的進程調度來保證可控的延遲
  • 綜上所述,master用多線程方式編寫是自然且高效的

例子②

  • 再舉一個TCP聊天服務器的例子,這里的“聊天”不完全指人與人聊 天,也可能是機器與機器“聊天”
  • 這種服務的特點是:并發連接之間有數據交換,從一個連接收到的數據要轉發給其他多個連接
  • 因此我們不能按模式3的做法,把多個連接分到多個進程中分別處理(這會帶來復雜的進程間通信),而只能用模式1或者模式2:
    • 如果純粹只有數據交換, 那么我想模式1也能工作得很好,因為現在的CPU足夠快,單線程應付幾百個連接不在話下
    • 如果功能進一步復雜化:加上關鍵字過濾、黑名單、防灌水等等功能,甚至要給聊天內容自動加上相關連接,每一項功能都會占用CPU資源
      • 這時就要考慮模式2了,因為單個CPU的處理能力顯得捉襟見肘, 順序處理導致消息轉發的延遲增加。這時我們考慮把空閑的多個CPU利用起來,自然的做法是把連接分散到多個線程上,例如按round-robin的 方式把1000個客戶連接分配到4個IO線程上。這樣充分利用多核加速。
      • 具體的例子見“muduo庫簡介之詳解muduo多線程模型”中的方案9,以及“muduo編程實例之“串并轉換”連接服務器機器自動化測試”文章

多線程中線程的分類

  • 據我的經驗,一個多線程服務程序中的線程大致可分為3類:
    • 1.IO線程,這類線程的主循環是IO multiplexing,阻塞地等在select/poll/epoll_wait系統調用上。這類線程也處理定時事件。當然它的功能不止IO,有些簡單計算也可以放入其中,比如消息的編碼或解碼
    • 2.計算線程,這類線程的主循環是blocking queue,阻塞地等在 conditionvariable上。這類線程一般位于thread pool中。這種線程通常不 涉及IO,一般要避免任何阻塞操作
    • 3.第三方庫所用的線程,比如logging,又比如database connection
  • 服務器程序一般不會頻繁地啟動和終止線程。甚至,在我寫過的程序里,create thread只在程序啟動的時候調用,在服務運行期間是不調用的
  • 總結:
    • 在多核時代,要想充分發揮CPU性能,多線程編程是不可避免的,“鴕鳥算法”不是辦法
    • 在學會多線程編程之前,我也一直認為單線程服務程序才是王道。在接觸多線程編程之后,經過一段時間的訓練和適應,我已能比較自如地編寫正確且足夠高效的多線程程序
    • 學習多線 程編程還有一個好處,即訓練異步思維,提高分析并發事件的能力這對設計分布式系統幫助巨大,因為運行在多臺機器上的服務進程本質上是異步的。熟悉多線程編程的話,很容易就能發現分布式系統在消息和 事件處理方面的race condition

五、附加

  • 本專題未完結,參閱下一篇文章(“多線程服務器的適用場合”的例釋與答疑):https://blog.csdn.net/qq_41453285/article/details/105005152

總結

以上是生活随笔為你收集整理的muduo网络库:09---多线程服务器之(单线程、多线程服务器的适用场合)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。