1. 程式人生 > 其它 >Python-多工總結

Python-多工總結

技術標籤:Python高階程式設計技巧python多執行緒

文章目錄

Python-多工

多工

什麼是多工

  • 多工就是可以同時做多件事情就叫多工

多工理解

  • 併發:CPU小於當前的執行的任務,是假的多執行緒
  • 並行:CPU大於當前的執行的任務,是真的多執行緒

執行緒

執行緒介紹

  • 執行緒是作業系統能夠進行運算排程的最小單位,它被包含在程序之中,是程序中的實際運作單位
  • 匯入:import threading

執行緒方法

  • start: 啟動執行緒
    • 該執行緒創建出來為主執行緒
    • 使用start會等子執行緒執行結束,然後主執行緒才結束
  • setDaemon : 守護執行緒
    • 不會等子執行緒執行結束
    • 當主執行緒結束就直接結束
  • join: 等待子執行緒執行結束
    • 放在start後使用
    • 當任務中存在多執行緒呼叫時,會先等子執行緒執行結束在去執行另外一個子執行緒
  • threading.enumerate : 檢視建立的執行緒
    • 注意:主執行緒也算在內
    • 是在start之後創建出來的子執行緒

使用執行緒完成多工

# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date  : 2020/12/28

"""
總結如下:
執行緒是在start執行之後建立
start : 啟動執行緒
target : 目標
setDaemon : 守護執行緒
join : 執行緒等待
"""
import threading
import time


def sing():
    for i in range(5):
        print('我正在唱歌')
        time.
sleep(1) def dance(): # 子執行緒 for i in range(5): print('我正在跳舞') time.sleep(1) # def demo(): # print('Hello world') if __name__ == '__main__': # target: 目標,呼叫的物件, th = threading.Thread(target=sing) th1 = threading.Thread(target=dance) # print(threading.enumerate()) # setDaemon: 守護執行緒,不會等子執行緒執行結束 # th.setDaemon(True) # th1.setDaemon(True) # 啟動執行緒 th.start() # 主執行緒,主執行緒會等到子執行緒執行結束 th1.start() print(threading.enumerate()) # 等待子執行緒執行結束 # th.join() # th1.join() # 檢視執行緒數量 # 為什麼結果卻為3個呢 # 因為還有一個主執行緒在內,存在兩個子執行緒 print(threading.enumerate())

類的方式建立執行緒

  • 當想用類的方式建立的時候,需要在類中即成thread.Thread(看匯入的情況而定)
  • 如果想進行傳參的話,需要使用到super函式來而定
  • 當想在類中使用多執行緒,要在類中的run方法中實現
  • 通過start來啟動多執行緒
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date  : 2020/12/28

import threading
import time


class Animal(threading.Thread):
    def run(self) -> None:
        print('this is animal')
        time.sleep(1)


if __name__ == '__main__':
    for i in range(5):
        r = Animal()
        r.start()

    print(threading.enumerate())

多執行緒共享全域性變數

  • 執行緒之間可以共享全域性變數,但是會出現計算出錯。可通過鎖來解決這麼一系列問題
  • 互斥鎖保證了每次只有一個執行緒進行寫入操作,從而保證了多執行緒情況下資料的正確性
  • 多執行緒之間傳參可通過args來進行
  • mutex = threading.Lock() : 建立鎖
  • mutex.acquire() : 上鎖操作
  • mutex.release() : 解鎖操作
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date  : 2020/12/29


import threading
import time
import dis
num = 100

"""
首先要知道,Python中的多執行緒並非真正的多執行緒,
Python中的執行緒也並非採用單個執行來執行多執行緒,
而Python中的多執行緒採用的是時間,輪轉來執行,什麼意思呢?
意思就是當第一個執行緒執行的差不多了,然後把一個執行緒扔出去,讓第一個執行緒等著,
在把第二個執行緒繼續執行,執行的差不多的時候,在把第二個執行緒扔出去,讓第二個執行緒等著,
所以這就出現了執行緒之間的共享全域性變數的一個問題,如何解決這個問題呢。
可以通過鎖來解決這麼一個問題的出現
"""

import threading

# 建立鎖
mutex = threading.Lock()


def demo(nums):
    global num
    # 上鎖操作
    mutex.acquire()
    for i in range(nums):
        num += 1
    # 解鎖操作
    mutex.release()
    print(f'demo--{num}')


def demo1(nums):
    global num
    # 上鎖操作
    mutex.acquire()
    for i in range(nums):
        num += 1
    mutex.release()
    print(f'demo1--{num}')


