進程,線程,GIL,Python多線程,生產者消費者模型都是什麼鬼

1. 操做系統基本知識,進程,線程

CPU是計算機的核心,承擔了全部的計算任務;html

操做系統是計算機的管理者,它負責任務的調度、資源的分配和管理,統領整個計算機硬件;那麼操做系統是如何進行任務調度的呢?python

1.1 任務調度

大部分操做系統(如Windows、Linux)的任務調度是採用時間片輪轉的搶佔式調度方式,也就是說一個任務執行一小段時間後強制暫停去執行下一個任務,每一個任務輪流執行。任務執行的一小段時間叫作時間片,任務正在執行時的狀態叫運行狀態,任務執行一段時間後強制暫停去執行下一個任務,被暫停的任務就處於就緒狀態等待下一個屬於它的時間片的到來。這樣每一個任務都能獲得執行,因爲CPU的執行效率很是高,時間片很是短,在各個任務之間快速地切換,給人的感受就是多個任務在「同時進行」,這也就是咱們所說的併發(別以爲併發有多高深,它的實現很複雜,但它的概念很簡單,就是一句話:多個任務同時執行)。多任務運行過程的示意圖以下:git

 

注意:當一個任務獲得CPU時,相關的資源必須已經就位,而後CPU開始執行,除了CPU之外的全部就構成了這個任務的執行環境,就是所謂的程序上下文,CPU每一次任務的切換都須要保存程序的上下文,這個上下文就是下一次CPU臨幸是的環境;github

因此任務的輪轉方法爲:先加載程序A的上下文,而後開始執行A,保存程序A的上下文,調入下一個要執行的程序B的程序上下文,而後開始執行B,保存程序B的上下文。。。。算法

1.2 進程

說到進程,須要先提一下程序編程

應用程序是用於實現某種功能的一組指令的有序集合(只不過是磁盤中可執行的二進制(或者其餘類型)的數據);應用程序運行於操做系統之上(只有將程序裝載到內存中,系統爲它分配了資源並被操做系統調用的時候纔開始它們的生命週期,即運行)bootstrap

進程(有時候被稱爲重量級進程)是具備必定獨立功能的程序關於某個數據集合上的一次運行活動,是系統進行資源分配和調度的一個獨立單位(不是最小單位),是應用程序運行的載體;c#

每個進程都有本身的地址空間、內存、數據棧以及其餘記錄起運行軌跡的輔助數據。操做系統管理其上運行的全部進程,併爲這些進程公平的分配時間、進程也能夠經過fork和spawn操做類完成其餘的任務。不過各個進程有本身的內存空間、數據棧等,因此只能使用進程間通信(interprocess communication, IPC),而不能直接共享信息。windows

咱們再看一下wiki上的解釋:安全

An executing instance of a program is called a process.

Each process provides the resources needed to execute a program. A process has a virtual address space, executable code, open handles to system objects, a security context, a unique process identifier, environment variables, a priority class, minimum and maximum working set sizes, and at least one thread of execution. Each process is started with a single thread, often called the primary thread, but can create additional threads from any of its threads.

1)進程的組成和特性

進程通常由程序、數據集合和進程控制塊三部分組成。程序用於描述進程要完成的功能,是控制進程執行的指令集;數據集合是程序在執行時所須要的數據和工做區;程序控制塊(Program Control Block,簡稱PCB),包含進程的描述信息和控制信息,是進程存在的惟一標誌。

進程的特性:

動態性:進程是程序的一次執行過程,是臨時的,有生命期的,是動態產生,動態消亡的;

併發性:任何進程均可以同其餘進程一塊兒併發執行;

獨立性:進程是系統進行資源分配和調度的一個獨立單位;

結構性:進程由程序、數據和進程控制塊三部分組成。

2)進程和程序的區別與聯繫

a)程序只是一組指令的有序集合,它自己沒有任何運行的含義,它只是一個靜態的實體。而進程則不一樣,它是程序在某個數據集上的執行。進程是一個動態的實體,它有本身的生命週期。它因建立而產生,因調度而運行,因等待資源或事件而被處於等待狀態,因完成任務而被撤消。反映了一個程序在必定的數據集上運行的所有動態過程。 

b)進程和程序並非一一對應的,一個程序執行在不一樣的數據集上就成爲不一樣的進程,能夠用進程控制塊來惟一地標識每一個進程。而這一點正是程序沒法作到的,因爲程序沒有和數據產生直接的聯繫,既使是執行不一樣的數據的程序,他們的指令的集合依然能夠是同樣的,因此沒法惟一地標識出這些運行於不一樣數據集上的程序。通常來講,一個進程確定有一個與之對應的程序,並且只有一個。而一個程序有可能沒有與之對應的進程(由於它沒有執行),也有可能有多個進程與之對應(運行在幾個不一樣的數據集上)。

c)進程還具備併發性和交往性,這也與程序的封閉性不一樣。進程和線程都是由操做系統所體會的程序運行的基本單元,系統利用該基本單元實現系統對應用的併發性。進程和線程的區別在於:簡而言之,一個程序至少有一個進程,一個進程至少有一個線程. 。

3)爲何還要線程

進程有不少優勢,它提供了多道編程,讓咱們感受咱們每一個人都擁有本身的CPU和其餘資源,能夠提升計算機的利用率。不少人就不理解了,既然進程這麼優秀,爲何還要線程呢?其實,仔細觀察就會發現進程仍是有不少缺陷的,好比:

a)進程只能在一個時間幹一件事,若是想同時幹兩件事或多件事,進程就無能爲力了。

b)進程在執行的過程當中若是阻塞,例如等待輸入,整個進程就會掛起,即便進程中有些工做不依賴於輸入的數據,也將沒法執行。

c)程序變得愈來愈複雜,對CPU的要求也愈來愈高(對多個任務之間上下文切換的效率要求愈來愈高),而進程之間的切換開銷較大,沒法知足需求

因而就有了線程

1.3 線程

首先,早期操做系統沒有線程的概念,那時,進程不可是擁有資源和獨立運行的最小單位,也是程序執行和任務調度的最小單位;可是隨着程序變得愈來愈複雜,系統對多個任務之間上下文切換的效率要求愈來愈高,而進程之間的切換開銷較大,沒法知足需求。因而發明了線程;

線程(輕量級進程)是程序執行中一個單一的順序控制流程,是程序執行流的最小單元(比進程更小的能獨立運行的基本單位),是CPU處理器調度和分派的基本單位。一個進程能夠有一個或多個線程,各個線程之間共享程序的內存空間(也就是所在進程的內存空間),每條線程並行執行不一樣的任務。

線程本身基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧);

