Python 多進程開發與多線程開發

我們先來了解什麼是進程?

程序並不能單獨運行,只有將程序裝載到內存中,系統爲它分配資源才能運行,而這種執行的程序就稱之爲進程。程序和進程的區別就在於:程序是指令的集合,它是進程運行的靜態描述文本;進程是程序的一次執行活動,屬於動態概念。

在多道編程中,我們允許多個程序同時加載到內存中,在操作系統的調度下,可以實現併發地執行。這是這樣的設計,大大提高了CPU的利用率。進程的出現讓每個用戶感覺到自己獨享CPU,因此,進程就是爲了在CPU上實現多道編程而提出的。

有了進程爲什麼還要線程?

進程提供了多道編程,充分發揮了計算機部件的並行性,提高了計算機的利用率,既然進程這麼優秀,爲什麼還要線程呢? 其實,還是有很多缺陷的,主要體現在兩點上:

  • 進程只能在一個時間幹一件事,如果想同時幹兩件事或多件事,進程就無能爲力了。

  • 進程在執行的過程中如果阻塞,例如等待輸入,整個進程就會掛起,即使進程中有些工作不依賴於輸入的數據,也將無法執行。


而解決辦法就是讓單個進程,接受請求、等待I/O、處理計算並行起來,這樣很明顯可以避免同步等待,提高執行效率,在實際操作系統中這樣的機制就是——線程。

線程的優點

因爲要併發,我們發明了進程,又進一步發明了線程。只不過進程和線程的併發層次不同:進程屬於在處理器這一層上提供的抽象;線程則屬於在進程這個層次上再提供了一層併發的抽象。如果我們進入計算機體系結構裏,就會發現,流水線提供的也是一種併發,不過是指令級的併發。這樣,流水線、線程、進程就從低到高在三個層次上提供我們所迫切需要的併發!

除了提高進程的併發度,線程還有個好處,就是可以有效地利用多處理器和多核計算機。現在的處理器有個趨勢就是朝着多核方向發展,在沒有線程之前,多核並不能讓一個進程的執行速度提高,原因還是上面所有的兩點限制。但如果講一個進程分解爲若干個線程,則可以讓不同的線程運行在不同的核上,從而提高了進程的執行速度。

例如:我們經常使用微軟的Word進行文字排版,實際上就打開了多個線程。這些線程一個負責顯示,一個接受鍵盤的輸入,一個進行存盤等等。這些線程一起運行,讓我們感覺到我們輸入和屏幕顯示同時發生,而不是輸入一些字符,過一段時間才能看到顯示出來。在我們不經意間,還進行了自動存盤操作。這就是線程給我們帶來的方便之處。

進程與線程的區別

  • 進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。

  • 線程是進程的一個實體CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。

  • 一個線程可以創建和撤銷另一個線程,同一個進程中的多個線程之間可以併發執行。

進程和線程的主要差別在於它們是不同的操作系統資源管理方式。進程有獨立的地址空間,一個進程崩潰後,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不同執行路徑。線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉就等於整個進程死掉,所以多進程的程序要比多線程的程序 健壯,但在進程切換時,耗費資源較大,效率要差一些。但對於一些要求同時進行並且又要共享某些變量的併發操作,只能用線程,不能用進程。


Python 多進程(multiprocessing

Unix/Linux操作系統提供了一個fork()系統調用,它非常特殊。普通的函數調用,調用一次,返回一次,但是fork()調用一次,返回兩次,因爲操作系統自動把當前進程(稱爲父進程)複製了一份(稱爲子進程),然後,分別在父進程和子進程內返回。

子進程永遠返回0,而父進程返回子進程的ID。這樣做的理由是,一個父進程可以fork出很多子進程,所以,父進程要記下每個子進程的ID,而子進程只需要調用getppid()就可以拿到父進程的ID。

Python的os模塊封裝了常見的系統調用,其中就包括fork,可以在Python程序中輕鬆創建子進程:

# multiprocessing.py
import os

print 'Process (%s) start...' % os.getpid()
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)

由於Windows沒有fork調用,上面的代碼在Windows上無法運行。

multiprocessing

multiprocessing是跨平臺版本的多進程模塊,它提供了一個Process類來代表一個進程對象,下面是示例代碼:

#!/usr/local/python27/bin/python2.7
# coding=utf8
# noinspection PyUnresolvedReferences

from multiprocessing import Process
import time

def f(n):
    time.sleep(1)
    print n*n

for i in range(10):
    p = Process(target=f,args=[i,])
    p.start()

這個程序如果用單進程寫則需要執行10秒以上的時間,而用多進程則啓動10個進程並行執行,只需要用1秒多的時間。


在一般情況下多個進程的內存資源是相互獨立的,而多線程可以共享同一個進程中的內存資源,示例代碼:

#!/usr/local/python27/bin/python2.7
# coding=utf8
# noinspection PyUnresolvedReferences
# 通過多進程和多線程對比,進程間內存無法共享,線程間的內存共享
from multiprocessing import Process
import threading
import time
lock = threading.Lock()

def run(info_list,n):
    lock.acquire()
    info_list.append(n)
    lock.release()
    print('%s\n' % info_list)

info = []

for i in range(10):
'''target爲子進程執行的函數,args爲需要給函數傳遞的參數'''    
    p = Process(target=run,args=[info,i])
    p.start()

'''這裏是爲了輸出整齊讓主進程的執行等一下子進程'''    
time.sleep(1)    
print('------------threading--------------')

for i in range(10):
    p = threading.Thread(target=run,args=[info,i])
    p.start()

執行結果:

wKiom1ZmXQeAhGkSAAAgRGj6pHw009.png

進程間通信

Queue

Queue是多進程安全的隊列,可以使用Queue實現多進程之間的數據傳遞。put方法用以插入數據到隊列中,put方法還有兩個可選參數:blocked和timeout。如果blocked爲True(默認值),並且timeout爲正值,該方法會阻塞timeout指定的時間,直到該隊列有剩餘的空間。如果超時,會拋出Queue.Full異常。如果blocked爲False,但該Queue已滿,會立即拋出Queue.Full異常。

 

get方法可以從隊列讀取並且刪除一個元素。同樣,get方法有兩個可選參數:blocked和timeout。如果blocked爲True(默認值),並且timeout爲正值,那麼在等待時間內沒有取到任何元素,會拋出Queue.Empty異常。如果blocked爲False,有兩種情況存在,如果Queue有一個值可用,則立即返回該值,否則,如果隊列爲空,則立即拋出Queue.Empty異常。Queue的一段示例代碼:

#!/usr/local/python27/bin/python2.7
# coding=utf8
# noinspection PyUnresolvedReferences
# 通過multiprocessing.Queue實現進程間內存共享
from multiprocessing import Process,Queue
import time

def write(q):
    for i in ['A','B','C','D','E']:
        print('Put %s to queue' % i)
        q.put(i)
        time.sleep(0.5)

def read(q):
    while True:
        v = q.get(True)
        print('get %s from queue' %v)

if __name__ == '__main__':
    q = Queue()
    pw = Process(target=write,args=(q,))
    pr = Process(target=read,args=(q,))
    pw.start()
    pr.start()
    pr.join()
    pr.terminate()

執行結果:

wKiom1ZmheCR9nRlAAAXJwKB584161.png

Value,Array

#!/usr/local/python27/bin/python2.7
# coding=utf8
# noinspection PyUnresolvedReferences
# 通過Value Array 實現進程間的內存共享
from multiprocessing import Value,Array,Process

def f(n,a,raw):
    n.value = 3.14
    for i in range(5):
        a[i] = -a[i]
    raw.append(9999)
    print(raw)

if __name__ == '__main__':
    num = Value('d',0.0)
    arr = Array('i',range(10))
    raw_list = range(10)
    print(num.value)
    print(arr[:])
    print(raw_list)

#調用子進程之後,重新打印array和value,值將會發生改變。 而raw_list 普通列表在外層打印則沒有發生改變。
    p = Process(target=f,args=(num,arr,raw_list))
    p.start()
    p.join()

    print(num.value)
    print(arr[:])
    print(raw_list)

執行結果:

wKioL1ZmieTQzmAtAAAh3cXl3kc963.png

Manager 

#!/usr/local/python27/bin/python2.7
# coding=utf8
# noinspection PyUnresolvedReferences
from multiprocessing import Process,Manager

def f(d,l):
    d[1] = '1'
    d['aa'] = 'hello World'
    l.reverse()

if __name__ == '__main__':
    manager = Manager()
    d = manager.dict()
    l = manager.list(range(10))
    p = Process(target=f,args=(d,l))
    p.start()
    p.join()
    print(d)
    print(l)

執行結果:

wKiom1Zmll-SUVaDAAAPtp8IIX4716.png

Pool (進程池)

用於批量創建子進程,可以靈活控制子進程的數量

#!/usr/local/python27/bin/python2.7
# coding=utf8
# noinspection PyUnresolvedReferences
from multiprocessing import Pool
import time

def f(x):
    print x*x
    time.sleep(2)
    return x*x

'''定義啓動的進程數量'''
pool = Pool(processes=5)
res_list = []

for i in range(10):
    '''以異步並行的方式啓動進程,如果要同步等待的方式,可以在每次啓動進程之後調用res.get()方法,也可以使用Pool.apply'''
    res = pool.apply_async(f,[i,])
    print('-------:',i)
    res_list.append(res)
