1. 程式人生 > 實用技巧 >併發程式設計之多執行緒

併發程式設計之多執行緒

一 執行緒理論

1 什麼是執行緒

在傳統作業系統中,每個程序有一個地址空間,而且預設就有一個控制執行緒執行緒顧名思義,就是一條流水線工作的過程,一條流水線必須屬於一個車間,一個車間的工作過程是一個程序

車間負責把資源整合到一起,是一個資源單位,而一個車間內至少有一個流水線

流水線的工作需要電源,電源就相當於cpu

所以,程序只是用來把資源集中到一起(程序只是一個資源單位,或者說資源集合),而執行緒才是cpu上的執行單位。

多執行緒(即多個控制執行緒)的概念是,在一個程序中存在多個控制執行緒,多個控制執行緒共享該程序的地址空間,相當於一個車間內有多條流水線,都共用一個車間的資源。

例如,北京地鐵與上海地鐵是不同的程序,而北京地鐵裡的13號線是一個執行緒,北京地鐵所有的線路共享北京地鐵所有的資源,比如所有的乘客可以被所有線路拉。

2 執行緒的建立開銷小

建立程序的開銷要遠大於執行緒?

如果我們的軟體是一個工廠,該工廠有多條流水線,流水線工作需要電源,電源只有一個即cpu(單核cpu)

一個車間就是一個程序,一個車間至少一條流水線(一個程序至少一個執行緒)

建立一個程序,就是建立一個車間(申請空間,在該空間內建至少一條流水線)

而建執行緒,就只是在一個車間內造一條流水線,無需申請空間,所以建立開銷小

程序之間是競爭關係,執行緒之間是協作關係?

車間直接是競爭/搶電源的關係,競爭(不同的程序直接是競爭關係,是不同的程式設計師寫的程式執行的,迅雷搶佔其他程序的網速,360把其他程序當做病毒乾死)
一個車間的不同流水線式協同工作的關係(同一個程序的執行緒之間是合作關係,是同一個程式寫的程式內開啟動,迅雷內的執行緒是合作關係,不會自己幹自己)

3 執行緒與程序的區別

執行緒共享建立它的程序的地址空間;程序有自己的地址空間。

執行緒可以直接訪問其程序的資料段;程序有自己的父程序的資料段副本。

執行緒可以直接與其程序的其他執行緒通訊;程序必須使用程序間通訊來與同級程序通訊。

新執行緒很容易建立;新程序需要父程序的複製。

執行緒可以對同一程序的執行緒進行相當大的控制;程序只能對子程序進行控制。

對主執行緒的更改(取消、優先順序更改等)可能會影響程序其他執行緒的行為;對父程序的更改不會影響子程序。

4 為何要用多執行緒

多執行緒指的是,在一個程序中開啟多個執行緒,簡單的講:如果多個任務共用一塊地址空間,那麼必須在一個程序內開啟多個執行緒。詳細的講分為4點:

  1. 多執行緒共享一個程序的地址空間

  2. 執行緒比程序更輕量級,執行緒比程序更容易建立可撤銷,在許多作業系統中,建立一個執行緒比建立一個程序要快10-100倍,在有大量執行緒需要動態和快速修改時,這一特性很有用

  3. 若多個執行緒都是cpu密集型的,那麼並不能獲得性能上的增強,但是如果存在大量的計算和大量的I/O處理,擁有多個執行緒允許這些活動彼此重疊執行,從而會加快程式執行的速度。

  4. 在多cpu系統中,為了最大限度的利用多核,可以開啟多個執行緒,比開程序開銷要小的多。(這一條並不適用於python)

5 多執行緒應用舉例

開啟一個字處理軟體程序,該程序肯定需要辦不止一件事情,比如監聽鍵盤輸入,處理文字,定時自動將文字儲存到硬碟,這三個任務操作的都是同一塊資料,因而不能用多程序。只能在一個程序裡併發地開啟三個執行緒,如果是單執行緒,那就只能是,鍵盤輸入時,不能處理文字和自動儲存,自動儲存時又不能輸入和處理文字。

6 在核心空間實現的執行緒

核心級執行緒:切換由核心控制,當執行緒進行切換的時候,由使用者態轉化為核心態。切換完畢要從核心態返回使用者態;可以很好的利用smp,即利用多核cpu。windows執行緒就是這樣的。

7 使用者級與核心級執行緒的對比