一個標準的線程由線程ID、當前指令指針(PC)、寄存器和堆棧組成。而進程由內存空間(代碼、數據、進程空間、打開的文件)和一個或多個線程組成。

線程有開始順序執行結束三部分。它有一個本身的指令指針,記錄本身運行到什麼地方。線程的運行可能被佔用(中斷),或者暫時的被掛起(睡眠),讓其餘的線程運行,這叫作讓步。一個進程中的各個線程之間共享同一片數據空間,因此線程之間能夠比進程之間更加方便的共享數據以及相互通信。

Wiki上對線程的定義:

A thread is an execution context, which is all the information a CPU needs to execute a stream of instructions.

Suppose you're reading a book, and you want to take a break right now, but you want to be able to come back and resume reading from the exact point where you stopped. One way to achieve that is by jotting down the page number, line number, and word number. So your execution context for reading a book is these 3 numbers.

If you have a roommate, and she's using the same technique, she can take the book while you're not using it, and resume reading from where she stopped. Then you can take it back, and resume it from where you were.

Threads work in the same way. A CPU is giving you the illusion that it's doing multiple computations at the same time. It does that by spending a bit of time on each computation. It can do that because it has an execution context for each computation. Just like you can share a book with your friend, many tasks can share a CPU.

On a more technical level, an execution context (therefore a thread) consists of the values of the CPU's registers.

Last: threads are different from processes. A thread is a context of execution, while a process is a bunch of resources associated with a computation. A process can have one or many threads.

Clarification: the resources associated with a process include memory pages (all the threads in a process have the same view of the memory), file descriptors (e.g., open sockets), and security credentials (e.g., the ID of the user who started the process).

1)進程與線程的關係

線程是程序執行的最小單位,是CPU處理器調度和分派的基本單位;而進程是操做系統分配資源的最小單位;一個進程由一個或多個線程組成,線程是一個進程中代碼的不一樣執行路線,某進程內的線程在其它進程不可見;

a)劃分尺度:線程更小,因此多線程程序併發性更高;一個線程能夠建立和撤銷另外一個線程;

b)資源分配:進程是資源分配的基本單位,同一進程內各個線程共享其資源(如打開文件和信號); 

c)地址空間:進程擁有獨立的地址空間,同一進程內各個線程共享其內存空間(包括代碼段、數據集、堆等);

d)處理器調度和切換:線程是處理器調度的基本單位;線程上下文切換比進程上下文切換要快得多,線程開銷比進程小不少;

e)執行:線程不能單獨執行,必須組成進程,一個進程至少有一個主線程。簡而言之,一個程序至少有一個進程,一個進程至少有一個線程;

線程在執行過程當中與進程仍是有區別的。每一個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。可是線程不可以獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。從邏輯角度來看,多線程的意義在於一個應用程序中,有多個執行部分能夠同時執行。但操做系統並無將多個線程看作多個獨立的應用,來實現進程的調度和管理以及資源分配。這就是進程和線程的重要區別。 

線程與進程共享關係示意圖:

 

wiki上關於進程線程關係的解釋:

a)Threads share the address space of the process that created it; processes have their own address space.

b)Threads have direct access to the data segment of its process; processes have their own copy of the data segment of the parent process.

c)Threads can directly communicate with other threads of its process; processes must use interprocess communication to communicate with sibling processes.

d)New threads are easily created; new processes require duplication of the parent process.

e)Threads can exercise considerable control over threads of the same process; processes can only exercise control over child processes.

f)Changes to the main thread (cancellation, priority change, etc.) may affect the behavior of the other threads of the process; changes to the parent process does not affect child processes.

1.4 多線程與多核

線程通常都是併發執行的。正是因爲這種併發和數據共享的機制使得多個任務合做變爲可能。實際上,在單CPU的系統中,真正的併發是不可能的,每一個線程會被安排成每次只運行一小會兒,而後就把CPU讓出來,讓其餘的線程去運行。那麼,若是有多顆CPU,準確的說是多個核心呢?

1)CPU

咱們常常看到CPU的參數:雙核心四線程,四核心四線程等等;究竟是什麼意思呢?

多核(心)處理器是指在一個處理器上集成多個運算核心從而提升計算能力,每個處理核心對應一個內核線程。那雙核心四線程是怎麼回事呢?由於使用了超線程技術,採用超線程技術(HT)將一個物理處理核心模擬成兩個邏輯處理核心,對應兩個內核線程。

咱們來看幾個CPU經常使用概念:

a)物理CPU :實際Server中插槽上的CPU個數,物理cpu數量;

b)CPU核心數:CPU的核心數是指物理上,也就是硬件上存在着幾個核心。好比,雙核就是包括2個相對獨立的CPU核心單元組,四核就包含4個相對獨立的CPU核心單元組。

c) 邏輯CPU:邏輯CPU數量=物理cpu數量 * 每顆CPU的核心數*2(若是支持並開啓HT,HT是intel的超線程技術)

d)線程數(內核線程):線程數就是邏輯CPU數目

e)查看CPU信息

-- windows:

cmd命令中輸入「wmic」,而後在出現的新窗口中輸入「cpu get *」便可查看物理CPU數、CPU核心數、線程數。其中:

  Name:表示物理CPU數
  NumberOfCores:表示CPU核心數
  NumberOfLogicalProcessors:表示CPU線程數

-- Linux:

Linux下top查看的CPU也是邏輯CPU個數

  #邏輯CPU個數

  cat /proc/cpuinfo | grep "processor" | sort –u | wc -l

  #物理CPU個數:

  cat /proc/cpuinfo | grep "physical id" | sort -u | wc -l

  #CPU核心數

  grep 'core id' /proc/cpuinfo | sort -u | wc -l

2)內核進程和用戶進程

上面講到邏輯CPU數目就是線程數,這個線程指的是內核線程,到底什麼是內核線程呢?

內核線程(Kernel Thread, KLT)就是直接由操做系統內核支持的線程,這種線程由內核來完成線程切換,內核經過操做調度器對線程進行調度,並負責將線程的任務映射到各個處理器上。通常一個處理核心對應一個內核線程。

程序通常不會直接去使用內核線程,而是去使用內核線程的一種高級接口——輕量級進程(Light Weight Process,LWP),輕量級進程就是咱們一般意義上所講的線程(咱們在這稱它爲用戶線程),因爲每一個輕量級進程都由一個內核線程支持,所以只有先支持內核線程,纔能有輕量級進程。用戶線程與內核線程的對應關係有三種模型:一對一模型、多對一模型、多對多模型;

在這以4個內核線程、3個用戶線程爲例對三種模型進行說明:

a)一對一模型

對於一對一模型來講,一個用戶線程就惟一地對應一個內核線程(反過來不必定成立,一個內核線程不必定有對應的用戶線程)。

