Python線程進程協程

Python線程進程協程

所講內容:

多任務的介紹

多任務的概念

線程(重點)

線程(注意點)

多線程-共享全局變量(重點)

多線程-共享全局變量-問題

同步

互斥鎖(重點)

案例:多任務版udp聊天器

進程以及狀態

進程的創建-multiprocessing

進程、線程對比

進程間通信-Queue

進程池Pool

應用:文件夾copy器(多進程版)

迭代器

生成器

協程-yield

協程-greenlet

協程-gevent

進程、線程、協程對比

併發下載器

多任務的介紹

現實生活中
有很多的場景中的事情是同時進行的,比如開車的時候 手和腳共同來駕駛汽車,再比如唱歌跳舞也是同時進行的;

試想,如果把唱歌和跳舞這2件事情分開依次完成的話,估計就沒有那麼好的效果了(想一下場景:先唱歌,然後在跳舞,O(∩_∩)O哈哈~)

程序中

如下程序,來模擬“唱歌跳舞”這件事情

#coding=utf-8

from time import sleep

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__':
    sing() #唱歌
    dance() #跳舞

運行結果如下:

正在唱歌...0
正在唱歌...1
正在唱歌...2
正在跳舞...0
正在跳舞...1
正在跳舞...2

!!!注意
很顯然剛剛的程序並沒有完成唱歌和跳舞同時進行的要求
如果想要實現“唱歌跳舞”同時進行,那麼就需要一個新的方法,叫做:多任務

回到頂部

多任務的概念

什麼叫“多任務”呢?簡單地說,就是操作系統可以同時運行多個任務。打個比方,你一邊在用瀏覽器上網,一邊在聽MP3,一邊在用Word趕作業,這就是多任務,至少同時有3個任務正在運行。還有很多任務悄悄地在後臺同時運行着,只是桌面上沒有顯示而已。

在這裏插入圖片描述

現在,多核CPU已經非常普及了,但是,即使過去的單核CPU,也可以執行多任務。由於CPU執行代碼都是順序執行的,那麼,單核CPU是怎麼執行多任務的呢?

答案就是操作系統輪流讓各個任務交替執行,任務1執行0.01秒,切換到任務2,任務2執行0.01秒,再切換到任務3,執行0.01秒……這樣反覆執行下去。表面上看,每個任務都是交替執行的,但是,由於CPU的執行速度實在是太快了,我們感覺就像所有任務都在同時執行一樣。

真正的並行執行多任務只能在多核CPU上實現,但是,由於任務數量遠遠多於CPU的核心數量,所以,操作系統也會自動把很多任務輪流調度到每個核心上執行。

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

注意:
併發:指的是任務數多餘cpu核數,通過操作系統的各種任務調度算法,實現用多個任務“一起”執行(實際上總有一些任務不在執行,因爲切換任務的速度相當快,看上去一起執行而已)
並行:指的是任務數小於等於cpu核數,即任務真的是一起執行的

回到頂部

線程(重點)

python的thread模塊是比較底層的模塊,python的threading模塊是對thread做了一些包裝的,可以更加方便的被使用
1. 使用threading模塊
單線程執行

#coding=utf-8
import time

def saySorry():
    print("親愛的,我錯了,我能喫飯了嗎?")
    time.sleep(1)

if __name__ == "__main__":
    for i in range(5):
        saySorry()

在這裏插入圖片描述

多線程執行

#coding=utf-8
import threading
import time

def saySorry():
    print("親愛的,我錯了,我能喫飯了嗎?")
    time.sleep(1)

if __name__ == "__main__":
    for i in range(5):
        t = threading.Thread(target=saySorry)
        t.start() #啓動線程,即讓線程開始執行

運行結果:
在這裏插入圖片描述
說明
可以明顯看出使用了多線程併發的操作,花費時間要短很多
當調用start()時,纔會真正的創建線程,並且開始執行

2. 主線程會等待所有的子線程結束後才結束

#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())

3. 查看線程數量

#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()

    while True:
        length = len(threading.enumerate())
        print('當前運行的線程數爲:%d'%length)
        if length<=1:
            break

        sleep(0.5)

回到頂部

線程(注意點)

  1. 線程執行代碼的封裝
    通過上一小節,能夠看出,通過使用threading模塊能完成多任務的程序開發,爲了讓每個線程的封裝性更完美,所以使用threading模塊時,往往會定義一個新的子類class,只要繼承threading.Thread就可以了,然後重寫run方法

示例如下:

#coding=utf-8
import threading
import time

class MyThread(threading.Thread):
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm "+self.name+' @ '+str(i) #name屬性中保存的是當前線程的名字
            print(msg)


if __name__ == '__main__':
    t = MyThread()
    t.start()

說明
python的threading.Thread類有一個run方法,用於定義線程的功能函數,可以在自己的線程類中覆蓋該方法。而創建自己的線程實例後,通過Thread類的start方法,可以啓動該線程,交給python虛擬機進行調度,當該線程獲得執行的機會時,就會調用run方法執行線程。
2. 線程的執行順序

#coding=utf-8
import threading
import time

class MyThread(threading.Thread):
    def run(self):
        for i in range(3):
            time.sleep(1)
            msg = "I'm "+self.name+' @ '+str(i)
            print(msg)
def test():
    for i in range(5):
        t = MyThread()
        t.start()
if __name__ == '__main__':
    test()

執行結果:(運行的結果可能不一樣,但是大體是一致的)

I'm Thread-1 @ 0
I'm Thread-2 @ 0
I'm Thread-5 @ 0
I'm Thread-3 @ 0
I'm Thread-4 @ 0
I'm Thread-3 @ 1
I'm Thread-4 @ 1
I'm Thread-5 @ 1
I'm Thread-1 @ 1
I'm Thread-2 @ 1
I'm Thread-4 @ 2
I'm Thread-5 @ 2
I'm Thread-2 @ 2
I'm Thread-1 @ 2
I'm Thread-3 @ 2

說明
從代碼和執行結果我們可以看出,多線程程序的執行順序是不確定的。當執行到sleep語句時,線程將被阻塞(Blocked),到sleep結束後,線程進入就緒(Runnable)狀態,等待調度。而線程調度將自行選擇一個線程執行。上面的代碼中只能保證每個線程都運行完整個run函數,但是線程的啓動順序、run函數中每次循環的執行順序都不能確定。

3. 總結
每個線程默認有一個名字,儘管上面的例子中沒有指定線程對象的name,但是python會自動爲線程指定一個名字。
當線程的run()方法結束時該線程完成。
無法控制線程調度程序,但可以通過別的方式來影響線程調度的方式。

回到頂部

多線程-共享全局變量(重點)

from threading import Thread
import time

g_num = 100

def work1():
    global g_num
    for i in range(3):
        g_num += 1

    print("----in work1, g_num is %d---"%g_num)


def work2():
    global g_num
    print("----in work2, g_num is %d---"%g_num)


print("---線程創建之前g_num is %d---"%g_num)

t1 = Thread(target=work1)
t1.start()

#延時一會,保證t1線程中的事情做完
time.sleep(1)

t2 = Thread(target=work2)
t2.start()

運行結果:

---線程創建之前g_num is 100---
----in work1, g_num is 103---
----in work2, g_num is 103---

