做蛋糕的網(wǎng)站科學新概念外鏈平臺
【一】什么是線程
-
在傳統(tǒng)操作系統(tǒng)中,每個進程有一個地址空間,而且默認就有一個控制線程
-
線程顧名思義,就是一條流水線工作的過程
-
-
一條流水線必須屬于一個車間,一個車間的工作過程是一個進程
-
車間負責把資源整合到一起,是一個資源單位,而一個車間內至少有一個流水線
-
流水線的工作需要電源,電源就相當于cpu
-
-
所以進程只是用來把資源集中到一起(進程只是一個資源單位,或者說資源集合),而線程才是cpu上的執(zhí)行單位。
-
多線程(即多個控制線程)的概念是在一個進程中存在多個控制線程,多個控制線程共享該進程的地址空間,相當于一個車間內有多條流水線,都共用一個車間的資源。
-
例如
-
-
北京地鐵與上海地鐵是不同的進程,而北京地鐵里的13號線是一個線程,北京地鐵所有的線路共享北京地鐵所有的資源,比如所有的乘客可以被所有線路拉。
-
【1】示例:
-
進程
-
-
資源單位
-
-
線程
-
-
執(zhí)行單位
-
-
將操作系統(tǒng)比喻成大的工廠
-
-
進程相當于工廠里面的車間
-
線程相當于車間里面的流水線
-
【2】小結
-
每一個進程必定自帶一個線程
-
進程:資源單位
-
-
起一個進程僅僅只是 在內存空間中開辟出一塊獨立的空間
-
-
線程:執(zhí)行單位
-
-
真正被CPU執(zhí)行的其實是進程里面的線程
-
線程指的就是代碼的執(zhí)行過程,執(zhí)行代碼中所需要使用到的資源都找所在的進程索要
-
-
進程和線程都是虛擬單位,只是為了我們更加方便的描述問題
【二】線程的創(chuàng)建開銷
【1】創(chuàng)建進程的開銷要遠大于線程
-
如果我們的軟件是一個工廠
-
該工廠有多條流水線
-
流水線工作需要電源
-
電源只有一個即cpu(單核cpu)
-
-
一個車間就是一個進程
-
-
-
-
一個車間至少一條流水線(一個進程至少一個線程)
-
-
-
-
創(chuàng)建一個進程
-
-
-
-
就是創(chuàng)建一個車間(申請空間,在該空間內建至少一條流水線)
-
-
-
-
而建線程
-
-
-
-
就只是在一個車間內造一條流水線
-
無需申請空間,所以創(chuàng)建開銷小
-
-
【2】進程之間是競爭關系,線程之間是協(xié)作關系
-
車間直接是競爭/搶電源的關系,競爭
-
-
不同的進程直接是競爭關系
-
不同的程序員寫的程序運行的迅雷搶占其他進程的網(wǎng)速
-
360把其他進程當做病毒干死
-
-
一個車間的不同流水線式協(xié)同工作的關系
-
-
同一個進程的線程之間是合作關系,是同一個程序寫的程序內開啟動
-
迅雷內的線程是合作關系,不會自己干自己
-
【三】線程和進程的區(qū)別
-
Threads share the address space of the process that created it; processes have their own address space.
-
-
線程共享創(chuàng)建它的進程的地址空間; 進程具有自己的地址空間。
-
-
Threads have direct access to the data segment of its process; processes have their own copy of the data segment of the parent process.
-
-
線程可以直接訪問其進程的數(shù)據(jù)段; 進程具有其父進程數(shù)據(jù)段的副本。
-
-
Threads can directly communicate with other threads of its process; processes must use interprocess communication to communicate with sibling processes.
-
-
線程可以直接與其進程中的其他線程通信; 進程必須使用進程間通信與同級進程進行通信。
-
-
New threads are easily created; new processes require duplication of the parent process.
-
-
新線程很容易創(chuàng)建; 新進程需要復制父進程。
-
-
Threads can exercise considerable control over threads of the same process; processes can only exercise control over child processes.
-
-
線程可以對同一進程的線程行使相當大的控制權。 進程只能控制子進程。
-
-
Changes to the main thread (cancellation, priority change, etc.) may affect the behavior of the other threads of the process; changes to the parent process does not affect child processes.
-
-
對主線程的更改(取消,優(yōu)先級更改等)可能會影響該進程其他線程的行為; 對父進程的更改不會影響子進程。
-
【四】為何要有多線程
【1】開設進程
-
申請內存空間 -- 耗資源
-
拷貝代碼 - 耗資源
【2】開設線程
-
一個進程內可以開設多個線程
-
在一個進程內開設多個線程無需再次申請內存空間及拷貝代碼操作
【3】總結線程的優(yōu)點
-
減少了資源的消耗
-
同一個進程下的多個線程資源共享
【4】什么是多線程
-
多線程指的是
-
-
在一個進程中開啟多個線程
-
簡單的講:如果多個任務共用一塊地址空間,那么必須在一個進程內開啟多個線程。
-
-
多線程共享一個進程的地址空間
-
-
線程比進程更輕量級,線程比進程更容易創(chuàng)建可撤銷,在許多操作系統(tǒng)中,創(chuàng)建一個線程比創(chuàng)建一個進程要快10-100倍,在有大量線程需要動態(tài)和快速修改時,這一特性很有用
-
-
若多個線程都是cpu密集型的,那么并不能獲得性能上的增強
-
-
但是如果存在大量的計算和大量的I/O處理,擁有多個線程允許這些活動彼此重疊運行,從而會加快程序執(zhí)行的速度。
-
-
在多cpu系統(tǒng)中,為了最大限度的利用多核,可以開啟多個線程,比開進程開銷要小的多。(這一條并不適用于Python)
【五】開設多線程的兩種方式
【1】方式一:直接調用Thread
from multiprocessing import Process from threading import Thread import time ? ? def task(name):print(f'當前任務:>>>{name} 正在運行')time.sleep(3)print(f'當前任務:>>>{name} 結束運行') ? ? def Thread_main():t = Thread(target=task, args=("dream",))# 創(chuàng)建線程的開銷非常小,幾乎代碼運行的一瞬間線程就已經(jīng)創(chuàng)建了t.start()'''當前任務:>>>dream 正在運行this is main process!this is main process!當前任務:>>>dream 結束運行''' ? ? def Process_main():p = Process(target=task, args=("dream",))p.start()'''this is main process!當前任務:>>>dream 正在運行當前任務:>>>dream 結束運行''' ? ? if __name__ == '__main__':Thread_main()# Process_main()print('this is main process!')
【2】方式二:繼承Thread父類
from threading import Thread import time ? ? class MyThread(Thread): ?def __init__(self, name):# 重寫了別人的方法,又不知道別人的方法里面有什么, 就調用父類的方法super().__init__()self.name = name ?# 定義 run 函數(shù)def run(self):print(f'{self.name} is running')time.sleep(3)print(f'{self.name} is ending') ? ? def main():t = MyThread('dream')t.start()print(f'this is a main process') ?"""dream is runningthis is a main processdream is ending""" ? ? if __name__ == '__main__':main()
【三】一個進程下開啟多個線程和多個子進程的區(qū)別
【1】線程比進程速度快
from threading import Thread from multiprocessing import Process import time ? ? def work():print('hello') ? ? def timer(func):def inner(*args, **kwargs):start_time = time.time()res = func(*args, **kwargs)print(f'函數(shù) {func.__name__} 運行時間為:{time.time() - start_time}')return res ?return inner ? ? @timer def work_process():# 在主進程下開啟子進程t = Process(target=work)t.start()print('主線程/主進程')'''主線程/主進程函數(shù) work_process 運行時間為:0.0043752193450927734hello''' ? ? @timer def work_thread():# 在主進程下開啟線程t = Thread(target=work)t.start()print('主線程/主進程')'''打印結果:hello主線程/主進程函數(shù) work_thread 運行時間為:0.0001499652862548828''' ? ? if __name__ == '__main__':# part1 : 多線程work_thread()# part2 : 多進程work_process()
【2】查看pid
from threading import Thread from multiprocessing import Process import os ? ? def work():print('hello', os.getpid()) ? ? def work_thread():# part1:在主進程下開啟多個線程,每個線程都跟主進程的pid一樣t1 = Thread(target=work)t2 = Thread(target=work)t1.start()t2.start()print('主線程/主進程pid', os.getpid()) ?# hello 5022# hello 5022# 主線程/主進程pid 5022 ? ? def work_process():# part2:開多個進程,每個進程都有不同的pidp1 = Process(target=work)p2 = Process(target=work)p1.start()p2.start()print('主線程/主進程pid', os.getpid()) ?# 主線程/主進程pid 5032# hello 5034# hello 5035 ? ? if __name__ == '__main__':# part1:在主進程下開啟多個線程,每個線程都跟主進程的pid一樣work_thread()# part2:開多個進程,每個進程都有不同的pidwork_process()
【3】同一進程內的線程共享進程內的數(shù)據(jù)
from threading import Thread from multiprocessing import Process ? ? def work():global nn = 0 ? ? def work_process():n = 100p = Process(target=work)p.start()p.join()print('主', n) ?# 毫無疑問子進程p已經(jīng)將自己的全局的n改成了0,但改的僅僅是它自己的,查看父進程的n仍然為100 ?# 主 100 ? ? def work_thread():n = 1t = Thread(target=work)t.start()t.join()print('主', n) ?# 查看結果為1,因為同一進程內的線程之間共享進程內的數(shù)據(jù) ? ? if __name__ == '__main__':# part1 多進程 : 子進程只改自己的work_process()# part2 多線程: 數(shù)據(jù)發(fā)生錯亂,同一進程內的線程之間共享數(shù)據(jù)work_thread()
【四】守護線程
【1】主線程死亡,子線程未死亡
-
主線程結束運行后不會馬上結束,而是等待其他非守護子線程結束之后才會結束
-
如果主線程死亡就代表者主進程也死亡,隨之而來的是所有子線程的死亡
from threading import Thread import time ? ? def work(name):print(f"當前{name} 是開始\n")time.sleep(2)print(f"當前{name} 是結束") ? ? def main():print(f'這是主函數(shù)main開始')task = Thread(target=work,args=('knight',))task.start()print(f'這是主函數(shù)main結束') ? ? if __name__ == '__main__':main() ? # 這是主函數(shù)main開始 # 當前knight 是開始 # 這是主函數(shù)main結束 ? # 當前knight 是結束
【2】主線程死亡,子線程也死亡
from threading import Thread import time ? ? def work(name):print(f"當前{name} 是開始\n")time.sleep(2)print(f"當前{name} 是結束") ? ? def main():print(f'這是主函數(shù)main開始')task = Thread(target=work,args=('knight',))task.daemon = True ?# 開啟守護進程,主線程結束,子線程也隨之結束task.start()print(f'這是主函數(shù)main結束') ? ? if __name__ == '__main__':main() ? # 這是主函數(shù)main開始 # 當前knight 是開始 # 這是主函數(shù)main結束
示例:對比是否被守護進程的區(qū)別
# 導入所需模塊 from threading import Thread import time ? ? # 定義函數(shù)foo,模擬一個耗時操作 def foo():# 打印開始信息print(f' this is foo begin')# 模擬耗時操作,暫停3秒time.sleep(3)# 打印結束信息print(f' this is foo end') ? ? # 定義另一個函數(shù)func,同樣模擬耗時操作 def func():# 打印開始信息print(f' this is func begin')# 模擬耗時操作,暫停1秒time.sleep(1)# 打印結束信息print(f' this is func end') ? ? # 主函數(shù) def main():# 創(chuàng)建線程 task_foo ,目標函數(shù)為footask_foo = Thread(target=foo)# 設置 task_foo 為守護線程# 意味著當主線程結束時,不論 task_foo 是否執(zhí)行完畢都會被強制終止task_foo.daemon = True# 創(chuàng)建線程 task_func ,目標函數(shù)為functask_func = Thread(target=func) ?# 啟動線程 task_footask_foo.start()# 啟動線程 task_functask_func.start() ?# 主線程繼續(xù)執(zhí)行,打印以下信息print(f' this is main') ? ? # 程序入口 if __name__ == '__main__':main()# this is main begin # this is foo begin# this is func begin# this is main end# this is func end
執(zhí)行過程
(1) 初始化階段
-
程序開始執(zhí)行時,首先會導入所需的模塊,并定義兩個函數(shù)
foo()
和func()
。 -
這兩個函數(shù)分別代表了兩個需要并發(fā)執(zhí)行的任務。
(2)線程創(chuàng)建與啟動
-
在
main()
函數(shù)中 -
首先通過
Thread
類創(chuàng)建了兩個線程實例t1
和t2
-
其中
t1
的目標函數(shù)是foo
,t2
的目標函數(shù)是func
。 -
然后將
t1
設置為守護線程(daemon=True),這意味著當主線程結束時,即使t1
尚未執(zhí)行完畢也會被系統(tǒng)終止。 -
之后,兩個線程通過
start()
方法啟動,這意味著它們將異步地執(zhí)行各自的目標函數(shù)。
原理分析
(1)并發(fā)執(zhí)行
-
-
t1
開始執(zhí)行,打印出“this is foo begin”,隨后進入3秒的等待狀態(tài)。 -
幾乎同時,
t2
也開始執(zhí)行,打印出“this is func begin”,并進入1秒的等待狀態(tài)。 -
由于線程調度機制,實際的打印順序可能會略有不同,但通常情況下
func()
會先于foo()
結束,因為它的等待時間較短。
-
(2)主線程執(zhí)行
-
主線程繼續(xù)執(zhí)行,打印出“this is main”。
-
由于
t1
被設置為守護線程,即便它還在睡眠中,當主線程執(zhí)行結束后,整個程序也會直接終止,此時t1
不論是否完成都會被系統(tǒng)停止。 -
而
t2
作為一個非守護線程,如果在主線程結束前已完成,則正常結束,否則也會隨程序終止。
【五】線程的互斥鎖
-
所有子線程都會進行阻塞操作,導致最后的改變只是改了一次
from threading import Thread import timemoney = 100def work():global money# 模擬獲取到車票信息temp = money# 模擬網(wǎng)絡延遲time.sleep(2)# 模擬購票money = temp - 1def main():task_list = [Thread(target=work) for i in range(100)][task.start() for task in task_list][task.join() for task in task_list]print(money)if __name__ == '__main__':main()# 99
解決方法
-
在數(shù)據(jù)發(fā)生改變的地方進行加鎖處理
from threading import Thread,Lock import timemoney = 100 mutex = Lock()def work():global money# 數(shù)據(jù)發(fā)生改變之前加鎖mutex.acquire()# 模擬獲取到車票信息temp = money# 模擬網(wǎng)絡延遲time.sleep(1)# 模擬購票money = temp - 1# 數(shù)據(jù)改變之后解鎖mutex.release()def main():task_list = [Thread(target=work) for i in range(100)][task.start() for task in task_list][task.join() for task in task_list]print(money)if __name__ == '__main__':main()# 0