線程之間的併發是真正的併發。一對一模型使用戶線程具備與內核線程同樣的優勢,一個線程因某種緣由阻塞時其餘線程的執行不受影響;此處,一對一模型也可讓多線程程序在多處理器的系統上有更好的表現。

但一對一模型也有兩個缺點:1.許多操做系統限制了內核線程的數量,所以一對一模型會使用戶線程的數量受到限制;2.許多操做系統內核線程調度時,上下文切換的開銷較大,致使用戶線程的執行效率降低。

 

b)多對一模型

多對一模型將多個用戶線程映射到一個內核線程上,線程之間的切換由用戶態的代碼來進行,所以相對一對一模型,多對一模型的線程切換速度要快許多;此外,多對一模型對用戶線程的數量幾乎無限制。但多對一模型也有兩個缺點:1.若是其中一個用戶線程阻塞,那麼其它全部線程都將沒法執行,由於此時內核線程也隨之阻塞了;2.在多處理器系統上,處理器數量的增長對多對一模型的線程性能不會有明顯的增長,由於全部的用戶線程都映射到一個處理器上了。

 c)多對多模型

多對多模型結合了一對一模型和多對一模型的優勢,將多個用戶線程映射到多個內核線程上。多對多模型的優勢有:1.一個用戶線程的阻塞不會致使全部線程的阻塞,由於此時還有別的內核線程被調度來執行;2.多對多模型對用戶線程的數量沒有限制;3.在多處理器的操做系統中,多對多模型的線程也能獲得必定的性能提高,但提高的幅度不如一對一模型的高。

在如今流行的操做系統中,大都採用多對多的模型。

 關於進程,線程,這裏介紹的仍是簡單,建議找一本關於操做系統的書好好研讀一番,如今咱們迴歸到Python

2. Python多線程與GIL

首先,讓咱們看一個問題,運行下面這段python代碼,看看CPU佔用率多少?

def dead_loop():
    while True:
        pass

dead_loop()

由於,個人電腦是雙核四線程,因此這個死循環的CPU使用率是由25%

那麼若是我使用多線程,運行兩個線程,這兩個線程的CPU利用率不是就到50%了嗎!試一下:

import threading

def dead_loop():
    while True:
        pass

# 新起一個死循環線程
t = threading.Thread(target=dead_loop)
t.start()

# 主線程也進入死循環
dead_loop()

可是,實際運行結果仍是25%;爲何會這樣呢?幕後黑手就是GIL

2.1 GIL

咱們先看一下官方對GIL的解釋

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.)

簡單的一句話包含了不少信息;

a)在Python衆多解釋器中,只有Cpython纔有GIL,JPython就沒有;由於CPython是大部分環境下默認的Python執行環境。因此在不少人的概念裏CPython就是Python,也就想固然的把GIL歸結爲Python語言的缺陷。明確一點,GIL並非Python的特性,它是在實現Python解析器(CPython)時所引入的一個概念,Python徹底能夠不依賴於GIL;

看一下CPython的源代碼

static PyThread_type_lock interpreter_lock = 0; /* This is the GIL */

這一行代碼摘自 ceval.c —— CPython 2.7 解釋器的源代碼,Guido van Rossum 的註釋」This is the GIL「 添加於2003 年,但這個鎖自己能夠追溯到1997年他的第一個多線程 Python 解釋器。在 Unix系統中,PyThread_type_lock 是標準 C  mutex_t 鎖的別名。當 Python 解釋器啓動時它初始化:

void
PyEval_InitThreads(void)
{
    interpreter_lock = PyThread_allocate_lock();
    PyThread_acquire_lock(interpreter_lock);
}

解釋器中的全部 C 代碼在執行 Python 時必須保持這個鎖。

b)GIL是一把互斥鎖

Python代碼的執行由Python虛擬機(也叫解釋器主循環)來控制,而對Python虛擬機的訪問由GIL(全局解釋器鎖)控制,GIL保證了在任意時刻,只有一個線程在解釋器中運行,就像單CPU系統運行多線程同樣,內存中能夠存放多個程序,但在任意時刻,只有一個線程在解釋器中運行;

c)GIL是歷史遺留問題,爲了解決線程安全的簡單粗暴作法

多線程編程能夠更有效地利用多核處理器,可是隨之帶來的就是線程間數據一致性和狀態同步的困難(線程安全);多核 CPU 在 1990 年代還屬於類科幻,Guido van Rossum 在創造 python 的時候,也想不到他的語言有一天會被用到極可能 1000+ 個核的 CPU 上面,一個全局鎖搞定多線程安全在那個時代應該是最簡單經濟的設計了。簡單而又能知足需求,那就是合適的設計(對設計來講,應該只有合適與否,而沒有好與很差)。

線程安全就是多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其餘線程不能進行訪問直到該線程讀取完,其餘線程纔可以使用。不會出現數據不一致或者數據污染。 線程不安全就是不提供數據訪問保護,有可能出現多個線程前後更改數據形成所獲得的數據是髒數據。

d)GIL的執行機理

記住一個原則:「一個線程運行 Python ,而其餘 N 個睡眠或者等待 I/O.」(One thread runs Python, while others sleep or await I/O)

多線程環境中,python虛擬機按如下方式執行:

  1. 設置GIL
  2. 切換到一個線程去執行
  3. 運行
    • 指定數量的字節碼指令(python2爲1000字節指令)或運行了執行時間(python3爲15ms)---搶佔式多任務處理
    • 線程主動讓出控制(能夠調用time.sleep(0)) -------協同式多任務處理
  4. 把線程設置完睡眠狀態
  5. 解鎖GIL
  6. 再次重複以上步驟

對全部面向 I/O 的(會調用內建的操做系統 C 代碼的)程序來講,GIL 會在這個 I/O 調用以前被釋放, 以容許其它的線程在這個線程等待 I/O 的時候運行。 若是某線程並未使用不少 I/O 操做,它會在本身的時間片內一直佔用處理器(和 GIL)。也就是說,I/O 密集型(程序大量時間花費在等待I/O操做,CPU老是閒置,在10%左右(如:網絡請求socket))的 Python 程序比計算密集型(程序線性執行,大量佔用CPU,老是接近100%(如:正則匹配替換大量文本))的程序更能充分利用多線程環境的好處。

線程什麼時候切換?一個線程不管什麼時候開始睡眠或等待網絡 I/O,其餘線程總有機會獲取 GIL 執行 Python 代碼。這是協同式多任務處理。CPython 也還有搶佔式多任務處理。若是一個線程不間斷地在 Python 2 中運行 1000 字節碼指令,或者不間斷地在 Python 3 運行15 毫秒,那麼它便會放棄 GIL,而其餘線程能夠運行。把這想象成舊日有多個線程但只有一個 CPU 時的時間片。如今,將具體討論這兩種多任務處理。