列表當做實參傳遞到線程中

from threading import Thread
import time

def work1(nums):
    nums.append(44)
    print("----in work1---",nums)


def work2(nums):
    #延時一會,保證t1線程中的事情做完
    time.sleep(1)
    print("----in work2---",nums)

g_nums = [11,22,33]

t1 = Thread(target=work1, args=(g_nums,))
t1.start()

t2 = Thread(target=work2, args=(g_nums,))
t2.start()

運行結果:

----in work1--- [11, 22, 33, 44]
----in work2--- [11, 22, 33, 44]

總結:
在一個進程內的所有線程共享全局變量,很方便在多個線程間共享數據
缺點就是,線程是對全局變量隨意遂改可能造成多線程之間對全局變量的混亂(即線程非安全)

回到頂部

多線程-共享全局變量-問題

多線程開發可能遇到的問題
假設兩個線程t1和t2都要對全局變量g_num(默認是0)進行加1運算,t1和t2都各對g_num加10次,g_num的最終的結果應該爲20。

但是由於是多線程同時操作,有可能出現下面情況:

在g_num=0時,t1取得g_num=0。此時系統把t1調度爲”sleeping”狀態,把t2轉換爲”running”狀態,t2也獲得g_num=0
然後t2對得到的值進行加1並賦給g_num,使得g_num=1
然後系統又把t2調度爲”sleeping”,把t1轉爲”running”。線程t1又把它之前得到的0加1後賦值給g_num。
這樣導致雖然t1和t2都對g_num加1,但結果仍然是g_num=1
測試1

import threading
import time

g_num = 0

