python之線程

>撩個概念 多任務

是麼是多任務呢? 我如今聽着音樂,同時瀏覽着網頁,在文檔中寫着筆記.是的,這就是多任務;對於計算機來講,就是同時執行多段的代碼;python

現在計算機都是多核CPU了,單核CPU也能夠執行多任務,咱們都知道計算機的代碼都是順序執行的,那麼,單核的CPU是如何實現多任務的呢?安全

答案就是在極短的時間內輪流切換各個任務,CPU的運算太快了,給咱們的感受就是在同時進行同樣;bash

這裏引進兩個概念:並行和併發多線程

併發: 例如在單核CPU中執行多個任務,這個就是併發(執行的任務數量大於CPU核數)併發

並行: 兩個任務在多核CPU機器中執行,兩個任務分別在不一樣的CPU中執行,這個就是並行(任務數小於CPU核數)app

例如: 學校運動會中,5000米決賽都是十幾我的,起跑時,人數多於賽道,那麼這種狀況就是併發;100決賽都是武林高手,待遇不一樣,每人一個跑道,這個就是並行.ide

>接下來 線程

在python3中,線程由threading模塊提供,來一窺threading面貌函數

threading模塊下經常使用的方法或者屬性學習

方法 說明
current_thread() 返回當前線程
active_count() 返回當前活躍的線程數量,主線程+子線程
get_ident() 返回當前線程
enumerate() 返回當前活動的Thread列表
main_thread() 返回主Thread對象
settrace(func) 爲全部線程設置一個 trace 函數
setprofile(func) 爲全部線程設置一個 profile 函數
stack_size([size]) 返回新建立線程棧大小;或爲後續建立的線程設定棧大小爲 size
TIMEOUT_MAX Lock.acquire(), RLock.acquire(), Condition.wait() 容許的最大超時時間

threading模塊包含的類測試

說明
Thread 基本的線程類
Lock 互斥鎖
RLock 可重入鎖,使單一進程再次得到已持有的鎖(遞歸鎖)
Condition 條件鎖,使得一個線程等待另外一個線程知足特定條件,好比改變狀態或某個值
Semaphore 信號鎖。爲線程間共享的有限資源提供一個」計數器」,若是沒有可用資源則會被阻塞
Event 事件鎖,任意數量的線程等待某個事件的發生,在該事件發生後全部線程被激活
Timer 一種計時器
Barrier Python3.2新增的「阻礙」類,必須達到指定數量的線程後才能夠繼續執行

threading模塊中Thread類的方法和屬性

方法與屬性 說明
start() 啓動線程,等待CPU調度
run() 線程被cpu調度後自動執行的方法
getName()、setName()和name 用於獲取和設置線程的名稱
setDaemon() 設置爲後臺線程或前臺線程(默認是False,前臺線程)。若是是後臺線程,主線程執行過程當中,後臺線程也在進行,主線程執行完畢後,後臺線程不論成功與否,均中止。若是是前臺線程,主線程執行過程當中,前臺線程也在進行,主線程執行完畢後,等待前臺線程執行完成後,程序才中止
ident 獲取線程的標識符。線程標識符是一個非零整數,只有在調用了start()方法以後該屬性纔有效,不然它只返回None
is_alive() 判斷線程是不是激活的(alive)。從調用start()方法啓動線程,到run()方法執行完畢或遇到未處理異常而中斷這段時間內,線程是激活的
isDaemon()方法和daemon屬性 是否爲守護線程
join([timeout]) 調用該方法將會使主調線程堵塞,直到被調用線程運行結束或超時。參數timeout是一個數值類型,表示超時時間,若是未提供該參數,那麼主調線程將一直堵塞到被調線程結束

- 單線程(001_single_thread.py)

import time

def single_thread():
    print("這個單線程執行:%s"%time.time())
    time.sleep(1)

def main():
    for _ in range(5):
        single_thread()

if __name__ == "__main__":
    main()
複製代碼

- 多線程(002_multi_thread.py)

import time
import threading

def single_thread():
    print("這個單線程執行:%s"%time.time())
    time.sleep(1)

def main():
    for _ in range(5):
       t = threading.Thread(target= single_thread)
       t.start()

if __name__ == "__main__":
    main()
複製代碼

執行結果:

test_code$ python3 001_single_thread.py 
這個單線程執行:1563089407.2469919
這個單線程執行:1563089408.248269
這個單線程執行:1563089409.2495542
這個單線程執行:1563089410.2508354
這個單線程執行:1563089411.2516193
test_code$ python3 002_mulit_thread.py 
這個多線程執行:1563089416.1928792
這個多線程執行:1563089416.1931264
這個多線程執行:1563089416.1932962
這個多線程執行:1563089416.1934686
這個多線程執行:1563089416.1936424
複製代碼

咱們剛看了單線程執行和兩個線程的執行效果,讓我回想起GIL裏講到的,在I/0密集操做程序中可使用多線程,這裏的耗時操做使用了time.sleep(1)來模仿了.接下來讓咱們學習更多的關於threading模塊的知識...

