Python多線程
- 模塊:Threading
- 概念:
- 線程:CPU執行程序的基本單位
- 父線程:與子線程而言是相對的。調用別的線程的程序(線程叫做父線程)
- 子線程:被別的程序所調用,則是調用者的子線程
- 守護進程:也稱之為后台進程,即主進程(前台進程)結束,守護進程也結束
- 主線程:最外層的線程(最初運行的程序)
- 注意:使用Pycharm時,運行多線程程序需要使用Terminal或者Run控制台,而不是Python Console否則得到的將不是原本的多線程結果。(比如守護線程會失效)。參考博客
一下用簡單的幾個例子來講解Python多線程的用法。
第一個例子:展示多線程與普通程序的區別
"""
最簡單的多線程demo
展示多線程程序與普通程序的區別
"""
import time
from threading import Thread
def demo1(doing):
print(f"I want to {doing}, {time.ctime()}")
time.sleep(2)
print(f"\n{doing} 結束")
def main():
print(f"開始, {time.ctime()}")
t1 = Thread(target=demo1, args=("聽歌",))
t2 = Thread(target=demo1, args=("吃飯",))
t1.start()
t2.start()
print(f"結束, {time.ctime()}")
if __name__ == '__main__':
main()
"""
開始, Thu May 6 20:22:43 2021
I want to 聽歌, Thu May 6 20:22:43 2021
I want to 吃飯, Thu May 6 20:22:43 2021
結束, Thu May 6 20:22:43 2021
聽歌 結束, Thu May 6 20:22:45 2021
吃飯 結束, Thu May 6 20:22:45 2021
"""
輸出格式經過了略微的調整,原始輸出肯能比較亂,原因之后會講。
由上可以看出,原本需要4秒才能執行完的程序,上面只用了2秒。即實現了並行,同時"聽歌"和"吃飯"。
Python實現多線程使用的是threading庫,使用Thread類創建線程對象,Thread對象有如下比較常用的方法:
-
Thread
(target=None, name=None, args=(), daemon=None)
以上為比較常用的參數,不是所有參數,詳細參數可參考文末的鏈接官網教程
target 是指明該線程運行的函數名
name 是我們給該線程取的名稱
args 是調用該函數需要傳遞的參數元組,末尾加逗號
daemon 設置該線程是否為守護線程 -
start()
表示啟動線程,讓其開始工作。該方法每個對象只能夠調用一次,否則拋出RuntimeError
,它安排對象的 run()方法在一個獨立的控制進程中調用。 -
run()
運行target
傳遞過去的函數 -
join
(timeout=None)
這個會阻塞調用該線程的線程,如果子線程調用該方法,主線程就算將自己的程序運行完了也不會結束,而是 會等到這個子線程運行完再結束。 -
setName() 和 getName()
分別是設置名字和獲取名字 -
isDaemon() 和 setDaemon()
分別是判斷是否是守護進程和設置守護進程
第二個例子:使用join()方法和將線程模塊化
import threading
import time
class MyThreading(threading.Thread):
def __init__(self, name):
super(MyThreading, self).__init__()
self.name = name
def run(self):
print(f"開始線程: {self.name}, {time.ctime()}")
time.sleep(2)
print(f"退出線程: {self.name}, {time.ctime()}")
class Test:
def __init__(self):
pass
def main_threading(self):
print(f"主線程開始運行, {time.ctime()}")
Thread1 = MyThreading("Thread1")
Thread2 = MyThreading("Thread2")
Thread1.start()
Thread2.start()
Thread1.join()
Thread2.join()
print(f"主線程結束運行, {time.ctime()}")
if __name__ == '__main__':
test = Test()
test.main_threading()
"""
主線程開始運行, Thu May 6 20:46:30 2021
開始線程: Thread1, Thu May 6 20:46:30 2021
開始線程: Thread2, Thu May 6 20:46:30 2021
退出線程: Thread2, Thu May 6 20:46:32 2021
退出線程: Thread1, Thu May 6 20:46:32 2021
主線程結束運行, Thu May 6 20:46:32 2021
"""
由第一個例子可以知道主線程本應該在子線程結束前結束,但是用了子線程調用join()后會阻塞父線程,致使在子線程結束完后才可結束。
第三個例子:守護線程的使用
以上例子中的子線程結束時間都是根據自身程序的運行時間覺得,而與父線程的是否結束無關,但我們在日常使用時,經常會有父線程結束,子線程就必須結束的一些線程,這種線程我們稱之為守護線程,或者后台進程,如我們的垃圾回收功能就是。
我們修改第二個例子中的Test()類
class Test:
def __init__(self):
pass
def main_threading(self):
print(f"主線程開始運行, {time.ctime()}")
Thread1 = MyThreading("Thread1")
Thread2 = MyThreading("Thread2")
Thread1.setDaemon(True)
Thread2.setDaemon(True)
Thread1.start()
Thread2.start()
print(f"主線程結束運行, {time.ctime()}")
"""
主線程開始運行, Thu May 6 20:48:56 2021
開始線程: Thread1, Thu May 6 20:48:56 2021
開始線程: Thread2, Thu May 6 20:48:56 2021
主線程結束運行, Thu May 6 20:48:56 2021
"""
由運行結果可以看出,父線程結束,子線程就無條件中止了。
第四個例子:
現在的CPU都是多核的,但是python的多線程是無法使用多核,而實現並行的方式是線程切換,只要速度足夠快,在某一段時間內,我們就會認為是並行的。這就是所謂的並發,而某一時刻多個線程同時工作則稱之為並行。我們可以用程序輸出來演示線程切換過程。
import time
from threading import Thread, current_thread
def run(n):
for i in range(n):
print(current_thread().name + " " + str(i))
thread = Thread(target=run, args=(20,), name="ChildThread")
# 如果設置子線程為守護進程,可以查看守護進程的結束與主線程的結束時間
# thread.setDaemon(True)
thread.start()
for i in range(10):
print(current_thread().name + " " + str(i))
"""
ChildThread 0
ChildThread 1
ChildThread 2
ChildThread 3
ChildThread 4
MainThread 0
MainThread 1
MainThread 2
MainThread 3
MainThread 4
MainThread 5
MainThread 6
MainThread 7
ChildThread 5
ChildThread 6
ChildThread 7
ChildThread 8
ChildThread 9
ChildThread 10
ChildThread 11
ChildThread 12
ChildThread 13
MainThread 8
MainThread 9
ChildThread 14
ChildThread 15
ChildThread 16
ChildThread 17
ChildThread 18
ChildThread 19
Process finished with exit code 0
"""
從運行結果我們就可以看出線程的切換。
第五個例子:
由於線程是切換的,那么所以某一個線程在執行時可能被打斷。加上python多線程使用的是同一塊內存,那么資源是共享的,所以當多個線程再某一段時間內,同時訪問某一資源,對其進行修改的話可能會導致混亂。
from threading import Thread
var = 0
def change(n):
global var
var += n
var -= n
def run_thread(n):
for i in range(1000000):
change(n)
thread1 = Thread(target=run_thread, args=(5,), name="thread1")
thread2 = Thread(target=run_thread, args=(8,), name="thread2")
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(var)
"""
11
"""
# 結果不一定
"""
理論上應該是0,但是當兩個線程交替執行,當循環次數足夠多時,結果就不一定是0
為了解決這個問題,采用了鎖的辦法
"""
第六個例子:就是解決上述問題的辦法,使用鎖
from threading import Lock, Thread
var = 0
lock = Lock()
def change(n):
global var
var += n
var -= n
def run_thread(n):
for i in range(20000000):
lock.acquire()
try:
change(n)
finally:
lock.release()
thread1 = Thread(target=run_thread, args=(5,), name="thread1")
thread2 = Thread(target=run_thread, args=(8,), name="thread2")
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(var)
"""
這樣得到的結果就一定是0
"""
可以這么理解這個鎖:人相當於線程,鎖相當於門鎖,一個人進廁所會鎖上門讓別人不能進來打擾,只有當他解鎖出來后別的人才可以進去。
Threading.Lock
類的主要方法如下:
acquire
(blocking=True, timeout=-1)- 這個方法用於獲取鎖。獲取的方式可以是阻塞方式,也可以是非阻塞方式。
- 差別就是,如果別的線程使用了鎖,而當前線程想獲取鎖時,可以干等着一直等到使用了鎖的線程釋放了鎖(解鎖),再獲取鎖並鎖定。這樣稱之為阻塞方式。
- 而在非阻塞的情況下(也就是blocking設置為False時),調用acquire方法,若是鎖可獲得,則返回True,並且上鎖,若是鎖被別的線程占用,則返回False。
timeout
就是設置阻塞等待的時間,若是在timeout
設置的時間內沒有獲得鎖則返回False,否則返回True並獲取鎖,再上鎖。若是timeout=-1
,那么將會無限等待。
release
()- 這個方法就是釋放鎖
- 若是在未鎖定時調用,會返回
RuntimeError
Python線程使用場景
由於Python的多線程受到了GIL的原因,Python的多線程無法實現真正的並行,所以多線程程序是通過頻繁的線程切換來實現的。
但是線程切換會帶來不小的開銷,頻繁的線程切換會造成資源的浪費。所以多線程在Python里似乎意味着並沒有帶來性能的提升,甚至降低了程序的性能。
但是有沒有一種操作是正好需要頻繁切換線程才能提高性能的呢?
答案是有的,那就是I/O操作,因為I/O操作不需要一直依靠CPU,只需要CPU向磁盤發起通知,然后就可以去執行其他的命令,等到數據准備好了再讀取數據就好了,就不需要CPU一直等待I/O操作直至完成。
所以Python多線程的使用場景就很明確了:
- 不適用於CPU計算密集型,而是適用於I/O密集型
以上就是我對python多線程的一些領悟,如有錯誤請積極指出,謝謝。