def work1(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("----in work1, g_num is %d---"%g_num)


def work2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("----in work2, g_num is %d---"%g_num)


print("---線程創建之前g_num is %d---"%g_num)

t1 = threading.Thread(target=work1, args=(100,))
t1.start()

t2 = threading.Thread(target=work2, args=(100,))
t2.start()

while len(threading.enumerate()) != 1:
    time.sleep(1)

print("2個線程對同一個全局變量操作之後的最終結果是:%s" % g_num)

運行結果:

---線程創建之前g_num is 0---
----in work1, g_num is 100---
----in work2, g_num is 200---
2個線程對同一個全局變量操作之後的最終結果是:200

測試2

import threading
import time

g_num = 0

def work1(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("----in work1, g_num is %d---"%g_num)


def work2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("----in work2, g_num is %d---"%g_num)


print("---線程創建之前g_num is %d---"%g_num)

t1 = threading.Thread(target=work1, args=(1000000,))
t1.start()

t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()

while len(threading.enumerate()) != 1:
    time.sleep(1)

print("2個線程對同一個全局變量操作之後的最終結果是:%s" % g_num)

運行結果:

---線程創建之前g_num is 0---
----in work1, g_num is 1088005---
----in work2, g_num is 1286202---
2個線程對同一個全局變量操作之後的最終結果是:1286202

結論
如果多個線程同時對同一個全局變量操作,會出現資源競爭問題,從而數據結果會不正確

回到頂部

同步

同步的概念
同步就是協同步調,按預定的先後次序進行運行。如:你說完,我再說。

"同"字從字面上容易理解爲一起動作

其實不是,"同"字應是指協同、協助、互相配合。

如進程、線程同步,可理解爲進程或線程A和B一塊配合,A執行到一定程度時要依靠B的某個結果,於是停下來,示意B運行;B執行,再將結果給A;A再繼續操作。

解決線程同時修改全局變量的方式
對於上一小節提出的那個計算錯誤的問題,可以通過線程同步來進行解決

思路,如下:

系統調用t1,然後獲取到g_num的值爲0,此時上一把鎖,即不允許其他線程操作g_num
t1對g_num的值進行+1
t1解鎖,此時g_num的值爲1,其他的線程就可以使用g_num了,而且是g_num的值不是0而是1
同理其他線程在對g_num進行修改時,都要先上鎖,處理完後再解鎖,在上鎖的整個過程中不允許其他線程訪問,就保證了數據的正確性

回到頂部

互斥鎖(重點)

當多個線程幾乎同時修改某一個共享數據的時候,需要進行同步控制

線程同步能夠保證多個線程安全訪問競爭資源,最簡單的同步機制是引入互斥鎖。

互斥鎖爲資源引入一個狀態:鎖定/非鎖定

某個線程要更改共享數據時,先將其鎖定,此時資源的狀態爲“鎖定”,其他線程不能更改;直到該線程釋放資源,將資源的狀態變成“非鎖定”,其他的線程才能再次鎖定該資源。互斥鎖保證了每次只有一個線程進行寫入操作,從而保證了多線程情況下數據的正確性。

在這裏插入圖片描述
threading模塊中定義了Lock類,可以方便的處理鎖定:

# 創建鎖
mutex = threading.Lock()

# 鎖定
mutex.acquire()

# 釋放
mutex.release()

注意:
如果這個鎖之前是沒有上鎖的,那麼acquire不會堵塞
如果在調用acquire對這個鎖上鎖之前 它已經被 其他線程上了鎖,那麼此時acquire會堵塞,直到這個鎖被解鎖爲止
使用互斥鎖完成2個線程對同一個全局變量各加100萬次的操作

import threading
import time

g_num = 0

def test1(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上鎖
        g_num += 1
        mutex.release()  # 解鎖

    print("---test1---g_num=%d"%g_num)

def test2(num):
    global g_num
    for i in range(num):
        mutex.acquire()  # 上鎖
        g_num += 1
        mutex.release()  # 解鎖

    print("---test2---g_num=%d"%g_num)

# 創建一個互斥鎖
# 默認是未上鎖的狀態
mutex = threading.Lock()

# 創建2個線程,讓他們各自對g_num加1000000次
p1 = threading.Thread(target=test1, args=(1000000,))
p1.start()

p2 = threading.Thread(target=test2, args=(1000000,))
p2.start()

# 等待計算完成
while len(threading.enumerate()) != 1:
    time.sleep(1)

print("2個線程對同一個全局變量操作之後的最終結果是:%s" % g_num)

運行結果:

---test1---g_num=1909909
---test2---g_num=2000000
2個線程對同一個全局變量操作之後的最終結果是:2000000

可以看到最後的結果,加入互斥鎖後,其結果與預期相符。

上鎖解鎖過程
當一個線程調用鎖的acquire()方法獲得鎖時,鎖就進入“locked”狀態。

每次只有一個線程可以獲得鎖。如果此時另一個線程試圖獲得這個鎖,該線程就會變爲“blocked”狀態,稱爲“阻塞”,直到擁有鎖的線程調用鎖的release()方法釋放鎖之後,鎖進入“unlocked”狀態。

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

總結
鎖的好處:

確保了某段關鍵代碼只能由一個線程從頭到尾完整地執行
鎖的壞處:

阻止了多線程併發執行,包含鎖的某段代碼實際上只能以單線程模式執行,效率就大大地下降了
由於可以存在多個鎖,不同的線程持有不同的鎖,並試圖獲取對方持有的鎖時,可能會造成死鎖

回到頂部

案例:多任務版udp聊天器

在這裏插入圖片描述
說明

編寫一個有2個線程的程序
線程1用來接收數據然後顯示
線程2用來檢測鍵盤數據然後通過udp發送數據

要求

實現上述要求
總結多任務程序的特點

參考代碼:

import socket
import threading


def send_msg(udp_socket):
    """獲取鍵盤數據,並將其發送給對方"""
    while True:
        # 1. 從鍵盤輸入數據
        msg = input("\n請輸入要發送的數據:")
        # 2. 輸入對方的ip地址
        dest_ip = input("\n請輸入對方的ip地址:")
        # 3. 輸入對方的port
        dest_port = int(input("\n請輸入對方的port:"))
        # 4. 發送數據
        udp_socket.sendto(msg.encode("utf-8"), (dest_ip, dest_port))


def recv_msg(udp_socket):
    """接收數據並顯示"""
    while True:
        # 1. 接收數據
        recv_msg = udp_socket.recvfrom(1024)
        # 2. 解碼
        recv_ip = recv_msg[1]
        recv_msg = recv_msg[0].decode("utf-8")
        # 3. 顯示接收到的數據
        print(">>>%s:%s" % (str(recv_ip), recv_msg))


def main():
    # 1. 創建套接字
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # 2. 綁定本地信息
    udp_socket.bind(("", 7890))

    # 3. 創建一個子線程用來接收數據
    t = threading.Thread(target=recv_msg, args=(udp_socket,))
    t.start()
    # 4. 讓主線程用來檢測鍵盤數據並且發送
    send_msg(udp_socket)

if __name__ == "__main__":
    main()

回到頂部

進程以及狀態

1. 進程

程序:例如xxx.py這是程序,是一個靜態的

進程:一個程序運行起來後,代碼+用到的資源 稱之爲進程,它是操作系統分配資源的基本單元。

不僅可以通過線程完成多任務,進程也是可以的

2. 進程的狀態
工作中,任務數往往大於cpu的核數,即一定有一些任務正在執行,而另外一些任務在等待cpu進行執行,因此導致了有了不同的狀態
在這裏插入圖片描述
就緒態:運行的條件都已經慢去,正在等在cpu執行
執行態:cpu正在執行其功能
等待態:等待某些條件滿足,例如一個程序sleep了,此時就處於等待態

回到頂部

進程的創建-multiprocessing

multiprocessing模塊就是跨平臺版本的多進程模塊,提供了一個Process類來代表一個進程對象,這個對象可以理解爲是一個獨立的進程,可以執行另外的事情

1. 2個while循環一起執行

# -*- coding:utf-8 -*-
from multiprocessing import Process
import time


def run_proc():
    """子進程要執行的代碼"""
    while True:
        print("----2----")
        time.sleep(1)


if __name__=='__main__':
    p = Process(target=run_proc)
    p.start()
    while True:
        print("----1----")
        time.sleep(1)

說明
創建子進程時,只需要傳入一個執行函數和函數的參數,創建一個Process實例,用start()方法啓動
2. 進程pid

# -*- coding:utf-8 -*-
from multiprocessing import Process
import os
import time

def run_proc():
    """子進程要執行的代碼"""
    print('子進程運行中,pid=%d...' % os.getpid())  # os.getpid獲取當前進程的進程號
    print('子進程將要結束...')

if __name__ == '__main__':
    print('父進程pid: %d' % os.getpid())  # os.getpid獲取當前進程的進程號
    p = Process(target=run_proc)
    p.start()

3. Process語法結構如下:

Process([group [, target [, name [, args [, kwargs]]]]])

target:如果傳遞了函數的引用,可以任務這個子進程就執行這裏的代碼
args:給target指定的函數傳遞的參數,以元組的方式傳遞
kwargs:給target指定的函數傳遞命名參數
name:給進程設定一個名字,可以不設定
group:指定進程組,大多數情況下用不到
Process創建的實例對象的常用方法:

start():啓動子進程實例(創建子進程)
is_alive():判斷進程子進程是否還在活着
join([timeout]):是否等待子進程執行結束,或等待多少秒
terminate():不管任務是否完成,立即終止子進程
Process創建的實例對象的常用屬性:

name:當前進程的別名,默認爲Process-N,N爲從1開始遞增的整數
pid:當前進程的pid(進程號)
4. 給子進程指定的函數傳遞參數

# -*- coding:utf-8 -*-
from multiprocessing import Process
import os
from time import sleep


def run_proc(name, age, **kwargs):
    for i in range(10):
        print('子進程運行中,name= %s,age=%d ,pid=%d...' % (name, age, os.getpid()))
        print(kwargs)
        sleep(0.2)

if __name__=='__main__':
    p = Process(target=run_proc, args=('test',18), kwargs={"m":20})
    p.start()
    sleep(1)  # 1秒中之後,立即結束子進程
    p.terminate()
    p.join()

運行結果:

子進程運行中,name= test,age=18 ,pid=45097...
{'m': 20}
子進程運行中,name= test,age=18 ,pid=45097...
{'m': 20}
子進程運行中,name= test,age=18 ,pid=45097...
{'m': 20}
子進程運行中,name= test,age=18 ,pid=45097...
{'m': 20}
子進程運行中,name= test,age=18 ,pid=45097...
{'m': 20}

5. 進程間不同享全局變量

# -*- coding:utf-8 -*-
from multiprocessing import Process
import os
import time

nums = [11, 22]

def work1():
    """子進程要執行的代碼"""
    print("in process1 pid=%d ,nums=%s" % (os.getpid(), nums))
    for i in range(3):
        nums.append(i)
        time.sleep(1)
        print("in process1 pid=%d ,nums=%s" % (os.getpid(), nums))

def work2():
    """子進程要執行的代碼"""
    print("in process2 pid=%d ,nums=%s" % (os.getpid(), nums))

if __name__ == '__main__':
    p1 = Process(target=work1)
    p1.start()
    p1.join()

    p2 = Process(target=work2)
    p2.start()

運行結果:

in process1 pid=11349 ,nums=[11, 22]
in process1 pid=11349 ,nums=[11, 22, 0]
in process1 pid=11349 ,nums=[11, 22, 0, 1]
in process1 pid=11349 ,nums=[11, 22, 0, 1, 2]
in process2 pid=11350 ,nums=[11, 22]

回到頂部

進程、線程對比

功能
進程,能夠完成多任務,比如 在一臺電腦上能夠同時運行多個QQ
線程,能夠完成多任務,比如 一個QQ中的多個聊天窗口

在這裏插入圖片描述
定義的不同
進程是系統進行資源分配和調度的一個獨立單位.

線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源.

區別
一個程序至少有一個進程,一個進程至少有一個線程.
線程的劃分尺度小於進程(資源比進程少),使得多線程程序的併發性高。
進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大地提高了程序的運行效率
在這裏插入圖片描述
線線程不能夠獨立執行,必須依存在進程中
可以將進程理解爲工廠中的一條流水線,而其中的線程就是這個流水線上的工人

在這裏插入圖片描述

優缺點
線程和進程在使用上各有優缺點:線程執行開銷小,但不利於資源的管理和保護;而進程正相反。

回到頂部

進程間通信-Queue

Process之間有時需要通信,操作系統提供了很多機制來實現進程間的通信。

1. Queue的使用
可以使用multiprocessing模塊的Queue實現多進程之間的數據傳遞,Queue本身是一個消息列隊程序,首先用一個小實例來演示一下Queue的工作原理:

#coding=utf-8
from multiprocessing import Queue
q=Queue(3) #初始化一個Queue對象,最多可接收三條put消息
q.put("消息1") 
q.put("消息2")
print(q.full())  #False
q.put("消息3")
print(q.full()) #True

#因爲消息列隊已滿下面的try都會拋出異常,第一個try會等待2秒後再拋出異常,第二個Try會立刻拋出異常
try:
    q.put("消息4",True,2)
except:
    print("消息列隊已滿,現有消息數量:%s"%q.qsize())

try:
    q.put_nowait("消息4")
except:
    print("消息列隊已滿,現有消息數量:%s"%q.qsize())

#推薦的方式,先判斷消息列隊是否已滿,再寫入
if not q.full():
    q.put_nowait("消息4")

#讀取消息時,先判斷消息列隊是否爲空,再讀取
if not q.empty():
    for i in range(q.qsize()):
        print(q.get_nowait())

運行結果:

False
True
消息列隊已滿,現有消息數量:3
消息列隊已滿,現有消息數量:3
消息1
消息2
消息3

說明
初始化Queue()對象時(例如:q=Queue()),若括號中沒有指定最大可接收的消息數量,或數量爲負值,那麼就代表可接受的消息數量沒有上限(直到內存的盡頭);

Queue.qsize():返回當前隊列包含的消息數量;

Queue.empty():如果隊列爲空,返回True,反之False ;

Queue.full():如果隊列滿了,返回True,反之False;

Queue.get([block[, timeout]]):獲取隊列中的一條消息,然後將其從列隊中移除,block默認值爲True;

1)如果block使用默認值,且沒有設置timeout(單位秒),消息列隊如果爲空,此時程序將被阻塞(停在讀取狀態),直到從消息列隊讀到消息爲止,如果設置了timeout,則會等待timeout秒,若還沒讀取到任何消息,則拋出"Queue.Empty"異常;

2)如果block值爲False,消息列隊如果爲空,則會立刻拋出"Queue.Empty"異常;

Queue.get_nowait():相當Queue.get(False);

Queue.put(item,[block[, timeout]]):將item消息寫入隊列,block默認值爲True;

1)如果block使用默認值,且沒有設置timeout(單位秒),消息列隊如果已經沒有空間可寫入,此時程序將被阻塞(停在寫入狀態),直到從消息列隊騰出空間爲止,如果設置了timeout,則會等待timeout秒,若還沒空間,則拋出"Queue.Full"異常;

