The Truth About Threads(關(guān)于線程的真相 )

如果你從未聽(tīng)過(guò)線程梭冠,這是一個(gè)基本的描述胸遇。

  1. 線程是操作系統(tǒng)(OS)提供的特性
  2. 提供給軟件開(kāi)發(fā)人員料祠,以便他們可以向操作系統(tǒng)表明程序的哪些部分可以并行運(yùn)行
  3. 操作系統(tǒng)決定如何與每個(gè)部分共享CPU資源,就像操作系統(tǒng)決定與同時(shí)運(yùn)行的所有其他不同程序(進(jìn)程)共享CPU資源一樣。

既然你正在讀一本異步通訊的書(shū)具伍,這一定是我要告訴你的部分藕筋,"線程是可怕的纵散,你永遠(yuǎn)不應(yīng)該使用它們,對(duì)嗎?" 不幸的是隐圾,情況并非如此簡(jiǎn)單伍掀。我們需要權(quán)衡使用線程的好處和風(fēng)險(xiǎn),就像任何技術(shù)選擇一樣暇藏。

這本書(shū)不應(yīng)該是關(guān)于線程的.但是這里有兩個(gè)問(wèn)題:異步是作為線程的替代提供的蜜笤,因此如果不進(jìn)行一些比較,就很難理解其價(jià)值主張;即使在使用異步時(shí)盐碱,您仍然可能需要處理線程和進(jìn)程把兔,因此您需要了解一些關(guān)于線程的知識(shí)沪伙。

Benefits of Threading(線程的好處)

這些是線程的主要好處:

  • 代碼易讀
    您的代碼可以并發(fā)地運(yùn)行,但是仍然可以以非常簡(jiǎn)單的县好、自頂向下的線性命令序列進(jìn)行設(shè)置围橡。and this is key—you can pretend, within the body of your functions, that no concurrency is happening.
  • 并發(fā)下的內(nèi)存共享
    您的代碼可以利用多個(gè)cpu,同時(shí)仍然有線程共享內(nèi)存聘惦。這在許多工作負(fù)載中非常重要某饰,例如儒恋,在不同進(jìn)程的不同內(nèi)存空間之間移動(dòng)大量數(shù)據(jù)的成本太高善绎。
  • 專(zhuān)有技術(shù)和現(xiàn)有規(guī)范
    有大量的知識(shí)和最佳實(shí)踐可用于編寫(xiě)線程應(yīng)用程序。這里還有大量現(xiàn)有的“阻塞”代碼诫尽,它們依賴(lài)于多線程進(jìn)行并發(fā)操作禀酱。

現(xiàn)在,對(duì)于Python牧嫉,關(guān)于并行性的觀點(diǎn)是有問(wèn)題的剂跟,因?yàn)镻ython解釋器使用全局鎖,稱(chēng)為全局解釋器鎖(GIL)酣藻,以保護(hù)解釋器本身的內(nèi)部狀態(tài)曹洽。也就是說(shuō),它提供了保護(hù)辽剧,以避免多個(gè)線程之間競(jìng)爭(zhēng)條件的潛在災(zāi)難性影響送淆。鎖的一個(gè)副作用是,它最終會(huì)將程序中的所有線程固定在一個(gè)CPU上怕轿。正如您可以想象的那樣偷崩,這否定了任何并行性性能好處(除非您使用Cython或Numba之類(lèi)的工具來(lái)繞過(guò)限制)。

然而撞羽,關(guān)于可感知的簡(jiǎn)單性阐斜,第一點(diǎn)很重要:在Python中使用線程非常簡(jiǎn)單,如果您以前沒(méi)有遇到過(guò)難以置信的競(jìng)爭(zhēng)條件錯(cuò)誤诀紊,線程提供了一個(gè)非常吸引人的并發(fā)模型谒出。即使您在過(guò)去經(jīng)歷過(guò)痛苦,線程仍然是一個(gè)引人注目的選擇邻奠,因?yàn)槟赡芤呀?jīng)學(xué)會(huì)了(艱難的方法)如何保持代碼簡(jiǎn)單和安全笤喳。

我在這里沒(méi)有篇幅來(lái)討論更安全的線程編程.但是一般來(lái)說(shuō),使用線程的最佳實(shí)踐是從并發(fā)中使用ThreadPoolExecutor類(lèi)惕澎。concurrent.futures.通過(guò)submit()傳遞所有需要的數(shù)據(jù)方法莉测。

from concurrent.futures import ThreadPoolExecutor as Executor
import time
def worker(data):
    # process the data
    pass