協同式多任務處理

當一項任務好比網絡 I/O啓動,而在長的或不肯定的時間,沒有運行任何 Python 代碼的須要,一個線程便會讓出GIL,從而其餘線程能夠獲取 GIL 而運行 Python。這種禮貌行爲稱爲協同式多任務處理,它容許併發;多個線程同時等待不一樣事件。

也就是說兩個線程各自分別鏈接一個套接字:
def do_connect():
    s = socket.socket()
    s.connect(('python.org', 80))  # drop the GIL
 
for i in range(2):
    t = threading.Thread(target=do_connect)
    t.start()

兩個線程在同一時刻只能有一個執行 Python ,但一旦線程開始鏈接,它就會放棄 GIL ,這樣其餘線程就能夠運行。這意味着兩個線程能夠併發等待套接字鏈接,這是一件好事。在一樣的時間內它們能夠作更多的工做。

讓咱們打開盒子,看看一個線程在鏈接創建時實際是如何放棄 GIL 的,在 socketmodule.c 中:

/* s.connect((host, port)) method */
static PyObject *
sock_connect(PySocketSockObject *s, PyObject *addro)
{
    sock_addr_t addrbuf;
    int addrlen;
    int res;
 
    /* convert (host, port) tuple to C address */
    getsockaddrarg(s, addro, SAS2SA(&addrbuf), &addrlen);
 
    Py_BEGIN_ALLOW_THREADS
    res = connect(s->sock_fd, addr, addrlen);
    Py_END_ALLOW_THREADS
 
    /* error handling and so on .... */
}

線程正是在Py_BEGIN_ALLOW_THREADS 宏處放棄 GIL;它被簡單定義爲:

PyThread_release_lock(interpreter_lock);

固然 Py_END_ALLOW_THREADS 從新獲取鎖。一個線程可能會在這個位置堵塞,等待另外一個線程釋放鎖;一旦這種狀況發生,等待的線程會搶奪回鎖,並恢復執行你的Python代碼。簡而言之:當N個線程在網絡 I/O 堵塞,或等待從新獲取GIL,而一個線程運行Python。

搶佔式多任務處理

Python線程能夠主動釋放 GIL,也能夠先發制人抓取 GIL 。

讓咱們回顧下 Python 是如何運行的。你的程序分兩個階段運行。首先,Python文本被編譯成一個名爲字節碼的簡單二進制格式。第二,Python解釋器的主迴路,一個名叫 pyeval_evalframeex() 的函數,流暢地讀取字節碼,逐個執行其中的指令。

當解釋器經過字節碼時,它會按期放棄GIL,而不須要通過正在執行代碼的線程容許,這樣其餘線程便能運行:默認狀況下,檢測間隔是1000 字節碼。全部線程都運行相同的代碼,並以相同的方式按期從他們的鎖中抽出。在 Python 3 GIL 的實施更加複雜,檢測間隔不是一個固定數目的字節碼,而是15 毫秒。然而,對於你的代碼,這些差別並不顯著。

e)應對GIL

在多核時代,編程的免費午飯沒有了。若是程序不能用併發擠幹每一個核的運算性能,那就意謂着會被淘汰。對軟件如此,對語言也是同樣。那 Python 的對策呢?

Python 的應對很簡單,以不變應萬變。在 python 3 中依然有 GIL。之因此不去掉,緣由嘛,不外如下幾點:

欲練神功,揮刀自宮

CPython 的 GIL 本意是用來保護全部全局的解釋器和環境狀態變量的。若是去掉 GIL,就須要多個更細粒度的鎖對解釋器的衆多全局狀態進行保護。或者採用 Lock-Free 算法。不管哪種,要作到多線程安全都會比單使用 GIL 一個鎖要難的多。並且改動的對象仍是有 20 年曆史的 CPython 代碼樹,更不論有這麼多第三方的擴展也在依賴 GIL。對 Python 社區來講,這不異於揮刀自宮,從新來過。

就算自宮,也未必成功

有位牛人曾經作了一個驗證用的 CPython,將 GIL 去掉,加入了更多的細粒度鎖。可是通過實際的測試,對單線程程序來講,這個版本有很大的性能降低,只有在利用的物理 CPU 超過必定數目後,纔會比 GIL 版本的性能好。這也難怪。單線程原本就不須要什麼鎖。單就鎖管理自己來講,鎖 GIL 這個粗粒度的鎖確定比管理衆多細粒度的鎖要快的多。而如今絕大部分的 python 程序都是單線程的。再者,從需求來講,使用 python 毫不是由於看中它的運算性能。就算能利用多核,它的性能也不可能和 C/C++ 比肩。費了大力氣把 GIL 拿掉,反而讓大部分的程序都變慢了,這不是南轅北轍嗎。

仍是曲線救國,試試其餘神功吧

 1. 使用多進程模塊Multiprocess

仍是回到咱們開始的那個CPU佔有率的實驗,fork一個子進程來實現兩個死循環:

from multiprocessing import Process, freeze_support

def dead_loop():
    while True:
        pass

if __name__ == '__main__':
    freeze_support()
    #fork一個子進程
    t = Process(target=dead_loop)
    t.start()
    dead_loop()

結果:

如咱們所預期的,出現了兩個cpu利用率的25%的進程;

multiprocessing庫的出現很大程度上是爲了彌補thread庫由於GIL而低效的缺陷。它完整的複製了一套thread所提供的接口方便遷移。惟一的不一樣就是它使用了多進程而不是多線程。每一個進程有本身的獨立的GIL,所以也不會出現進程之間的GIL爭搶。

固然multiprocessing也不是萬能良藥。它的引入會增長程序實現時線程間數據通信和同步的困難。就拿計數器來舉例子,若是咱們要多個線程累加同一個變量,對於thread來講,申明一個global變量,用thread.Lock的context包裹住三行就搞定了。而multiprocessing因爲進程之間沒法看到對方的數據,只能經過在主線程申明一個Queue,put再get或者用share memory的方法。這個額外的實現成本使得原本就很是痛苦的多線程程序編碼,變得更加痛苦了。

2. 使用其餘解析器,像JPython和IronPython這樣的解析器因爲實現語言的特性,他們不須要GIL的幫助。然而因爲用了Java/C#用於解析器實現,他們也失去了利用社區衆多C語言模塊有用特性的機會。因此這些解析器也所以一直都比較小衆。