2)如果block值爲False,消息列隊如果沒有空間可寫入,則會立刻拋出"Queue.Full"異常;

Queue.put_nowait(item):相當Queue.put(item, False);
2. Queue實例
我們以Queue爲例,在父進程中創建兩個子進程,一個往Queue裏寫數據,一個從Queue裏讀數據:

from multiprocessing import Process, Queue
import os, time, random

# 寫數據進程執行的代碼:
def write(q):
    for value in ['A', 'B', 'C']:
        print('Put %s to queue...' % value)
        q.put(value)
        time.sleep(random.random())

# 讀數據進程執行的代碼:
def read(q):
    while True:
        if not q.empty():
            value = q.get(True)
            print('Get %s from queue.' % value)
            time.sleep(random.random())
        else:
            break

if __name__=='__main__':
    # 父進程創建Queue,並傳給各個子進程:
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    # 啓動子進程pw,寫入:
    pw.start()    
    # 等待pw結束:
    pw.join()
    # 啓動子進程pr,讀取:
    pr.start()
    pr.join()
    # pr進程裏是死循環,無法等待其結束,只能強行終止:
    print('')
    print('所有數據都寫入並且讀完')

回到頂部

進程池Pool

當需要創建的子進程數量不多時,可以直接利用multiprocessing中的Process動態成生多個進程,但如果是上百甚至上千個目標,手動的去創建進程的工作量巨大,此時就可以用到multiprocessing模塊提供的Pool方法。

初始化Pool時,可以指定一個最大進程數,當有新的請求提交到Pool中時,如果池還沒有滿,那麼就會創建一個新的進程用來執行該請求;但如果池中的進程數已經達到指定的最大值,那麼該請求就會等待,直到池中有進程結束,纔會用之前的進程來執行新的任務,請看下面的實例:

# -*- coding:utf-8 -*-
from multiprocessing import Pool
import os, time, random

def worker(msg):
    t_start = time.time()
    print("%s開始執行,進程號爲%d" % (msg,os.getpid()))
    # random.random()隨機生成0~1之間的浮點數
    time.sleep(random.random()*2) 
    t_stop = time.time()
    print(msg,"執行完畢,耗時%0.2f" % (t_stop-t_start))

po = Pool(3)  # 定義一個進程池,最大進程數3
for i in range(0,10):
    # Pool().apply_async(要調用的目標,(傳遞給目標的參數元祖,))
    # 每次循環將會用空閒出來的子進程去調用目標
    po.apply_async(worker,(i,))

print("----start----")
po.close()  # 關閉進程池,關閉後po不再接收新的請求
po.join()  # 等待po中所有子進程執行完成,必須放在close語句之後
print("-----end-----")

運行結果:

----start----
0開始執行,進程號爲21466
1開始執行,進程號爲21468
2開始執行,進程號爲21467
0 執行完畢,耗時1.01
3開始執行,進程號爲21466
2 執行完畢,耗時1.24
4開始執行,進程號爲21467
3 執行完畢,耗時0.56
5開始執行,進程號爲21466
1 執行完畢,耗時1.68
6開始執行,進程號爲21468
4 執行完畢,耗時0.67
7開始執行,進程號爲21467
5 執行完畢,耗時0.83
8開始執行,進程號爲21466
6 執行完畢,耗時0.75
9開始執行,進程號爲21468
7 執行完畢,耗時1.03
8 執行完畢,耗時1.05
9 執行完畢,耗時1.69
-----end-----

multiprocessing.Pool常用函數解析:

apply_async(func[, args[, kwds]]) :使用非阻塞方式調用func(並行執行,堵塞方式必須等待上一個進程退出才能執行下一個進程),args爲傳遞給func的參數列表,kwds爲傳遞給func的關鍵字參數列表;
close():關閉Pool,使其不再接受新的任務;
terminate():不管任務是否完成,立即終止;
join():主進程阻塞,等待子進程的退出, 必須在close或terminate之後使用;

進程池中的Queue
如果要使用Pool創建進程,就需要使用multiprocessing.Manager()中的Queue(),而不是multiprocessing.Queue(),否則會得到一條如下的錯誤信息:

RuntimeError: Queue objects should only be shared between processes through inheritance.

下面的實例演示了進程池中的進程如何通信:

# -*- coding:utf-8 -*-

# 修改import中的Queue爲Manager
from multiprocessing import Manager,Pool
import os,time,random

def reader(q):
    print("reader啓動(%s),父進程爲(%s)" % (os.getpid(), os.getppid()))
    for i in range(q.qsize()):
        print("reader從Queue獲取到消息:%s" % q.get(True))

def writer(q):
    print("writer啓動(%s),父進程爲(%s)" % (os.getpid(), os.getppid()))
    for i in "itcast":
        q.put(i)