if __name__ == '__main__':
    # 多執行緒之間傳參,可以通過args
    # 傳入多引數要以元祖的形式
    t = threading.Thread(target=demo, args=(1000000, ))
    t1 = threading.Thread(target=demo1, args=(1000000,))

    t.start()
    t1.start()
    time.sleep(2)
    print(f'main--{num}')

# def funt(a):
#     a += 1
#     print(a)
#
# print(dis.dis(funt))

執行緒同步

  • 執行緒同步我們需要使用threading.Condition()完成執行緒同步
# 執行緒同步
cond = threading.Condition()

# 等待
cond.wait()

# 喚醒
cond.notify()

實現效果:

  • 天貓精靈:小愛同學
  • 小愛同學:在
  • 天貓精靈:現在幾點了?
  • 小愛同學:你猜猜現在幾點了
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date  : 2020/12/29

import threading

"""
天貓精靈:小愛同學
小愛同學:在
天貓精靈:現在幾點了?
小愛同學:你猜猜現在幾點了
"""

# 執行緒同步
cond = threading.Condition()


class Tmall(threading.Thread):
    def __init__(self, name):
        super(Tmall, self).__init__(name=name)

    def run(self) -> None:
        cond.acquire()  # 上鎖操作

        print(f'{self.name} : 小愛同學')
        cond.wait()
        print(f'{self.name} : 現在幾點了?')
        cond.notify()
        # 解鎖操作
        cond.release()


class love(threading.Thread):
    def __init__(self, name):
        super(love, self).__init__(name=name)

    def run(self) -> None:
        cond.acquire()
        print(f'{self.name} : 在')
        cond.notify()
        cond.wait()
        print(f'{self.name} : 你猜猜現在幾點了')

        cond.release()


if __name__ == '__main__':

    tianmao = Tmall(name='天貓精靈')
    xiaoai = love(name='小愛同學')

    tianmao.start()
    xiaoai.start()


程序

程序定義

  • 程序是計算機中的程式關於某資料集合上的一次運動,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎。並且程序是執行緒的容器。

程序概念

  • 程序是一個實體。每一個程序都有它自己的地址空間。
  • 程序是一個執行中的程式
  • 程序是作業系統中最基本、重要的概念
  • 任務排程和執行的基本單位
  • 多個程序同時執行的順序是隨機的

程序與程式區別

  • 程序:正在執行的程式。動態的,暫時的
  • 程式:沒有執行的程式碼,是一個靜態的,永久的

程序方法

  • 通過multiprocessing.Process模組
  • group : 引數未使用,預設值為None
  • target : 表示呼叫物件,即子執行緒要執行的任務
  • args : 表示呼叫的位置引數元祖
  • kwargs : 表示呼叫物件的字典
  • name : 子程序名稱

使用程序完成多工

  • 注意: 多執行緒同時執行的順序是隨機的
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date  : 2020/12/31

"""
總結如下:
什麼是多執行緒?
多執行緒就是相當於就是作業系統執行多個任務,
程序是執行緒的容器,一個程序最少要有一個執行緒
程序是正在執行的程式,動態的
"""

# 匯入程序
import multiprocessing
# 當想匯入程序佇列時,需要從multiprocessing匯入
# 安全佇列
from multiprocessing import queues
import time


def test():
    while True:
        print('1')
        time.sleep(1)


def test1():
    while True:
        print('2')
        time.sleep(1)


def test2():
    while True:
        print('3')
        time.sleep(1)


if __name__ == '__main__':
    # Process : 建立程序的方法
    t = multiprocessing.Process(target=test)
    t1 = multiprocessing.Process(target=test1)
    t2 = multiprocessing.Process(target=test2)

    # 啟動多執行緒
    t.start()
    t1.start()
    t2.start()

    print(4)

通過繼承Process類建立程序

class Demo(multiprocessing.Process):
    def run(self):
        while True:
            print("--1--")
            time.sleep(1)
            
            
if __name__ == '__main__':
    p1 = Demo()
    p1.start()

多程序共享全域性變數

  • 注意⚠️: 多程序之間是不能進行共享全域性變數的,但想通過共享全域性變數,只能通過多程序的queue佇列(注意:這裡說的是多程序的佇列,而不是普通的佇列)
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date  : 2021/1/5

import multiprocessing
import time

"""
總結如下:
多程序之間是不能進行共享全域性變數的
"""
num = 100


def num1():
    global num
    num += 100
    print(f'num1 for {num}')    # 200


def num2():
    print(f'num2 for {num}')    # 100