3. 若是不想用多進程這樣重量級的解決方案,還有個更完全的方案,放棄 Python,改用 C/C++。固然,你也不用作的這麼絕,只須要把關鍵部分用 C/C++ 寫成 Python 擴展,其它部分仍是用 Python 來寫,讓 Python 的歸 Python,C 的歸 C。通常計算密集性的程序都會用 C 代碼編寫並經過擴展的方式集成到 Python 腳本里(如 NumPy 模塊)。在擴展裏就徹底能夠用 C 建立原生線程,並且不用鎖 GIL,充分利用 CPU 的計算資源了。不過,寫 Python 擴展老是讓人以爲很複雜。好在 Python 還有另外一種與 C 模塊進行互通的機制 : ctypes;

最後總結一下:

  • 由於GIL的存在,只有IO Bound場景下得多線程會獲得較好的性能
  • 若是對並行計算性能較高的程序能夠考慮把核心部分也成C模塊,或者索性用其餘語言實現
  • GIL在較長一段時間內將會繼續存在,可是會不斷對其進行改進

2.2 Python多線程應用

經常使用模塊兩個:thread和threading;threading是高級模塊,建議不要使用thread,很明顯的一個緣由是:thread模塊在主線程退出時,全部其餘線程沒有被清除就退出了。但threading模塊能夠確保全部子線程都退出後,進程纔會結束;

1)threading模塊基本應用

多線程模塊有兩種方式

a)直接調用

 1 import threading
 2 from time import sleep, ctime
 3 
 4 #每一個線程執行的時間
 5 loop_time_list = (4, 2) 6 
 7 #線程執行的函數
 8 def loop(loop_mem, loop_time):
 9     print('start loop %s at %s' % (loop_mem, ctime()))
10     sleep(loop_time)
11     print('loop %s done at %s' % (loop_mem, ctime()))
12 
13 
14 def main():
15     print('Programming start at: %s' % ctime())
16     threads = []
17     #線程數
18     loop_num = range(len(loop_time_list))
19 
20     #create threads
21     for i in loop_num:
22         t = threading.Thread(target=loop, args=(i, loop_time_list[i],))
23         threads.append(t)
24 
25     #start threads
26     for i in loop_num:
27         threads[i].start()
28 
29     print('all done at: %s' % ctime())
30 
31 if __name__ == '__main__':
32     main()

b)面向對象方式調用

 1 import threading
 2 from time import sleep, ctime
 3 
 4 #每一個線程執行的時間
 5 loop_time_list = (4, 2)
 6 
 7 class MyThread(threading.Thread):
 8     def __init__(self, func, args):
 9         threading.Thread.__init__(self)
10         self.func = func
11         self.args = args
12     def run(self):
13         self.func(*self.args)
14 
15 #線程執行的函數
16 def loop(loop_mem, loop_time):
17     print('start loop %s at %s' % (loop_mem, ctime()))
18     sleep(loop_time)
19     print('loop %s done at %s' % (loop_mem, ctime()))
20 
21 
22 def main():
23     print('Programming start at: %s' % ctime())
24     threads = []
25     #線程數
26     loop_num = range(len(loop_time_list))
27 
28     #create threads
29     for i in loop_num:
30         t = MyThread(loop, (i, loop_time_list[i]))
31         threads.append(t)
32 
33     #start threads
34     for i in loop_num:
35         threads[i].start()
36 
37     print('all done at: %s' % ctime())
38 
39 if __name__ == '__main__':
40     main()

本質上就是建立了一個繼承自threading.Thread的類,在構造函數中執行了threading.Thread的構造方法,重寫run方法;能夠經過IDE的斷點查看

看源碼會發現,調用順序爲:

start()->_bootstrap()->_bootstrap_inner()->run()

 在run方法中:

 1 def run(self):
 2     """Method representing the thread's activity.
 3 
 4     You may override this method in a subclass. The standard run() method
 5     invokes the callable object passed to the object's constructor as the
 6     target argument, if any, with sequential and keyword arguments taken
 7     from the args and kwargs arguments, respectively.
 8 
 9     """
10     try:
11         if self._target:
12             self._target(*self._args, **self._kwargs)
13     finally:
14         # Avoid a refcycle if the thread is running a function with
15         # an argument that has a member that points to the thread.
16         del self._target, self._args, self._kwargs

能夠看到,在run方法裏運行了_target,在threading.Thread的構造函數中:

self._target = target

因此,最終調用的是run方法

擴展,多線程如何獲取線程返回值,其實就是面向對象內容:

import threading


class MyThread(threading.Thread):

    def __init__(self,func,args=()):
        super(MyThread,self).__init__()
        self.func = func
        self.args = args

    def run(self):
        self.result = self.func(*self.args)

    def get_result(self):
        try:
            return self.result  # 若是子線程不使用join方法,此處可能會報沒有self.result的錯誤
        except Exception:
            return None


def foo(a,b,c):
    time.sleep(1)
    return a*2,b*2,c*2

st = time.time()
li = []
for i in xrange(4):
    t = MyThread(foo,args=(i,i+1,i+2))
    li.append(t)
    t.start()

for t in li:
    t.join()  # 必定要join,否則主線程比子線程跑的快,會拿不到結果
    print(t.get_result())

et = time.time()
print(et - st)

2)threading.Thread類

threading的Thread類是主要的運行對象,看一下這個類中的主要方法

其中的start()和run()咱們已經瞭解過了

a)join(timeout=None)

前面咱們成功使用了多線程,讓咱們看一下結果:

首先,主線程提早結束,可是在主線程已經退出的狀況下,子線程沒有被強制退出,而是繼續執行,直到全部子線程都退出,進程才結束;這是優於thread模塊的地方;

可是,若是咱們但願主線程不要提早結束呢?或者說,在子線程執行的過程當中,掛起主線程,等到子線程執行結束後,再恢復主線程運行呢?有,使用join方法

import threading
from time import sleep, ctime

#每一個線程執行的時間
loop_time_list = (4, 2)

#線程執行的函數
def loop(loop_mem, loop_time):
    print('start loop %s at %s' % (loop_mem, ctime()))
    sleep(loop_time)
    print('loop %s done at %s' % (loop_mem, ctime()))


def main():
    print('Programming start at: %s' % ctime())
    threads = []
    #線程數
    loop_num = range(len(loop_time_list))

    #create threads
    for i in loop_num:
        t = threading.Thread(target=loop, args=(i, loop_time_list[i]), )
        threads.append(t)

    #start threads
    for i in loop_num:
        threads[i].start()

    for i in loop_num:
        threads[i].join()

    print('all done at: %s' % ctime())

if __name__ == '__main__':
    main()

結果:

完美解決;

有時,有的線程執行時間過久,咱們不但願由於一個線程而讓整個程序阻塞,就能夠經過設置timeout來解決;好比對上例的join方法作簡單修改:

    for i in loop_num:
        threads[i].join(timeout=3)

 

 完美;

