python網絡-多線程(22)

1、什麼是線程

線程(英語:thread)是操做系統可以進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運做單位。同一進程中的多條線程將共享該進程中的所有系統資源,一個進程能夠有不少線程,每條線程並行執行不一樣的任務。python

 

2、線程和進程的區別

一、舉例:

  • 進程,可以完成多任務,好比 在一臺電腦上可以同時運行多個QQ
  • 線程,可以完成多任務,好比 一個QQ中的多個聊天窗口

二、定義的不一樣

  • 進程是系統進行資源分配和調度的一個獨立單位.重點是資源分配和調度
  • 線程是進程的一個實體,CPU調度和分派的基本單位它是比進程更小的能獨立運行的基本單位.

三、功能不一樣

  • 一個程序至少有一個進程,一個進程至少有一個線程.
  • 線程的劃分尺度小於進程(資源比進程少),使得多線程程序的併發性高。
  • 進程在執行過程當中擁有獨立的內存單元,而多個線程共享內存,從而極大地提升了程序的運行效率
  • 線程不可以獨立執行,必須依存在進程中

四、優缺點算法

  • 線程和進程在使用上各有優缺點:線程執行開銷小,但不利於資源的管理和保護;而進程正相反。

 

3、threading模塊

一、單線程執行數據庫

#coding=utf-8
import time

def sayHi():
    print("Hello I am Se7eN_HOU")
    time.sleep(1)

if __name__ == "__main__":
    for i in range(5):
        sayHi()

運行結果爲:安全

二、多線程執行多線程

#coding=utf-8
import threading
import time

def sayHi():
    print("Hello I am Se7eN_HOU")
    time.sleep(1)

if __name__ == "__main__":
    for i in range(5):
        t = threading.Thread(target = sayHi)
        t.start()

運行結果爲:併發

說明app

  1. 能夠明顯看出使用了多線程併發的操做,花費時間要短不少
  2. 建立好的線程,須要調用start()方法來啓動

三、查看線程數量異步

#coding=utf-8
import threading
from time import sleep,ctime

def sing():
    for i in range(3):
        print("正在唱歌...%d"%i)
        sleep(1)

def dance():
    for i in range(3):
        print("正在跳舞...%d"%i)
        sleep(1)

if __name__ == '__main__':
    print("---開始---")

    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)

    t1.start()
    t2.start()

    while True:
        length = len(threading.enumerate())
        print("當前運行的線程數爲:%d"%length)
        if length<=1:
            break
        sleep(0.5)

運行結果爲:async

 

4、Thread子類的封裝

經過使用threading模塊能完成多任務的程序開發,爲了讓每一個線程的封裝性更完美,因此使用threading模塊時,每每會定義一個新的子類class,只要繼承threading.Thread就能夠了,而後重寫run方法函數

#coding=utf-8
import threading
import time

class MyThread1(threading.Thread):
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm "+self.name+' @ '+str(i) #name屬性中保存的是當前線程的名字
            print(msg)

class MyThread2(threading.Thread):
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm "+self.name+' @ '+str(i) #name屬性中保存的是當前線程的名字
            print(msg)

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

    t1.start()
    t2.start()

運行結果爲:

說明

  • python的threading.Thread類有一個run方法,用於定義線程的功能函數,能夠在本身的線程類中覆蓋該方法。
  • 建立本身的線程實例後,經過Thread類的start方法,能夠啓動該線程,當該線程得到執行的機會時,就會調用run方法執行線程。

2. 線程的執行順序

#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) #name屬性中保存的是當前線程的名字
            print(msg)

def test():
    for i in range(1,5):
        t=MyThread()
        t.start()

if __name__ == '__main__':
   test()

運行結果爲:

說明:

從代碼和執行結果咱們能夠看出,多線程程序的執行順序是不肯定的。當執行到sleep語句時,線程將被阻塞(Blocked),到sleep結束後,線程進入就緒(Runnable)狀態,等待調度。而線程調度將自行選擇一個線程執行。上面的代碼中只能保證每一個線程都運行完整個run函數,可是線程的啓動順序、run函數中每次循環的執行順序都不能肯定。

總結:

  1. 每一個線程必定會有一個名字,儘管上面的例子中沒有指定線程對象的name,可是python會自動爲線程指定一個名字。
  2. 當線程的run()方法結束時該線程完成。
  3. 沒法控制線程調度程序,但能夠經過別的方式來影響線程調度的方式。

線程的幾種狀態

 

 

5、多線程-全局變量(共享)、局部變量(不共享)

一、共享全局變量

from threading import Thread
import time

g_num = 100

def work1():
    global g_num
    for i in range(3):
        g_num += 1
        print("----in work1, g_num is %d---"%g_num)

def work2():
    global g_num
    print("----in work2, g_num is %d---"%g_num)

print("---線程建立以前,g_num is %d---"%g_num)

t1 = Thread(target=work1)
t1.start()
#延時一會,保證t1線程中的事情作完
time.sleep(1)
t2 = Thread(target=work2)
t2.start()