一: 以下是使用者級執行緒和核心級執行緒的區別:

  1. 核心支援執行緒是OS核心可感知的,而使用者級執行緒是OS核心不可感知的。
  2. 使用者級執行緒的建立、撤消和排程不需要OS核心的支援,是在語言(如Java)這一級處理的;而核心支援執行緒的建立、撤消和排程都需OS核心提供支援,而且與程序的建立、撤消和排程大體是相同的。
  3. 使用者級執行緒執行系統呼叫指令時將導致其所屬程序被中斷,而核心支援執行緒執行系統呼叫指令時,只導致該執行緒被中斷。
  4. 在只有使用者級執行緒的系統內,CPU排程還是以程序為單位,處於執行狀態的程序中的多個執行緒,由使用者程式控制執行緒的輪換執行;在有核心支援執行緒的系統內,CPU排程則以執行緒為單位,由OS的執行緒排程程式負責執行緒的排程。
  5. 使用者級執行緒的程式實體是執行在使用者態下的程式,而核心支援執行緒的程式實體則是可以執行在任何狀態下的程式。

二: 核心執行緒的優缺點

  優點:

  1. 當有多個處理機時,一個程序的多個執行緒可以同時執行。

  缺點:

  1. 由核心進行排程。

三: 使用者程序的優缺點

  優點:

  1. 執行緒的排程不需要核心直接參與,控制簡單。
  2. 可以在不支援執行緒的作業系統中實現。
  3. 建立和銷燬執行緒、執行緒切換代價等執行緒管理的代價比核心執行緒少得多。
  4. 允許每個程序定製自己的排程演算法,執行緒管理比較靈活。
  5. 執行緒能夠利用的表空間和堆疊空間比核心級執行緒多。
  6. 同一程序中只能同時有一個執行緒在執行,如果有一個執行緒使用了系統呼叫而阻塞,那麼整個程序都會被掛起。另外,頁面失效也會產生同樣的問題。

  缺點:

  1. 資源排程按照程序進行,多個處理機下,同一個程序中的執行緒只能在同一個處理機下分時複用

二 開啟執行緒的兩種方式

# 第一種

from threading import Thread
import time

def tsak():
    print('執行緒開始...')
    time.sleep(1)
    print('執行緒結束...')


if __name__ == '__main__':
    p=Thread(target=tsak,)
    p.start()
    print('主...')
# 執行緒輕量級,傳輸太快


# 第二種

from threading import Thread
import time


class Task(Thread):
    def run(self) -> None:
        print('執行緒開始...')
        time.sleep(1)
        print('執行緒結束...')


if __name__ == '__main__':
    t = Task()
    t.run()
    print('主...')

三 執行緒的相關方法

Thread例項物件的方法
  # isAlive(): 返回執行緒是否活動的。
  # getName(): 返回執行緒名。
  # setName(): 設定執行緒名。

threading模組提供的一些方法:
  # threading.currentThread(): 返回當前的執行緒變數。
  # threading.enumerate(): 返回一個包含正在執行的執行緒的list。正在執行指執行緒啟動後、結束前,不包括啟動前和終止後的執行緒。
  # threading.activeCount(): 返回正在執行的執行緒數量,與len(threading.enumerate())有相同的結果。

主執行緒等待子執行緒結束

from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.start()
    t.join()
    print('主執行緒')
    print(t.is_alive())
    '''
    egon say hello
    主執行緒
    False
    '''

四 守護執行緒

無論是程序還是執行緒,都遵循:守護xxx會等待主xxx執行完畢後被銷燬

需要強調的是:執行完畢並非終止執行

#1.對主程序來說,執行完畢指的是主程序程式碼執行完畢

#2.對主執行緒來說,執行完畢指的是主執行緒所在的程序內所有非守護執行緒統統執行完畢,主執行緒才算執行完畢

詳細解釋:

#1 主程序在其程式碼結束後就已經算執行完畢了(守護程序在此時就被回收),然後主程序會一直等非守護的子程序都執行完畢後回收子程序的資源(否則會產生殭屍程序),才會結束,

#2 主執行緒在其他非守護執行緒執行完畢後才算執行完畢(守護執行緒在此時就被回收)。因為主執行緒的結束意味著程序的結束,程序整體
from threading import Thread
import time
def sayhi(name):
    time.sleep(2)
    print('%s say hello' %name)

if __name__ == '__main__':
    t=Thread(target=sayhi,args=('egon',))
    t.setDaemon(True) #必須在t.start()之前設定
    t.start()

    print('主執行緒')
    print(t.is_alive())
    '''
    主執行緒
    True
    '''

