什麼是操做系統html
可能不少人都會說,咱們平時裝的windows7 windows10都是操做系統,沒錯,他們都是操做系統。還有沒有其餘的?python
想一想咱們使用的手機,Google公司的Android系統,Apple公司筆記本上的的MacOSX、IPhone的IOS,他們都是操做系統。linux
那麼咱們想一想,操做系統均可以作什麼?git
咱們買來電腦的後第一件事情就是安裝操做系統,有的電腦則在咱們買來的時候已經安裝好了操做系統,好比說品牌機(Dell、HP、lenovo)。github
咱們能夠在操做系統上經過安裝程序來看視頻,聽音樂,玩遊戲、瀏覽網頁,還能夠彈出移動硬盤和U 盤、管理咱們硬盤中的文件等等;咱們經過操做系統來和計算機交互,系統協調咱們安排給計算機的各類任務。操做系統(Operating System, OS)是指控制和管理整個計算機系統的硬件和軟件資源,併合理地組織調度計算機的工做和資源的分配,以提供給用戶和其餘軟件方便的接口和環境的程序集合。計算機操做系統是隨着計算機研究和應用的發展逐步造成並發展起來的,它是計算機系統中最基本的系統軟件。算法
咱們經過操做系統來使用計算機。 知道了系統是用來作什麼的,咱們來了解一下系統的發展歷史。(操做系統工做方式的演變——20世紀五六十年代)。編程
手工操做(無操做系統)windows
人們先把程序紙帶(或卡片)裝上計算機,而後啓動輸入機把程序和送入計算機,接着經過控制檯開關啓動程序運行。計算完畢,打印機輸出計算結果,用戶卸下並取走紙帶(或卡片)。第二個用戶上機,重複一樣的步驟。緩存
特色: 用戶獨佔機器,CPU等待手工操做,CPU利用不充分。安全
因爲手工操做的滿速度和計算機的高速度之間造成了尖銳矛盾,手工操做的方式是計算機的資源利用率極低,惟一的解決辦法只有擺脫手工操做,實現做業的自動過渡。
批處理系統
批處理系統: 加載計算機上的一個監督軟件,在監督程序的控制下,計算機可以自動的、成批的處理一個或多個用戶的做業(做業包括程序、數據、命令)。
首先出現的是聯機批處理系統,即做業的輸入輸出由CPU來處理。
聯機批處理系統
在主機和輸入機之間增長兩個存儲設備——磁帶機,在監督程序的自動控制下,計算機自動完成任務。
成批的把輸入機上的用戶做業讀入磁帶,依次把磁帶上的用戶做業讀入主機內存並執行,執行完成後把計算結果想輸出機輸出。 完成一批做業後,監督程度又從輸入機讀取做業存入磁帶機。按照上面的步驟重複處理任務。監督程序不停的處理各個做業,實現了做業的自動轉接,減小了做業的創建時間和手工操做時間,有效的克服了人機矛盾,提升了計算機資源的利用率。
問題: 在輸入做業和輸出結果時,CPU仍是會處於線空閒狀態,等待慢速的輸入/輸出設備完成工做——主機處於忙等狀態。
脫機批處理系統
爲了克服與緩解告訴主機與慢速外設(輸入輸出設備),提升CPU利用率,用又引入了脫機批處理系統,即輸入輸出脫離主機控制。
顯著特徵就是:增長一臺不與主機直接相連衛星機。衛星機用來從輸入機上讀取用戶做業並放到磁帶機上;將磁帶機上的執行結果傳給輸出機。這樣主機再也不與慢速的輸入輸出設備鏈接。主機與衛星機二者並行工做,分工明確,可充分發揮主機的告訴計算能力。
問題:每次主機內存中僅存放一道做業,每當它運行期間發出輸入/輸出(I/O)請求後,高速的CPU便處於等待低速的I/O完成狀態,導致CPU空閒。
多道程序系統
多道程序設計技術,就是指容許多個程序同時進入內存並運行。即同時把多個程序放入內存,並容許它們交替在CPU中運行,它們共享系統中的各類硬、軟件資源。當一道程序因I/O請求而暫停運行時,CPU便當即轉去運行另外一道程序。
單道程序運行過程 :在A程序計算時,I/O空閒, A程序I/O操做時,CPU空閒(B程序也是一樣);必須A工做完成後,B才能進入內存中開始工做,二者是串行的,所有完成共需時間=T1+T2。
多道程序運行過程 :將A、B兩道程序同時存放在內存中,它們在系統的控制下,可相互穿插、交替地在CPU上運行:當A程序因請求I/O操做而放棄CPU時,B程序就可佔用CPU運行,這樣 CPU再也不空閒,而正進行A I/O操做的I/O設備也不空閒,顯然,CPU和I/O設備都處於「忙」狀態,大大提升了資源的利用率,從而也提升了系統的效率,A、B所有完成所需時間<T1+T2。
多道程序設計技術不只使CPU獲得充分利用,同時改善I/O設備和內存的利用率,從而提升了整個系統的資源利用率和系統吞吐量(單位時間內處理做業(程序)的個數),最終提升了整個系統的效率
多道:系統內可同時容納多個做業。這些做業放在外存中,組成一個後備隊列,系統按必定的調度原則每次從後備做業隊列中選取一個或多個做業進入內存運行,運行做業結束、退出運行和後備做業進入運行均由系統自動實現,從而在系統中造成一個自動轉接的、連續的做業流。
成批:在系統運行過程當中,不容許用戶與其做業發生交互做用,即:做業一旦進入系統,用戶就不能直接干預其做業的運行。批處理系統的追求目標:提升系統資源利用率和系統吞吐量,以及做業流程的自動化。批處理系統的一個重要缺點:不提供人機交互能力,給用戶使用計算機帶來不便。
雖然用戶獨佔全機資源,而且直接控制程序的運行,能夠隨時瞭解程序運行狀況。但這種工做方式因獨佔全機形成資源效率極低。
20世紀60年代中期,在前述的批處理系統中,引入多道程序設計技術後造成多道批處理系統。
多道批處理系統的一個重要缺點:不提供人機交互能力,給用戶使用計算機帶來不便。雖然用戶獨佔全機資源,而且直接控制程序的運行,能夠隨時瞭解程序運行狀況。但這種工做方式因獨佔全機形成資源效率極低。即便CPU能夠1分鐘運算100W次,若是做業是按照每分鐘100次來作運算,資源被大大浪費。
分時系統
分時技術:把處理機的運行時間分紅很短的時間片,按時間片輪流把處理機分配給各聯機做業使用。若某個做業在分配給它的時間片內不能完成其計算,則該做業暫時中斷,把處理機讓給另外一做業使用,等待下一輪時再繼續其運行。因爲計算機速度很快,做業運行輪轉得很快,給每一個用戶的印象是,好象他獨佔了一臺計算機。而每一個用戶能夠經過本身的終端向系統發出各類操做控制命令,在充分的人機交互狀況下,完成做業的運行。具備上述特徵的計算機系統稱爲分時系統,它容許多個用戶同時聯機使用計算機。
問題: 沒法對特殊任務作出及時響應
實時系統
雖然多道批處理系統和分時系統能得到較使人滿意的資源利用率和系統響應時間,但卻不能知足實時控制與實時信息處理兩個應用領域的需求。因而就產生了實時系統,即系統可以及時響應隨機發生的外部事件,並在嚴格的時間範圍內完成對該事件的處理。
實時系統可分紅兩類:
實時控制系統。當用于飛機飛行、導彈發射等的自動控制時,要求計算機能儘快處理測量系統測得的數據,及時地對飛機或導彈進行控制,或將有關信息經過顯示終端提供給決策人員。當用於軋鋼、石化等工業生產過程控制時,也要求計算機能及時處理由各種傳感器送來的數據,而後控制相應的執行機構。
實時信息處理系統。當用於預約飛機票、查詢有關航班、航線、票價等事宜時,或當用於銀行系統、情報檢索系統時,都要求計算機能對終端設備發來的服務請求及時予以正確的回答。此類對響應及時性的要求稍弱於第一類。
實時操做系統的主要特色:
及時響應,每個信息接收、分析處理和發送的過程必須在嚴格的時間限制內完成。
高可靠性,需採起冗餘措施,雙機系統先後臺工做,也包括必要的保密措施等。
通用操做系統
操做系統的三種基本類型:多道批處理系統、分時系統、實時系統。
具備多種類型操做特徵的操做系統。能夠同時兼有多道批處理、分時、實時處理的功能,或其中兩種以上的功能。
例如:實時處理+批處理=實時批處理系統。首先保證優先處理實時任務,插空進行批處理做業。常把實時任務稱爲前臺做業,批做業稱爲後臺做業。
再如:批處理+分時處理=分時批處理系統。即:時間要求不強的做業放入「後臺」(批處理)處理,需頻繁交互的做業在「前臺」(以去銀行辦理業務,這個銀行只有一個窗口能夠辦理業務。離業務窗口不遠的地方是等候區,二者之間走路須要1分鐘。分時)處理,處理機優先運行「前臺」做業。
從上世紀60年代中期,國際上開始研製一些大型的通用操做系統。這些系統試圖達到功能齊全、可適應各類應用範圍和操做方式變化無窮的環境的目標。可是,這些系統過於複雜和龐大,不只付出了巨大的代價,且在解決其可靠性、可維護性和可理解性方面都遇到很大的困難。
咱們舉例來講明一下系統的發展過程:
最開始的時候,每次只能一我的去業務窗口辦理業務,等第一我的業務辦理完成,回到等候區後,下一我的才能夠去窗口辦理業務。可是,每次在用戶走向/離開業務窗口的時候,都須要等到一分鐘,辦理一我的的業務就須要等待2分鐘。若是業務處理一我的的業務須要一個小時的時候,這個問題並不明顯,可是隨着業務窗口辦理業務的速度加快,變成10分鐘處理一個業務的時候,這個問題就凸顯出來了。不那裏業務的速度越快,問題就明顯。 ( 此時至關於操做系統的發展史中的——手工操做)。
爲了改進上面的問題,安排了一個調度員T,每次調度員T從等候區叫10我的,來窗口排隊辦理業務,這樣就相對上面來講,節省了不少時間,可是還有一個問題。在每次隊伍走向業務窗口和離開窗口的時候,仍是會浪費時間。(此時至關於聯機批處理系統)在此基礎上改進,調度員T 每次安排多個隊伍,在處理第一個隊伍的時候,隊伍2已經被調度員T安排好, 這樣就避免了在 每次隊伍走向業務窗口和離開窗口的時候浪費的時間。(此時至關於聯機批處理系統)若是業務窗口在爲某人辦理業務的時候,辦理業務的人來了個電話,這個時候業務窗口就須要等待他打完電話後才能繼續辦理業務。
爲了解決上述問題,業務窗口又進行了改進,此次是業務窗口一個讓5我的同時等待窗口(而不是窗口前只等待一我的),若是在辦理業務的時候,第一我的來電話,業務窗口就先暫停辦理第一我的的業務,此時去辦理第二我的的業務,若是第二我的此時也來了電話,業務窗口就去辦理第三我的的業務。這樣業務窗口就提升了工做效率。 在相同的時間內辦理了更過的業務。 (此當至關於多道程序系統)辦理業務的時候,一我的獨佔業務窗口,資源效率低。
業務窗口再次進行了改進。 業務窗口同時接待10我的,沒10秒處理一我的的業務,時間到了之後,無論有沒有處理完成當前業務,都會在下一個10秒鐘去處理下一我的的業務,這樣去輪流給10我的處理業務。隨着業務窗口辦理業務速度的提升,變成每一秒處理一我的的業務。這樣對在也窗口前的10我的來講,他們的業務就好像被同時處理同樣。(至關於分時操做系統)。若是這個時候,行長的親戚來辦理業務,可是行長親戚不想等,但願本身的業務立刻被處理。這個時候,就沒法知足需求了。如今沒法知足實時問題的處理。
業務窗口再次改進,對待特殊的業務需求立刻處理。這樣就能夠對實時發生的問題進行處理,實時問題優先處理。(此時至關於實時系統)更形象的例子是,好比咱們在開飛機,忽然發現前面有一座大山,這個時候就須要咱們立刻進行規避動做,躲開大山,對於非實時系統在此時須要有一個響應時間,若是響應時間過長,飛機就會撞山。
因爲辦理業務的時候須要處理多種狀況,將上面的各類狀況進行綜合,吸收各自的優勢,這樣業務窗口就能根據狀況來處理業務。(至關於通用操做系統)早期的操做系統很是多樣化,生產商生產出針對各自硬件的系統。每個操做系統都有很不一樣的命令模式、操做過程和調試工具,即便它們來自同一個生產商。最能反映這一情況的是,廠家每生產一臺新的機器都會配備一套新的操做系統。
同一廠家相同的操做系統
這種狀況一直持續到二十世紀六十年代IBM公司開發了System/360系列機器。儘管這些機器在性能上有明顯的差別,可是他們有統一的操做系統——OS/360
1965年時,AT&T公司 下貝爾實驗室(Bell Labs)加入一項由奇異電子(General Electric)和麻省理工學院(MIT)合做的計劃;該計劃要創建一套多使用者、多任務、多層次(multi-user、multi- processor、multi-level)的MULTICS操做系統。
Multics 的目標是整合分時技術以及當時其餘先進技術,容許用戶在遠程終端經過電話(撥號)登陸到主機,而後能夠編輯文檔,閱讀電子郵件,運行計算器等等。可是項目目標太過激進,進度嚴重滯後。最後,直到1969年AT&T 高層決定放棄這個項目。
其中有一個 叫Ken Thompson 的人 ,由於工做須要,他但願開發一個小小的做業系統,,他花了一個月的時間 在這臺PDP-7上寫了一個做業系統,和一些經常使用的工具程序,——這就是鼎鼎大名的Unics——後被更名爲Unix。
到了1970年,PDP-7卻只能支持兩個用戶 ,由於PDP-7的性能不佳,肯·湯普遜 與丹尼斯·裏奇決定把初版UNIX移植到PDP-11/20的機器上,開發第二版UNIcs。在性能提高後,真正能夠提供多人同時使用, 布萊恩·柯林漢提議將它的名稱改成UNIX。
Unix被稱爲計算機/互聯網行業的基石。
肯·湯普遜的同事看到他寫的程序很好用,都開始使用這個系統,中間通過了屢次改版。
因爲當時的機器結構不一樣,因此每次安裝系統時,都須要從新編寫一遍。初版的Unix是使用匯編語言和B語言來開發的,B語言不夠強大,因此Thompson和Ritchie對其進行了改造,並於1971年共同發明了C語言。
1973年Thompson和丹尼斯·裏奇用C語言重寫了UNIX。這個時候,Unix的正式版本發行了。
同年,學術界參與到UNIX的開發工做中,重要的就是加州伯克利(Berkeley)大學。伯克利大學的Bill Joy在獲取了UNIX的核心源碼後,着手修改爲適合本身機器的版本,而且同時增長了不少工具軟件與編譯程序,最終將其命名爲Berkeley Software Distribution(BSD)。
因爲UNIX的高度可移植性與強大的性能,加上當時並無版權糾紛,因此不少商業公司開始了UNIX操做系統的開發,例如AT&T本身的System V、IBM的AIX以及HP與DEC等公司,都採用本身的主機與本身的UNIX操做系統。但當時 並無統一的硬件標準,不一樣公司生產的硬件 不同,不一樣公司開發的的程序 沒法兼容使用,只能運行在本身公司生產的硬件裏。這個時候也沒有人針對我的電腦來開發unix系統。
Windows系統、蘋果系統? 先看一下下圖:
一直到1979年,AT&T推出 System V 第七版 Unix ,這個時候開始支持我的電腦。出於商業上的考慮,AT&T決定收回unix的版權,最重要的就是不可對學生提供源代碼。學校受到很大的衝擊,教學受到影響。
這個時候有一位 Andrew Tanenbaum(譚邦寧)教授,在1984-1986年間寫了一個叫Minix的Unix Like 核心程序;意思爲:mini unix,而且與 unix兼容、支持X86 我的電腦。爲避免版權糾紛,在編寫的時候不看unix的源代碼。因爲譚邦寧教授 認爲Minix主要用在教育事業上,因此對MInix的開發只是點到爲止,不能知足用戶的需求。在1988年間,Linus Torvalds進入了赫爾辛基大學,選讀了計算機科學系。在就學期間,託瓦茲接觸到了Unix 這個操做系統,可是使用unix須要等待,其餘人使用的時候他就沒法使用。他就想「我爲何不本身搞一部Unix玩?」 , 不久以後,他就據說有一個相似 Unix 的系統,和 Unix 徹底兼容,還能夠在 Intel 386 機器上面跑的操做系統,因而他在購買了最新的 Intel 386 的我的計算機後,就當即安裝了 Minix 這個操做系統。
託瓦茲跟在研究Minix的過程當中,發現 Minix 雖然真的很棒,可是譚寧邦教授就是不肯意進行功能的增強,致使一堆工程師在操做系統功能上面的慾求不滿! 這個時候年輕的託瓦茲就想:『既然如此,那我何不本身來改寫一個我想要的操做系統?』 二是他就開始了核心程序的撰寫了。到了1991年,Linus Torvalds在BBS上面貼了一則消息,宣稱他以bash、gcc等工具寫了一個小小的核心程序,不過還不夠,他但願這個程序能夠得到你們的一些修改建議,這個核心程序能夠在Intel的386機器上運行。同時提供了下載地址。這讓不少人感興趣,今後便開始了Linux不平凡的路程。
Linux 應用領域
企業服務器 - 企業
嵌入式 - 手機、我的數字助理(PDA)、消費性電子產品及航空航天等領域中
桌面- 我的電腦
其餘
Android
2003 安迪·魯賓創辦了Android公司 。Android--基於Linux內核的開放移動操做系統
2005年,Android公司被Google收購。
2007年11月5日,谷歌公司正式公佈Android操做系統。
蘋果公司
Apple-I
Macintosh,簡稱Mac
微軟
Windows timeline history
蘋果與微軟
1973年施樂公司開發除了Alto——真正意義上的我的PC,有鍵盤、顯示器、圖形界面、以太網等。 可是並無重視。
1979年,喬布斯據說了Alto,決定去看看,看到之後震驚了,回去就讓技術人員去實現圖形界面,爲此還從施樂挖了好多技術人員,開發Lisa項目;而後最後失敗了。可是爲後來的Macintosh,積攢了好多經驗。
1980年微軟和IBM合做PC系統,微軟以捆綁的方式在IBM-PC上預裝DOS,廉價銷售(5$)許可證。
1981年,喬布斯邀請蓋茨看Macintosh樣機,想讓微軟幫助Macintosh開發應用軟件,蓋茨看到Macintosh的圖形後,也震驚了,心想,這東西要是上市,個人DOS立馬完蛋,將來的天下是圖形的。不過喬布斯看出了蓋茨的信息,就要求微軟在給蘋果開發軟件過程當中學到的東西用於任何非蘋果的設備上。可是喬布斯忽略了,不讓爲微軟編寫相似Macintosh的系統。
蓋茨瞭解到Macintosh效法於施樂,因而也從施樂挖人,開發本身的圖形系統——windows。微軟把win的研發放在第一位,耽誤了Macintosh的發佈。
1984年Macintosh 發佈,風靡世界。
1985年 windows1.0發佈,喬布斯發現win很想Macintosh,就說蓋茨偷了蘋果的東西。
進程與線程的一個簡單解釋
進程(process)和線程(thread)是操做系統的基本概念,可是它們比較抽象,不容易掌握。
最近,我讀到一篇材料,發現有一個很好的類比,能夠把它們解釋地清晰易懂。
1.
計算機的核心是CPU,它承擔了全部的計算任務。它就像一座工廠,時刻在運行。
2.
假定工廠的電力有限,一次只能供給一個車間使用。也就是說,一個車間開工的時候,其餘車間都必須停工。背後的含義就是,單個CPU一次只能運行一個任務。
3.
進程就比如工廠的車間,它表明CPU所能處理的單個任務。任一時刻,CPU老是運行一個進程,其餘進程處於非運行狀態。
4.
一個車間裏,能夠有不少工人。他們協同完成一個任務。
5.
線程就比如車間裏的工人。一個進程能夠包括多個線程。
6.
車間的空間是工人們共享的,好比許多房間是每一個工人均可以進出的。這象徵一個進程的內存空間是共享的,每一個線程均可以使用這些共享內存。
7.
但是,每間房間的大小不一樣,有些房間最多隻能容納一我的,好比廁所。裏面有人的時候,其餘人就不能進去了。這表明一個線程使用某些共享內存時,其餘線程必須等它結束,才能使用這一塊內存。
8.
一個防止他人進入的簡單方法,就是門口加一把鎖。先到的人鎖上門,後到的人看到上鎖,就在門口排隊,等鎖打開再進去。這就叫"互斥鎖"(Mutual exclusion,縮寫 Mutex),防止多個線程同時讀寫某一塊內存區域。
9.
還有些房間,能夠同時容納n我的,好比廚房。也就是說,若是人數大於n,多出來的人只能在外面等着。這比如某些內存區域,只能供給固定數目的線程使用。
10.
這時的解決方法,就是在門口掛n把鑰匙。進去的人就取一把鑰匙,出來時再把鑰匙掛回原處。後到的人發現鑰匙架空了,就知道必須在門口排隊等着了。這種作法叫作"信號量"(Semaphore),用來保證多個線程不會互相沖突。
不難看出,mutex是semaphore的一種特殊狀況(n=1時)。也就是說,徹底能夠用後者替代前者。可是,由於mutex較爲簡單,且效率高,因此在必須保證資源獨佔的狀況下,仍是採用這種設計。
11.
操做系統的設計,所以能夠歸結爲三點:
(1)以多進程形式,容許多個任務同時運行;
(2)以多線程形式,容許單個任務分紅不一樣的部分運行;
(3)提供協調機制,一方面防止進程之間和線程之間產生衝突,另外一方面容許進程之間和線程之間共享資源。
(完)
摘要地址:http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
學習電腦和編程語言就會遇到進程和線程,初學者每每會在此陷入迷茫和糾結中。其實弄清這兩個概念不是很難。從必定意義上講,進程就是一個應用程序在處理機上的一次執行過程,它是一個動態的概念,而線程是進程中的一部分,進程包含多個線程在運行。
一般在一個進程中能夠包含若干個線程,它們能夠利用進程所擁有的資源。在引入線程的操做系統中,一般都是把進程做爲分配資源的基本單位,而把線程做爲獨立運行和獨立調度的基本單位。因爲線程比進程更小,基本上不擁有系統資源,故對它的調度所付出的開銷就會小得多,能更高效的提升系統內多個程序間併發執行的程度。
進程與線程的區別:
進程和線程的主要差異在於它們是不一樣的操做系統資源管理方式。進程有獨立的地址空間,一個進程崩潰後,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不一樣執行路徑。線程有本身的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉就等於整個進程死掉,因此多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。但對於一些要求同時進行而且又要共享某些變量的併發操做,只能用線程,不能用進程。
1) 簡而言之,一個程序至少有一個進程,一個進程至少有一個線程.
2) 線程的劃分尺度小於進程,使得多線程程序的併發性高。
3) 另外,進程在執行過程當中擁有獨立的內存單元,而多個線程共享內存,從而極大地提升了程序的運行效率。
4) 線程在執行過程當中與進程仍是有區別的。每一個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。可是線程不可以獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
5) 從邏輯角度來看,多線程的意義在於一個應用程序中,有多個執行部分能夠同時執行。但操做系統並無將多個線程看作多個獨立的應用,來實現進程的調度和管理以及資源分配。這就是進程和線程的重要區別。
說說優缺點
線程和進程在使用上各有優缺點:線程執行開銷小,但不利於資源的管理和保護;而進程正相反。同時,線程適合於在SMP(多核處理機)機器上運行,而進程則能夠跨機器遷移。
進程的基本狀態:
1、就緒(Ready)狀態
當進程已分配到除CPU之外的全部必要資源後,只要在得到CPU,即可當即執行,進程這時的狀態就稱爲就緒狀態。在一個系統中處於就緒狀態的進程可能有多個,一般將他們排成一個隊列,稱爲就緒隊列。
2、執行狀態
進程已得到CPU,其程序正在執行。在單處理機系統中,只有一個進程處於執行狀態;再多處理機系統中,則有多個進程處於執行狀態。
3、阻塞狀態
正在執行的進程因爲發生某事件而暫時沒法繼續執行時,便放棄處理機而處於暫停狀態,亦即程序的執行受到阻塞,把這種暫停狀態稱爲阻塞狀態,有時也稱爲等待狀態或封鎖狀態。
三種進程之間的轉換圖:
# 管道( pipe ):管道是一種半雙工的通訊方式,數據只能單向流動,並且只能在具備親緣關係的進程間使用。進程的親緣關係一般是指父子進程關係。
# 有名管道 (named pipe) : 有名管道也是半雙工的通訊方式,可是它容許無親緣關係進程間的通訊。
# 信號量( semophore ) : 信號量是一個計數器,能夠用來控制多個進程對共享資源的訪問。它常做爲一種鎖機制,防止某進程正在訪問共享資源時,其餘進程也訪問該資源。所以,主要做爲進程間以及同一進程內不一樣線程之間的同步手段。
# 消息隊列( message queue ) : 消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點。
# 信號 ( sinal ) : 信號是一種比較複雜的通訊方式,用於通知接收進程某個事件已經發生。
# 共享內存( shared memory ) :共享內存就是映射一段能被其餘進程所訪問的內存,這段共享內存由一個進程建立,但多個進程均可以訪問。共享內存是最快的 IPC 方式,它是針對其餘進程間通訊方式運行效率低而專門設計的。它每每與其餘通訊機制,如信號兩,配合使用,來實現進程間的同步和通訊。
# 套接字( socket ) : 套解口也是一種進程間通訊機制,與其餘通訊機制不一樣的是,它可用於不一樣及其間的進程通訊。
進程有不少優勢,它提供了多道編程,讓咱們感受咱們每一個人都擁有本身的CPU和其餘資源,能夠提升計算機的利用率。不少人就不理解了,既然進程這麼優秀,爲何還要線程呢?其實,仔細觀察就會發現進程仍是有不少缺陷的,主要體如今兩點上:
進程只能在一個時間幹一件事,若是想同時幹兩件事或多件事,進程就無能爲力了。
進程在執行的過程當中若是阻塞,例如等待輸入,整個進程就會掛起,即便進程中有些工做不依賴於輸入的數據,也將沒法執行。
例如,咱們在使用qq聊天, qq作爲一個獨立進程若是同一時間只能幹一件事,那他如何實如今同一時刻 即能監聽鍵盤輸入、又能監聽其它人給你發的消息、同時還能把別人發的消息顯示在屏幕上呢?你會說,操做系統不是有分時麼?但個人親,分時是指在不一樣進程間的分時呀, 即操做系統處理一會你的qq任務,又切換到word文檔任務上了,每一個cpu時間片分給你的qq程序時,你的qq仍是隻能同時幹一件事呀。
再直白一點, 一個操做系統就像是一個工廠,工廠裏面有不少個生產車間,不一樣的車間生產不一樣的產品,每一個車間就至關於一個進程,且你的工廠又窮,供電不足,同一時間只能給一個車間供電,爲了能讓全部車間都能同時生產,你的工廠的電工只能給不一樣的車間分時供電,可是輪到你的qq車間時,發現只有一個幹活的工人,結果生產效率極低,爲了解決這個問題,應該怎麼辦呢?。。。。沒錯,你確定想到了,就是多加幾個工人,讓幾我的工人並行工做,這每一個工人,就是線程!
什麼是線程
Threading用於提供線程相關的操做,線程是應用程序中工做的最小單元。
threading模塊對象
描述 | |
Thread | 表示一個線程的執行的對象 |
Lock | 鎖原語對象 |
RLock | 可重入鎖對象。使單線程能夠再次得到已經得到了的鎖(遞歸鎖定) |
Event | 通用的條件變量。多個線程能夠等待某個事件的發生,在事件發生後,全部的線程都會被激活 |
BoundedSemaphore | 每次容許幾個線程經過 |
Timer | 等待多久在開始運行 |
例子:
import threading import time def sayhi(num): #定義每一個線程要運行的函數 print("running on number:%s" %num) time.sleep(3) if __name__ == '__main__': t1 = threading.Thread(target=sayhi,args=(1,)) #生成一個線程實例 t2 = threading.Thread(target=sayhi,args=(2,)) #生成另外一個線程實例 t1.start() #啓動線程 t2.start() #啓動另外一個線程 print(t1.getName()) #獲取線程名 print(t2.getName())
上述代碼建立了10個「前臺」線程,而後控制器就交給了CPU,CPU根據指定算法進行調度,分片執行指令。
更多方法:
import threading import time class MyThread(threading.Thread): def __init__(self,num): threading.Thread.__init__(self) self.num = num def run(self):#定義每一個線程要運行的函數 print("running on number:%s" %self.num) time.sleep(3) if __name__ == '__main__': t1 = MyThread(1) t2 = MyThread(2) t1.start() t2.start()
線程鎖(Lock、RLock)
因爲線程之間是進行隨機調度,而且每一個線程可能只執行n條執行以後,當多個線程同時修改同一條數據時可能會出現髒數據,因此,出現了線程鎖 - 同一時刻容許一個線程執行操做。
請求鎖定 — 進入鎖定池等待 — 獲取鎖 — 已鎖定 — 釋放鎖
Lock(指令鎖)是可用的最低級的同步指令。Lock處於鎖定狀態時,不被特定的線程擁有。Lock包含兩種狀態——鎖定和非鎖定,以及兩個基本的方法。
能夠認爲Lock有一個鎖定池,當線程請求鎖定時,將線程至於池中,直到得到鎖定後出池。池中的線程處於狀態圖中的同步阻塞狀態。
構造方法:
Lock()
實例方法:
acquire([timeout]): 使線程進入同步阻塞狀態,嘗試得到鎖定。
release(): 釋放鎖。使用前線程必須已得到鎖定,不然將拋出異常。
沒有鎖
import time import threading def addNum(): global num #在每一個線程中都獲取這個全局變量 print('--get num:',num ) time.sleep(1) num -=1 #對此公共變量進行-1操做 num = 100 #設定一個共享變量 thread_list = [] for i in range(100): t = threading.Thread(target=addNum) t.start() thread_list.append(t) for t in thread_list: #等待全部線程執行完畢 t.join() print('final num:', num )
正常來說,這個num結果應該是0, 但在python 2.7上多運行幾回,會發現,最後打印出來的num結果不老是0,爲何每次運行的結果不同呢? 哈,很簡單,假設你有A,B兩個線程,此時都 要對num 進行減1操做, 因爲2個線程是併發同時運行的,因此2個線程頗有可能同時拿走了num=100這個初始變量交給cpu去運算,當A線程去處完的結果是99,但此時B線程運算完的結果也是99,兩個線程同時CPU運算的結果再賦值給num變量後,結果就都是99。那怎麼辦呢? 很簡單,每一個線程在要修改公共數據時,爲了不本身在還沒改完的時候別人也來修改此數據,能夠給這個數據加一把鎖, 這樣其它線程想修改此數據時就必須等待你修改完畢並把鎖釋放掉後才能再訪問此數據。
*注:不要在3.x上運行,不知爲何,3.x上的結果老是正確的,多是自動加了鎖
加鎖版本
import time import threading def addNum(): global num #在每一個線程中都獲取這個全局變量 print('--get num:',num ) time.sleep(1) lock.acquire() #修改數據前加鎖 num -=1 #對此公共變量進行-1操做 lock.release() #修改後釋放 num = 100 #設定一個共享變量 thread_list = [] lock = threading.Lock() #生成全局鎖 for i in range(100): t = threading.Thread(target=addNum) t.start() thread_list.append(t) for t in thread_list: #等待全部線程執行完畢 t.join() print('final num:', num )
RLock(遞歸鎖)
RLock(可重入鎖)是一個能夠被同一個線程請求屢次的同步指令。RLock使用了「擁有的線程」和「遞歸等級」的概念,處於鎖定狀態時,RLock被某個線程擁有。擁有RLock的線程能夠再次調用acquire(),釋放鎖時須要調用release()相同次數。
能夠認爲RLock包含一個鎖定池和一個初始值爲0的計數器,每次成功調用 acquire()/release(),計數器將+1/-1,爲0時鎖處於未鎖定狀態。
構造方法:
RLock()
說白了就是在一個大鎖中還要再包含子鎖
import threading,time def run1(): print("grab the first part data") lock.acquire() global num num +=1 lock.release() return num def run2(): print("grab the second part data") lock.acquire() global num2 num2+=1 lock.release() return num2 def run3(): lock.acquire() res = run1() print('--------between run1 and run2-----') res2 = run2() lock.release() print(res,res2) if __name__ == '__main__': num,num2 = 0,0 lock = threading.RLock() for i in range(10): t = threading.Thread(target=run3) t.start() while threading.active_count() != 1: print(threading.active_count()) else: print('----all threads done---') print(num,num2)
信號量(Semaphore)
互斥鎖 同時只容許一個線程更改數據,而Semaphore是同時容許必定數量的線程更改數據 ,好比廁全部3個坑,那最多隻容許3我的上廁所,後面的人只能等裏面有人出來了才能再進去。
import threading,time def run(n): semaphore.acquire() time.sleep(1) print("run the thread: %s\n" %n) semaphore.release() if __name__ == '__main__': num= 0 semaphore = threading.BoundedSemaphore(5) #最多容許5個線程同時運行 for i in range(20): t = threading.Thread(target=run,args=(i,)) t.start() while threading.active_count() != 1: pass #print threading.active_count() else: print('----all threads done---') print(num)
事件(event)
python線程的事件用於主線程控制其餘線程的執行,事件主要提供了三個方法 set、wait、clear。
Event內部包含了一個標誌位,初始的時候爲false。
能夠使用使用set()來將其設置爲true;
或者使用clear()將其重新設置爲false;
能夠使用is_set()來檢查標誌位的狀態;
另外一個最重要的函數就是wait(timeout=None),用來阻塞當前線程,直到event的內部標誌位被設置爲true或者timeout超時。若是內部標誌位爲true則wait()函數理解返回。
事件處理的機制:全局定義了一個「Flag」,若是「Flag」值爲 False,那麼當程序執行 event.wait 方法時就會阻塞,若是「Flag」值爲True,那麼event.wait 方法時便再也不阻塞。
經過Event來實現兩個或多個線程間的交互,下面是一個紅綠燈的例子,即起動一個線程作交通指揮燈,生成幾個線程作車輛,車輛行駛按紅燈停,綠燈行的規則。
import threading,time import random def light(): if not event.isSet(): event.set() #wait就不阻塞 #綠燈狀態 count = 0 while True: if count < 10: print('\033[42;1m--green light on---\033[0m') elif count <13: print('\033[43;1m--yellow light on---\033[0m') elif count <20: if event.isSet(): event.clear() print('\033[41;1m--red light on---\033[0m') else: count = 0 event.set() #打開綠燈 time.sleep(1) count +=1 def car(n): while 1: time.sleep(random.randrange(10)) if event.isSet(): #綠燈 print("car [%s] is running.." % n) else: print("car [%s] is waiting for the red light.." %n) if __name__ == '__main__': event = threading.Event() Light = threading.Thread(target=light) Light.start() for i in range(3): t = threading.Thread(target=car,args=(i,)) t.start()
這裏還有一個event使用的例子,員工進公司門要刷卡, 咱們這裏設置一個線程是「門」, 再設置幾個線程爲「員工」,員工看到門沒打開,就刷卡,刷完卡,門開了,員工就能夠經過。
#_*_coding:utf-8_*_ __author__ = 'Alex Li' import threading import time import random def door(): door_open_time_counter = 0 while True: if door_swiping_event.is_set(): print("\033[32;1mdoor opening....\033[0m") door_open_time_counter +=1 else: print("\033[31;1mdoor closed...., swipe to open.\033[0m") door_open_time_counter = 0 #清空計時器 door_swiping_event.wait() if door_open_time_counter > 3:#門開了已經3s了,該關了 door_swiping_event.clear() time.sleep(0.5) def staff(n): print("staff [%s] is comming..." % n ) while True: if door_swiping_event.is_set(): print("\033[34;1mdoor is opened, passing.....\033[0m") break else: print("staff [%s] sees door got closed, swipping the card....." % n) print(door_swiping_event.set()) door_swiping_event.set() print("after set ",door_swiping_event.set()) time.sleep(0.5) door_swiping_event = threading.Event() #設置事件 door_thread = threading.Thread(target=door) door_thread.start() for i in range(5): p = threading.Thread(target=staff,args=(i,)) time.sleep(random.randrange(3)) p.start()
條件(Condition)
能夠把Condition理解爲一把高級的瑣,它提供了比Lock, RLock更高級的功能,容許咱們可以控制複雜的線程同步問題。threadiong.Condition在內部維護一個瑣對象(默認是RLock),能夠在建立Condigtion對象的時候把瑣對象做爲參數傳入。Condition也提供了acquire, release方法,其含義與瑣的acquire, release方法一致,其實它只是簡單的調用內部瑣對象的對應的方法而已。Condition還提供了以下方法(特別要注意:這些方法只有在佔用瑣(acquire)以後才能調用,不然將會報RuntimeError異常。):
Condition.wait([timeout]):
wait方法釋放內部所佔用的瑣,同時線程被掛起,直至接收到通知被喚醒或超時(若是提供了timeout參數的話)。當線程被喚醒並從新佔有瑣的時候,程序纔會繼續執行下去。
Condition.notify():
喚醒一個掛起的線程(若是存在掛起的線程)。注意:notify()方法不會釋放所佔用的瑣。
Condition.notify_all()
Condition.notifyAll()
喚醒全部掛起的線程(若是存在掛起的線程)。注意:這些方法不會釋放所佔用的瑣。
使得線程等待,只有知足某條件時,才釋放n個線程
import threading def run(n): con.acquire() con.wait() print("run the thread: %s" %n) con.release() if __name__ == '__main__': con = threading.Condition() for i in range(10): t = threading.Thread(target=run, args=(i,)) t.start() while True: inp = input('>>>') if inp == 'q': break con.acquire() con.notify(int(inp)) con.release()
def condition_func():
ret = False
inp = input('>>>')
if inp == '1':
ret = True
return ret
def run(n):
con.acquire()
con.wait_for(condition_func)
print("run the thread: %s" %n)
con.release()
if __name__ == '__main__':
con = threading.Condition()
for i in range(10):
t = threading.Thread(target=run, args=(i,))
t.start()
Timer
定時器,指定n秒後執行某操做
def hello(): print("hello, world") t = Timer(30.0, hello) t.start() # after 30 seconds, "hello, world" will be printed
Queue 模塊實現了多生產者、多消費者隊列。它特別適用於信息必須在多個線程間安全地交換的多線程程序中。這個模塊中的 Queue 類實現了全部必須的鎖語義。它依賴於 Python 中的線程支持的可用性;參見threading 模塊。
模塊實現了三類隊列,主要差異在於取得數據的順序上。在FIFO(First In First Out,先進先出)隊列中,最先加入的任務會被最早獲得。在LIFO(Last In First Out,後進先出)隊列中,最後加入的任務會被最早獲得(就像棧同樣)。在優先隊列中,任務被保持有序(使用heapq模塊),擁有最小值的任務(優先級最高)被最早獲得。
模塊實現了三類隊列:FIFO(First In First Out,先進先出,默認爲該隊列)、LIFO(Last In First Out,後進先出)、基於優先級的隊列。如下爲其經常使用方法:
先進先出 q = Queue.Queue(maxsize) 後進先出 a = Queue.LifoQueue(maxsize) 優先級 Queue.PriorityQueue(maxsize) Queue.qsize() 返回隊列的大小 Queue.empty() 若是隊列爲空,返回True,反之False Queue.full() 若是隊列滿了,返回True,反之False Queue.full 與 maxsize 大小對應 Queue.put(item) 寫入隊列,timeout等待時間 非阻塞 Queue.get([block[, timeout]]) 獲取隊列,timeout等待時間 Queue.get_nowait() 至關Queue.get(False) Queue.put_nowait(item) 至關Queue.put(item, False) Queue.task_done() 在完成一項工做以後,函數向任務已經完成的隊列發送一個信號 Queue.join(): 實際上意味着等到隊列爲空,再執行別的操做
Queue 模塊定義了下列的類和異常:
構造一個FIFO隊列。maxsize是個整數,指明瞭隊列中能存放的數據個數的上限。一旦達到上限,插入會致使阻塞,直到隊列中的數據被消費掉。若是maxsize小於或者等於0,隊列大小沒有限制。定義隊列時有一個默認的參數maxsize, 若是不指定隊列的長度,即manxsize=0,那麼隊列的長度爲無限長,若是定義了大於0的值,那麼隊列的長度就是maxsize。
構造一個LIFO隊列。maxsize是個整數,指明瞭隊列中能存放的數據個數的上限。一旦達到上限,插入會致使阻塞,直到隊列中的數據被消費掉。若是maxsize小於或者等於0,隊列大小沒有限制。
出現於版本2.6.
構造一個優先隊列。maxsize是個整數,指明瞭隊列中能存放的數據個數的上限。一旦達到上限,插入會致使阻塞,直到隊列中的數據被消費掉。若是maxsize小於或者等於0,隊列大小沒有限制。
擁有最小值的任務會被最早獲得(sorted(list(entries))[0]的返回值即爲擁有最小值的任務)。任務的典型模式就是如(priority_number, data)這樣的元組。
出現於版本2.6.
在空的Queue對象上調用非阻塞的get()(或者get_nowait())會拋出此異常。
在滿的Queue對象上調用非阻塞的put()(或者put_nowait())會拋出此異常。
Queue對象(Queue、LifoQueue和PriorityQueue)提供了下述的公共方法。
返回隊列的近似大小。注意,隊列大小大於0並不保證接下來的get()調用不會被阻塞,隊列大小小於maxsize也不保證接下來的put()調用不會被阻塞。
若是隊列爲空返回True,不然返回False。若是empty()返回True並不保證接下來的put()調用不會被阻塞。相似的,若是empty()返回False也不能保證接下來的get()調用不會被阻塞。
若是隊列是滿的返回True,不然返回False。若是full()返回True並不能保證接下來的get()調用不會被阻塞。相似的,若是full()返回False並不能保證接下來的put()調用不會被阻塞。
將item放入隊列中。若是可選的參數block爲真且timeout爲空對象(默認的狀況,阻塞調用,無超時),若有必要(好比隊列滿),阻塞調用線程,直到有空閒槽可用。若是timeout是個正整數,阻塞調用進程最多timeout秒,若是一直無空閒槽可用,拋出Full異常(帶超時的阻塞調用)。若是block爲假,若是有空閒槽可用將數據放入隊列,不然當即拋出Full異常(非阻塞調用,timeout被忽略)。
出現於版本2.3: timeout參數。
等同於put(item, False)(非阻塞調用)。
從隊列中移除並返回一個數據。若是可選的參數block爲真且timeout爲空對象(默認的狀況,阻塞調用,無超時),阻塞調用進程直到有數據可用。若是timeout是個正整數,阻塞調用進程最多timeout秒,若是一直無數據可用,拋出Empty異常(帶超時的阻塞調用)。若是block爲假,若是有數據可用返回數據,不然當即拋出Empty異常(非阻塞調用,timeout被忽略)。
出現於版本2.3: timeout參數。
等同於get(False)(非阻塞調用)。
爲了跟蹤入隊任務被消費者線程徹底的處理掉,Queue對象提供了兩個額外的方法。
意味着以前入隊的一個任務已經完成。由隊列的消費者線程調用。每個get()調用獲得一個任務,接下來的task_done()調用告訴隊列該任務已經處理完畢。
若是當前一個join()正在阻塞,它將在隊列中的全部任務都處理完時恢復執行(即每個由put()調用入隊的任務都有一個對應的task_done()調用)。
若是該方法被調用的次數多於被放入隊列中的任務的個數,ValueError異常會被拋出。
出現於版本2.5。
阻塞調用線程,直到隊列中的全部任務被處理掉。
只要有數據被加入隊列,未完成的任務數就會增長。當消費者線程調用task_done()(意味着有消費者取得任務並完成任務),未完成的任務數就會減小。當未完成的任務數降到0,join()解除阻塞。
出現於版本2.5。
import queue # q = queue.Queue(2) # # q.put(1) # q.put(2) # #q.put(3) # #q.put_nowait(3) # # print(q.empty()) # print(q.full()) # # print(q.get()) # print(q.get()) 權重: q = queue.PriorityQueue() # q.put((3,'aaaaa')) # q.put((3,'bbbbb')) # q.put((1,'ccccc')) # q.put((3,'ddddd')) q.put([6,'alex']) q.put([3,'jack']) q.put([5,'rain']) print(q.get()) print(q.get())
GIL,全局解釋器鎖
總結:
多進程,多線程,提供併發
IO密集型:多線程
計算密集型:多進程
首先須要明確的一點是 GIL
並非Python的特性,它是在實現Python解析器(CPython)時所引入的一個概念。就比如C++是一套語言(語法)標準,可是能夠用不一樣的編譯器來編譯成可執行代碼。有名的編譯器例如GCC,INTEL C++,Visual C++等。Python也同樣,一樣一段代碼能夠經過CPython,PyPy,Psyco等不一樣的Python執行環境來執行。像其中的JPython就沒有GIL。然而由於CPython是大部分環境下默認的Python執行環境。因此在不少人的概念裏CPython就是Python,也就想固然的把 GIL
歸結爲Python語言的缺陷。因此這裏要先明確一點:GIL並非Python的特性,Python徹底能夠不依賴於GIL
那麼CPython實現中的GIL又是什麼呢?GIL全稱 Global Interpreter Lock
爲了不誤導,咱們仍是來看一下官方給出的解釋:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
好吧,是否是看上去很糟糕?一個防止多線程併發執行機器碼的一個Mutex,乍一看就是個BUG般存在的全局鎖嘛!別急,咱們下面慢慢的分析。
因爲物理上得限制,各CPU廠商在覈心頻率上的比賽已經被多核所取代。爲了更有效的利用多核處理器的性能,就出現了多線程的編程方式,而隨之帶來的就是線程間數據一致性和狀態同步的困難。 即便在CPU內部的Cache也不例外 ,爲了有效解決多份緩存之間的數據同步時各廠商花費了很多心思,也不可避免的帶來了必定的性能損失。
Python固然也逃不開,爲了利用多核,Python開始支持多線程。 而解決多線程之間數據完整性和狀態同步的最簡單方法天然就是加鎖。 因而有了GIL這把超級大鎖,而當愈來愈多的代碼庫開發者接受了這種設定後,他們開始大量依賴這種特性(即默認python內部對象是thread-safe的,無需在實現時考慮額外的內存鎖和同步操做)。
慢慢的這種實現方式被發現是蛋疼且低效的。但當你們試圖去拆分和去除GIL的時候,發現大量庫代碼開發者已經重度依賴GIL而很是難以去除了。有多難?作個類比,像MySQL這樣的「小項目」爲了把Buffer Pool Mutex這把大鎖拆分紅各個小鎖也花了從5.5到5.6再到5.7多個大版爲期近5年的時間,本且仍在繼續。MySQL這個背後有公司支持且有固定開發團隊的產品走的如此艱難,那又更況且Python這樣核心開發和代碼貢獻者高度社區化的團隊呢?
因此簡單的說GIL的存在更多的是歷史緣由。若是推到重來,多線程的問題依然仍是要面對,可是至少會比目前GIL這種方式會更優雅。
從上文的介紹和官方的定義來看,GIL無疑就是一把全局排他鎖。毫無疑問全局鎖的存在會對多線程的效率有不小影響。甚至就幾乎等於Python是個單線程的程序。那麼讀者就會說了,全局鎖只要釋放的勤快效率也不會差啊。只要在進行耗時的IO操做的時候,能釋放GIL,這樣也仍是能夠提高運行效率的嘛。或者說再差也不會比單線程的效率差吧。理論上是這樣,而實際上呢?Python比你想的更糟。
Python GIL實際上是功能和性能之間權衡後的產物,它尤爲存在的合理性,也有較難改變的客觀因素。從本分的分析中,咱們能夠作如下一些簡單的總結:
- 由於GIL的存在,只有IO Bound場景下得多線程會獲得較好的性能
- 若是對並行計算性能較高的程序能夠考慮把核心部分也成C模塊,或者索性用其餘語言實現
- GIL在較長一段時間內將會繼續存在,可是會不斷對其進行改進
在併發編程中使用生產者和消費者模式可以解決絕大多數併發問題。該模式經過平衡生產線程和消費線程的工做能力來提升程序的總體處理數據的速度。
爲何要使用生產者和消費者模式
在線程世界裏,生產者就是生產數據的線程,消費者就是消費數據的線程。在多線程開發當中,若是生產者處理速度很快,而消費者處理速度很慢,那麼生產者就必須等待消費者處理完,才能繼續生產數據。一樣的道理,若是消費者的處理能力大於生產者,那麼消費者就必須等待生產者。爲了解決這個問題因而引入了生產者和消費者模式。
什麼是生產者消費者模式
生產者消費者模式是經過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通信,而經過阻塞隊列來進行通信,因此生產者生產完數據以後不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列裏取,阻塞隊列就至關於一個緩衝區,平衡了生產者和消費者的處理能力。
下面來學習一個最基本的生產者消費者模型的例子
#!/usr/bin/env python #-*- coding:utf-8 -*- import threading import queue import time def consumer(name): while True: print('%s 取到骨頭[%s]並吃了' % (name,q.get())) time.sleep(0.5) q.task_done() def producer(name): count = 0 #while q.qsize() <5: for i in range(10): print('%s生成了骨頭' %name,count) q.put(count) count += 1 time.sleep(0.3) q.join() print("------吃完了-------") #生成一個隊列 q = queue.Queue(maxsize=4) #生成兩個線程 p = threading.Thread(target=producer,args=('Tom',)) #p2 = threading.Thread(target=producer,args=('Tom',)) c = threading.Thread(target=consumer,args=('Jack',)) p.start() #p2.start() c.start()
第二個:
import time,random import queue,threading q = queue.Queue() def Producer(name): count = 0 while count <20: time.sleep(random.randrange(3)) q.put(count) print('Producer %s has produced %s baozi..' %(name, count)) count +=1 def Consumer(name): count = 0 while count <20: time.sleep(random.randrange(4)) if not q.empty(): data = q.get() print(data) print('\033[32;1mConsumer %s has eat %s baozi...\033[0m' %(name, data)) else: print("-----no baozi anymore----") count +=1 p1 = threading.Thread(target=Producer, args=('A',)) c1 = threading.Thread(target=Consumer, args=('B',)) p1.start() c1.start()
#!/usr/bin/env python #-*- coding:utf-8 -*- # !/usr/bin/env python import threading, time import queue # 導入消息隊列模塊 import random # 導入隨機數模塊,是爲了模擬生產者與消費者速度不一致的情形 q = queue.Queue() # 實例化一個對象 def Producer(name): # 生產者函數 for i in range(20): q.put(i) # 將結果放入消息隊列中 print('\033[32;1mProducer %s has made %s baozi....\033[0m' % (name, i)) time.sleep(random.randrange(3)) # 生產者的生產速度,3s內 def Consumer(name): # 消費者函數 count = 0 while count < 20: data = q.get() # 取用消息隊列中存放的結果 print('\033[31;1mConsumer %s has eatem %s baozi...chihuo...\033[0m' % (name, data)) count += 1 time.sleep(random.randrange(4)) # 消費者的消費速度,4s內 p = threading.Thread(target=Producer, args=('Tom',)) c = threading.Thread(target=Consumer, args=('Jack',)) p.start() c.start()
利用這個程序,很好地模擬了前面「廚師作包子顧客吃包子」的例子,而從程序的執行結果中也能夠看出,線程的執行是異步的,儘管如此,數據仍是進行了交互,做用是:在多線程和多線程之間進行數據交互的時候,不會出現數據的阻塞。
建立進程的類:Process([group [, target [, name [, args [, kwargs]]]]]),target表示調用對象,args表示調用對象的位置參數元組。kwargs表示調用對象的字典。name爲別名。group實質上不使用。
方法:is_alive()、join([timeout])、run()、start()、terminate()。其中,Process以start()啓動某個進程。
屬性:authkey、daemon(要經過start()設置)、exitcode(進程在運行時爲None、若是爲–N,表示被信號N結束)、name、pid。其中daemon是父進程終止後自動終止,且本身不能產生新進程,必須在start()以前設置。
例1.1:建立函數並將其做爲單個進程
import multiprocessing
import time
def worker(interval):
n = 5
while n > 0:
print("The time is {0}".format(time.ctime()))
time.sleep(interval)
n -= 1
if __name__ == "__main__":
p = multiprocessing.Process(target=worker, args=(3,))
p.start()
print("p.pid:", p.pid)
print("p.name:", p.name)
print("p.is_alive:", p.is_alive())
結果:
p.pid: 15084
p.name: Process-1
p.is_alive: True
The time is Mon Dec 12 11:31:52 2016
The time is Mon Dec 12 11:31:55 2016
The time is Mon Dec 12 11:31:58 2016
The time is Mon Dec 12 11:32:01 2016
The time is Mon Dec 12 11:32:04 2016
要讓Python程序實現多進程(multiprocessing),咱們先了解操做系統的相關知識。
Unix/Linux操做系統提供了一個fork()
系統調用,它很是特殊。普通的函數調用,調用一次,返回一次,可是fork()
調用一次,返回兩次,由於操做系統自動把當前進程(稱爲父進程)複製了一份(稱爲子進程),而後,分別在父進程和子進程內返回。
子進程永遠返回0
,而父進程返回子進程的ID。這樣作的理由是,一個父進程能夠fork出不少子進程,因此,父進程要記下每一個子進程的ID,而子進程只須要調用getppid()
就能夠拿到父進程的ID。
Python的os
模塊封裝了常見的系統調用,其中就包括fork
,能夠在Python程序中輕鬆建立子進程:
import os print('Process (%s) start...' % os.getpid()) # Only works on Unix/Linux/Mac: pid = os.fork() if pid == 0: print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid())) else: print('I (%s) just created a child process (%s).' % (os.getpid(), pid))
結果:
Process (876) start... I (876) just created a child process (877). I am child process (877) and my parent is 876.
因爲Windows沒有fork
調用,上面的代碼在Windows上沒法運行。因爲Mac系統是基於BSD(Unix的一種)內核,因此,在Mac下運行是沒有問題的,推薦你們用Mac學Python!
有了fork
調用,一個進程在接到新任務時就能夠複製出一個子進程來處理新任務,常見的Apache服務器就是由父進程監聽端口,每當有新的http請求時,就fork出子進程來處理新的http請求。
若是你打算編寫多進程的服務程序,Unix/Linux無疑是正確的選擇。因爲Windows沒有fork
調用,難道在Windows上沒法用Python編寫多進程的程序?
因爲Python是跨平臺的,天然也應該提供一個跨平臺的多進程支持。multiprocessing
模塊就是跨平臺版本的多進程模塊。
multiprocessing
模塊提供了一個Process
類來表明一個進程對象,下面的例子演示了啓動一個子進程並等待其結束:
from multiprocessing import Process import os # 子進程要執行的代碼 def run_proc(name): print('Run child process %s (%s)...' % (name, os.getpid())) if __name__=='__main__': print('Parent process %s.' % os.getpid()) p = Process(target=run_proc, args=('test',)) print('Child process will start.') p.start() p.join() print('Child process end.')
執行結果以下:
Parent process 928. Process will start. Run child process test (929)... Process end.
建立子進程時,只須要傳入一個執行函數和函數的參數,建立一個Process
實例,用start()
方法啓動,這樣建立進程比fork()
還要簡單。
join()
方法能夠等待子進程結束後再繼續往下運行,一般用於進程間的同步。
Pipe方法返回(conn1, conn2)表明一個管道的兩個端。Pipe方法有duplex參數,若是duplex參數爲True(默認值),那麼這個管道是全雙工模式,也就是說conn1和conn2都可收發。duplex爲False,conn1只負責接受消息,conn2只負責發送消息。
import multiprocessing import time def proc1(pipe): while True: for i in xrange(10000): print "send: %s" %(i) pipe.send(i) time.sleep(1) def proc2(pipe): while True: print "proc2 rev:", pipe.recv() time.sleep(1) def proc3(pipe): while True: print "PROC3 rev:", pipe.recv() time.sleep(1) if __name__ == "__main__": pipe = multiprocessing.Pipe() p1 = multiprocessing.Process(target=proc1, args=(pipe[0],)) p2 = multiprocessing.Process(target=proc2, args=(pipe[1],)) #p3 = multiprocessing.Process(target=proc3, args=(pipe[1],)) p1.start() p2.start() #p3.start() p1.join() p2.join() #p3.join()
進程池內部維護一個進程序列,當使用時,則去進程池中獲取一個進程,若是進程池序列中沒有可供使用的進進程,那麼程序就會等待,直到進程池中有可用進程爲止。進程池設置最好等於CPU核心數量
構造方法:
Pool([processes[, initializer[, initargs[, maxtasksperchild[, context]]]]])
processes :使用的工做進程的數量,若是processes是None那麼使用 os.cpu_count()返回的數量。
initializer: 若是initializer是None,那麼每個工做進程在開始的時候會調用initializer(*initargs)。
maxtasksperchild:工做進程退出以前能夠完成的任務數,完成後用一個新的工做進程來替代原進程,來讓閒置的資源被釋放。maxtasksperchild默認是None,意味着只要Pool存在工做進程就會一直存活。
context: 用在制定工做進程啓動時的上下文,通常使用 multiprocessing.Pool() 或者一個context對象的Pool()方法來建立一個池,兩種方法都適當的設置了context
方法:
apply(func[, args[, kwds]]) :使用arg和kwds參數調用func函數,結果返回前會一直阻塞,因爲這個緣由,apply_async()更適合併發執行,另外,func函數僅被pool中的一個進程運行。
apply_async(func[, args[, kwds[, callback[, error_callback]]]]) : apply()方法的一個變體,會返回一個結果對象。若是callback被指定,那麼callback能夠接收一個參數而後被調用,當結果準備好回調時會調用callback,調用失敗時,則用error_callback替換callback。 Callbacks應被當即完成,不然處理結果的線程會被阻塞。
close() : 阻止更多的任務提交到pool,待任務完成後,工做進程會退出。
terminate() : 無論任務是否完成,當即中止工做進程。在對pool對象進程垃圾回收的時候,會當即調用terminate()。
join() : wait工做線程的退出,在調用join()前,必須調用close() or terminate()。這樣是由於被終止的進程須要被父進程調用wait(join等價與wait),不然進程會成爲殭屍進程
進程池中有兩個方法:
若是要啓動大量的子進程,能夠用進程池的方式批量建立子進程:
from multiprocessing import Pool import os, time, random def long_time_task(name): print('Run task %s (%s)...' % (name, os.getpid())) start = time.time() time.sleep(random.random() * 3) end = time.time() print('Task %s runs %0.2f seconds.' % (name, (end - start))) if __name__=='__main__': print('Parent process %s.' % os.getpid()) p = Pool(4) for i in range(5): p.apply_async(long_time_task, args=(i,)) print('Waiting for all subprocesses done...') p.close() p.join() print('All subprocesses done.')
執行結果以下:
Parent process 669. Waiting for all subprocesses done... Run task 0 (671)... Run task 1 (672)... Run task 2 (673)... Run task 3 (674)... Task 2 runs 0.14 seconds. Run task 4 (673)... Task 1 runs 0.27 seconds. Task 3 runs 0.86 seconds. Task 0 runs 1.41 seconds. Task 4 runs 1.91 seconds. All subprocesses done.
代碼解讀:
對Pool
對象調用join()
方法會等待全部子進程執行完畢,調用join()
以前必須先調用close()
,調用close()
以後就不能繼續添加新的Process
了。
請注意輸出的結果,task 0
,1
,2
,3
是馬上執行的,而task 4
要等待前面某個task完成後才執行,這是由於Pool
的默認大小在個人電腦上是4,所以,最多同時執行4個進程。這是Pool
有意設計的限制,並非操做系統的限制。若是改爲:
p = Pool(5)
就能夠同時跑5個進程。
因爲Pool
的默認大小是CPU的核數,若是你不幸擁有8核CPU,你要提交至少9個子進程才能看到上面的等待效果。
不少時候,子進程並非自身,而是一個外部進程。咱們建立了子進程後,還須要控制子進程的輸入和輸出。
subprocess
模塊可讓咱們很是方便地啓動一個子進程,而後控制其輸入和輸出。
下面的例子演示瞭如何在Python代碼中運行命令nslookup www.python.org
,這和命令行直接運行的效果是同樣的:
import subprocess print('$ nslookup www.python.org') r = subprocess.call(['nslookup', 'www.python.org']) print('Exit code:', r)
運行結果:
$ nslookup www.python.org Server: 192.168.19.4 Address: 192.168.19.4#53 Non-authoritative answer: www.python.org canonical name = python.map.fastly.net. Name: python.map.fastly.net Address: 199.27.79.223 Exit code: 0
若是子進程還須要輸入,則能夠經過communicate()
方法輸入:
import subprocess print('$ nslookup') p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, err = p.communicate(b'set q=mx\npython.org\nexit\n') print(output.decode('utf-8')) print('Exit code:', p.returncode)
上面的代碼至關於在命令行執行命令nslookup
,而後手動輸入:
set q=mx python.org exit
運行結果以下:
$ nslookup Server: 192.168.19.4 Address: 192.168.19.4#53 Non-authoritative answer: python.org mail exchanger = 50 mail.python.org. Authoritative answers can be found from: mail.python.org internet address = 82.94.164.166 mail.python.org has AAAA address 2001:888:2000:d::a6 Exit code: 0
Process
之間確定是須要通訊的,操做系統提供了不少機制來實現進程間的通訊。Python的multiprocessing
模塊包裝了底層的機制,提供了Queue
、Pipes
等多種方式來交換數據。
咱們以Queue
爲例,在父進程中建立兩個子進程,一個往Queue
裏寫數據,一個從Queue
裏讀數據:
from multiprocessing import Process, Queue import os, time, random # 寫數據進程執行的代碼: def write(q): print('Process to write: %s' % os.getpid()) for value in ['A', 'B', 'C']: print('Put %s to queue...' % value) q.put(value) time.sleep(random.random()) # 讀數據進程執行的代碼: def read(q): print('Process to read: %s' % os.getpid()) while True: value = q.get(True) print('Get %s from queue.' % value) if __name__=='__main__': # 父進程建立Queue,並傳給各個子進程: q = Queue() pw = Process(target=write, args=(q,)) pr = Process(target=read, args=(q,)) # 啓動子進程pw,寫入: pw.start() # 啓動子進程pr,讀取: pr.start() # 等待pw結束: pw.join() # pr進程裏是死循環,沒法等待其結束,只能強行終止: pr.terminate()
運行結果以下:
Process to write: 50563 Put A to queue... Process to read: 50564 Get A from queue. Put B to queue... Get B from queue. Put C to queue... Get C from queue.
在Unix/Linux下,multiprocessing
模塊封裝了fork()
調用,使咱們不須要關注fork()
的細節。因爲Windows沒有fork
調用,所以,multiprocessing
須要「模擬」出fork
的效果,父進程全部Python對象都必須經過pickle序列化再傳到子進程去,全部,若是multiprocessing
在Windows下調用失敗了,要先考慮是否是pickle失敗了。
在Unix/Linux下,能夠使用fork()
調用實現多進程。
要實現跨平臺的多進程,能夠使用multiprocessing
模塊。
進程間通訊是經過Queue
、Pipes
等實現的。
多線程相似於同時執行多個不一樣程序,多線程運行有以下優勢:
線程在執行過程當中與進程仍是有區別的。每一個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。可是線程不可以獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
每一個線程都有他本身的一組CPU寄存器,稱爲線程的上下文,該上下文反映了線程上次運行該線程的CPU寄存器的狀態。
指令指針和堆棧指針寄存器是線程上下文中兩個最重要的寄存器,線程老是在進程獲得上下文中運行的,這些地址都用於標誌擁有線程的進程地址空間中的內存。
線程能夠分爲:
Python3 線程中經常使用的兩個模塊爲:
thread 模塊已被廢棄。用戶能夠使用 threading 模塊代替。因此,在 Python3 中不能再使用"thread" 模塊。爲了兼容性,Python3 將 thread 重命名爲 "_thread"。
咱們前面提到了進程是由若干線程組成的,一個進程至少有一個線程。
因爲線程是操做系統直接支持的執行單元,所以,高級語言一般都內置多線程的支持,Python也不例外,而且,Python的線程是真正的Posix Thread,而不是模擬出來的線程。
Python的標準庫提供了兩個模塊:_thread
和threading
,_thread
是低級模塊,threading
是高級模塊,對_thread
進行了封裝。絕大多數狀況下,咱們只須要使用threading
這個高級模塊。
Python中使用線程有兩種方式:函數或者用類來包裝線程對象。
函數式:調用 _thread 模塊中的start_new_thread()函數來產生新線程。語法以下:
_thread.start_new_thread ( function, args[, kwargs] )
參數說明:
實例:
#!/usr/bin/python3 import _thread import time # 爲線程定義一個函數 def print_time( threadName, delay): count = 0 while count < 5: time.sleep(delay) count += 1 print ("%s: %s" % ( threadName, time.ctime(time.time()) )) # 建立兩個線程 try: _thread.start_new_thread( print_time, ("Thread-1", 2, ) ) _thread.start_new_thread( print_time, ("Thread-2", 4, ) ) except: print ("Error: 沒法啓動線程") while 1: pass
執行以上程序輸出結果以下:
Thread-1: Wed Apr 6 11:36:31 2016 Thread-1: Wed Apr 6 11:36:33 2016 Thread-2: Wed Apr 6 11:36:33 2016 Thread-1: Wed Apr 6 11:36:35 2016 Thread-1: Wed Apr 6 11:36:37 2016 Thread-2: Wed Apr 6 11:36:37 2016 Thread-1: Wed Apr 6 11:36:39 2016 Thread-2: Wed Apr 6 11:36:41 2016 Thread-2: Wed Apr 6 11:36:45 2016 Thread-2: Wed Apr 6 11:36:49 2016
執行以上程後能夠按下 ctrl-c to 退出。
啓動一個線程就是把一個函數傳入並建立Thread
實例,而後調用start()
開始執行:
import time, threading # 新線程執行的代碼: def loop(): print('thread %s is running...' % threading.current_thread().name) n = 0 while n < 5: n = n + 1 print('thread %s >>> %s' % (threading.current_thread().name, n)) time.sleep(1) print('thread %s ended.' % threading.current_thread().name) print('thread %s is running...' % threading.current_thread().name) t = threading.Thread(target=loop, name='LoopThread') t.start() t.join() print('thread %s ended.' % threading.current_thread().name)
執行結果以下:
thread MainThread is running... thread LoopThread is running... thread LoopThread >>> 1 thread LoopThread >>> 2 thread LoopThread >>> 3 thread LoopThread >>> 4 thread LoopThread >>> 5 thread LoopThread ended. thread MainThread ended.
因爲任何進程默認就會啓動一個線程,咱們把該線程稱爲主線程,主線程又能夠啓動新的線程,Python的threading
模塊有個current_thread()
函數,它永遠返回當前線程的實例。主線程實例的名字叫MainThread
,子線程的名字在建立時指定,咱們用LoopThread
命名子線程。名字僅僅在打印時用來顯示,徹底沒有其餘意義,若是不起名字Python就自動給線程命名爲Thread-1
,Thread-2
……
Python3 經過兩個標準庫 _thread 和 threading 提供對線程的支持。
_thread 提供了低級別的、原始的線程以及一個簡單的鎖,它相比於 threading 模塊的功能仍是比較有限的。
threading 模塊除了包含 _thread 模塊中的全部方法外,還提供的其餘方法:
除了使用方法外,線程模塊一樣提供了Thread類來處理線程,Thread類提供瞭如下方法:
多線程和多進程最大的不一樣在於,多進程中,同一個變量,各自有一份拷貝存在於每一個進程中,互不影響,而多線程中,全部變量都由全部線程共享,因此,任何一個變量均可以被任何一個線程修改,所以,線程之間共享數據最大的危險在於多個線程同時改一個變量,把內容給改亂了。
來看看多個線程同時操做一個變量怎麼把內容給改亂了:
import time, threading # 假定這是你的銀行存款: balance = 0 def change_it(n): # 先存後取,結果應該爲0: global balance balance = balance + n balance = balance - n def run_thread(n): for i in range(100000): change_it(n) t1 = threading.Thread(target=run_thread, args=(5,)) t2 = threading.Thread(target=run_thread, args=(8,)) t1.start() t2.start() t1.join() t2.join() print(balance)
咱們定義了一個共享變量balance
,初始值爲0
,而且啓動兩個線程,先存後取,理論上結果應該爲0
,可是,因爲線程的調度是由操做系統決定的,當t一、t2交替執行時,只要循環次數足夠多,balance
的結果就不必定是0
了。
緣由是由於高級語言的一條語句在CPU執行時是若干條語句,即便一個簡單的計算:
balance = balance + n
也分兩步:
balance + n
,存入臨時變量中;balance
。也就是能夠當作:
x = balance + n balance = x
因爲x是局部變量,兩個線程各自都有本身的x,當代碼正常執行時:
初始值 balance = 0 t1: x1 = balance + 5 # x1 = 0 + 5 = 5 t1: balance = x1 # balance = 5 t1: x1 = balance - 5 # x1 = 5 - 5 = 0 t1: balance = x1 # balance = 0 t2: x2 = balance + 8 # x2 = 0 + 8 = 8 t2: balance = x2 # balance = 8 t2: x2 = balance - 8 # x2 = 8 - 8 = 0 t2: balance = x2 # balance = 0 結果 balance = 0
可是t1和t2是交替運行的,若是操做系統如下面的順序執行t一、t2:
初始值 balance = 0 t1: x1 = balance + 5 # x1 = 0 + 5 = 5 t2: x2 = balance + 8 # x2 = 0 + 8 = 8 t2: balance = x2 # balance = 8 t1: balance = x1 # balance = 5 t1: x1 = balance - 5 # x1 = 5 - 5 = 0 t1: balance = x1 # balance = 0 t2: x2 = balance - 8 # x2 = 0 - 8 = -8 t2: balance = x2 # balance = -8 結果 balance = -8
究其緣由,是由於修改balance
須要多條語句,而執行這幾條語句時,線程可能中斷,從而致使多個線程把同一個對象的內容改亂了。
兩個線程同時一存一取,就可能致使餘額不對,你確定不但願你的銀行存款莫名其妙地變成了負數,因此,咱們必須確保一個線程在修改balance
的時候,別的線程必定不能改。
若是咱們要確保balance
計算正確,就要給change_it()
上一把鎖,當某個線程開始執行change_it()
時,咱們說,該線程由於得到了鎖,所以其餘線程不能同時執行change_it()
,只能等待,直到鎖被釋放後,得到該鎖之後才能改。因爲鎖只有一個,不管多少線程,同一時刻最多隻有一個線程持有該鎖,因此,不會形成修改的衝突。建立一個鎖就是經過threading.Lock()
來實現:
balance = 0 lock = threading.Lock() def run_thread(n): for i in range(100000): # 先要獲取鎖: lock.acquire() try: # 放心地改吧: change_it(n) finally: # 改完了必定要釋放鎖: lock.release()
當多個線程同時執行lock.acquire()
時,只有一個線程能成功地獲取鎖,而後繼續執行代碼,其餘線程就繼續等待直到得到鎖爲止。
得到鎖的線程用完後必定要釋放鎖,不然那些苦苦等待鎖的線程將永遠等待下去,成爲死線程。因此咱們用try...finally
來確保鎖必定會被釋放。
鎖的好處就是確保了某段關鍵代碼只能由一個線程從頭至尾完整地執行,壞處固然也不少,首先是阻止了多線程併發執行,包含鎖的某段代碼實際上只能以單線程模式執行,效率就大大地降低了。其次,因爲能夠存在多個鎖,不一樣的線程持有不一樣的鎖,並試圖獲取對方持有的鎖時,可能會形成死鎖,致使多個線程所有掛起,既不能執行,也沒法結束,只能靠操做系統強制終止。
若是多個線程共同對某個數據修改,則可能出現不可預料的結果,爲了保證數據的正確性,須要對多個線程進行同步。
使用 Thread 對象的 Lock 和 Rlock 能夠實現簡單的線程同步,這兩個對象都有 acquire 方法和 release 方法,對於那些須要每次只容許一個線程操做的數據,能夠將其操做放到 acquire 和 release 方法之間。以下:
多線程的優點在於能夠同時運行多個任務(至少感受起來是這樣)。可是當線程須要共享數據時,可能存在數據不一樣步的問題。
考慮這樣一種狀況:一個列表裏全部元素都是0,線程"set"從後向前把全部元素改爲1,而線程"print"負責從前日後讀取列表並打印。
那麼,可能線程"set"開始改的時候,線程"print"便來打印列表了,輸出就成了一半0一半1,這就是數據的不一樣步。爲了不這種狀況,引入了鎖的概念。
鎖有兩種狀態——鎖定和未鎖定。每當一個線程好比"set"要訪問共享數據時,必須先得到鎖定;若是已經有別的線程好比"print"得到鎖定了,那麼就讓線程"set"暫停,也就是同步阻塞;等到線程"print"訪問完畢,釋放鎖之後,再讓線程"set"繼續。
通過這樣的處理,打印列表時要麼所有輸出0,要麼所有輸出1,不會再出現一半0一半1的尷尬場面。
實例:
#!/usr/bin/python3 import threading import time class myThread (threading.Thread): def __init__(self, threadID, name, counter): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counter def run(self): print ("開啓線程: " + self.name) # 獲取鎖,用於線程同步 threadLock.acquire() print_time(self.name, self.counter, 3) # 釋放鎖,開啓下一個線程 threadLock.release() def print_time(threadName, delay, counter): while counter: time.sleep(delay) print ("%s: %s" % (threadName, time.ctime(time.time()))) counter -= 1 threadLock = threading.Lock() threads = [] # 建立新線程 thread1 = myThread(1, "Thread-1", 1) thread2 = myThread(2, "Thread-2", 2) # 開啓新線程 thread1.start() thread2.start() # 添加線程到線程列表 threads.append(thread1) threads.append(thread2) # 等待全部線程完成 for t in threads: t.join() print ("退出主線程")
執行以上程序,輸出結果爲:
開啓線程: Thread-1 開啓線程: Thread-2 Thread-1: Wed Apr 6 11:52:57 2016 Thread-1: Wed Apr 6 11:52:58 2016 Thread-1: Wed Apr 6 11:52:59 2016 Thread-2: Wed Apr 6 11:53:01 2016 Thread-2: Wed Apr 6 11:53:03 2016 Thread-2: Wed Apr 6 11:53:05 2016 退出主線程
若是你不幸擁有一個多核CPU,你確定在想,多核應該能夠同時執行多個線程。
若是寫一個死循環的話,會出現什麼狀況呢?
打開Mac OS X的Activity Monitor,或者Windows的Task Manager,均可以監控某個進程的CPU使用率。
咱們能夠監控到一個死循環線程會100%佔用一個CPU。
若是有兩個死循環線程,在多核CPU中,能夠監控到會佔用200%的CPU,也就是佔用兩個CPU核心。
要想把N核CPU的核心所有跑滿,就必須啓動N個死循環線程。
試試用Python寫個死循環:
import threading, multiprocessing def loop(): x = 0 while True: x = x ^ 1 for i in range(multiprocessing.cpu_count()): t = threading.Thread(target=loop) t.start()
啓動與CPU核心數量相同的N個線程,在4核CPU上能夠監控到CPU佔用率僅有102%,也就是僅使用了一核。
可是用C、C++或Java來改寫相同的死循環,直接能夠把所有核心跑滿,4核就跑到400%,8核就跑到800%,爲何Python不行呢?
由於Python的線程雖然是真正的線程,但解釋器執行代碼時,有一個GIL鎖:Global Interpreter Lock,任何Python線程執行前,必須先得到GIL鎖,而後,每執行100條字節碼,解釋器就自動釋放GIL鎖,讓別的線程有機會執行。這個GIL全局鎖實際上把全部線程的執行代碼都給上了鎖,因此,多線程在Python中只能交替執行,即便100個線程跑在100核CPU上,也只能用到1個核。
GIL是Python解釋器設計的歷史遺留問題,一般咱們用的解釋器是官方實現的CPython,要真正利用多核,除非重寫一個不帶GIL的解釋器。
因此,在Python中,能夠使用多線程,但不要期望能有效利用多核。若是必定要經過多線程利用多核,那隻能經過C擴展來實現,不過這樣就失去了Python簡單易用的特色。
不過,也不用過於擔憂,Python雖然不能利用多線程實現多核任務,但能夠經過多進程實現多核任務。多個Python進程有各自獨立的GIL鎖,互不影響。
多線程編程,模型複雜,容易發生衝突,必須用鎖加以隔離,同時,又要當心死鎖的發生。
Python解釋器因爲設計時有GIL全局鎖,致使了多線程沒法利用多核。多線程的併發在Python中就是一個美麗的夢。
Python進程池 前面咱們講過CPU在某一時刻只能執行一個進程,那爲何上面10個進程還可以併發執行呢?實際在CPU在處理上面10個進程時是在不停的切換執行這10個進程,但因爲上面10個進程的程序代碼都是十分簡單的,並無涉及什麼複雜的功能,而且,CPU的處理速度實在是很是快,因此這樣一個過程在咱們人爲感知裏確實是在併發執行的,實際只不過是CPU在不停地切換而已,這是經過增長切換的時間來達到目的的。 10個簡單的進程能夠產生這樣的效果,那試想一下,若是我有100個進程須要CPU執行,但由於CPU還要進行其它工做,只能一次再處理10個進程(切換處理),不然有可能會影響其它進程工做,這下可怎麼辦?這時候就能夠用到Python中的進程池來進行調控了,在Python中,能夠定義一個進程池和這個池的大小,假如定義進程池的大小爲10,那麼100個進程能夠分10次放進進程池中,而後CPU就能夠10次併發完成這100個進程了。
進程池內部維護一個進程序列,當使用時,則去進程池中獲取一個進程,若是進程池序列中沒有可供使用的進進程,那麼程序就會等待,直到進程池中有可用進程爲止。
進程池中有兩個方法:
from multiprocessing import Process,Pool import time def Foo(i): time.sleep(2) return i+100 def Bar(arg): print('-->exec done:',arg) pool = Pool(5) for i in range(10): pool.apply_async(func=Foo, args=(i,),callback=Bar) #pool.apply(func=Foo, args=(i,)) print('end') pool.close() pool.join()#進程池中進程執行完畢後再關閉,若是註釋,那麼程序直接關閉。