b)守護線程Daemon

threading模塊建立的線程默認是非守護線程;

守護線程 daemon thread

守護線程, 是指在程序運行的時候在後臺提供一種通用服務的線程, 好比垃圾回收線程就是一個很稱職的守護者, 而且這種線程並不屬於程序中不可或缺的部分. 所以, 當全部的非守護線程結束時, 程序也就終止了, 同時會殺死進程中的全部守護線程. 反過來講, 只要任何非守護線程還在運行, 程序就不會終止.

 1 import threading
 2 from time import sleep, ctime
 3 
 4 #每一個線程執行的時間
 5 loop_time_list = (4, 2)
 6 
 7 #線程執行的函數
 8 def loop(loop_mem, loop_time):
 9     print('start loop %s at %s' % (loop_mem, ctime()))
10     sleep(loop_time)
11     print('loop %s done at %s' % (loop_mem, ctime()))
12 
13 
14 def main():
15     print('Programming start at: %s' % ctime())
16     threads = []
17     #線程數
18     loop_num = range(len(loop_time_list))
19 
20     #create threads
21     for i in loop_num:
22         t = threading.Thread(target=loop, args=(i, loop_time_list[i]), )
23         threads.append(t)
24 
25     #將第一個線程(執行4s)設置爲守護線程
26     threads[0].setDaemon(True)
27 
28     #start threads
29     for i in loop_num:
30         threads[i].start()
31 
32     print('all done at: %s' % ctime())
33 
34 if __name__ == '__main__':
35     main()

結果:

由於守護線程loop 0的運行時間爲4s,而非守護線程loop 1的運行時間爲2s,當loop 0運行結束,主線程運行結束後;程序就退出了,而沒有等待守護線程loop 0之行結束

3)Thread類的其餘對象

a)線程鎖(互斥鎖Mutex)LOCK和RLOCK

首先,給出結論;即便Python擁有GIL,很大程度上保證了線程安全,可是,有時仍然須要加鎖來保護共享的可變狀態;

 爲何呢?GIL不是保證了同一時間只有一個線程進入python虛擬機運行嗎!

讓咱們先看一段代碼:

n = 0
 
def foo():
    global n
    n += 1

讓咱們看一下這個函數用 Python 的標準 dis 模塊編譯的字節碼:

>>> import dis
>>> dis.dis(foo)
LOAD_GLOBAL              0 (n)
LOAD_CONST               1 (1)
INPLACE_ADD
STORE_GLOBAL             0 (n)

代碼的一行中, n += 1,被編譯成 4 個字節碼,進行 4 個基本操做:

1. 將 n 值加載到堆棧上

2. 將常數 1 加載到堆棧上

3. 將堆棧頂部的兩個值相加

4. 將總和存儲回n

 注意對於n值,每一個線程都會有一個加載和恢復n值的工做;咱們知道一個線程每運行 1000 字節碼,就會被解釋器打斷奪走 GIL 。若是運氣很差,這(打斷)可能發生在線程加載 n 值到堆棧期間,以及把它存儲回 n 期間。很容易能夠看到這個過程會如何致使更新丟失:

 好比,若是我起了100個線程來執行foo()函數,結果理論上應該是100,但有時可能會看到99,98;因此,儘管有GIL,仍然須要加鎖來保護共享的可變狀態

 可是,對於原子操做,好比sort()就不須要加鎖;感興趣的能夠了解一下

如今來加鎖吧

 1 import threading
 2 
 3 def addNum():
 4     global num
 5     #得到鎖
 6     lock.acquire()
 7     num += 1
 8     #釋放鎖
 9     lock.release()
10 
11 def main():
12     threads = []
13     for i in range(100):
14         t = threading.Thread(target=addNum)
15         threads.append(t)
16     for i in range(100):
17         threads[i].start()
18     for i in range(100):
19         threads[i].join()
20     print('num: ', num)
21 
22 num = 0
23 lock = threading.Lock()
24 
25 if __name__ == '__main__':
26     main()

RLOCK就是在加多重鎖;

b)Semaphorre(信號量)

互斥鎖 同時只容許一個線程更改數據,而Semaphore是同時容許必定數量的線程更改數據 ,好比原來的廁所只有一個坑,那麼就配一把鑰匙,誰拿到鑰匙誰上;如今我在這個廁所裏多加了兩個坑,那麼就能夠多配兩把鑰匙,這樣就能夠有3我的同時上;其餘人只能在外邊排隊了;能夠看出來mutex是semaphore的一種特殊狀況(n=1時)。也就是說,徹底能夠用後者替代前者。可是,由於mutex較爲簡單,且效率高,因此在必須保證資源獨佔的狀況下,仍是採用這種設計。

 1 import threading
 2 import time
 3 
 4 def addNum():
 5     global num
 6     #得到鎖
 7     semap.acquire()
 8     num += 1
 9     print('current num:', num)
10     time.sleep(2)
11     #釋放鎖
12     semap.release()
13 
14 def main():
15     threads = []
16     for i in range(40):
17         t = threading.Thread(target=addNum)
18         threads.append(t)
19     for i in range(40):
20         threads[i].start()
21     for i in range(40):
22         threads[i].join()
23     print('final num: ', num)
24 
25 num = 0
26 semap = threading.BoundedSemaphore(4)
27 
28 if __name__ == '__main__':
29     main()

經過結果能夠看到current num是一次性打印4個;MySQL的最大鏈接數就是這樣實現的

c)Timer

定時器,start()之後等待n秒之後再執行

 1 import threading
 2 import time
 3 
 4 def addNum():
 5     global num
 6     num += 1
 7     print('current num:', num)
 8     time.sleep(2)
 9 
10 def main():
11     threads = []
12     for i in range(40):
13         #定時器,設置爲3s
14         t = threading.Timer(3, addNum)
15         threads.append(t)
16     for i in range(40):
17         #存在定時器,3s之後再開始執行
18         threads[i].start()
19     for i in range(40):
20         threads[i].join()
21     print('final num: ', num)
22 
23 num = 0
24 
25 if __name__ == '__main__':
26     main()

d)event

線程的事件處理,事件主要提供了四個方法 set、wait、clear,isSet

事件處理的機制:全局定義了一個「Flag」,若是「Flag」值爲 False,那麼當程序執行 event.wait 方法時就會阻塞,若是「Flag」值爲True,那麼event.wait 方法時便再也不阻塞。

clear:將「Flag」設置爲False

set:將「Flag」設置爲True

咱們能夠經過event來模擬一個假的異步模型

 1 import threading
 2 import time
 3 
 4 def producer():
 5     print('廚師:等人買包子')
 6     event.wait()
 7     event.clear()
 8     print('廚師:有人來買包子了,開始作包子')
 9     time.sleep(3)