pool.close()
pool.join()
for r in res_list:
    print(r.get(timeout=5))

Pool對象調用join()方法會等待所有子進程執行完畢,調用join()之前必須先調用close(),調用close()之後就不能繼續添加新的Process了。


Python 多線程(threading

上面介紹了線程的作用,在python的標準庫中提供了兩個模塊:thread和threading,threading是對thread進行了封裝的高級模塊。啓動一個線程就是把一個函數傳給Thread實例,然後調用start()方法。 

先來看一個示例程序:

#!/usr/local/python27/bin/python2.7
# coding=utf8
# noinspection PyUnresolvedReferences

from threading import Thread
import time

def f(n):
    time.sleep(1)
    num = n*n
    print('%s\n' % num)

l1 = range(10)
for i in l1:
    p = Thread(target=f,args=(i,))
    p.start()

Lock

多線程和多進程最大的不同在於,多進程中,同一個變量,各自有一份拷貝存在於每個進程中,互不影響,而多線程中,所有變量都由所有線程共享,所以,任何一個變量都可以被任何一個線程修改,因此,線程之間共享數據最大的危險在於多個線程同時改一個變量,把內容給改亂了。

示例代碼:

#!/usr/local/python27/bin/python2.7
# coding=utf8
# noinspection PyUnresolvedReferences

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了,因爲修改balance需要多條語句,而執行這幾條語句時,線程可能中斷,從而導致多個線程把同一個對象的內容改亂了。


兩個線程同時一存一取,就可能導致數據不對,如果要確保balance計算正確,就需要給change_i()上一把鎖,確保一個線程在修改balance的時候,別的線程一定不能改。


下面看示例代碼,創建鎖通過threading.Lock()來實現:

#!/usr/local/python27/bin/python2.7
# coding=utf8
# noinspection PyUnresolvedReferences

import time, threading

balance = 0
lock = threading.Lock()

def change_it(n):
    # 先加後減,結果應該爲0:
    global balance
    balance = balance + n
    balance = balance - n

def run_thread(n):
    for i in range(100000):
        lock.acquire()
        try:
            change_it(n)
        finally:
            lock.release()

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

生產者消費者模型

#!/usr/local/python27/bin/python2.7
# coding=utf8
# noinspection PyUnresolvedReferences

from threading import Thread
from Queue import Queue
import time

class procuder(Thread):
    '''
    @:param name:生產者名稱
    @:param queue:容器
    '''
    def __init__(self,name,queue):
        self.__Name = name
        self.__Queue = queue
        super(procuder,self).__init__()

    def run(self):
        while True:
            if self.__Queue.full():
                time.sleep(1)
            else:
                self.__Queue.put('baozi')
                time.sleep(1)
                print('%s 生產了一個包子' %(self.__Name))

class consumer(Thread):
    def __init__(self,name,queue):
        self.__Name = name
        self.__Queue = queue
        '''執行父類的構造方法'''
        super(consumer,self).__init__()
    def run(self):
        while True:
            if self.__Queue.empty():
                time.sleep(1)
            else:
                self.__Queue.get()
                time.sleep(1)
                print('%s 吃了一個包子' %(self.__Name))

que = Queue(maxsize=100)

tuchao1 = procuder('小塗1',que)
tuchao1.start()
tuchao2 = procuder('小塗2',que)
tuchao2.start()
tuchao3 = procuder('小塗3',que)
tuchao3.start()

for i in range(20):
    name = '小喻%d' %(i,)
    temp = consumer(name,que)
    temp.start()

python 多線程開發之事件

#!/usr/local/python27/bin/python2.7
# coding=utf8
# noinspection PyUnresolvedReferences

import threading
import time

def producer():
    print ('店家:本店出售包子。。。。')
    '''觸發事務等待'''
    event.wait()
    '''清楚事務狀態'''
    event.clear()
    print ('小明:老闆,請給我一個肉包子。。。。')
    print ('店家:正在生產包子,請稍等xxxxxxxxx')
    time.sleep(6)
    print ('店家:你要的包子已經好了------')
    event.set()

def consumer():
    print('小明去買包子。。。。')
    '''解除事務等待'''
    event.set()
    time.sleep(2)
    while True:
        if event.isSet():
            print ('謝謝!')
            break
        else:
            print ('包子還沒好嗎? 等待中...')
            time.sleep(1)


event = threading.Event()

p = threading.Thread(target=producer)
c = threading.Thread(target=consumer)

c.start()
p.start()

執行結果:

wKioL1Zm-9ugWaNzAAAaNrwSsYU296.png

GIL鎖(Global Interpreter Lock

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鎖,互不影響。


參考文獻:

http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/00143192823818768cd506abbc94eb5916192364506fa5d000

http://www.cnblogs.com/hazir/archive/2011/05/09/2447287.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章