五 GIL鎖

python
#1 python的直譯器有很多,cpython,jpython,pypy(python寫的直譯器)
#2 python的庫多,庫都是基於cpython寫起來的,其他直譯器沒有那麼多的庫
#3 cpython中有一個全域性大鎖,每條執行緒要執行,必須獲取到這個鎖
#4 為什麼會有這個鎖呢?python的垃圾回收機制
#5 python的多執行緒其實就是單執行緒
#6 某個執行緒想要執行,必須先拿到GIL,我們可以把GIL看作是“通行證”,
並且在一個python程序中,GIL只有一個。拿不到通行證的執行緒,就不允許進入CPU執行

# 7 總結:cpython直譯器中有一個全域性鎖(GIL),執行緒必須獲取到GIL才能執行,
  我們開的多執行緒,不管有幾個cpu,同一時刻,只有一個執行緒在執行
  多程序下的多執行緒除外
 (python的多執行緒,不能利用多核優勢)


# 8 如果是io密集型操作:開多執行緒
# 9如果是計算密集型:開多程序
以上兩句話,只針對與cpython直譯器

六 同步鎖

三個需要注意的點:
#1.執行緒搶的是GIL鎖,GIL鎖相當於執行許可權,拿到執行許可權後才能拿到互斥鎖Lock,其他執行緒也可以搶到GIL,但如果發現Lock仍然沒有被釋放則阻塞,即便是拿到執行許可權GIL也要立刻交出來

#2.join是等待所有,即整體序列,而鎖只是鎖住修改共享資料的部分,即部分序列,要想保證資料安全的根本原理在於讓併發變成序列,join與互斥鎖都可以實現,毫無疑問,互斥鎖的部分序列效率要更高

#3. 一定要看本小節最後的GIL與互斥鎖的經典分析

GIL VS Lock

機智的同學可能會問到這個問題,就是既然你之前說過了,Python已經有一個GIL來保證同一時間只能有一個執行緒來執行了,為什麼這裡還需要lock?

 首先我們需要達成共識:鎖的目的是為了保護共享的資料,同一時間只能有一個執行緒來修改共享的資料

然後,我們可以得出結論:保護不同的資料就應該加不同的鎖。

 最後,問題就很明朗了,GIL 與Lock是兩把鎖,保護的資料不一樣,前者是直譯器級別的(當然保護的就是直譯器級別的資料,比如垃圾回收的資料),後者是保護使用者自己開發的應用程式的資料,很明顯GIL不負責這件事,只能使用者自定義加鎖處理,即Lock

過程分析:所有執行緒搶的是GIL鎖,或者說所有執行緒搶的是執行許可權

  執行緒1搶到GIL鎖,拿到執行許可權,開始執行,然後加了一把Lock,還沒有執行完畢,即執行緒1還未釋放Lock,有可能執行緒2搶到GIL鎖,開始執行,執行過程中發現Lock還沒有被執行緒1釋放,於是執行緒2進入阻塞,被奪走執行許可權,有可能執行緒1拿到GIL,然後正常執行到釋放Lock。。。這就導致了序列執行的效果

# 只有GIL鎖沒有普通互斥鎖
from threading import Thread
import time

money = 100


def task():
    global money
    time.sleep(1)
    money -= 1


if __name__ == '__main__':
    lis = []
    for i in range(10):
        t = Thread(target=task)
        t.start()
        lis.append(t)
        print(money)  # 此時列印的是遇到IO之前的money

    for t in lis:
        t.join()
        print(money)  # 資料發生混亂,由於併發讀取資料並修改


# 有普通互斥鎖
from threading import Thread, Lock
import time

money = 100
matex = Lock()


def task():
    global money
    matex.acquire()
    time.sleep(1)
    money -= 1
    matex.release()


if __name__ == '__main__':
    lis = []
    for i in range(10):
        t = Thread(target=task)
        t.start()
        lis.append(t)
        print(money)  # 此時列印的是遇到IO之前的money
    # 先執行完子執行緒的在執行主執行緒
    for t in lis:
        t.join()
        print('-----')
        print(money)  # 在互斥鎖內的執行成了序列,每一個執行緒要上鎖時其他程序不能執行這一段內容要等待解鎖
    print(money)

七 死鎖現象與遞迴鎖