使用多線程併發操做,花費時間要短不少 當調用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('---開始---:%s'%ctime())

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

    t1.start()
    t2.start()

    #sleep(5) # 屏蔽此行代碼,試試看,程序是否會立馬結束?
    print('---結束---:%s'%ctime())
複製代碼

>查看線程數量

import threading
from time import sleep,ctime

def sing():
    for i in range(2):
        sleep(1)
    print("sing_ending...")

def dance():
    for i in range(3):
        sleep(1)

if __name__ == "__main__":
    print("----開始----:%s"%ctime())

    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(1)
複製代碼

運行結果:

----開始----:Sun Jul 14 17:11:24 2019
當前運行的線程數量爲:3
當前運行的線程數量爲:3
當前運行的線程數量爲:3
sing_ending...
當前運行的線程數量爲:2
當前運行的線程數量爲:1
test_code$ 
複製代碼

>建立線程的第二種方式

  • 使用的都是在threading.Thread()實例化時,給裏面傳入對應的參數 threading.Thread(self, group=None, target=None, name=None, args=(),kwargs=None, *, daemon=None)

    • group: 預留參數
    • target: 一個可調用對象,在線程執行後使用
    • name: 線程的名字,默認爲"Thread-N"
    • args,kwargs: 傳遞的參數列表和關鍵字參數
  • 還有一種建立線程的方式是繼承threading.Thread類,重寫run方法,咱們來嘗試第二種方式

import threading
import time

class MyThread(threading.Thread):
	def run(self):
		print("I`m thread %s" % self.name)

if __name__ == '__main__':
	t = MyThread()
	t.start()
複製代碼

執行結果:

I`m thread Thread-1
複製代碼

總結

  • 建立本身的線程類時,須要重寫run方法,建立本身的線程實例後,經過調用Thread類的start方法能夠啓動該線程,交給python虛擬機進行調度,當該線程得到執行機會時,就會調用run方法執行線程,run()方法執行完,線程結束.

>線程的執行順序

import threading
import time

class MyThread(threading.Thread):
	def run(self):
		for i in range(2):
			time.sleep(0.5)
			print("I`m %s %s" % (self.name, i))

def main():
	for i in range(5):
		t = MyThread()
		t.start()

if __name__ == '__main__':
	main()
複製代碼

執行結果:

I`m Thread-2 0
I`m Thread-3 0
I`m Thread-1 0
I`m Thread-4 0
I`m Thread-5 0
I`m Thread-2 1
I`m Thread-3 1
I`m Thread-4 1
I`m Thread-1 1
I`m Thread-5 1
複製代碼

總結

  • 多線程的執行結果是不肯定的,當執行到sleep時,線程將會被阻塞(Blocked),等到sleep結束後,線程進入就緒狀態(Runnable)狀態,等待調度;而線程的調度是隨機選擇一個線程執行.

>多線程,共享全局變量

import threading
import time

num = 100
def count_test1():
	global num
	for i in range(10):
		num += 1
	print("count_test1-->num:%s"%num)

def count_test2():
	global num
	for i in range(5):
		num += 1
	print("count_test2-->num:%s"%num)

print("最原始的num:%s"%num)

t1 = threading.Thread(target=count_test1)
t1.start()

time.sleep(2) #讓t1執行完成

t2 = threading.Thread(target=count_test2)
t2.start()
複製代碼

執行結果:

最原始的num:100
count_test1-->num:110
count_test2-->num:115
複製代碼

>使用列表來測試

import threading
import time

def count_test1(num_list):
	num_list.append(10000)
	print("count_test1-->num:%s"%num_list)

def count_test2(num_list):
	print("count_test2-->num:%s"%num_list)

num_list = [11, 22, 33, 44]

t1 = threading.Thread(target=count_test1, args=(num_list,))
t1.start()

time.sleep(1) #讓t1執行完成

t2 = threading.Thread(target=count_test2, args=(num_list,))
t2.start()
複製代碼

執行結果:

count_test1-->num:[11, 22, 33, 44, 10000]
count_test2-->num:[11, 22, 33, 44, 10000]
複製代碼

總結

  • 在一個進程內線程共享全局變量,多線程方便共享數據
  • 缺點就是,線程對全局變量的隨意修改會形成線程之間對全局變量的混亂(即線程非安全)

>多線程的資源競爭問題

兩個線程(t1,t2)對同一個全局變量(global_num)進行修改,正常狀況下,t1對global_num加10,而後t2對global_num加10,最終global_num爲20.

But,在多線程中,存在這種狀況,t1獲取到global_num,此時系統將t1設置爲"sleep"狀態,這時t2獲取到global_num,對global_num進行加1,完成後,系統將t2設置爲"sleep"狀態,將t1設置爲"running"狀態,此時t1拿到的global_num是t2修改前的值,這時進行修改就會和t2修改重複.

測試1(循環數爲100)

import threading
import time