if __name__ == '__main__':
    m1 = multiprocessing.Process(target=num1)
    m2 = multiprocessing.Process(target=num2)

    # 啟動多程序
    m1.start()
    m2.start()

多程序共享全域性變數+多程序佇列

  • 使用多程序佇列可以解決這麼個問題
  • q = multiprocessing.Queue() : 建立程序佇列
  • q.get() : 從佇列取值
  • q.put(): 將一個數據存入佇列
  • q.qsize() : 返回佇列的大小
  • q.empty() : 判斷佇列是否為空
  • q.full() : 判斷佇列是否滿了
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date  : 2021/1/5

import multiprocessing
import time

"""
總結如下:
多程序之間是不能進行共享全域性變數的
當想進行共享全域性變數
"""
num = 100


def num1(q):
    global num
    num += 100
    q.put(num)  # 200
    print(f'num1 for {num}')    # 200


def num2(q):
    # print(f'num2 for {num}')    # 100
    data = q.get()  # 200
    print(f'num2 for {data}')


if __name__ == '__main__':
    q = multiprocessing.Queue()
    m1 = multiprocessing.Process(target=num1, args=(q, ))
    m2 = multiprocessing.Process(target=num2, args=(q, ))

    # 啟動多程序
    m1.start()
    m2.start()

程序池

  • 當需要建立的子程序數量不多時,可以直接利用multiprocessing中的Process動態生成多個程序,但是如果是上百甚至上千個目標,手動的去建立的程序的工作量巨大,此時就可以用到multiprocessing模組提供的Pool方法,也就是程序池。
  • apply_asyn : 非阻塞,非同步形式
  • close : 關閉程序池,使其不在接受新的任務
  • join : 主程序阻塞,等待子程序的退出,join方法要在close後使用
  • 程序池之間通訊使用的是:q = multiprocessing.Manager().Queue()
# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date  : 2021/1/5


import multiprocessing


def demo1(q):
    q.put("a")


def demo2(q):
    data = q.get("a")
    print(data)


if __name__ == '__main__':
    # 當使用程序池的時候並不能使用程序使用的佇列
    # 需要使用到程序池的佇列
    q = multiprocessing.Manager().Queue()

    # 建立程序池
    po = multiprocessing.Pool(2)

    # 非同步啟動,非阻塞
    po.apply_async(demo1, args=(q, ))
    po.apply_async(demo2, args=(q, ))

    # 同步啟動,阻塞
    # po.apply()

    # 關閉程序池
    po.close()

    # 等待子程序池結束
    # 呼叫join之前,先呼叫close函式,否則會出錯
    # 執行完close後不會有新的程序加入到pool,join函式等待所有子程序結束
    po.join()

多工資料夾複製

# -*- coding: utf-8 -*-
# @Author: Small-J
# @Date  : 2021/1/5

"""
獲取使用者要複製的資料夾名字
建立一個新的資料夾
獲取資料夾所有待拷貝的檔名字
建立程序池
新增拷貝任務
"""

import multiprocessing
import os
import time


def copy_file(old_folder_name, new_folder_name, file_name, q):
    """複製檔案內容"""
    with open(old_folder_name + '/' + file_name, 'rb')as f:
        content = f.read()

    with open(new_folder_name + '/' + file_name, 'wb')as f1:
        f1.write(content)


def main():
    # 獲取使用者要複製的資料夾名字
    old_folder_name = input('請輸入你要建立的資料夾名字:')

    # 建立一個新的資料夾
    new_folder_name = old_folder_name + '[附件]'

    if not os.path.exists(new_folder_name):
        os.mkdir(new_folder_name)

    # 獲取資料夾中所有的內容名字
    file_name_list = os.listdir(old_folder_name)

    # 建立程序池佇列
    q = multiprocessing.Manager().Queue()

    # 建立程序池
    po = multiprocessing.Pool(2)

    # 新增拷貝任務
    for file_name in file_name_list:
        po.apply_async(copy_file, args=(old_folder_name, new_folder_name, file_name, q))

    # 關閉程序池
    po.close()

    # 等待子程序執行結束
    po.join()

    all_file_num = len(file_name_list)
    copy_ok_num = 1
    while True:
        copy_ok_num += 1
        time.sleep(1)
        print("\r複製的進度為:%.2f %%" % (copy_ok_num * 100 / all_file_num), end='')

        if copy_ok_num >= all_file_num:
            break


if __name__ == '__main__':
    main()

總結如下:

  • 也存在著執行緒池和程序池
  • IO密集型適用於多執行緒
  • 計算密集型適用多程序
  • 多執行緒無法利用多核CPU發揮優勢,會輪流使用CPU來發揮優勢
  • 多程序是利用多核CPU發揮優勢