所謂死鎖: 是指兩個或兩個以上的程序或執行緒在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的程序稱為死鎖程序,如下就是死鎖

from threading import Thread, Lock
import time

mutexA = Lock()
mutexB = Lock()


def eat_apple(name):
    mutexA.acquire()
    print(f'{name}獲得了a鎖')
    mutexB.acquire()
    print(f'{name}獲得了b鎖')
    print('開始吃蘋果並且吃完了')
    mutexB.release()
    print(f'{name}釋放了b鎖')
    mutexA.release()
    print(f'{name}釋放了a鎖')


def eat_egg(name):
    mutexB.acquire()
    print(f'{name}獲得了b鎖')
    time.sleep(2)
    # 碰到IO就是下一個執行緒來執行然後獲得了a鎖,並且在b鎖這裡卡住
    mutexA.acquire()
    print(f'{name}獲得了a鎖')
    print('開始吃雞蛋並且吃完了')
    mutexA.release()
    print(f'{name}釋放了a鎖')
    mutexB.release()
    print(f'{name}釋放了b鎖')


if __name__ == '__main__':
    lis = ['egon', 'justin', 'arther', 'tank']
    for name in lis:
        t1 = Thread(target=eat_apple, args=(name,))
        t2 = Thread(target=eat_egg, args=(name,))
        t1.start()
        t2.start()

# 死鎖現場,egon與justin同時都需要獲取兩個鎖才能執行
# 但是egon拿了A鎖,等B鎖,justin拿了B鎖,等A鎖,兩者互相等對方釋放自己需要的鎖,但是兩者由於等鎖都暫停了

解決方法,遞迴鎖,在Python中為了支援在同一執行緒中多次請求同一資源,python提供了可重入鎖RLock。

這個RLock內部維護著一個Lock和一個counter變數,counter記錄了acquire的次數,從而使得資源可以被多次require。直到一個執行緒所有的acquire都被release,其他的執行緒才能獲得資源。上面的例子如果使用RLock代替Lock,則不會發生死鎖:

from threading import Thread, RLock
import time

# 遞迴鎖(可重入),同一個執行緒可以多次acquire,每acquire一次,內部計數器增加一次,
# 每relaese一次,內部計數器減少一次,這樣就解決死鎖問題,因為死鎖的是因為有兩把鎖,然後兩個執行緒互相佔著一把,
# 遞迴鎖只有一把且可線上程內重複,從根部解決這個問題,但是隻有計數為零時,其他執行緒才能引入遞迴鎖
# 這樣就不會出現互相等對方將佔用的鎖釋放的情況

mutexA = RLock()
mutexB = mutexA


def eat_apple(name):
    mutexA.acquire()
    print(f'{name}獲得了a鎖')
    mutexB.acquire()
    print(f'{name}獲得了b鎖')
    print('開始吃蘋果並且吃完了')
    mutexB.release()
    print(f'{name}釋放了b鎖')
    mutexA.release()
    print(f'{name}釋放了a鎖')


def eat_egg(name):
    mutexB.acquire()
    print(f'{name}獲得了b鎖')
    time.sleep(2)  # 遇到了IO,轉到下個執行緒去執行 ,但計數不為零其他人都獲取不到這把鎖,
                    # 於是轉回上一個執行緒,將鎖釋放完
    mutexA.acquire()
    print(f'{name}獲得了a鎖')
    print('開始吃雞蛋並且吃完了')
    mutexA.release()
    print(f'{name}釋放了a鎖')
    mutexB.release()
    print(f'{name}釋放了b鎖')


if __name__ == '__main__':
    lis = ['egon', 'justin', 'arther', 'tank']
    for name in lis:
        t1 = Thread(target=eat_apple, args=(name,))
        t2 = Thread(target=eat_egg, args=(name,))
        t1.start()
        t2.start()

八 訊號量

同進程的一樣

Semaphore管理一個內建的計數器,
每當呼叫acquire()時內建計數器-1;
呼叫release() 時內建計數器+1;
計數器不能小於0;當計數器為0時,acquire()將阻塞執行緒直到其他執行緒呼叫release()。

例項:(同時只有5個執行緒可以獲得semaphore,即可以限制最大連線數為5):

from threading import Thread,Semaphore
import threading
import time
# def func():
#     if sm.acquire():
#         print (threading.currentThread().getName() + ' get semaphore')
#         time.sleep(2)
#         sm.release()
def func():
    sm.acquire()
    print('%s get sm' %threading.current_thread().getName())
    time.sleep(3)
    sm.release()