data = 'your-input-data'
with Executor(max_workers=10) as exe:
    future = exe.submit(worker, data)

ThreadPoolExecutor提供了一個(gè)非常簡(jiǎn)單的接口,用于在線程中運(yùn)行函數(shù)唧喉。如果需要捣卤,可以使用ProcessPoolExecutor將線程池轉(zhuǎn)換為子進(jìn)程池忍抽。它們有相同的api,這意味著對(duì)代碼的改動(dòng)是最小的董朝。executor API 同樣使用在異步中鸠项。

通常情況下,您會(huì)希望您的任務(wù)是短期的子姜,這樣當(dāng)您的程序需要關(guān)閉時(shí)祟绊,您可以簡(jiǎn)單地調(diào)用Executor.shutdown(wait=True)并等待一兩秒鐘,以允許執(zhí)行程序完成哥捕。

最重要的是:如果可能的話牧抽,您應(yīng)該盡量防止您的線程代碼(在前面的示例中,worker()函數(shù))訪問(wèn)或?qū)懭肴魏稳肿兞?

線程的不足

線程的缺點(diǎn)已經(jīng)在其他一些地方提到過(guò)遥赚,但為了完整起見(jiàn)扬舒,我們還是在這里收集它們:

  • 線程是很困難的
    線程錯(cuò)誤和線程程序中的競(jìng)爭(zhēng)條件是最難修復(fù)的錯(cuò)誤類(lèi)型。有了經(jīng)驗(yàn)凫佛,就有可能設(shè)計(jì)出不容易出現(xiàn)這些問(wèn)題的新軟件讲坎。但在不經(jīng)測(cè)試的、未經(jīng)精心設(shè)計(jì)的軟件中愧薛,它們幾乎是不可能修復(fù)的晨炕,即使是專(zhuān)家
  • 線程是資源密集型
    線程需要額外的操作系統(tǒng)資源來(lái)創(chuàng)建,例如預(yù)先分配的每個(gè)線程的堆椇谅空間瓮栗,它預(yù)先消耗進(jìn)程虛擬內(nèi)存。這是32位操作系統(tǒng)的一個(gè)大問(wèn)題碘箍,因?yàn)槊總€(gè)進(jìn)程的尋址空間限制在3GB遵馆。如今,隨著64位操作系統(tǒng)的廣泛使用丰榴,虛擬內(nèi)存不再像以前那樣珍貴(虛擬內(nèi)存的可尋址空間通常是48位;即货邓。256 TiB) 。 在現(xiàn)代的桌面操作系統(tǒng)中四濒,每個(gè)線程的堆椈豢觯空間所需的物理內(nèi)存甚至在需要時(shí)才由操作系統(tǒng)分配,包括每個(gè)線程的堆椀馏。空間戈二。例如,在一個(gè)擁有8 GB內(nèi)存的現(xiàn)代64位Fedora 29 Linux上喳资,用這個(gè)簡(jiǎn)短的代碼片段創(chuàng)建了10,000個(gè)不做任何事情的線程:
import os
from time import sleep
from threading import Thread
threads = [
Thread(target=lambda: sleep(60)) for i in range(1000)
]
[t.start() for t in threads]
print(f'PID = {os.getpid()}')
[t.join() for t in threads]
圖片.png

預(yù)先分配的虛擬內(nèi)存是驚人的~ 80gb(因?yàn)槊總€(gè)線程有8mb的堆椌蹩裕空間!),但是駐留內(nèi)存只有~ 130mb仆邓。在32位Linux系統(tǒng)上鲜滩,由于3 GB的用戶(hù)空間地址空間限制伴鳖,我無(wú)法創(chuàng)建這么多空間,無(wú)論實(shí)際物理內(nèi)存的消耗是多少徙硅。為了在32位系統(tǒng)上解決這個(gè)問(wèn)題榜聂,有時(shí)需要減少預(yù)配置的堆棧大小,現(xiàn)在在Python中仍然可以這樣做嗓蘑,使用thread .stack_size([size]).顯然须肆,減少堆棧大小會(huì)影響運(yùn)行時(shí)的安全性,這與函數(shù)調(diào)用可以嵌套的程度有關(guān)桩皿,包括遞歸.單線程協(xié)程沒(méi)有這些問(wèn)題豌汇,是并發(fā)I/O的一個(gè)更好的選擇。

  • 線程會(huì)影響吞吐量
    在非常高的并發(fā)級(jí)別(例如业簿,> 5000個(gè)線程)瘤礁,由于上下文切換成本,也會(huì)對(duì)吞吐量產(chǎn)生影響.假設(shè)你能弄明白如何配置你的操作系統(tǒng)梅尤,甚至允許你創(chuàng)建那么多線程.在最近的macOS版本中,例如岩调,為了測(cè)試前面的10,000個(gè)do-nothing-threads示例巷燥,我完全放棄了提高限制的嘗試
  • 線程是死板的
    操作系統(tǒng)將不斷地與所有線程共享CPU時(shí)間,而不管某個(gè)線程是否準(zhǔn)備好工作.例如号枕,一個(gè)線程可能正在等待套接字上的數(shù)據(jù)缰揪,但是OS調(diào)度器仍然可能在任何實(shí)際工作需要完成之前在該線程之間來(lái)回切換數(shù)千次。(在異步世界中葱淳,select()系統(tǒng)調(diào)用用于檢查等待套接字的協(xié)同程序是否需要切換; 否則钝腺,協(xié)同程序甚至不會(huì)被喚醒,完全避免了任何轉(zhuǎn)換成本赞厕。)