程序與執行緒區別

  • 根本區別
    • 程序: 作業系統資源分配的基本單位
    • 執行緒: 任務呼叫和執行的基本單位
  • 開銷
    • 程序: 通過複製程式碼+資源建立子程序,每個程序都有獨立的程式碼和資料空間,程式之間的切換會有較大的開銷
    • 執行緒: 在同一份程式碼裡,建立執行緒,共享記憶體,開銷較小。
  • 分配記憶體
    • 程序:系統在執行的時候為每個程序分配不同的記憶體空間
    • 執行緒: 執行緒所使用的資源是它所屬的程序的資源
  • 包含關係
    • 程序: 一個程序可以擁有多個執行緒
    • 執行緒: 執行緒是程序的一部分

協程

  • 協程,又稱為微執行緒, 它是實現多工的另一種方式,只不過是比現場更小的執行單位。因為它自帶CPU的上下文,這樣只要在合適的時機,我們就可以把一個協程切換到另一個協程。

協程與執行緒差異

  • 執行緒:每個執行緒都有自己換成Cache等等資料,作業系統還會做這些資料的恢復操作,所以執行緒的切換非常消耗效能
  • 協程:單純的操作CPU的上下文,所以一秒切換上百萬次系統都能扛住。說喲完成多工效率比執行緒和程序都高
# 使用yield來實現協程
import time


def task():
    while True:
        print('--1--')
        time.sleep(1)
        yield 'task'


def task1():
    while True:
        print('--2--')
        time.sleep(1)
        yield 'task2'


def main():
    t1 = task()
    t2 = task1()

    # print(t2)     # 物件地址值
    # print(t1)

    # next(t1)
    # next(t2)
    while True:
        next(t1)
        next(t2)


if __name__ == '__main__':
    main()

greenlet的使用

  • greenlet也是實現協程的一種方式,但是它實現協程並不會手動的去切換,還需要手動的實現。
  • 安裝:pip install greenlet
# -*- coding: utf-8 -*-
# Author : Small-J
# 2021/1/8 11:44 上午

from greenlet import greenlet
import time


def demo1():
    while True:
        print("demo1")
        gr2.switch()
        time.sleep(0.5)


def demo2():
    while True:
        print("demo2")
        # 通過switch到其他協程地方,該協程會被掛起
        gr1.switch()
        time.sleep(0.5)


if __name__ == '__main__':
    gr1 = greenlet(demo1)
    gr2 = greenlet(demo2)

    gr1.switch()

gevent的使用

  • 比greenlet更強大的並且能夠主動切換任務的模組gevent
  • 安裝: pip install gevent
  • 原理:當一個greenlet遇到IO操作時,比如訪問網路,就主動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行
  • 由於IO操作非常耗時,經常使程式處於等待狀態,有了genvet為我們自動切換協程,就保證總有greenlet在執行
  • 在爬取圖片等操作的時候適用於領域
# -*- coding: utf-8 -*-
# Author : Small-J
# 2021/1/8 11:55 上午

import gevent
import time

# 破解time.sleep延遲補丁,除了time,各種延遲都適配
from gevent import monkey

# 補丁
monkey.patch_all()
"""
總結如下:
如果使用time.sleep就會變成同步的形式去執行,先是f1執行完之後在執行
    但如果想time.sleep被識別的話,就需要匯入monkey模組
    
    
需要使用gevent.sleep來實現,為什麼需要呢?
    因為需要一種延遲的效果來展示,如網路請求延遲也可以
"""


def f1(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        # gevent.sleep(0.5)
        time.sleep(0.5)


def f2(n):
    for i in range(n):
        print(gevent.getcurrent(), i)
        # gevent.sleep(0.5)
        time.sleep(0.5)


if __name__ == '__main__':
    # 建立協程
    # g1 = gevent.spawn(f1, 5)
    # g2 = gevent.spawn(f2, 5)
    # g1.join()
    # g2.join()

    # 簡化上面的操作
    # greenlets, timeout=None, raise_error=False, count=None
    gevent.joinall([
        gevent.spawn(f1, 5),
        gevent.spawn(f2, 5)
    ])

最後總結

  • 程序是資源分配的單位
  • 執行緒是作業系統排程的單位
  • 程序切換需要的資源最大,效率很低
  • 執行緒切換需要的資源一般,效率一般
  • 協程切換任務資源很小,效率高
  • 多執行緒、多程序根據CPU核數不一樣可能是並行的,但是協程是在一個執行緒中,所以是併發