Python | Python學習之多線程詳解


多進程詳解

在Python中如何建立多線程?

  1. 經過Thread建立多線程css

  2. 經過Thread子類建立多線程python

python的threading模塊是對thread作了一些包裝的,能夠更加方便的被使用,線程的方法和進程的基本類似,這裏就很少贅述,下面舉幾個栗子:數據庫

#例一線程的基本用法
#coding=utf-8
import threading
import time
def xianyu():
    print("鹹魚普拉思")
    time.sleep(1)
if __name__ == "__main__":
    for i in range(5):
        t = threading.Thread(target=xianyu)
        t.start() #啓動線程,即讓線程開始執行
輸出:
鹹魚普拉思
鹹魚普拉思
鹹魚普拉思
鹹魚普拉思
鹹魚普拉思
[Finished in 1.1s]

#例二使用Threading子類建立多線程
#coding=utf-8
import threading
import time
class MyThread(threading.Thread):
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm "+self.name+' @ '+str(i)
            print(msg)
def test():
    for i in range(5):
        t = MyThread()
        t.start()
if __name__ == '__main__':
    test()
輸出:
I'm Thread-1 @ 0
I'
m Thread-2 @ 0
I'm Thread-3 @ 0
I'
m Thread-4 @ 0
I'm Thread-5 @ 0
I'
m Thread-1 @ 1
I'm Thread-2 @ 1
I'
m Thread-4 @ 1
I'm Thread-3 @ 1
I'
m Thread-5 @ 1
I'm Thread-1 @ 2
I'
m Thread-2 @ 2
I'm Thread-5 @ 2
I'
m Thread-4 @ 2
I'm Thread-3 @ 2
[Finished in 3.2s]












































多線程和多進程的執行有什麼區別?

  1. 多進程是多份程序同時執行編程

  2. 多線程是在一份程序下多個執行指針同時執行設計模式

  3. 多線程並不須要線程間通訊,線程間共享全局變量,進程間不共享全局變量安全

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

  5. 線程的劃分尺度小於進程(資源比進程少),使得多線程程序的併發性高。 多線程

  6. 進程在執行過程當中擁有獨立的內存單元,而多個線程共享內存,從而極大地提升了程序的運行效率 併發

  7. 且線程不可以獨立執行,必須依存在進程中 app

  8. 線程執行開銷小,但不利於資源的管理和保護,而進程正相反

線程的幾種狀態

線程在執行過程當中,若是中途執行sleep語句時,線程會進入到阻塞狀態,當sleep結束以後,線程進入就緒狀態,等待調度而線程調度將自行選擇一個線程執行 。
具體線程狀態變換參照下圖:

線程狀態變化圖

線程之間共享全局變量

舉個栗子:

from threading import Thread
import time
num = 100
def work1():
    global num
    for i in range(3):
        num += 1
    print("----in work1, num is %d---"%num)
def work2():
    global num
    print("----in work2, num is %d---"%num)
print("---線程建立以前g_num is %d---"%num)
t1 = Thread(target=work1)
t1.start()
#延時一會,保證t1線程中的事情作完
time.sleep(1)
t2 = Thread(target=work2)
t2.start()
輸出:
---線程建立以前g_num is 100---
----in work1, num is 103---
----in work2, num is 103---
[Finished in 1.1s]






















總結:

  1. 在一個進程內的全部線程共享全局變量,可以在不適用其餘方式的前提下完成多線程之間的數據共享(這點要比多進程要好)

  2. 缺點就是,線程是對全局變量隨意遂改可能形成多線程之間對全局變量的混亂(即線程非安全)

什麼是線程不安全?

舉個栗子:

from threading import Thread
import time
num = 0
def test1():
    global num
    for i in range(1000000):
        num += 1
    print("---test1---num=%d"%g_numnum
def test2():
    global num
    for i in range(1000000):
        num += 1
    print("---test2---num=%d"%num)
p1 = Thread(target=test1)
p1.start()
# time.sleep(3)
p2 = Thread(target=test2)
p2.start()
print("---num=%d---"%num)
輸出:
當time.sleep(3),沒有取消屏蔽時
---num=235159---
---test1---num=1172632
---test2---num=1334237
[Finished in 0.3s]
當time.sleep(3),取消屏蔽時
---test1---num=1000000
---num=1014670---
---test2---num=2000000
[Finished in 3.3s]





























上面舉的栗子就是線程不安全的現象,具體能夠解釋爲,線程1對數據num進行自增的時候,獲取的值是num=0,此時系統把線程1調度爲」sleeping」狀態 ,而線程2在作一樣操做時獲取的num值仍是爲0,同時作自增1的操做,這時在線程2中num的值爲1,此時系統把線程2調度爲」sleeping」狀態,線程1再作自增操做時,num仍是剛剛獲取到的0,長此往復下去,最終的結果就不是咱們所預期的了。

沒有控制多個線程對同一資源的訪問,對數據形成破壞,使得線程運行的結果不可預期 ,這種現象就是線程不安全。

如何避免線程不安全的現象發生?

當多個線程幾乎同時修改某一個共享數據的時候,須要進行同步控制,線程同步可以保證多個線程安全訪問競爭資源,最簡單的同步機制是引入互斥鎖。互斥鎖爲資源引入一個狀態:鎖定/非鎖定。

某個線程要更改共享數據時,先將其鎖定,此時資源的狀態爲「鎖定」,其餘線程不能更改;直到該線程釋放資源,將資源的狀態變成「非鎖定」,其餘的線程才能再次鎖定該資源。互斥鎖保證了每次只有一個線程進行寫入操做,從而保證了多線程狀況下數據的正確性。
舉個栗子:

from threading import Thread, Lock
import time
num = 0
def test1():
    global num
    for i in range(1000000):
        #True表示堵塞 即若是這個鎖在上鎖以前已經被上鎖了,那麼這個線程會在這裏一直等待到解鎖爲止 
        #False表示非堵塞,即無論本次調用可以成功上鎖,都不會卡在這,而是繼續執行下面的代碼
        mutexFlag = mutex.acquire(True
        if mutexFlag:
            num += 1
            mutex.release()
    print("---test1---num=%d"%num)
def test2():
    global num
    for i in range(1000000):
        mutexFlag = mutex.acquire(True#True表示堵塞
        if mutexFlag:
            num += 1
            mutex.release()
    print("---test2---num=%d"%num)
#建立一個互斥鎖
#這個所默認是未上鎖的狀態
mutex = Lock()
p1 = Thread(target=test1)
p1.start()
p2 = Thread(target=test2)
p2.start()
print("---num=%d---"%num)
輸出:
---num=61866---
---test1---num=1861180
---test2---num=2000000
































當一個線程調用鎖的acquire()方法得到鎖時,鎖就進入「locked」狀態。每次只有一個線程能夠得到鎖。若是此時另外一個線程試圖得到這個鎖,該線程就會變爲「blocked」狀態,稱爲「阻塞」,直到擁有鎖的線程調用鎖的release()方法釋放鎖以後,鎖進入「unlocked」狀態。線程調度程序從處於同步阻塞狀態的線程中選擇一個來得到鎖,並使得該線程進入運行狀態。

加鎖確保了某段關鍵代碼只能由一個線程從頭至尾完整地執行,可是阻止了多線程併發執行,包含鎖的某段代碼實際上只能以單線程模式執行,效率就大大地降低了,因爲能夠存在多個鎖,不一樣的線程持有不一樣的鎖,並試圖獲取對方持有的鎖時,可能會形成死鎖

什麼是死鎖?

在線程間共享多個資源的時候,若是兩個線程分別佔有一部分資源而且同時等待對方的資源,就會形成死鎖。

舉個栗子:

#coding=utf-8
import threading
import time

class MyThread1(threading.Thread):
    def run(self):
        if mutexA.acquire():
            print(self.name+'----do1---up----')
            time.sleep(1)

            if mutexB.acquire():
                print(self.name+'----do1---down----')
                mutexB.release()
            mutexA.release()

class MyThread2(threading.Thread):
    def run(self):
        if mutexB.acquire():
            print(self.name+'----do2---up----')
            time.sleep(1)
            if mutexA.acquire():
                print(self.name+'----do2---down----')
                mutexA.release()
            mutexB.release()

mutexA = threading.Lock()
mutexB = threading.Lock()

if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()
































生產者與消費者模型

咱們能夠經過生產者和消費者模型來解決線程的同步,和線程安全。

Python的Queue模塊中提供了同步的、線程安全的隊列類,包括FIFO(先入先出)隊列Queue,LIFO(後入先出)隊列LifoQueue,和優先級隊列PriorityQueue。這些隊列都實現了鎖原語(能夠理解爲原子操做,即要麼不作,要麼就作完),可以在多線程中直接使用,可使用隊列來實現線程間的同步。

舉個栗子:

#encoding=utf-8
import threading
import time

#python2中
# from Queue import Queue
#python3中
from queue import Queue
class Producer(threading.Thread):
    def run(self):
        global queue
        count = 0
        while True:
            if queue.qsize() < 1000:
                for i in range(100):
                    count = count +1
                    msg = '生成產品'+str(count)
                    queue.put(msg)
                    print(msg)
            time.sleep(0.5)
class Consumer(threading.Thread):
    def run(self):
        global queue
        while True:
            if queue.qsize() > 100:
                for i in range(3):
                    msg = self.name + '消費了 '+queue.get()
                    print(msg)
            time.sleep(1)
if __name__ == '__main__':
    queue = Queue()
    for i in range(500):
        queue.put('初始產品'+str(i))
    for i in range(2):
        p = Producer()
        p.start(),
    for i in range(5):
        c = Consumer()
        c.start()






































什麼是生產者與消費者模型?

生產者消費者模式是經過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通信,而經過阻塞隊列來進行通信,因此生產者生產完數據以後不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列裏取,阻塞隊列就至關於一個緩衝區,平衡了生產者和消費者的處理能力。這個阻塞隊列就是用來給生產者和消費者解耦的。縱觀大多數設計模式,都會找一個第三者出來進行解耦。

想使用全局變量有不想加鎖怎麼辦?

在多線程環境下,每一個線程都有本身的數據。一個線程使用本身的局部變量比使用全局變量好,由於局部變量只有線程本身能看見,不會影響其餘線程,而全局變量的修改必須加鎖。

舉個栗子:

import threading
# 建立全局ThreadLocal對象:
local_school = threading.local()
def process_student():
    # 獲取當前線程關聯的student:
    std = local_school.student
    print('Hello, %s (in %s)' % (std, threading.current_thread().name))
def process_thread(name):
    # 綁定ThreadLocal的student:
    local_school.student = name
    process_student()
t1 = threading.Thread(target= process_thread, args=('鹹魚',), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=('普拉思',), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()
輸出:
Hello, 鹹魚 (in Thread-A)
Hello, 普拉思 (in Thread-B)



















全局變量local_school就是一個ThreadLocal對象,每一個Thread對它均可以讀寫student屬性,但互不影響。你能夠把local_school當作全局變量,但每一個屬性如local_school.student都是線程的局部變量,能夠任意讀寫而互不干擾,也不用管理鎖的問題,ThreadLocal內部會處理。
能夠理解爲全局變量local_school是一個dict,不但能夠用local_school.student,還能夠綁定其餘變量,如local_school.teacher等等。
ThreadLocal最經常使用的地方就是爲每一個線程綁定一個數據庫鏈接,HTTP請求,用戶身份信息等,這樣一個線程的全部調用到的處理函數均可以很是方便地訪問這些資源。
一個ThreadLocal變量雖然是全局變量,但每一個線程都只能讀寫本身線程的獨立副本,互不干擾。ThreadLocal解決了參數在一個線程中各個函數之間互相傳遞的問題


同步調用和異步調用?

同步調用就是你喊你朋友吃飯,你朋友在忙,你就一直在那等,等你朋友忙完了 ,大家一塊兒去。
異步調用就是你喊你朋友吃飯,你朋友說知道了,待會忙完去找你 ,你就去作別的了。

舉個栗子:

from multiprocessing import Pool
import time
import os
def test():
    print("---進程池中的進程---pid=%d,ppid=%d--"%(os.getpid(),os.getppid()))
    for i in range(3):
        print("----%d---"%i)
        time.sleep(1)
    return "hahah"
def test2(args):
    print("---callback func--pid=%d"%os.getpid())
    print("---callback func--args=%s"%args)
pool = Pool(3)
pool.apply_async(func=test,callback=test2)
time.sleep(5)
print("----主進程-pid=%d----"%os.getpid())
輸出:
---進程池中的進程---pid=9401,ppid=9400--
----0---
----1---
----2---
---callback func--pid=9400
---callback func--args=hahah
----主進程-pid=9400----























注意:這裏的callback是由主進程執行的,當子進程死亡,主進程回調函數。

什麼是GIL鎖?

Python全局解釋鎖(GIL)簡單來講就是一個互斥體(或者說鎖),這樣的機制只容許一個線程來控制Python解釋器。這就意味着在任何一個時間點只有一個線程處於執行狀態。
因此在python中多線程是假的,由於在執行過程當中CPU中只有一個線程在執行。
當你使用多進程時,你的效率是高於多線程的。
Python GIL常常被認爲是一個神祕而困難的話題,可是請記住做爲一名Python支持者,只有當您正在編寫C擴展或者您的程序中有計算密集型的多線程任務時纔會被GIL影響。


寫在後面

這是鹹魚的第三篇python學習筆記,上次的推文給你們送了實戰的乾貨,接下來我會再整理下雲盤中的資源,爭取給你們再送波補貼。

推薦閱讀

Python | Python學習之深淺拷貝

Python | Python學習之多進程詳解


鹹魚普拉思

一隻鹹魚在編程路上的摸爬滾打,記錄摸索中的點點滴滴。


本文分享自微信公衆號 - 鹹魚學Python(xianyuxuepython)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索