if __name__=="__main__":
    print("(%s) start" % os.getpid())
    q = Manager().Queue()  # 使用Manager中的Queue
    po = Pool()
    po.apply_async(writer, (q,))

    time.sleep(1)  # 先讓上面的任務向Queue存入數據,然後再讓下面的任務開始從中取數據

    po.apply_async(reader, (q,))
    po.close()
    po.join()
    print("(%s) End" % os.getpid())

運行結果:

(11095) start
writer啓動(11097),父進程爲(11095)
reader啓動(11098),父進程爲(11095)
reader從Queue獲取到消息:i
reader從Queue獲取到消息:t
reader從Queue獲取到消息:c
reader從Queue獲取到消息:a
reader從Queue獲取到消息:s
reader從Queue獲取到消息:t
(11095) End

回到頂部

應用:文件夾copy器(多進程版)

import multiprocessing
import os
import time
import random


def copy_file(queue, file_name,source_folder_name,  dest_folder_name):
    """copy文件到指定的路徑"""
    f_read = open(source_folder_name + "/" + file_name, "rb")
    f_write = open(dest_folder_name + "/" + file_name, "wb")
    while True:
        time.sleep(random.random())
        content = f_read.read(1024)
        if content:
            f_write.write(content)
        else:
            break
    f_read.close()
    f_write.close()

    # 發送已經拷貝完畢的文件名字
    queue.put(file_name)


def main():
    # 獲取要複製的文件夾
    source_folder_name = input("請輸入要複製文件夾名字:")

    # 整理目標文件夾
    dest_folder_name = source_folder_name + "[副本]"

    # 創建目標文件夾
    try:
        os.mkdir(dest_folder_name)
    except:
        pass  # 如果文件夾已經存在,那麼創建會失敗

    # 獲取這個文件夾中所有的普通文件名
    file_names = os.listdir(source_folder_name)

    # 創建Queue
    queue = multiprocessing.Manager().Queue()

    # 創建進程池
    pool = multiprocessing.Pool(3)

    for file_name in file_names:
        # 向進程池中添加任務
        pool.apply_async(copy_file, args=(queue, file_name, source_folder_name, dest_folder_name))

    # 主進程顯示進度
    pool.close()

    all_file_num = len(file_names)
    while True:
        file_name = queue.get()
        if file_name in file_names:
            file_names.remove(file_name)

        copy_rate = (all_file_num-len(file_names))*100/all_file_num
        print("\r%.2f...(%s)" % (copy_rate, file_name) + " "*50, end="")
        if copy_rate >= 100:
            break
    print()

if name == “main”:
main()

回到頂部

迭代器

迭代是訪問集合元素的一種方式。迭代器是一個可以記住遍歷的位置的對象。迭代器對象從集合的第一個元素開始訪問,直到所有的元素被訪問完結束。迭代器只能往前不會後退。

1. 可迭代對象
我們已經知道可以對list、tuple、str等類型的數據使用for…in…的循環語法從其中依次拿到數據進行使用,我們把這樣的過程稱爲遍歷,也叫迭代。

但是,是否所有的數據類型都可以放到for…in…的語句中,然後讓for…in…每次從中取出一條數據供我們使用,即供我們迭代嗎?

>>> for i in 100:
...     print(i)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
>>>
# int整型不是iterable,即int整型不是可以迭代的

# 我們自定義一個容器MyList用來存放數據,可以通過add方法向其中添加數據
>>> class MyList(object):
...     def __init__(self):
...             self.container = []
...     def add(self, item):
...             self.container.append(item)
...
>>> mylist = MyList()
>>> mylist.add(1)
>>> mylist.add(2)
>>> mylist.add(3)
>>> for num in mylist:
...     print(num)
...
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'MyList' object is not iterable
>>>
# MyList容器的對象也是不能迭代的

我們自定義了一個容器類型MyList,在將一個存放了多個數據的MyList對象放到for…in…的語句中,發現for…in…並不能從中依次取出一條數據返回給我們,也就說我們隨便封裝了一個可以存放多條數據的類型卻並不能被迭代使用。

我們把可以通過for…in…這類語句迭代讀取一條數據供我們使用的對象稱之爲可迭代對象(Iterable)**。

2. 如何判斷一個對象是否可以迭代
可以使用 isinstance() 判斷一個對象是否是 Iterable 對象:

In [50]: from collections import Iterable

In [51]: isinstance([], Iterable)
Out[51]: True

In [52]: isinstance({}, Iterable)
Out[52]: True

In [53]: isinstance('abc', Iterable)
Out[53]: True

In [54]: isinstance(mylist, Iterable)
Out[54]: False

In [55]: isinstance(100, Iterable)
Out[55]: False

3. 可迭代對象的本質
我們分析對可迭代對象進行迭代使用的過程,發現每迭代一次(即在for…in…中每循環一次)都會返回對象中的下一條數據,一直向後讀取數據直到迭代了所有數據後結束。那麼,在這個過程中就應該有一個“人”去記錄每次訪問到了第幾條數據,以便每次迭代都可以返回下一條數據。我們把這個能幫助我們進行數據迭代的“人”稱爲迭代器(Iterator)。

可迭代對象的本質就是可以向我們提供一個這樣的中間“人”即迭代器幫助我們對其進行迭代遍歷使用。

可迭代對象通過__iter__方法向我們提供一個迭代器,我們在迭代一個可迭代對象的時候,實際上就是先獲取該對象提供的一個迭代器,然後通過這個迭代器來依次獲取對象中的每一個數據.

那麼也就是說,一個具備了__iter__方法的對象,就是一個可迭代對象。

>>> class MyList(object):
...     def __init__(self):
...             self.container = []
...     def add(self, item):
...             self.container.append(item)
...     def __iter__(self):
...             """返回一個迭代器"""
...             # 我們暫時忽略如何構造一個迭代器對象
...             pass
...
>>> mylist = MyList()
>>> from collections import Iterable
>>> isinstance(mylist, Iterable)
True
>>>
# 這回測試發現添加了__iter__方法的mylist對象已經是一個可迭代對象了

4. iter()函數與next()函數
list、tuple等都是可迭代對象,我們可以通過iter()函數獲取這些可迭代對象的迭代器。然後我們可以對獲取到的迭代器不斷使用next()函數來獲取下一條數據。iter()函數實際上就是調用了可迭代對象的__iter__方法。

>>> li = [11, 22, 33, 44, 55]
>>> li_iter = iter(li)
>>> next(li_iter)
11
>>> next(li_iter)
22
>>> next(li_iter)
33
>>> next(li_iter)
44
>>> next(li_iter)
55
>>> next(li_iter)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

注意,當我們已經迭代完最後一個數據之後,再次調用next()函數會拋出StopIteration的異常,來告訴我們所有數據都已迭代完成,不用再執行next()函數了。

5. 如何判斷一個對象是否是迭代器
可以使用 isinstance() 判斷一個對象是否是 Iterator 對象:

In [56]: from collections import Iterator

In [57]: isinstance([], Iterator)
Out[57]: False

In [58]: isinstance(iter([]), Iterator)
Out[58]: True

In [59]: isinstance(iter("abc"), Iterator)
Out[59]: True