運行結果爲:

---線程建立以前,g_num is 100---
----in work1, g_num is 101---
----in work1, g_num is 102---
----in work1, g_num is 103---
----in work2, g_num is 103---

二、列表當作實參傳遞到線程中

from threading import Thread
import time

def work1(nums):
    nums.append(44)
    print("----in work1---",nums)

def work2(nums):
    #延時一會,保證t1線程中的事情作完
    time.sleep(1)
    print("----in work2---",nums)

g_nums = [11,22,33]

t1 = Thread(target=work1, args=(g_nums,))
t1.start()

t2 = Thread(target=work2, args=(g_nums,))
t2.start()

運行結果爲:

----in work1--- [11, 22, 33, 44]
----in work2--- [11, 22, 33, 44]

二、不共享局部變量

import threading
from time import sleep

def test(sleepTime):
    num=1
    sleep(sleepTime)
    num+=1
    print('---(%s)--num=%d'%(threading.current_thread(), num))


t1 = threading.Thread(target = test,args=(5,))
t2 = threading.Thread(target = test,args=(1,))

t1.start()
t2.start()

運行結果爲:

---(<Thread(Thread-2, started 12236)>)--num=2
---(<Thread(Thread-1, started 5644)>)--num=2

總結:

  • 在一個進程內的全部線程共享全局變量,可以在不使用其餘方式的前提下完成多線程之間的數據共享(這點要比多進程要好)
  • 缺點就是,線程是對全局變量隨意遂改可能形成多線程之間對全局變量的混亂(即線程非安全)
  • 在多線程開發中,全局變量是多個線程都共享的數據,而局部變量等是各自線程的,是非共享的

 

6、同步

一、線程衝突

假設兩個線程t1和t2都要對num=0進行增1運算,t1和t2都各對num修改10次,num的最終的結果應該爲20。可是因爲是多線程訪問,有可能出現下面狀況:

在num=0時,t1取得num=0。此時系統把t1調度爲」sleeping」狀態,把t2轉換爲」running」狀態,t2也得到num=0。而後t2對獲得的值進行加1並賦給num,使得num=1。而後系統又把t2調度爲」sleeping」,把t1轉爲」running」。線程t1又把它以前獲得的0加1後賦值給num。這樣,明明t1和t2都完成了1次加1工做,但結果仍然是num=1。

from threading import Thread
import time

g_num = 0

def test1():
    global g_num
    for i in range(1000000):
        g_num += 1

    print("---test1---g_num=%d"%g_num)

def test2():
    global g_num
    for i in range(1000000):
        g_num += 1

    print("---test2---g_num=%d"%g_num)


p1 = Thread(target=test1)
p1.start()
#time.sleep(3) #取消屏蔽以後 再次運行程序,結果會不同

p2 = Thread(target=test2)
p2.start()

print("---g_num=%d---"%g_num)
time.sleep(3)
print("---g_num=%d---"%g_num)

運行結果爲:

---g_num=138526---
---test1---g_num=1264273
---test2---g_num=1374945
---g_num=1374945---

取消屏蔽以後,再次運行結果以下:

---test1---g_num=1000000
---g_num=1029220---
---test2---g_num=2000000
---g_num=2000000---

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

2. 什麼是同步

  • 同步就是協同步調,按預約的前後次序進行運行。如:你說完,我再說。
  • "同"字從字面上容易理解爲一塊兒動做,其實不是,"同"字應是指協同、協助、互相配合。
  • 如進程、線程同步,可理解爲進程或線程A和B一塊配合,A執行到必定程度時要依靠B的某個結果,因而停下來,示意B運行;B依言執行,再將結果給A;A再繼續操做。

3. 解決問題的思路

對於上面提出的那個計算錯誤的問題,能夠經過線程同步來進行解決思路,以下:

  1. 系統調用t1,而後獲取到num的值爲0,此時上一把鎖,即不容許其餘如今操做num
  2. 對num的值進行+1
  3. 解鎖,此時num的值爲1,其餘的線程就可使用num了,並且是num的值不是0而是1
  4. 同理其餘線程在對num進行修改時,都要先上鎖,處理完後再解鎖,在上鎖的整個過程當中不容許其餘線程訪問,就保證了數據的正確性

 

7、線程互斥鎖

1、線程互斥鎖介紹

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

互斥鎖爲資源引入一個狀態:鎖定/非鎖定

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

threading模塊中定義了Lock類,能夠方便的處理鎖定:

#建立鎖
mutex = threading.Lock()
#鎖定
mutex.acquire([blocking])
#釋放
mutex.release()

其中,鎖定方法acquire能夠有一個blocking參數。

  • 若是設定blocking爲True,則當前線程會堵塞,直到獲取到這個鎖爲止(若是沒有指定,那麼默認爲True)
  • 若是設定blocking爲False,則當前線程不會堵塞

使用互斥鎖實現上面的例子的代碼以下:

from threading import Thread, Lock
import time

g_num = 0