這都不是新的知識(shí)艳狐,線程作為編程模型的問(wèn)題也不是特定于平臺(tái)的。例如皿桑,這是關(guān)于線程的Microsoft Visual c++文檔:

Windows API中的中央并發(fā)機(jī)制是線程毫目。通常使用CreateThread函數(shù)創(chuàng)建線程。雖然線程的創(chuàng)建和使用相對(duì)容易诲侮,但是操作系統(tǒng)會(huì)分配大量的時(shí)間和其他資源來(lái)管理它們镀虐。此外,盡管每個(gè)線程都保證與具有相同優(yōu)先級(jí)的任何其他線程接收相同的執(zhí)行時(shí)間沟绪,但是相關(guān)的開(kāi)銷(xiāo)要求您創(chuàng)建足夠大的任務(wù)刮便。對(duì)于更小或更細(xì)粒度的任務(wù),與并發(fā)相關(guān)的開(kāi)銷(xiāo)可能會(huì)超過(guò)并行運(yùn)行任務(wù)的好處绽慈。

但是——我聽(tīng)說(shuō)你們抗議了——這是Windows恨旱,對(duì)吧?Unix系統(tǒng)肯定沒(méi)有這些問(wèn)題吧?下面是來(lái)自Mac開(kāi)發(fā)者庫(kù)的線程編程指南的類(lèi)似建議:

在內(nèi)存使用和性能方面抄肖,線程對(duì)程序(和系統(tǒng))有實(shí)際的成本。每個(gè)線程都需要在內(nèi)核內(nèi)存空間和程序的內(nèi)存空間中分配內(nèi)存窖杀。管理線程和協(xié)調(diào)其調(diào)度所需的核心結(jié)構(gòu)使用有線內(nèi)存存儲(chǔ)在內(nèi)核中漓摩。線程的堆棧空間和每個(gè)線程的數(shù)據(jù)存儲(chǔ)在程序的內(nèi)存空間中入客。大多數(shù)這些結(jié)構(gòu)都是在您第一次創(chuàng)建thread-a進(jìn)程時(shí)創(chuàng)建并初始化的管毙,由于需要與內(nèi)核進(jìn)行交互,這個(gè)進(jìn)程的開(kāi)銷(xiāo)相對(duì)較大桌硫。

它們?cè)?a target="_blank">并發(fā)編程指南中有更進(jìn)一步說(shuō)明:

在過(guò)去夭咬,將并發(fā)引入應(yīng)用程序需要?jiǎng)?chuàng)建一個(gè)或多個(gè)額外的線程。不幸的是铆隘,編寫(xiě)線程代碼具有挑戰(zhàn)性卓舵。線程是一種必須手動(dòng)管理的低級(jí)工具“蚰疲考慮到應(yīng)用程序的最佳線程數(shù)量可以根據(jù)當(dāng)前系統(tǒng)負(fù)載和底層硬件動(dòng)態(tài)更改掏湾,實(shí)現(xiàn)正確的線程解決方案變得非常困難,但不是不可能實(shí)現(xiàn)的肿嘲。此外融击,通常與線程一起使用的同步機(jī)制增加了軟件設(shè)計(jì)的復(fù)雜性和風(fēng)險(xiǎn),卻不能保證提高性能雳窟。

這些主題貫穿始終:

  • 線程使代碼難以推理尊浪。
  • 線程是大規(guī)模并發(fā)(數(shù)千個(gè)并發(fā)任務(wù))的低效模型

接下來(lái),讓我們看一個(gè)關(guān)于線程的案例研究封救,它強(qiáng)調(diào)了第一點(diǎn)拇涤,也是最重要的一點(diǎn)。