6. 迭代器Iterator
通過上面的分析,我們已經知道,迭代器是用來幫助我們記錄每次迭代訪問到的位置,當我們對迭代器使用next()函數的時候,迭代器會向我們返回它所記錄位置的下一個位置的數據。實際上,在使用next()函數的時候,調用的就是迭代器對象的__next__方法(Python3中是對象的__next__方法,Python2中是對象的next()方法)。所以,我們要想構造一個迭代器,就要實現它的__next__方法。但這還不夠,python要求迭代器本身也是可迭代的,所以我們還要爲迭代器實現__iter__方法,而__iter__方法要返回一個迭代器,迭代器自身正是一個迭代器,所以迭代器的__iter__方法返回自身即可。

一個實現了__iter__方法和__next__方法的對象,就是迭代器。

class MyList(object):
    """自定義的一個可迭代對象"""
    def __init__(self):
        self.items = []

    def add(self, val):
        self.items.append(val)

    def __iter__(self):
        myiterator = MyIterator(self)
        return myiterator


class MyIterator(object):
    """自定義的供上面可迭代對象使用的一個迭代器"""
    def __init__(self, mylist):
        self.mylist = mylist
        # current用來記錄當前訪問到的位置
        self.current = 0

    def __next__(self):
        if self.current < len(self.mylist.items):
            item = self.mylist.items[self.current]
            self.current += 1
            return item
        else:
            raise StopIteration

    def __iter__(self):
        return self


if __name__ == '__main__':
    mylist = MyList()
    mylist.add(1)
    mylist.add(2)
    mylist.add(3)
    mylist.add(4)
    mylist.add(5)
    for num in mylist:
        print(num)

7. for…in…循環的本質
for item in Iterable 循環的本質就是先通過iter()函數獲取可迭代對象Iterable的迭代器,然後對獲取到的迭代器不斷調用next()方法來獲取下一個值並將其賦值給item,當遇到StopIteration的異常後循環結束。

8. 迭代器的應用場景
我們發現迭代器最核心的功能就是可以通過next()函數的調用來返回下一個數據值。如果每次返回的數據值不是在一個已有的數據集合中讀取的,而是通過程序按照一定的規律計算生成的,那麼也就意味着可以不用再依賴一個已有的數據集合,也就是說不用再將所有要迭代的數據都一次性緩存下來供後續依次讀取,這樣可以節省大量的存儲(內存)空間。

舉個例子,比如,數學中有個著名的斐波拉契數列(Fibonacci),數列中第一個數爲0,第二個數爲1,其後的每一個數都可由前兩個數相加得到:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …

現在我們想要通過for…in…循環來遍歷迭代斐波那契數列中的前n個數。那麼這個斐波那契數列我們就可以用迭代器來實現,每次迭代都通過數學計算來生成下一個數。

class FibIterator(object):
    """斐波那契數列迭代器"""
    def __init__(self, n):
        """
        :param n: int, 指明生成數列的前n個數
        """
        self.n = n
        # current用來保存當前生成到數列中的第幾個數了
        self.current = 0
        # num1用來保存前前一個數,初始值爲數列中的第一個數0
        self.num1 = 0
        # num2用來保存前一個數,初始值爲數列中的第二個數1
        self.num2 = 1

    def __next__(self):
        """被next()函數調用來獲取下一個數"""
        if self.current < self.n:
            num = self.num1
            self.num1, self.num2 = self.num2, self.num1+self.num2
            self.current += 1
            return num
        else:
            raise StopIteration

    def __iter__(self):
        """迭代器的__iter__返回自身即可"""
        return self


if __name__ == '__main__':
    fib = FibIterator(10)
    for num in fib:
        print(num, end=" ")

9. 並不是只有for循環能接收可迭代對象
除了for循環能接收可迭代對象,list、tuple等也能接收。

li = list(FibIterator(15))
print(li)
tp = tuple(FibIterator(6))
print(tp)

回到頂部

生成器

1. 生成器
利用迭代器,我們可以在每次迭代獲取數據(通過next()方法)時按照特定的規律進行生成。但是我們在實現一個迭代器時,關於當前迭代到的狀態需要我們自己記錄,進而才能根據當前狀態生成下一個數據。爲了達到記錄當前狀態,並配合next()函數進行迭代使用,我們可以採用更簡便的語法,即生成器(generator)。生成器是一類特殊的迭代器。

2. 創建生成器方法1
要創建一個生成器,有很多種方法。第一種方法很簡單,只要把一個列表生成式的 [ ] 改成 ( )

In [15]: L = [ x*2 for x in range(5)]

In [16]: L
Out[16]: [0, 2, 4, 6, 8]

In [17]: G = ( x*2 for x in range(5))

In [18]: G
Out[18]: <generator object <genexpr> at 0x7f626c132db0>

In [19]:

創建 L 和 G 的區別僅在於最外層的 [ ] 和 ( ) , L 是一個列表,而 G 是一個生成器。我們可以直接打印出列表L的每一個元素,而對於生成器G,我們可以按照迭代器的使用方法來使用,即可以通過next()函數、for循環、list()等方法使用。

In [19]: next(G)
Out[19]: 0

In [20]: next(G)
Out[20]: 2

In [21]: next(G)
Out[21]: 4

In [22]: next(G)
Out[22]: 6

In [23]: next(G)
Out[23]: 8

In [24]: next(G)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-24-380e167d6934> in <module>()
----> 1 next(G)

StopIteration:

In [25]:
In [26]: G = ( x*2 for x in range(5))

In [27]: for x in G:
   ....:     print(x)
   ....:     
0
2
4
6
8

In [28]:

3. 創建生成器方法2
generator非常強大。如果推算的算法比較複雜,用類似列表生成式的 for 循環無法實現的時候,還可以用函數來實現。

我們仍然用上一節提到的斐波那契數列來舉例,回想我們在上一節用迭代器的實現方式:

class FibIterator(object):
    """斐波那契數列迭代器"""
    def __init__(self, n):
        """
        :param n: int, 指明生成數列的前n個數
        """
        self.n = n
        # current用來保存當前生成到數列中的第幾個數了
        self.current = 0
        # num1用來保存前前一個數,初始值爲數列中的第一個數0
        self.num1 = 0
        # num2用來保存前一個數,初始值爲數列中的第二個數1
        self.num2 = 1

    def __next__(self):
        """被next()函數調用來獲取下一個數"""
        if self.current < self.n:
            num = self.num1
            self.num1, self.num2 = self.num2, self.num1+self.num2
            self.current += 1
            return num
        else:
            raise StopIteration

    def __iter__(self):
        """迭代器的__iter__返回自身即可"""
        return self

注意,在用迭代器實現的方式中,我們要藉助幾個變量(n、current、num1、num2)來保存迭代的狀態。現在我們用生成器來實現一下。

In [30]: def fib(n):
   ....:     current = 0
   ....:     num1, num2 = 0, 1
   ....:     while current < n:
   ....:         num = num1
   ....:         num1, num2 = num2, num1+num2
   ....:         current += 1
   ....:         yield num
   ....:     return 'done'
   ....:

In [31]: F = fib(5)

In [32]: next(F)
Out[32]: 1

In [33]: next(F)
Out[33]: 1

In [34]: next(F)
Out[34]: 2

In [35]: next(F)
Out[35]: 3

In [36]: next(F)
Out[36]: 5