10     print('廚師:你的包子作好了')
11     event.set()
12 
13 def consumer():
14     print('客戶:老闆,買包子')
15     event.set()
16     time.sleep(1)
17     print('客戶:老闆,快點')
18     event.wait()
19     print('客戶:謝謝老闆,真好吃')
20 
21 event = threading.Event()
22 p = threading.Thread(target=producer)
23 c = threading.Thread(target=consumer)
24 p.start()
25 c.start()

結果:

廚師:等人買包子
客戶:老闆,買包子
廚師:有人來買包子了,開始作包子
客戶:老闆,快點
廚師:你的包子作好了
客戶:謝謝老闆,真好吃

實際上,上邊並不是一個真正的異步模型,真正的異步模型是當客戶等待包子作好的過程當中還能夠幹別的事情,只是須要不斷地去問老闆「包子好了沒」;等包子好了再回來付錢買包子;

經過isSet方法能夠實現這個異步:

 1 import threading
 2 import time
 3 
 4 def producer():
 5     print('廚師:等人買包子')
 6     event.wait()
 7     event.clear()
 8     print('廚師:有人來買包子了,開始作包子')
 9     time.sleep(5)
10     print('廚師:你的包子作好了')
11     event.set()
12 
13 def consumer():
14     print('客戶:老闆,買包子')
15     event.set()
16     time.sleep(1)
17     while not event.isSet():
18         print('客戶:老闆還沒好啊!餓死了')
19         print('客戶:我先乾點別的吧,睡1秒')
20         time.sleep(1)
21     print('客戶:謝謝老闆,真好吃')
22 
23 event = threading.Event()
24 p = threading.Thread(target=producer)
25 c = threading.Thread(target=consumer)
26 p.start()
27 c.start()

結果:

廚師:等人買包子
客戶:老闆,買包子
廚師:有人來買包子了,開始作包子
客戶:老闆還沒好啊!餓死了
客戶:我先乾點別的吧,睡1秒
客戶:老闆還沒好啊!餓死了
客戶:我先乾點別的吧,睡1秒
客戶:老闆還沒好啊!餓死了
客戶:我先乾點別的吧,睡1秒
客戶:老闆還沒好啊!餓死了
客戶:我先乾點別的吧,睡1秒
廚師:你的包子作好了
客戶:謝謝老闆,真好吃

這樣就沒有阻塞了,客戶在等待期間也能夠幹別的了

4)queue模塊

queue模塊用於進行線程間通信,讓各個線程之間共享數據;並且queue是線程安全的

a)queue模塊提供3種隊列:

1. class queue.Queue(maxsize=0) #先進先出

2. class queue.LifoQueue(maxsize=0) #後進先出

3. class queue.PriorityQueue(maxsize=0) #存儲數據時可設置優先級的隊列

其中maxsize是隊列的最大規模,若是maxsize<=0,那麼隊列就是無限大

>>>
>>> from queue import Queue
>>> q = Queue(3)
>>> q.put('first_in')
>>> q.put('second_in')
>>> q.put('third_in')
>>>
>>> q.get()
'first_in'
>>> q.get()
'second_in'
>>> q.get()
'third_in'
>>>
>>> from queue import LifoQueue
>>> q_L = LifoQueue(3)
>>> q_L.put('first_in')
>>> q_L.put('second_in')
>>> q_L.put('third_in'))
>>> q_L.put('third_in')
>>>
>>>
>>> q_L.get()
'third_in'
>>> q_L.get()
'second_in'
>>> q_L.get()
'first_in'

>>>
>>> from queue import PriorityQueue
>>> q_P = PriorityQueue(3)
>>> q_P.put((6,'first_in'))
>>> q_P.put((1,'second_in'))
>>> q_P.put((10,'third_in'))
>>>
>>> q_P.get()
(1, 'second_in')
>>> q_P.get()
(6, 'first_in')
>>> q_P.get()
(10, 'third_in')
>>>
b)常見的兩個異常:
1. exception  queue. Empty
Exception raised when non-blocking  get() (or  get_nowait()) is called on a  Queue object which is empty.
2. exception  queue. Full
Exception raised when non-blocking  put() (or  put_nowait()) is called on a  Queue object which is full.
c)Queue隊列經常使用方法:

方法應用起來很是簡單,本身試驗一下便可,這裏不作贅述

5)生產者消費者模型以及python線程間通訊

在併發編程中使用生產者和消費者模式可以解決絕大多數併發問題。該模式經過平衡生產線程和消費線程的工做能力來提升程序的總體處理數據的速度。

隨着軟件業的發展,互聯網用戶的日漸增多,併發這門藝術的興起彷佛是那麼合情合理。每日PV十多億的淘寶,處理併發的手段可謂是業界一流。用戶訪問淘寶首頁的平均等待時間只有區區幾秒,可是服務器所處理的流程十分複雜。首先負責首頁的服務器就有好幾千臺,經過計算把與用戶路由最近的服務器處理首頁的返回。其次是網頁上的資源,就JS和CSS文件就有上百個,還有圖片資源等。它能在幾秒內加載出來。

而在大型電商網站中,他們的服務或者應用解耦以後,是經過消息隊列在彼此間通訊的。消息隊列和應用之間的架構關係就是生產者消費者模型。

生產者:負責產生數據的模塊(此處的模塊是廣義的,能夠是類、函數、線程、進程等)。

消費者:處理數據的模塊。

在線程世界裏,生產者就是生產數據的線程,消費者就是消費數據的線程。在多線程開發當中,若是生產者處理速度很快,而消費者處理速度很慢,那麼生產者就必須等待消費者處理完,才能繼續生產數據。一樣的道理,若是消費者的處理能力大於生產者,那麼消費者就必須等待生產者。爲了解決這個問題因而引入了生產者和消費者模式。

生產者消費者模式是經過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通信,而經過阻塞隊列來進行通信,因此生產者生產完數據以後不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列裏取,阻塞隊列就至關於一個緩衝區,平衡了生產者和消費者的處理能力。在這個模型中,最關鍵就是內存緩衝區爲空的時候消費者必須等待,而內存緩衝區滿的時候,生產者必須等待。其餘時候能夠是個動態平衡。

生產者消費者模式的優勢:

a)解耦

假設生產者和消費者分別是兩個類。若是讓生產者直接調用消費者的某個方法,那 麼生產者對於消費者就會產生依賴(也就是耦合)。未來若是消費者的代碼發生變化, 可能會影響到生產者。而若是二者都依賴於某個緩衝區,二者之間不直接依賴,耦合也 就相應下降了。