if __name__ == '__main__':
    sm=Semaphore(5)
    for i in range(23):
        t=Thread(target=func)
        t.start()

九 Event

同進程的一樣

執行緒的一個關鍵特性是每個執行緒都是獨立執行且狀態不可預測。如果程式中的其 他執行緒需要通過判斷某個執行緒的狀態來確定自己下一步的操作,這時執行緒同步問題就會變得非常棘手。為了解決這些問題,我們需要使用threading庫中的Event物件。 物件包含一個可由執行緒設定的訊號標誌,它允許執行緒等待某些事件的發生。在 初始情況下,Event物件中的訊號標誌被設定為假。如果有執行緒等待一個Event物件, 而這個Event物件的標誌為假,那麼這個執行緒將會被一直阻塞直至該標誌為真。一個執行緒如果將一個Event物件的訊號標誌設定為真,它將喚醒所有等待這個Event物件的執行緒。如果一個執行緒等待一個已經被設定為真的Event物件,那麼它將忽略這個事件, 繼續執行

event.isSet():返回event的狀態值;

event.wait():如果 event.isSet()==False將阻塞執行緒;

event.set(): 設定event的狀態值為True,所有阻塞池的執行緒啟用進入就緒狀態, 等待作業系統排程;

event.clear():恢復event的狀態值為False。
# 一些執行緒需要等到其他執行緒執行完成之後才能執行,類似於發射訊號
# 比如一個執行緒等待另一個執行緒執行結束再繼續執行


from threading import Thread, Event, Lock
import time

event = Event()
mutex = Lock()


def girl(name):
    print(f'{name}現在不單身,戀愛ing')
    time.sleep(5)
    print(f'{name}分手了,給屌絲男發訊號')
    event.set()


def boy(name):
    print(f'{name}等待女孩分手')
    mutex.acquire()
    event.wait()
    time.sleep(0.1)
    print(f'女孩分手了,衝')
    mutex.release()


if __name__ == '__main__':
    lyf = Thread(target=girl, args=('如花',))
    lyf.start()

    for i in range(10):
        b = Thread(target=boy, args=(f'屌絲男{i}號',))
        b.start()


# 作業:讀取檔案,一個執行緒讀取前一半發訊號給另一個執行緒讓它去讀取後一半


from threading import Thread, Event
import os

event = Event()


def read_first():
    data = os.path.getsize('a.txt') // 2
    with open('a.txt', 'r', encoding='utf-8')as f:
        print(f.read(data))
        print('前半部分已讀出')
        event.set()


def read_last():
    event.wait()
    print('接收到了我開始讀了')
    data = os.path.getsize('a.txt') // 2
    with open('a.txt', 'r', encoding='utf-8')as f:
        f.seek(data, 0)
        print(f.read())
        print('後半部分已讀出')


if __name__ == '__main__':
    r1 = Thread(target=read_first)
    r2 = Thread(target=read_last)
    r1.start()
    r2.start()

十 執行緒queue

# 程序queue和執行緒不是一個

# 執行緒queue

from queue import Queue, LifoQueue, PriorityQueue

# 執行緒間通訊,因為共享變數會出現資料不安全問題,用執行緒queue通訊,不需要加鎖,
# 內部自帶,queue是執行緒安全的

'''
三種執行緒Queue
    -Queue:佇列,先進先出
    -PriorityQueue:優先順序佇列,誰小誰先出
    -LifoQueue:棧,後進後出
    
'''
# 如何使用
# q=Queue(5)
# q.put("lqz")
# q.put("egon")
# q.put("鐵蛋")
# q.put("鋼彈")
# q.put("金蛋")
#
#
# # q.put("銀蛋")
# # q.put_nowait("銀蛋")
# # 取值
# print(q.get())
# print(q.get())
# print(q.get())
# print(q.get())
# print(q.get())
# # 卡住
# # print(q.get())
# # q.get_nowait()
# # 是否滿,是否空
# print(q.full())
# print(q.empty())

# LifoQueue,後進後出

q=LifoQueue(5)
q.put("lqz")
q.put("egon")
q.put("鐵蛋")
q.put("鋼彈")
q.put("金蛋")
#
# q.put("ddd蛋")
print(q.get())


#PriorityQueue:數字越小,級別越高