In [37]: next(F)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-37-8c2b02b4361a> in <module>()
----> 1 next(F)

StopIteration: done

在使用生成器實現的方式中,我們將原本在迭代器__next__方法中實現的基本邏輯放到一個函數中來實現,但是將每次迭代返回數值的return換成了yield,此時新定義的函數便不再是函數,而是一個生成器了。簡單來說:只要在def中有yield關鍵字的 就稱爲 生成器

此時按照調用函數的方式( 案例中爲F = fib(5) )使用生成器就不再是執行函數體了,而是會返回一個生成器對象( 案例中爲F ),然後就可以按照使用迭代器的方式來使用生成器了。

In [38]: for n in fib(5):
   ....:     print(n)
   ....:     
1
1
2
3
5

In [39]:

但是用for循環調用generator時,發現拿不到generator的return語句的返回值。如果想要拿到返回值,必須捕獲StopIteration錯誤,返回值包含在StopIteration的value中:

In [39]: g = fib(5)

In [40]: while True:
   ....:     try:
   ....:         x = next(g)
   ....:         print("value:%d"%x)      
   ....:     except StopIteration as e:
   ....:         print("生成器返回值:%s"%e.value)
   ....:         break
   ....:     
value:1
value:1
value:2
value:3
value:5
生成器返回值:done

In [41]:

總結
使用了yield關鍵字的函數不再是函數,而是生成器。(使用了yield的函數就是生成器)
yield關鍵字有兩點作用:
保存當前運行狀態(斷點),然後暫停執行,即將生成器(函數)掛起
將yield關鍵字後面表達式的值作爲返回值返回,此時可以理解爲起到了return的作用
可以使用next()函數讓生成器從斷點處繼續執行,即喚醒生成器(函數)
Python3中的生成器可以使用return返回最終運行的返回值,而Python2中的生成器不允許使用return返回一個返回值(即可以使用return從生成器中退出,但return後不能有任何表達式)。
4. 使用send喚醒
我們除了可以使用next()函數來喚醒生成器繼續執行外,還可以使用send()函數來喚醒執行。使用send()函數的一個好處是可以在喚醒的同時向斷點處傳入一個附加數據。

例子:執行到yield時,gen函數作用暫時保存,返回i的值; temp接收下次c.send(“python”),send發送過來的值,c.next()等價c.send(None)

In [10]: def gen():
   ....:     i = 0
   ....:     while i<5:
   ....:         temp = yield i
   ....:         print(temp)
   ....:         i+=1
   ....:

使用send


In [43]: f = gen()

In [44]: next(f)
Out[44]: 0

In [45]: f.send('haha')
haha
Out[45]: 1

In [46]: next(f)
None
Out[46]: 2

In [47]: f.send('haha')
haha
Out[47]: 3

In [48]:


使用next函數


In [11]: f = gen()

In [12]: next(f)
Out[12]: 0

In [13]: next(f)
None
Out[13]: 1

In [14]: next(f)
None
Out[14]: 2

In [15]: next(f)
None
Out[15]: 3

In [16]: next(f)
None
Out[16]: 4

In [17]: next(f)
None
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-17-468f0afdf1b9> in <module>()
----> 1 next(f)

StopIteration:

使用__next__()方法(不常使用)

In [18]: f = gen()

In [19]: f.__next__()
Out[19]: 0

In [20]: f.__next__()
None
Out[20]: 1

In [21]: f.__next__()
None
Out[21]: 2

In [22]: f.__next__()
None
Out[22]: 3

In [23]: f.__next__()
None
Out[23]: 4

In [24]: f.__next__()
None
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-24-39ec527346a9> in <module>()
----> 1 f.__next__()

StopIteration:

回到頂部

協程-yield

協程,又稱微線程,纖程。英文名Coroutine。

協程是啥
協程是python箇中另外一種實現多任務的方式,只不過比線程更小佔用更小執行單元(理解爲需要的資源)。 爲啥說它是一個執行單元,因爲它自帶CPU上下文。這樣只要在合適的時機, 我們可以把一個協程 切換到另一個協程。 只要這個過程中保存或恢復 CPU上下文那麼程序還是可以運行的。

通俗的理解:在一個線程中的某個函數,可以在任何地方保存當前函數的一些臨時變量等信息,然後切換到另外一個函數中執行,注意不是通過調用函數的方式做到的,並且切換的次數以及什麼時候再切換到原來的函數都由開發者自己確定

協程和線程差異
在實現多任務時, 線程切換從系統層面遠不止保存和恢復 CPU上下文這麼簡單。 操作系統爲了程序運行的高效性每個線程都有自己緩存Cache等等數據,操作系統還會幫你做這些數據的恢復操作。 所以線程的切換非常耗性能。但是協程的切換隻是單純的操作CPU的上下文,所以一秒鐘切換個上百萬次系統都抗的住。

簡單實現協程

import time

def work1():
    while True:
        print("----work1---")
        yield
        time.sleep(0.5)

def work2():
    while True:
        print("----work2---")
        yield
        time.sleep(0.5)

def main():
    w1 = work1()
    w2 = work2()
    while True:
        next(w1)
        next(w2)

if __name__ == "__main__":
    main()

運行結果:

----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
----work1---
----work2---
...省略...

回到頂部

協程-greenlet

爲了更好使用協程來完成多任務,python中的greenlet模塊對其封裝,從而使得切換任務變的更加簡單

安裝方式
使用如下命令安裝greenlet模塊:

sudo pip3 install greenlet
#coding=utf-8

from greenlet import greenlet
import time

def test1():
    while True:
        print "---A--"
        gr2.switch()
        time.sleep(0.5)

def test2():
    while True:
        print "---B--"
        gr1.switch()
        time.sleep(0.5)

gr1 = greenlet(test1)
gr2 = greenlet(test2)

#切換到gr1中運行
gr1.switch()

運行效果

---A--
---B--
---A--
---B--
---A--
---B--
---A--
---B--
...省略...

回到頂部

協程-gevent

greenlet已經實現了協程,但是這個還的人工切換,是不是覺得太麻煩了,不要捉急,python還有一個比greenlet更強大的並且能夠自動切換任務的模塊gevent

其原理是當一個greenlet遇到IO(指的是input output 輸入輸出,比如網絡、文件操作等)操作時,比如訪問網絡,就自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。

由於IO操作非常耗時,經常使程序處於等待狀態,有了gevent爲我們自動切換協程,就保證總有greenlet在運行,而不是等待IO

安裝

pip3 install gevent

1. gevent的使用

import gevent

def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

運行結果

<Greenlet at 0x10e49f550: f(5)> 0
<Greenlet at 0x10e49f550: f(5)> 1
<Greenlet at 0x10e49f550: f(5)> 2
<Greenlet at 0x10e49f550: f(5)> 3
<Greenlet at 0x10e49f550: f(5)> 4
<Greenlet at 0x10e49f910: f(5)> 0
<Greenlet at 0x10e49f910: f(5)> 1
<Greenlet at 0x10e49f910: f(5)> 2
<Greenlet at 0x10e49f910: f(5)> 3
<Greenlet at 0x10e49f910: f(5)> 4
<Greenlet at 0x10e49f4b0: f(5)> 0
<Greenlet at 0x10e49f4b0: f(5)> 1
<Greenlet at 0x10e49f4b0: f(5)> 2
<Greenlet at 0x10e49f4b0: f(5)> 3
<Greenlet at 0x10e49f4b0: f(5)> 4