舉個例子,咱們去郵局投遞信件,若是不使用郵筒(也就是緩衝區),你必須得把 信直接交給郵遞員。有同窗會說,直接給郵遞員不是挺簡單的嘛?其實不簡單,你必須 得認識誰是郵遞員,才能把信給他(光憑身上穿的制服,萬一有人假冒,就慘了)。這 就產生和你和郵遞員之間的依賴(至關於生產者和消費者的強耦合)。萬一哪天郵遞員 換人了,你還要從新認識一下(至關於消費者變化致使修改生產者代碼)。而郵筒相對 來講比較固定,你依賴它的成本就比較低(至關於和緩衝區之間的弱耦合)。

b)支持併發

因爲生產者與消費者是兩個獨立的併發體,他們之間是用緩衝區做爲橋樑鏈接,生產者只須要往緩衝區裏丟數據,就能夠繼續生產下一個數據,而消費者只須要從緩衝區了拿數據便可,這樣就不會由於彼此的處理速度而發生阻塞。

接上面的例子,若是咱們不使用郵筒,咱們就得在郵局等郵遞員,直到他回來,咱們把信件交給他,這期間咱們啥事兒都不能幹(也就是生產者阻塞),或者郵遞員得挨家挨戶問,誰要寄信(至關於消費者輪詢)。

c)支持忙閒不均

緩衝區還有另外一個好處。若是製造數據的速度時快時慢,緩衝區的好處就體現出來 了。當數據製造快的時候,消費者來不及處理,未處理的數據能夠暫時存在緩衝區中。 等生產者的製造速度慢下來,消費者再慢慢處理掉。

爲了充分複用,咱們再拿寄信的例子來講事。假設郵遞員一次只能帶走1000封信。 萬一某次碰上情人節(也多是聖誕節)送賀卡,須要寄出去的信超過1000封,這時 候郵筒這個緩衝區就派上用場了。郵遞員把來不及帶走的信暫存在郵筒中,等下次過來 時再拿走。

實際應用:

在版本升級項目中,信息服務器要接收大批量的客戶端請求,原來那種串行化的 處理,根本沒法及時處理客戶端請求,形成信息服務器大量請求堆積,致使丟包異 常嚴重。以後就採用了生產者消費者模式,在業務請求與業務處理間,創建了一個List 類型的緩衝區,服務端接收到業務請求,就往裏扔,而後再去接收下一個業務請求,而 多個業務處理線程,就會去緩衝區裏取業務請求處理。這樣就大大提升了服務器的相 應速度。

Python中應用:

 1 from threading import Thread, RLock
 2 from queue import Queue
 3 import time
 4 
 5 q = Queue(10)
 6 count = 0
 7 l = RLock()
 8 
 9 #建立生產者
10 class Producer(Thread):
11     def __init__(self, name, que):
12         super(Producer, self).__init__()
13         self.__name = name
14         self.__que = que
15 
16     def run(self):
17         while True:
18             global count
19             l.acquire()
20             count += 1
21             l.release()
22             self.__que.put(count)
23             print('%s produce baozi %s' % (self.__name, count))
24             time.sleep(0.5)
25             self.__que.join()
26 
27 #建立消費者
28 class Consumer(Thread):
29     def __init__(self, name, que):
30         super(Consumer, self).__init__()
31         self.__name = name
32         self.__que = que
33 
34     def run(self):
35         while True:
36             data = self.__que.get()
37             print('%s eat baozi %s' % (self.__name, data))
38             time.sleep(1)
39             self.__que.task_done()
40 
41 def main():
42     #建立1個生產者,3個消費者
43     p1 = Producer('winter', q)
44     c1 = Consumer('elly', q)
45     c2 = Consumer('jack', q)
46     c3 = Consumer('frank', q)
47     p1.start()
48     c1.start()
49     c2.start()
50     c3.start()
51 
52 if __name__ == '__main__':
53     main()

生產者消費者模型設計要合理,若是生產者慢了,能夠增長生產者,消費者慢了,增長消費者;

實際應用中,生產者,消費者多是兩套不一樣的系統,不會存在於一個進程裏,甚至不在同一臺設備上;而queue.Queue只能用於線程間通信,那麼該怎麼辦呢?

採用消息隊列,好比rabbitMQ;

 最後,上傳一篇將進程線程作了很好的類比的一篇文章

1. 計算機的核心是CPU,它承擔了全部的計算任務。它就像一座工廠,時刻在運行。

2. 假定工廠的電力有限,一次只能供給一個車間使用。也就是說,一個車間開工的時候,其餘車間都必須停工。背後的含義就是,單個CPU一次只能運行一個任務。

3. 進程就比如工廠的車間,它表明CPU所能處理的單個任務。任一時刻,CPU老是運行一個進程,其餘進程處於非運行狀態。

4. 一個車間裏,能夠有不少工人。他們協同完成一個任務。

5. 線程就比如車間裏的工人。一個進程能夠包括多個線程。

6. 車間的空間是工人們共享的,好比許多房間是每一個工人均可以進出的。這象徵一個進程的內存空間是共享的,每一個線程均可以使用這些共享內存。

7.但是,每間房間的大小不一樣,有些房間最多隻能容納一我的,好比廁所。裏面有人的時候,其餘人就不能進去了。這表明一個線程使用某些共享內存時,其餘線程必須等它結束,才能使用這一塊內存。

8. 一個防止他人進入的簡單方法,就是門口加一把鎖。先到的人鎖上門,後到的人看到上鎖,就在門口排隊,等鎖打開再進去。這就叫「互斥鎖」(Mutual exclusion,縮寫 Mutex),防止多個線程同時讀寫某一塊內存區域。

9. 還有些房間,能夠同時容納n我的,好比廚房。也就是說,若是人數大於n,多出來的人只能在外面等着。這比如某些內存區域,只能供給固定數目的線程使用。這時的解決方法,就是在門口掛n把鑰匙。進去的人就取一把鑰匙,出來時再把鑰匙掛回原處。後到的人發現鑰匙架空了,就知道必須在門口排隊等着了。這種作法叫作「信號量」(Semaphore),用來保證多個線程不會互相沖突。

不難看出,mutex是semaphore的一種特殊狀況(n=1時)。也就是說,徹底能夠用後者替代前者。可是,由於mutex較爲簡單,且效率高,因此在必須保證資源獨佔的狀況下,仍是採用這種設計。

10.操做系統的設計,所以能夠歸結爲三點:

(1)以多進程形式,容許多個任務同時運行;

(2)以多線程形式,容許單個任務分紅不一樣的部分運行;

(3)提供協調機制,一方面防止進程之間和線程之間產生衝突,另外一方面容許進程之間和線程之間共享資源。

相關文章
相關標籤/搜索