def test1():
    global g_num
    for i in range(1000000):
        #True表示堵塞 即若是這個鎖在上鎖以前已經被上鎖了,那麼這個線程會在這裏一直等待到解鎖爲止 
        #False表示非堵塞,即無論本次調用可以成功上鎖,都不會卡在這,而是繼續執行下面的代碼
        mutexFlag = mutex.acquire(True) 
        if mutexFlag:#鎖住
            g_num += 1
            mutex.release()#解鎖
    print("---test1---g_num=%d"%g_num)

def test2():
    global g_num
    for i in range(1000000):
        mutexFlag = mutex.acquire(True) #True表示堵塞
        if mutexFlag:#鎖住
            g_num += 1
            mutex.release()#解鎖
    print("---test2---g_num=%d"%g_num)

#建立一個互斥鎖
#這個所默認是未上鎖的狀態
mutex = Lock()

p1 = Thread(target=test1)
p1.start()


p2 = Thread(target=test2)
p2.start()

time.sleep(5)
print("---g_num=%d---"%g_num)

運行結果爲:

---test1---g_num=1942922
---test2---g_num=2000000
---g_num=2000000---

二、上鎖解鎖過程

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

總結

鎖的好處:

  • 確保了某段關鍵代碼只能由一個線程從頭至尾完整地執行

鎖的壞處:

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

三、死鎖

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

儘管死鎖不多發生,但一旦發生就會形成應用的中止響應。下面看一個死鎖的例子

#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()

運行結果爲:

Thread-1----do1---up----
Thread-2----do2---up----

此時已經進入到了死鎖狀態

 

 

四、 避免死鎖

  • 程序設計時要儘可能避免(銀行家算法)
  • 添加超時時間等

 

8、同步應用

from threading import Thread,Lock
from time import sleep

class Task1(Thread):
    def run(self):
        while True:
            if lock1.acquire():
                print("------Task 1 -----")
                sleep(0.5)
                lock2.release()

class Task2(Thread):
    def run(self):
        while True:
            if lock2.acquire():
                print("------Task 2 -----")
                sleep(0.5)
                lock3.release()

class Task3(Thread):
    def run(self):
        while True:
            if lock3.acquire():
                print("------Task 3 -----")
                sleep(0.5)
                lock1.release()

#使用Lock建立出的鎖默認沒有「鎖上」
lock1 = Lock()
#建立另一把鎖,而且「鎖上」
lock2 = Lock()
lock2.acquire()
#建立另一把鎖,而且「鎖上」
lock3 = Lock()
lock3.acquire()

t1 = Task1()
t2 = Task2()
t3 = Task3()

t1.start()
t2.start()
t3.start()

運行結果爲:

可使用互斥鎖完成多個任務,有序的進程工做,這就是線程的同步

 

9、ThreadLocal

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

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=("Se7eN",), name='Thread-A')
t2 = threading.Thread(target= process_thread, args=("HOU",), name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

運行結果爲:

Hello, Se7eN (in Thread-A)
Hello, HOU (in Thread-B)

說明:

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

 

10、異步

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

 11、並行和併發

不管是並行仍是併發,在用戶看來都是'同時'運行的,無論是進程仍是線程,都只是一個任務而已,真是幹活的是cpu,cpu來作這些任務,而一個cpu同一時刻只能執行一個任務。

併發是僞並行,即看起來是同時運行。單個cpu+多道技術就能夠實現併發,(並行也屬於併發),簡單的能夠理解爲快速在多個線程來回切換,感受好像同時在作多個事情。

只有具有多個cpu才能實現並行,單核下,能夠利用多道技術,多個核,每一個核也均可以利用多道技術(多道技術是針對單核而言的)。          

有四個核,六個任務,這樣同一時間有四個任務被執行,假設分別被分配給了cpu1,cpu2,cpu3,cpu4,一旦任務1遇到I/O就被迫中斷執行,此時任務5就拿到cpu1的時間片去執行,這就是單核下的多道技術 ,而一旦任務1的I/O結束了,操做系統會從新調用它(需知進程的調度、分配給哪一個cpu運行,由操做系統說了算),可能被分配給四個cpu中的任意一個去執行。

  多道技術:內存中同時存入多道(多個)程序,cpu從一個進程快速切換到另一個,使每一個進程各自運行幾十或幾百毫秒,這樣,雖然在某一個瞬間,一個cpu只能執行一個任務,但在1秒內,cpu卻能夠運行多個進程,這就給人產生了並行的錯覺,即僞併發,以此來區分多處理器操做系統的真正硬件並行(多個cpu共享同一個物理內存)

  • 同步執行:一個進程在執行某個任務時,另一個進程必須等待其執行完畢,才能繼續執行
  • 異步執行:一個進程在執行某個任務時,另一個進程無需等待其執行完畢,就能夠繼續執行,當有消息返回時,系統會通知後者進行處理,這樣能夠提升執行效率

    舉個例子,打電話時就是同步通訊,發短息時就是異步通訊。

相關文章
相關標籤/搜索