1. 操作系統基本知識,進程,線程
CPU是計算機的核心,承擔了所有的計算任務;
操作系統是計算機的管理者,它負責任務的調度、資源的分配和管理,統領整個計算機硬件;那么操作系統是如何進行任務調度的呢?
1.1 任務調度
大部分操作系統(如Windows、Linux)的任務調度是采用時間片輪轉的搶占式調度方式,也就是說一個任務執行一小段時間后強制暫停去執行下一個任務,每個任務輪流執行。任務執行的一小段時間叫做時間片,任務正在執行時的狀態叫運行狀態,任務執行一段時間后強制暫停去執行下一個任務,被暫停的任務就處於就緒狀態等待下一個屬於它的時間片的到來。這樣每個任務都能得到執行,由於CPU的執行效率非常高,時間片非常短,在各個任務之間快速地切換,給人的感覺就是多個任務在“同時進行”,這也就是我們所說的並發(別覺得並發有多高深,它的實現很復雜,但它的概念很簡單,就是一句話:多個任務同時執行)。多任務運行過程的示意圖如下:

注意:當一個任務得到CPU時,相關的資源必須已經就位,然后CPU開始執行,除了CPU以外的所有就構成了這個任務的執行環境,就是所謂的程序上下文,CPU每一次任務的切換都需要保存程序的上下文,這個上下文就是下一次CPU臨幸是的環境;
所以任務的輪轉方法為:先加載程序A的上下文,然后開始執行A,保存程序A的上下文,調入下一個要執行的程序B的程序上下文,然后開始執行B,保存程序B的上下文。。。。
1.2 進程
說到進程,需要先提一下程序
應用程序是用於實現某種功能的一組指令的有序集合(只不過是磁盤中可執行的二進制(或者其他類型)的數據);應用程序運行於操作系統之上(只有將程序裝載到內存中,系統為它分配了資源並被操作系統調用的時候才開始它們的生命周期,即運行)
進程(有時候被稱為重量級進程)是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,是系統進行資源分配和調度的一個獨立單位(不是最小單位),是應用程序運行的載體;
每一個進程都有自己的地址空間、內存、數據棧以及其他記錄起運行軌跡的輔助數據。操作系統管理其上運行的所有進程,並為這些進程公平的分配時間、進程也可以通過fork和spawn操作類完成其他的任務。不過各個進程有自己的內存空間、數據棧等,所以只能使用進程間通訊(interprocess communication, IPC),而不能直接共享信息。
我們再看一下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虛擬機按以下方式執行:
- 設置GIL
- 切換到一個線程去執行
- 運行
- 指定數量的字節碼指令(python2為1000字節指令)或運行了執行時間(python3為15ms)---搶占式多任務處理
- 線程主動讓出控制(可以調用time.sleep(0)) -------協同式多任務處理
- 把線程設置完睡眠狀態
- 解鎖GIL
- 再次重復以上步驟
對所有面向 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()(orget_nowait()) is called on aQueueobject which is empty. -
2. exception
queue.Full -
Exception raised when non-blocking
put()(orput_nowait()) is called on aQueueobject 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)提供協調機制,一方面防止進程之間和線程之間產生沖突,另一方面允許進程之間和線程之間共享資源。