案例研究:機(jī)器人和餐具

圖片.png

其次誉结,也是更重要的一點(diǎn)鹅士,我們過(guò)去(現(xiàn)在也仍然不相信)標(biāo)準(zhǔn)的多線程模型,它是共享內(nèi)存搶占式并發(fā):我們?nèi)匀徽J(rèn)為沒(méi)有人能夠在“a = a + 1”是不確定的語(yǔ)言中編寫(xiě)正確的程序搓彻。

我講了一個(gè)餐廳的故事如绸,里面的類(lèi)人機(jī)器人——ThreadBots——做了所有的工作。在這個(gè)比喻里旭贬,每個(gè)工人健康就是一個(gè)線程怔接。在下面的案例中,我們將了解為什么線程被認(rèn)為是不安全的稀轨。

# ThreadBot for table service
import threading
from queue import Queue
from attr import attrs, attrib # making class creation easy.

# attrs 初始化裝飾器
@attrs
class Cutlery:
    knives = attrib(default=0)
    forks = attrib(default=0)

    def give(self, to:'Cutlery', knives=0, forks=0):
        self.change(-knives, -forks)
        to.change(knives, forks)

    def change(self, knives, forks):
        self.knives += knives
        self.forks += forks

# Thread 子類(lèi)
class ThreadBot(threading.Thread):
    tasks = Queue()
    def __int__(self):
        super().__init__(target=self.manage_table)
        # bot 等待table扼脐,同時(shí)響應(yīng)cutlery
        self.cutlery = Cutlery(knives=0, forks=0)
        # 機(jī)器人還將被分配任務(wù)。它們將被添加到這個(gè)任務(wù)隊(duì)列
        # 然后機(jī)器人將在其主處理循環(huán)期間執(zhí)行它們
        # self.tasks = Queue()

    # 這個(gè)機(jī)器人的主要程序是這個(gè)無(wú)限循環(huán)。
    # 如果你需要關(guān)閉一個(gè)bot瓦侮,你必須給他們關(guān)閉任務(wù)艰赞。
    def manage_table(self):
        while True:
            task = self.tasks.get()
            if task == 'prepare table':
                kitchen.give(to=self.cutlery, knives=4, forks=4)
            elif task == 'clear table':
                self.cutlery.give(to=kitchen, knives=4, forks=4)
            elif task == 'shutdown':
                return



kitchen = Cutlery(knives=100, forks=100)
bots = [ThreadBot() for i in range(10)]  # 創(chuàng)建10個(gè)

import sys


for bot in bots:
    for i in range(int(sys.argv[1])):
        # print(bot,type(bot))
        bot.tasks.put('prepare table')
        bot.tasks.put('clear table')
    bot.tasks.put('shutdown')

print('Kitchen inventory before service: ', kitchen)
for bot in bots:
    bot.start()

for bot in bots:
    bot.join()
print("kitchen inventory after service: ", kitchen)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市肚吏,隨后出現(xiàn)的幾起案子方妖,更是在濱河造成了極大的恐慌,老刑警劉巖罚攀,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件党觅,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡斋泄,警方通過(guò)查閱死者的電腦和手機(jī)杯瞻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)炫掐,“玉大人魁莉,你說(shuō)我怎么就攤上這事∧嘉福” “怎么了旗唁?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)摔认。 經(jīng)常有香客問(wèn)我逆皮,道長(zhǎng),這世上最難降的妖魔是什么参袱? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮秽梅,結(jié)果婚禮上抹蚀,老公的妹妹穿的比我還像新娘。我一直安慰自己企垦,他們只是感情好环壤,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著钞诡,像睡著了一般郑现。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上荧降,一...
    開(kāi)封第一講書(shū)人閱讀 49,806評(píng)論 1 290
  • 那天接箫,我揣著相機(jī)與錄音,去河邊找鬼朵诫。 笑死辛友,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的剪返。 我是一名探鬼主播废累,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼邓梅,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了邑滨?” 一聲冷哼從身側(cè)響起日缨,我...
    開(kāi)封第一講書(shū)人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎掖看,沒(méi)想到半個(gè)月后匣距,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乙各,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年墨礁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耳峦。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡恩静,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蹲坷,到底是詐尸還是另有隱情驶乾,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布循签,位于F島的核電站级乐,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏县匠。R本人自食惡果不足惜风科,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望乞旦。 院中可真熱鬧贼穆,春花似錦、人聲如沸兰粉。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)玖姑。三九已至愕秫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間焰络,已是汗流浹背戴甩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留舔琅,地道東北人等恐。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親课蔬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子囱稽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348