# q=PriorityQueue(3)
# q.put((-10,'金蛋'))
# q.put((100,'銀蛋'))
# q.put((101,'鐵蛋'))
# q.put((1010,'鐵dd蛋'))  # 不能再放了
#
# print(q.get())
# print(q.get())
# print(q.get())


十一 Python標準模組--concurrent.futures

#1 介紹
concurrent.futures模組提供了高度封裝的非同步呼叫介面
ThreadPoolExecutor:執行緒池,提供非同步呼叫
ProcessPoolExecutor: 程序池,提供非同步呼叫
Both implement the same interface, which is defined by the abstract Executor class.

#2 基本方法
#submit(fn, *args, **kwargs)
非同步提交任務

#map(func, *iterables, timeout=None, chunksize=1) 
取代for迴圈submit的操作

#shutdown(wait=True) 
相當於程序池的pool.close()+pool.join()操作
wait=True,等待池內所有任務執行完畢回收完資源後才繼續
wait=False,立即返回,並不會等待池內的任務執行完畢
但不管wait引數為何值,整個程式都會等到所有任務執行完畢
submit和map必須在shutdown之前

#result(timeout=None)
取得結果

#add_done_callback(fn)
回撥函式

map用法

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

import os,time,random
def task(n):
    print('%s is runing' %os.getpid())
    time.sleep(random.randint(1,3))
    return n**2

if __name__ == '__main__':

    executor=ThreadPoolExecutor(max_workers=3)

    # for i in range(11):
    #     future=executor.submit(task,i)

    executor.map(task,range(1,12)) #map取代了for+submit

回撥函式

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
from multiprocessing import Pool
import requests
import json
import os

def get_page(url):
    print('<程序%s> get %s' %(os.getpid(),url))
    respone=requests.get(url)
    if respone.status_code == 200:
        return {'url':url,'text':respone.text}

def parse_page(res):
    res=res.result()
    print('<程序%s> parse %s' %(os.getpid(),res['url']))
    parse_res='url:<%s> size:[%s]\n' %(res['url'],len(res['text']))
    with open('db.txt','a') as f:
        f.write(parse_res)


if __name__ == '__main__':
    urls=[
        'https://www.baidu.com',
        'https://www.python.org',
        'https://www.openstack.org',
        'https://help.github.com/',
        'http://www.sina.com.cn/'
    ]

    # p=Pool(3)
    # for url in urls:
    #     p.apply_async(get_page,args=(url,),callback=pasrse_page)
    # p.close()
    # p.join()

    p=ProcessPoolExecutor(3)
    for url in urls:
        p.submit(get_page,url).add_done_callback(parse_page) #parse_page拿到的是一個future物件obj,需要用obj.result()拿到結果

十二 IO密集型與計算密集型

'''

---以下只針對於cpython直譯器
-在單核情況下:
    -開多執行緒還是開多程序
        不管幹什麼最終都要開執行緒,因為執行緒是CPU排程的最小單位

-在多核情況下:
    -如果是計算密集型,需要開程序,能被多個cpu排程執行

    -如果IO密集型,需要開執行緒,CPU遇到IO會切換到其他執行緒執行,執行緒更輕量級,切換IO速度更快且資源更小

'''

from threading import Thread
from multiprocessing import Process
import time




# 計算密集型

def task():
    count = 0
    for i in range(10000000):
        count += 1


if __name__ == '__main__':
    ctime = time.time()
    lis = []
    for i in range(10):
        t = Thread(target=task)  # 開執行緒:6.038380861282349  即使多核每次也只有一個執行緒去執行計算任務
        # t=Process(target=task) #開程序:4.292168140411377  可以多核開多個程序然後每個程序呼叫執行緒去執行計算任務
        t.start()
        lis.append(t)

    for j in lis:
        j.join()
    # 以免時間過長cpu選擇掛起去執行主執行緒或主程序
    print(time.time() - ctime)


# IO密集型
def task():
    time.sleep(2)


if __name__ == '__main__':
    ctime = time.time()
    lis = []
    for i in range(100):
        # t = Thread(target=task)  # 開執行緒:2.0329859256744385  執行緒的IO,由於執行緒更輕量級,堵塞-執行,切換的速度更快
        t = Process(target=task)  # 開程序:5.154810190200806   程序的IO,涉及多方資源的調配,堵塞-就緒-執行,切換速度較慢
        t.start()
        lis.append(t)

    for j in lis:
        j.join()
    # 以免時間過長cpu選擇掛起去執行主執行緒或主程序
    print(time.time() - ctime)