一、簡介
什么是線程?
線程也叫輕量級進程,是操作系統能夠進行運算調度的最小單位,它被包涵在進程之中,是進程中的實際運作單位。
線程自己不擁有系統資源,只擁有一點兒在運行中必不可少的資源,但它可與同屬一個進程的其他線程共享進程所擁有的全部資源。一個線程可以創建和撤銷另一個線程,同一個進程中的多個線程之間可以并發執行。
為什么要使用多線程?
線程在程序中是獨立的、并發的執行流。與分隔的進程相比,進程中線程之間的隔離程度要小,它們共享內存、文件句柄和其他進程應有的狀態。
因為線程的劃分尺度小于進程,使得多線程程序的并發性高。進程在執行過程之中擁有獨立的內存單元,而多個線程共享內存,從而極大的提升了程序的運行效率。
線程比進程具有更高的性能,這是由于同一個進程中的線程都有共性,多個線程共享一個進程的虛擬空間。線程的共享環境包括進程代碼段、進程的共有數據等,利用這些共享的數據,線程之間很容易實現通信。
操作系統在創建進程時,必須為改進程分配獨立的內存空間,并分配大量的相關資源,但創建線程則簡單得多。因此,使用多線程來實現并發比使用多進程的性能高得要多。
總結起來,使用多線程編程具有如下幾個優點:
進程之間不能共享內存,但線程之間共享內存非常容易。
操作系統在創建進程時,需要為該進程重新分配系統資源,但創建線程的代價則小得多。因此使用多線程來實現多任務并發執行比使用多進程的效率高。python語言內置了多線程功能支持,而不是單純地作為底層操作系統的調度方式,從而簡化了python的多線程編程。
二、創建多線程和多進程
2.1 普通方式創建
2.1.1 多線程
import threading
from threading import Lock,Thread
import time
def run(n):
print('task', n)
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
time.sleep(1)
print('0s')
time.sleep(1)
if __name__ == '__main__':
t1 = threading.Thread(target=run, args=('t1',)) # target是要執行的函數名(不是函數),args是函數對應的參數,以元組的形式存在
t2 = threading.Thread(target=run, args=('t2',))
t1.start()
t2.start()
2.1.2 多進程
相比較于threading模塊用于創建python多線程,python提供multiprocessing用于創建多進程。先看一下創建進程的兩種方式。
import time
import multiprocessing
def run(n):
print('task', n)
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
time.sleep(1)
print('0s')
time.sleep(1)
if __name__ == '__main__':
t1 = multiprocessing.Process(target=run, args=('t1',))
t2 = multiprocessing.Process(target=run, args=('t2',))
t1.start()
t2.start()
2.2 自定義對象方式創建
2.2.1 多線程
繼承threading.Thread來定義線程類,其本質是重構Thread類中的run方法
import threading
import time
class MyThread(threading.Thread):
def __init__(self, n):
super().__init__()
self.n = n
def run(self) -> None:
print('task', self.n)
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
time.sleep(1)
print('0s')
time.sleep(1)
if __name__ == '__main__':
t1 = MyThread('t1')
t2 = MyThread('t2')
t1.start()
t2.start()
2.2.2 多進程
改變父類為multiprocessing.Process即可。
import time
import multiprocessing
# class MyThread(multiprocessing.Process):
def __init__(self, n):
super().__init__()
self.n = n
def run(self) -> None:
print('task', self.n)
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
time.sleep(1)
print('0s')
time.sleep(1)
if __name__ == '__main__':
t1 = MyThread('t1')
t2 = MyThread('t2')
t1.start()
t2.start()
2.3 守護線程
下面這個例子,這里使用setDaemon(True)把所有的子線程都變成了主線程的守護線程,因此當主線程結束后,子線程也會隨之結束,所以當主線程結束后,整個程序就退出了。
所謂’線程守護’,就是主線程不管該線程的執行情況,只要是其他子線程結束且主線程執行完畢,主線程都會關閉。也就是說: 主線程不等待該守護線程的執行完再去關閉。
import time
import threading
def run(n):
print('task', n)
time.sleep(1)
print('3s')
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
if __name__ == '__main__':
t = threading.Thread(target=run, args=('t1',))
t.setDaemon(True)
t.start()
print('end')
通過執行結果可以看出,設置守護線程之后,當主線程結束時,子線程也將立即結束,不再執行。
為了讓守護線程執行結束之后,主線程再結束,我們可以使用join方法,讓主線程等待子線程執行
def run(n):
print('task', n)
time.sleep(2)
print('5s')
time.sleep(2)
print('3s')
time.sleep(2)
print('1s')
if __name__ == '__main__':
t = threading.Thread(target=run, args=('t1',))
t.setDaemon(True) # 把子線程設置為守護線程,必須在start()之前設置
t.start()
t.join() # 設置主線程等待子線程結束
print('end')
2.4 資源池
2.4.1 線程池
2.4.1.1 threadpool模塊
threadpool是一個比較老的模塊了,逐漸被其他模塊取代
import threadpool
import time
def sayhello(a):
print("hello: " + a)
time.sleep(2)
if __name__ == '__main__':
global result
seed = ["a", "b", "c"]
# 定義一個線程池
task_pool = threadpool.ThreadPool(5)
# 創建了要開啟多線程的函數,函數相關參數和回調函數
requests = threadpool.makeRequests(sayhello, seed)
# 將所有要運行的請求放到線程池中(參數數量決定任務數量)
for req in requests:
task_pool.putRequest(req)
# 等待所有線程完成后退出
task_pool.wait()
2.4.1.2 concurrent.futures模塊
concurrent.futures模塊是python3中自帶的模塊,python2.7以上版本也可以安裝使用。
線程池優秀的設計理念在于:他返回的結果并不是執行完畢后的結果,而是futures的對象,這個對象會在未來存儲線程執行完畢的結果,這一點也是異步編程的核心。python為了提高與統一可維護性,多線程多進程和協程的異步編程都是采取同樣的方式。
from concurrent.futures import ThreadPoolExecutor
import time
def sleeper(secs):
time.sleep(secs)
print('I slept for {} seconds'.format(secs))
return secs
with ThreadPoolExecutor(max_workers=3) as executor:
times = [4, 1, 2]
start_t = time.time()
futs = [executor.submit(sleeper, secs) for secs in times]
for fut in futs:
print(fut.result())
print(time.time() - start_t)
# 結果如下
I slept for 1 seconds
I slept for 2 seconds
I slept for 4 seconds
4
1
2
上述例子是直接遍歷future任務來獲取返回結果,可以發現for循環會按順序遍歷所有的future任務,如果有一個任務未執行完會一直堵塞,直到該任務完成后才會繼續遍歷。
可以通過as_completed方法來避免情況發生,提高效率
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def sleeper(secs):
time.sleep(secs)
print('I slept for {} seconds'.format(secs))
return secs
with ThreadPoolExecutor(max_workers=3) as executor:
times = [4, 1, 2]
start_t = time.time()
futs = [executor.submit(sleeper, secs) for secs in times]
for fut in as_completed(futs):
print(fut.result())
print(time.time() - start_t)
# 結果如下
I slept for 1 seconds
1
I slept for 2 seconds
2
I slept for 4 seconds
4
可以發現,as_completed對集合進行了重新排序,將執行完成的任務放到集合的前面,避免了已經執行完成的任務被前面的任務堵塞,導致效率降低。
還可以使用回調函數來進行后序處理,使用的是同一個線程
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
import threading
def sleeper(secs):
print("threadPool:" + str(threading.currentThread()))
time.sleep(secs)
print('I slept for {} seconds'.format(secs))
return secs
def call_back(arg):
print("callback")
with ThreadPoolExecutor(max_workers=3) as executor:
print("Main:" + str(threading.currentThread()))
times = [4, 1, 2]
start_t = time.time()
futs = [executor.submit(sleeper, secs) for secs in times]
for fut in as_completed(futs):
fut.add_done_callback(call_back)
print(fut.result())
print(time.time() - start_t)
# 結果如下
I slept for 1 seconds
callback
1
I slept for 2 seconds
callback
2
I slept for 4 seconds
callback
4
2.4.1.3 自定義線程池
import threading
import Queue
import hashlib
import logging
from utils.progress import PrintProgress
from utils.save import SaveToSqlite
class ThreadPool(object):
def __init__(self, thread_num, args):
self.args = args
self.work_queue = Queue.Queue()
self.save_queue = Queue.Queue()
self.threads = []
self.running = 0
self.failure = 0
self.success = 0
self.tasks = {}
self.thread_name = threading.current_thread().getName()
self.__init_thread_pool(thread_num)
# 線程池初始化
def __init_thread_pool(self, thread_num):
# 下載線程
for i in range(thread_num):
self.threads.append(WorkThread(self))
# 打印進度信息線程
self.threads.append(PrintProgress(self))
# 保存線程
self.threads.append(SaveToSqlite(self, self.args.dbfile))
# 添加下載任務
def add_task(self, func, url, deep):
# 記錄任務,判斷是否已經下載過
url_hash = hashlib.new('md5', url.encode("utf8")).hexdigest()
if not url_hash in self.tasks:
self.tasks[url_hash] = url
self.work_queue.put((func, url, deep))
logging.info("{0} add task {1}".format(self.thread_name, url.encode("utf8")))
# 獲取下載任務
def get_task(self):
# 從隊列里取元素,如果block=True,則一直阻塞到有可用元素為止。
task = self.work_queue.get(block=False)
return task
def task_done(self):
# 表示隊列中的某個元素已經執行完畢。
self.work_queue.task_done()
# 開始任務
def start_task(self):
for item in self.threads:
item.start()
logging.debug("Work start")
def increase_success(self):
self.success += 1
def increase_failure(self):
self.failure += 1
def increase_running(self):
self.running += 1
def decrease_running(self):
self.running -= 1
def get_running(self):
return self.running
# 打印執行信息
def get_progress_info(self):
progress_info = {}
progress_info['work_queue_number'] = self.work_queue.qsize()
progress_info['tasks_number'] = len(self.tasks)
progress_info['save_queue_number'] = self.save_queue.qsize()
progress_info['success'] = self.success
progress_info['failure'] = self.failure
return progress_info
def add_save_task(self, url, html):
self.save_queue.put((url, html))
def get_save_task(self):
save_task = self.save_queue.get(block=False)
return save_task
def wait_all_complete(self):
for item in self.threads:
if item.isAlive():
# join函數的意義,只有當前執行join函數的線程結束,程序才能接著執行下去
item.join()
# WorkThread 繼承自threading.Thread
class WorkThread(threading.Thread):
# 這里的thread_pool就是上面的ThreadPool類
def __init__(self, thread_pool):
threading.Thread.__init__(self)
self.thread_pool = thread_pool
#定義線程功能方法,即,當thread_1,...,thread_n,調用start()之后,執行的操作。
def run(self):
print (threading.current_thread().getName())
while True:
try:
# get_task()獲取從工作隊列里獲取當前正在下載的線程,格式為func,url,deep
do, url, deep = self.thread_pool.get_task()
self.thread_pool.increase_running()
# 判斷deep,是否獲取新的鏈接
flag_get_new_link = True
if deep >= self.thread_pool.args.deep:
flag_get_new_link = False
# 此處do為工作隊列傳過來的func,返回值為一個頁面內容和這個頁面上所有的新鏈接
html, new_link = do(url, self.thread_pool.args, flag_get_new_link)
if html == '':
self.thread_pool.increase_failure()
else:
self.thread_pool.increase_success()
# html添加到待保存隊列
self.thread_pool.add_save_task(url, html)
# 添加新任務,即,將新頁面上的不重復的鏈接加入工作隊列。
if new_link:
for url in new_link:
self.thread_pool.add_task(do, url, deep + 1)
self.thread_pool.decrease_running()
# self.thread_pool.task_done()
except Queue.Empty:
if self.thread_pool.get_running() <= 0:
break
except Exception, e:
self.thread_pool.decrease_running()
# print str(e)
break
2.4.2 進程池
進程池同樣使用multiprocessing模塊
from multiprocessing import Pool
import time,os
def worker(arg):
print("子進程{}執行中, 父進程{}".format(os.getpid(),os.getppid()))
time.sleep(2)
print("子進程{}終止".format(os.getpid()))
if __name__ == "__main__":
print("本機為",os.cpu_count(),"核 CPU")
print("主進程{}執行中, 開始時間={}".format(os.getpid(), time.strftime('%Y-%m-%d %H:%M:%S')))
start = time.time()
l = Pool(processes=5)
# 創建子進程實例
for i in range(10):
# l.apply(worker,args=(i,)) # 同步執行(Python官方建議廢棄)
l.apply_async(worker,args=(i,)) # 異步執行
# 關閉進程池,停止接受其它進程
l.close()
# 阻塞進程
l.join()
stop = time.time()
print("主進程終止,結束時間={}".format(time.strftime('%Y-%m-%d %H:%M:%S')))
print("總耗時 %s 秒" % (stop - start))
可以通過異步回調來進行后序操作
from multiprocessing import Process,Pool
import os
import time
import random
#子進程任務
def download(f):
print('__進程池中的進程——pid=%d,ppid=%d'%(os.getpid(),os.getppid()))
for i in range(3):
print(f,'--文件--%d'%i)
time.sleep(random.randint(1, 9))
# time.sleep(1)
return {"result": 1, "info": '下載完成!'}
#主進程調用回調函數
def alterUser(msg):
print("----callback func --pid=%d"%os.getpid())
print("get result:", msg["info"])
if __name__ == "__main__":
p = Pool(3)
p.apply_async(func=download, args=(1111,), callback=alterUser)
p.apply_async(func=download, args=(2222,), callback=alterUser)
p.apply_async(func=download, args=(3333,), callback=alterUser)
#當func執行完畢后,return的東西會給到回調函數callback
print("---start----")
p.close()#關閉進程池,關閉后,p不再接收新的請求。
p.join()
print("---end-----")
2.5 Subprocess模塊
python提供了Sunprocess模塊可以在程序執行過程中,調用外部的程序。
如我們可以在python程序中打開記事本,打開cmd,或者在某個時間點關機:
>>> import subprocess
>>> subprocess.Popen(['cmd'])
<subprocess.Popen object at 0x0339F550>
>>> subprocess.Popen(['notepad'])
<subprocess.Popen object at 0x03262B70>
>>> subprocess.Popen(['shutdown', '-p'])
三、資源和鎖(多線程)
線程時進程的執行單元,進程時系統分配資源的最小執行單位,所以在同一個進程中的多線程是共享資源的。
由于線程之間是進行隨機調度,并且每個線程可能只執行n條執行之后,當多個線程同時修改同一條數據時可能會出現臟數據,所以出現了線程鎖,即同一時刻允許一個線程執行操作。線程鎖用于鎖定資源,可以定義多個鎖,像下面的代碼,當需要獨占某一個資源時,任何一個鎖都可以鎖定這個資源,就好比你用不同的鎖都可以把這個相同的門鎖住一樣。
由于線程之間是進行隨機調度的,如果有多個線程同時操作一個對象,如果沒有很好地保護該對象,會造成程序結果的不可預期,我們因此也稱為“線程不安全”。
為了防止上面情況的發生,就出現了鎖(Lock)。
3.1 互斥鎖
此處有一個公共資源,就是n。如果是單線程執行,那么最終n會被扣除到0,但是多線程下資源不安全,最終結果不是0;所以我們需要添加鎖來保證資源安全。
def work():
global n
lock.acquire() # 注銷掉鎖會導致線程不安全
temp = n
time.sleep(0.1)
n = temp-1
lock.release() # 注銷掉鎖會導致線程不安全
if __name__ == '__main__':
lock = threading.Lock()
n = 100
l = []
for i in range(100):
p = threading.Thread(target=work)
l.append(p)
p.start()
for p in l:
p.join()
print(n)
3.2 遞歸鎖(可重入鎖)
RLcok類的用法和Lock類一模一樣,但它支持嵌套,在多個鎖沒有釋放的時候一般會使用RLock類。
import threading
import time
def func1():
global gl_num
rlock.acquire()
gl_num += 1
time.sleep(1)
print(gl_num)
print("enter func1")
func2()
rlock.release() # 直到RLock所有鎖釋放后,其他線程才能獲取鎖
def func2():
rlock.acquire() # 線程獲取鎖后可以再次獲取相同的鎖
print("enter func2")
rlock.release()
if __name__ == '__main__':
gl_num = 0
rlock = threading.RLock() # 此處換成Lock鎖,則會造成死鎖
for i in range(5):
t = threading.Thread(target=func1)
t.start()
3.3 信號量(BoundedSemaphore類)
互斥鎖同時只允許一個線程更改數據,而Semaphore是同時允許一定數量的線程更改數據,比如加油站有5個油箱,那最多只允許5輛車停放加油,后面的車只能等里面有車出來了才能再進去。
如下程序,有20輛車和5個油箱,每輛車停放3秒完成加油后開走,下一輛再停入加油,直到所有車都完成加油開走。
import threading
import time
def run(n):
semaphore.acquire() #加鎖
print(f'車輛 {n} 停放加油')
time.sleep(5)
print(f'車輛 {n} 離開')
semaphore.release() #釋放
if __name__== '__main__':
num=0
semaphore = threading.BoundedSemaphore(5) #最多允許5個線程同時運行
for i in range(20):
t = threading.Thread(target=run, args={i})
time.sleep(0.5)
t.start()
while threading.active_count() > 1:
pass
else:
print('----------所有車輛完成加油-----------')
3.4 主線程控制其他線程的執行
python線程的事件用于主線程控制其他線程的執行,事件是一個簡單的線程同步對象,其主要提供以下的幾個方法:
- clear:將flag設置為 False
- set:將flag設置為 True
- is_set:判斷是否設置了flag
wait會一直監聽flag,如果沒有檢測到flag就一直處于阻塞狀態。
事件處理的機制:全局定義了一個Flag,當Flag的值為False,那么event.wait()就會阻塞,當flag值為True,那么event.wait()便不再阻塞.
import threading
import time
def lighter():
count = 0
event.set() # 初始者為綠燈
while True:
if count % 20 > 10:
event.clear() # 紅燈,清除標志位
print('紅燈亮,停止通行')
else:
event.set() # 綠燈,設置標志位
print('綠燈亮,可以通行')
time.sleep(1)
count += 1
def car(name):
while True:
if event.is_set(): # 判斷是否設置了標志位
print(f'{name} 通行')
time.sleep(1)
else:
print(f'{name} 停止通行')
event.wait() # 阻塞直到標志位被設置為True
print(f'{name} 通行')
if __name__ == "__main__":
event = threading.Event()
light = threading.Thread(target=lighter, )
light.start()
car = threading.Thread(target=car, args=('司機',))
car.start()
3.5 線程間的通信
在一個進程中,不同子線程負責不同的任務,t1子線程負責獲取到數據,t2子線程負責把數據保存的本地,那么他們之間的通信使用Queue來完成。因為再一個進程中,數據變量是共享的,即多個子線程可以對同一個全局變量進行操作修改,Queue是加了鎖的安全消息隊列。
import threading
import time
import queue
q = queue.Queue(maxsize=5) #q在t1和t2兩個子線程之間通信共享,一個存入數據,一個使用數據。
def t1(q):
while 1:
for i in range(10):
q.put(i)
def t2(q):
while not q.empty():
print('隊列中的數據量:'+str(q.qsize()))
# q.qsize()是獲取隊列中剩余的數量
print('取出值:'+str(q.get()))
# q.get()是一個堵塞的,會等待直到獲取到數據
print('-----')
time.sleep(0.1)
t1 = threading.Thread(target=t1,args=(q,))
t2 = threading.Thread(target=t2,args=(q,))
t1.start()
t2.start()
四、相關概念
4.1 GIL 全局解釋器
在非python環境中,如java和c,單核情況下,同時只能有一個任務執行,多核時可以支持多個線程同時執行。但是在python中,無論有多少個核,一個進程中同時只能執行一個線程。究其原因,這就是由于GIL的存在導致的。
GIL的全稱是全局解釋器,來源是python設計之初的考慮,為了數據安全所做的決定。某個線程想要執行,必須先拿到GIL,我們可以把GIL看做是“通行證”,并且在一個python進程之中,GIL只有一個。
拿不到通行證的線程,就不允許進入CPU執行。GIL只在cpython中才有,因為cpython調用的是c語言的原生線程,所以他不能直接操作cpu,而只能利用GIL保證同一時間只能有一個線程拿到數據。而在pypy和jpython中是沒有GIL的。
python在使用多線程的時候,調用的是c語言的原生過程。
示例如下
累加數字到100000000,比較單線程和雙線程的時間。
import threading
import multiprocessing
import time
def tstart(n):
var = 0
for i in range(n):
var += 1
if __name__ == '__main__':
t1 = threading.Thread(target=tstart, args=(50000000,))
# t1 = multiprocessing.Process(target=tstart, args=(50000000,))
t2 = threading.Thread(target=tstart, args=(50000000,))
# t2 = multiprocessing.Process(target=tstart, args=(50000000,))
start_time = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
print("Two thread cost time: %s" % (time.time() - start_time))
start_time = time.time()
tstart(100000000)
print("Main thread cost time: %s" % (time.time() - start_time))
# 結果如下:
# Two thread cost time: 5.507142066955566
# Main thread cost time: 5.363916635513306
這里多線程耗時比單線程耗時要多,原因就是GIL鎖導致的即使多線程也只有一個核在進行運算,線程間的切換導致了耗時增加,起到了反作用。
多進程耗時如下
import threading
import multiprocessing
import time
def tstart(n):
var = 0
for i in range(n):
var += 1
if __name__ == '__main__':
# t1 = threading.Thread(target=tstart, args=(50000000,))
t1 = multiprocessing.Process(target=tstart, args=(50000000,))
# t2 = threading.Thread(target=tstart, args=(50000000,))
t2 = multiprocessing.Process(target=tstart, args=(50000000,))
start_time = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
print("Two thread cost time: %s" % (time.time() - start_time))
start_time = time.time()
tstart(100000000)
print("Main thread cost time: %s" % (time.time() - start_time))
# 結果如下:
# Two thread cost time: 2.793893337249756
# Main thread cost time: 5.109605073928833
可以看到多線程將執行時間減少了將近一半。
也印證了CPU密集型的任務不能用多線程執行,而應該用多進程執行。而IO密集型的任務在CPU可以使用多線程進行操作,效果比較理想
。
4.2 多線程和多進程
每個進程都包含至少一個線程:主線程,每個主線程可以開啟多個子線程,由于GIL鎖機制的存在,每個進程里的若干個線程同一時間只能有一個被執行;但是使用多進程就可以保證多個線程被多個CPU同時執行。
python編寫多進程能更充分地利用多核CPU的性能,大大提升程序的運行速度。
多進程必須注意的是,要加上
if __name__ == '__main__':
pass
python多線程和多進程不存在優劣之分,兩者都有著各自的應用環境。
- 線程幾乎不占資源,系統開銷少,切換速度快,而且同一個進程的多個線程之間能很容易地實現數據共享
- 而創建進程需要為它分配單獨的資源,系統開銷大,切換速度慢,而且不同進程之間的數據默認是不可共享的。
掌握了兩者各自的特點,才能在實際編程中根據任務需求采取更加適合的方案。
多進程:消耗CPU操作,CPU密集計算。
多線程:適合大量的IO操作。
4.3 線程與進程區別
下面簡單的比較一下線程與進程
- 進程是資源分配的基本單位,線程是CPU執行和調度的基本單位;
- 通信/同步方式:
-
進程
通信方式:管道,FIFO,消息隊列,信號,共享內存,socket,stream流;
同步方式:PV信號量,管程 -
線程
同步方式:互斥鎖,遞歸鎖,條件變量,信號量
通信方式:位于同一進程的線程共享進程資源,因此線程間沒有類似于進程間用于數據傳遞的通信方式,線程間的通信主要是用于線程同步。
-
進程
- CPU上真正執行的是線程,線程比進程輕量,其切換和調度代價比進程要小;
- 線程間對于共享的進程數據需要考慮線程安全問題,由于進程之間是隔離的,擁有獨立的內存空間資源,相對比較安全,只能通過上面列出的IPC(Inter-Process Communication)進行數據傳輸;
- 系統有一個個進程組成,每個進程包含代碼段、數據段、堆空間和棧空間,以及操作系統共享部分 ,有等待,就緒和運行三種狀態;
- 一個進程可以包含多個線程,線程之間共享進程的資源(文件描述符、全局變量、堆空間等),寄存器變量和棧空間等是線程私有的;
- 操作系統中一個進程掛掉不會影響其他進程,如果一個進程中的某個線程掛掉而且OS對線程的支持是多對一模型,那么會導致當前進程掛掉;
- 如果CPU和系統支持多線程與多進程,多個進程并行執行的同時,每個進程中的線程也可以并行執行,這樣才能最大限度的榨取硬件的性能;
4.4 線程和進程的上下文切換
進程切換過程切換牽涉到非常多的東西,寄存器內容保存到任務狀態段TSS,切換頁表,堆棧等。簡單來說可以分為下面兩步:
- 頁全局目錄切換,使CPU到新進程的線性地址空間尋址;
- 切換內核態堆棧和硬件上下文,硬件上下文包含CPU寄存器的內容,存放在TSS中;
線程運行于進程地址空間,切換過程不涉及到空間的變換,只牽涉到第二步;
4.5 密集型任務類型
python針對不同類型的代碼執行效率也是不同的,任務可以分為I/O密集型和計算密集型,而多線程在切換中又分為I/O切換和時間切換。
- CPU密集型任務(各種循環處理、計算等),在這種情況下,由于計算工作多,ticks技術很快就會達到閥值,然后出發GIL的釋放與再競爭(多個線程來回切換當然是需要消耗資源的),所以python下的多線程對CPU密集型代碼并不友好。
- IO密集型任務(文件處理、網絡爬蟲等設計文件讀寫操作),多線程能夠有效提升效率(單線程下有IO操作會進行IO等待,造成不必要的時間浪費,而開啟多線程能在線程A等待時,自動切換到線程B,可以不浪費CPU的資源,從而能提升程序的執行效率)。所以python的多線程對IO密集型代碼比較友好。