num = 0
def count_test1():
	global num
	for i in range(100):
		num += 1
	print("count_test1-->num:%s"%num)

def count_test2():
	global num
	for i in range(100):
		num += 1
	print("count_test2-->num:%s"%num)


t1 = threading.Thread(target=count_test1)
t2 = threading.Thread(target=count_test2)

t1.start()
t2.start()

t1.join()
t2.join()

print("最終的num:%s"%num)
複製代碼

測試結果:

count_test1-->num:100
count_test2-->num:200
最終的num:200
複製代碼

測試2(循環數爲100000)

import threading
import time

num = 0
def count_test1():
	global num
	for i in range(100000):
		num += 1
	print("count_test1-->num:%s"%num)

def count_test2():
	global num
	for i in range(100000):
		num += 1
	print("count_test2-->num:%s"%num)


t1 = threading.Thread(target=count_test1)
t2 = threading.Thread(target=count_test2)

t1.start()
t2.start()

t1.join()
t2.join()

print("最終的num:%s"%num)
複製代碼

測試結果:

count_test1-->num:100000
count_test2-->num:153462
最終的num:153462
複製代碼

總結

  • 若是多個線程對同一個全局變量操做,會出現資源問題,從而致使數據不許確

>解決資源競爭問題使用互斥鎖

  • threading模塊中定義了Lock類,能夠實現鎖
    • 建立鎖對象: mutex = threading.Lock()
    • 上鎖: mutex.acquire()
    • 釋放鎖: mutex.release()
  • 注意:
    • 若是這個鎖以前是沒有上鎖的,那麼acquire就不會阻塞
    • 若是調用acquire以前這個鎖是被其它線程上了鎖的,那麼acquire就會阻塞,知道這個鎖被釋放

使用互斥鎖(循環數爲100000)

import threading
import time

num = 0
def count_test1():
	global num
	for i in range(100000):
		mutex.acquire()
		num += 1
		mutex.release()
	print("count_test1-->num:%s"%num)

def count_test2():
	global num
	for i in range(100000):
		mutex.acquire()
		num += 1
		mutex.release()
	print("count_test2-->num:%s"%num)

mutex = threading.Lock()
t1 = threading.Thread(target=count_test1)
t2 = threading.Thread(target=count_test2)

t1.start()
t2.start()

t1.join()
t2.join()

print("最終的num:%s"%num)
複製代碼

執行結果:

count_test1-->num:188038
count_test2-->num:200000
最終的num:200000
複製代碼

上鎖釋放鎖的過程

當一個線程調用鎖的acquire()方法得到鎖時,鎖就進入「locked」狀態

每次只有一個線程能夠得到鎖,若是此時另外一個線程試圖得到這個鎖,該線程就會變爲"blocked"狀態,稱爲"阻塞",直到擁有鎖的線程調用鎖的release()方法釋放鎖以後,鎖進入"unlocked"狀態。

線程調度程序從處於同步阻塞狀態的線程中選擇一個來得到鎖,並使得該線程進入運行(running)狀態

總結

  • 鎖的好處
    • 確保了一段代碼只能由一個線程從前到尾完整執行
  • 鎖的壞處
    • 阻止了多線程的併發執行,包含鎖的代碼段只能是單線程執行,大大下降了效率
    • 可能會存在多個鎖,在獲取鎖和釋放鎖時容易形成死鎖

>死鎖問題

情侶吵架後,都在等待對方道歉,若是雙方一直等待對方先開口,那麼結果就悲劇了...

情侶吵架和死鎖有什麼聯繫呢?若是兩個線程共享全局變量,兩個線程分別佔有必定的資源而且咋等待對方的資源,就會形成死鎖問題

#coding=utf-8
import threading
import time

class MyThread1(threading.Thread):
    def run(self):
        # 對mutexA上鎖
        mutexA.acquire()

        # mutexA上鎖後,延時1秒,等待另外那個線程 把mutexB上鎖
        print(self.name+'----do1---up----')
        time.sleep(1)

        # 此時會堵塞,由於這個mutexB已經被另外的線程搶先上鎖了
        mutexB.acquire()
        print(self.name+'----do1---down----')
        mutexB.release()

        # 對mutexA解鎖
        mutexA.release()

class MyThread2(threading.Thread):
    def run(self):
        # 對mutexB上鎖
        mutexB.acquire()

        # mutexB上鎖後,延時1秒,等待另外那個線程 把mutexA上鎖
        print(self.name+'----do2---up----')
        time.sleep(1)

        # 此時會堵塞,由於這個mutexA已經被另外的線程搶先上鎖了
        mutexA.acquire()
        print(self.name+'----do2---down----')
        mutexA.release()

        # 對mutexB解鎖
        mutexB.release()

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

if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()
複製代碼

執行結果

程序會卡住: 按唱、跳、Rap鍵+c退出
複製代碼

總結

  • 如何避免死鎖
    • 程序設計上儘可能避免
    • 添加超時時間等
相關文章
相關標籤/搜索