可以看到,3個greenlet是依次運行而不是交替運行

2. gevent切換執行

import gevent

def f(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        #用來模擬一個耗時操作,注意不是time模塊中的sleep
        gevent.sleep(1)

g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()

運行結果

<Greenlet at 0x7fa70ffa1c30: f(5)> 0
<Greenlet at 0x7fa70ffa1870: f(5)> 0
<Greenlet at 0x7fa70ffa1eb0: f(5)> 0
<Greenlet at 0x7fa70ffa1c30: f(5)> 1
<Greenlet at 0x7fa70ffa1870: f(5)> 1
<Greenlet at 0x7fa70ffa1eb0: f(5)> 1
<Greenlet at 0x7fa70ffa1c30: f(5)> 2
<Greenlet at 0x7fa70ffa1870: f(5)> 2
<Greenlet at 0x7fa70ffa1eb0: f(5)> 2
<Greenlet at 0x7fa70ffa1c30: f(5)> 3
<Greenlet at 0x7fa70ffa1870: f(5)> 3
<Greenlet at 0x7fa70ffa1eb0: f(5)> 3
<Greenlet at 0x7fa70ffa1c30: f(5)> 4
<Greenlet at 0x7fa70ffa1870: f(5)> 4
<Greenlet at 0x7fa70ffa1eb0: f(5)> 4

3. 給程序打補丁

from gevent import monkey
import gevent
import random
import time

def coroutine_work(coroutine_name):
    for i in range(10):
        print(coroutine_name, i)
        time.sleep(random.random())

gevent.joinall([
        gevent.spawn(coroutine_work, "work1"),
        gevent.spawn(coroutine_work, "work2")
])

運行結果

work1 0
work1 1
work1 2
work1 3
work1 4
work1 5
work1 6
work1 7
work1 8
work1 9
work2 0
work2 1
work2 2
work2 3
work2 4
work2 5
work2 6
work2 7
work2 8
work2 9
from gevent import monkey
import gevent
import random
import time

# 有耗時操作時需要
monkey.patch_all()  # 將程序中用到的耗時操作的代碼,換爲gevent中自己實現的模塊

def coroutine_work(coroutine_name):
    for i in range(10):
        print(coroutine_name, i)
        time.sleep(random.random())

gevent.joinall([
        gevent.spawn(coroutine_work, "work1"),
        gevent.spawn(coroutine_work, "work2")
])

運行結果

work1 0
work2 0
work1 1
work1 2
work1 3
work2 1
work1 4
work2 2
work1 5
work2 3
work1 6
work1 7
work1 8
work2 4
work2 5
work1 9
work2 6
work2 7
work2 8
work2 9

回到頂部

進程、線程、協程對比

請仔細理解如下的通俗描述
有一個老闆想要開個工廠進行生產某件商品(例如剪子)
他需要花一些財力物力製作一條生產線,這個生產線上有很多的器件以及材料這些所有的 爲了能夠生產剪子而準備的資源稱之爲:進程
只有生產線是不能夠進行生產的,所以老闆的找個工人來進行生產,這個工人能夠利用這些材料最終一步步的將剪子做出來,這個來做事情的工人稱之爲:線程
這個老闆爲了提高生產率,想到3種辦法:
1.在這條生產線上多招些工人,一起來做剪子,這樣效率是成倍増長,即單進程 多線程方式
2.老闆發現這條生產線上的工人不是越多越好,因爲一條生產線的資源以及材料畢竟有限,所以老闆又花了些財力物力購置了另外一條生產線,然後再招些工人這樣效率又再一步提高了,即多進程 多線程方式
3.老闆發現,現在已經有了很多條生產線,並且每條生產線上已經有很多工人了(即程序是多進程的,每個進程中又有多個線程),爲了再次提高效率,老闆想了個損招,規定:如果某個員工在上班時臨時沒事或者再等待某些條件(比如等待另一個工人生產完謀道工序 之後他才能再次工作) ,那麼這個員工就利用這個時間去做其它的事情,那麼也就是說:如果一個線程等待某些條件,可以充分利用這個時間去做其它事情,其實這就是:協程方式

簡單總結

1.進程是資源分配的單位
2.線程是操作系統調度的單位
3.進程切換需要的資源很最大,效率很低
4.線程切換需要的資源一般,效率一般(當然了在不考慮GIL的情況下)
5.協程切換任務資源很小,效率高
6.多進程、多線程根據cpu核數不一樣可能是並行的,但是協程是在一個線程中 所以是併發

回到頂部

併發下載器

併發下載原理

from gevent import monkey
import gevent
import urllib.request

# 有耗時操作時需要
monkey.patch_all()

def my_downLoad(url):
    print('GET: %s' % url)
    resp = urllib.request.urlopen(url)
    data = resp.read()
    print('%d bytes received from %s.' % (len(data), url))

gevent.joinall([
        gevent.spawn(my_downLoad, 'http://www.baidu.com/'),
        gevent.spawn(my_downLoad, 'http://www.itcast.cn/'),
        gevent.spawn(my_downLoad, 'http://www.itheima.com/'),
])

運行結果

GET: http://www.baidu.com/
GET: http://www.itcast.cn/
GET: http://www.itheima.com/
111327 bytes received from http://www.baidu.com/.
172054 bytes received from http://www.itheima.com/.
215035 bytes received from http://www.itcast.cn/.

從上能夠看到是先發送的獲取baidu的相關信息,然後依次是itcast、itheima,但是收到數據的先後順序不一定與發送順序相同,這也就體現出了異步,即不確定什麼時候會收到數據,順序不一定

實現多個視頻下載

from gevent import monkey
import gevent
import urllib.request

#有IO才做時需要這一句
monkey.patch_all()

def my_downLoad(file_name, url):
    print('GET: %s' % url)
    resp = urllib.request.urlopen(url)
    data = resp.read()

    with open(file_name, "wb") as f:
        f.write(data)

    print('%d bytes received from %s.' % (len(data), url))

gevent.joinall([
        gevent.spawn(my_downLoad, "1.mp4", 'http://oo52bgdsl.bkt.clouddn.com/05day-08-%E3%80%90%E7%90%86%E8%A7%A3%E3%80%91%E5%87%BD%E6%95%B0%E4%BD%BF%E7%94%A8%E6%80%BB%E7%BB%93%EF%BC%88%E4%B8%80%EF%BC%89.mp4'),
        gevent.spawn(my_downLoad, "2.mp4", 'http://oo52bgdsl.bkt.clouddn.com/05day-03-%E3%80%90%E6%8E%8C%E6%8F%A1%E3%80%91%E6%97%A0%E5%8F%82%E6%95%B0%E6%97%A0%E8%BF%94%E5%9B%9E%E5%80%BC%E5%87%BD%E6%95%B0%E7%9A%84%E5%AE%9A%E4%B9%89%E3%80%81%E8%B0%83%E7%94%A8%28%E4%B8%8B%29.mp4'),
])

上面的url可以換爲自己需要下載視頻、音樂、圖片等網址

回到頂部

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章