執行緒、程式與協程

貓xian森發表於2017-03-16

眾所周知, 計算機是由軟體和硬體組成. 硬體中的CPU主要用於解釋指令和處理資料, 軟體中的作業系統負責資源的管理和分配以及任務的排程. 而程式則是執行在作業系統上具有特定功能的軟體. 每當程式執行完成特定功能的時候, 為了保證程式的獨立執行不受影響往往需要程式控制塊(專門管理和控制程式執行的資料結構)的作用.
說了以上這麼多基本理論知識, 接下來我們談談程式. 程式本質上就是一個程式在一個資料集上的動態執行過程. 程式通常由程式, 資料集和程式控制塊三部分組成.

  • 程式: 描述程式需要完成的功能以及如何去完成
  • 資料集: 程式執行過程中需要使用的資源(包括IO資源和基本資料)
  • 程式控制塊: 記錄程式的外部特徵以及描述其執行過程. 作業系統正是通過它來控制和管理程式

而執行緒在現在的多處理器電子裝置中是最小的處理單元. 一個程式可以有多個執行緒, 這些執行緒之間彼此共享該程式的資源. 但是程式之間預設是相互獨立的, 若資料共享則需要另外特定的操作. 這裡做一個比喻. 現在有一個大型工廠, 該工廠負責生產汽車. 同時這個工廠又有多個車間, 每個車間負責不同的功能, 有的生產輪胎, 有的生產方向盤等等. 每個車間又有多個車間工人, 這些工人相互合作, 彼此共享資源來共同生產輪胎方向盤等等. 這裡的工廠就相當於一個應用程式, 而每個車間相當於一個程式, 每個車間工人就相當於執行緒.

普通多執行緒建立使用

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import threading
import time

def showThreading(arg):
    time.sleep(1)
    print("current thread is : ",arg)

if __name__ == '__main__':
    for tmp in range(10):
        t=threading.Thread(target=showThreading,args=(tmp,))
        t.start()
    print('main thread has been stopped')複製程式碼

執行結果如下:

執行緒、程式與協程
簡單多執行緒執行結果

  • 由輸出結果可知, 子執行緒之間是併發執行的, 而且在阻塞1秒的時間內主執行緒也執行完畢
  • 當主執行緒執行完畢, 子執行緒還能繼續執行是因為當前的t.setDaemon(False)預設為false. 為false表明當前執行緒為前臺執行緒, 主執行緒執行完畢後仍需等待前臺執行緒執行完畢之後方能結束當前程式; 為true表明當前執行緒為後臺執行緒, 主執行緒執行完畢後則當前程式結束, 不關注後臺執行緒是否執行完畢

執行緒、程式與協程
Daemon為True時的執行結果

  • t=threading.Thread(target=showThreading,args=(tmp,)) 這一句建立一個執行緒, target=表明執行緒所執行的函式, args= 表明函式的引數
  • t.start() 執行緒準備完畢等待cpu排程處理, 當執行緒被cpu排程後會自動執行執行緒物件的run方法(自定義執行緒類時候可用)
  • t.setName(string) 為當前執行緒設定名字
  • t.getName() 獲取當前執行緒的名字
  • t.join() 該方法表示主執行緒必須在此位置等待子執行緒執行完畢後才能繼續執行主執行緒後面的程式碼, 當且僅當setDaemon為false時有效

自定義執行緒類

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import threading,time

class MyThread(threading.Thread):
    def __init__(self,target,arg=()):
        super(MyThread, self).__init__()
        self.target=target
        self.arg=arg

    def run(self):
        self.target(self.arg)

def test(arg):
    time.sleep(1)
    print("current thread is : ",arg)

if __name__ == '__main__':
    for tmp in range(10):
        mt=MyThread(target=test,arg=(tmp,))
        mt.start()
    print("main thread has been stopped")複製程式碼
  • class MyThread(threading.Thread): 自定義執行緒類需要繼承threading.Thread
  • super(MyThread, self).__init__() 自定義執行緒類初始化時候需將當前物件傳遞給父類並執行父類的初始化方法
  • run(self) 執行緒啟動之後會執行該方法

由於CPU對執行緒是隨機排程執行, 並且往往會在當前執行緒執行一小段程式碼之後便直接換為其他執行緒執行, 如此往復迴圈直到所有的執行緒執行結束. 因此在一個共享資源和資料的程式中, 多個執行緒對同一資源操或者同一資料操作容易造成資源搶奪和產生髒資料. 此時我們引入鎖的概念, 對這種資源和資料進行加鎖, 直到當前執行緒操作完畢再釋放鎖讓其他執行緒操作.

我們先看看不加鎖時候對資料的操作情況:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import threading, time

NUM = 0


def add():
    global NUM
    NUM += 1
    name=t.getName()
    time.sleep(1)
    print('current thread is: ',name ,' current NUM is: ',NUM )


if __name__ == '__main__':
    for tmp in range(10):
        t=threading.Thread(target=add)
        t.start()
    print("main thread has been stopped !")複製程式碼

執行緒、程式與協程
不加鎖執行結果

  • 從圖中可知資料已經不是我們期望的結果, 此時輸出的是10個執行緒對該資料操作完的結果, 我們期望的是輸出每個執行緒對該資料操作後的結果. 顯然程式碼的執行順序並不是一個執行緒一個執行緒依次執行, 而是彼此穿插交錯執行
  • 注意time.sleep(1) 這一句讓執行緒阻塞的位置會影響執行緒的執行順序

我們再來看看加鎖的情況:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import threading, time

NUM = 0


def add():
    global NUM
    lock.acquire()
    NUM += 1
    name=t.getName()
    lock.release()
    time.sleep(1)
    print('current thread is: ',name ,' current NUM is: ',NUM )

if __name__ == '__main__':
    lock=threading.Lock()
    for tmp in range(10):
        t=threading.Thread(target=add)
        t.start()
    print("main thread has been stopped !")複製程式碼

執行緒、程式與協程
加鎖後的執行結果

  • lock=threading.Lock() 例項化鎖物件
  • lock.acquire() 從該句開始加鎖
  • lock.release() 釋放鎖

python中在threading模組中定義了一下幾種鎖:

  • Lock(不可巢狀), RLock(可巢狀), 兩個都是普通鎖, 同一時刻只允許一個執行緒被執行, 是互斥鎖
  • Semaphore 訊號量鎖, 該鎖允許一定數量的執行緒同時運算元據
  • event 事件機制鎖, 根據Flag的真假來控制執行緒
  • condition 條件鎖, 只有滿足某條件時候才能釋放執行緒

Semaphore 訊號量鎖使用:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import threading, time


def test():
    semaphore.acquire()
    print("current thread is: ", t.getName())
    time.sleep(1)
    semaphore.release()

if __name__ == '__main__':
    semaphore = threading.BoundedSemaphore(5)
    for tmp in range(20):
        t = threading.Thread(target=test)
        t.start()複製程式碼
  • semaphore = threading.BoundedSemaphore(5) 獲得訊號量鎖物件
  • semaphore.acquire() 加鎖
  • semaphore.release() 釋放鎖

event 事件機制鎖使用

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import threading,time

def test():
    print(t.getName())
    event.wait()


if __name__ == '__main__':
    event=threading.Event()
    for tmp in range(10):
        t=threading.Thread(target=test)
        t.start()
    print("zhe middle of main thread")
    if input("input your flag: ")=='1':
        event.set()
    print("main thread has been stopped")複製程式碼
  • event=threading.Event() 獲取事件鎖物件
  • event.wait() 檢測標誌位flag, 為true則放行該執行緒, 為false則阻塞該執行緒
  • event.set() 將標誌位flag設定為true
  • event.clear() 將標誌位flag設定為false

condition 條件鎖使用

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import threading


def condition():
    inp = input("input your condition: ")
    print(inp)
    if inp == "yes":
        return True
    return False


def test():
    cd.acquire()
    # cd.wait(1)
    cd.wait_for(condition)
    # cd.notify(2)
    print(t.getName())
    cd.release()


if __name__ == '__main__':
    cd = threading.Condition()
    for tmp in range(10):
        t = threading.Thread(target=test)
        t.start()
        t.join()
    print("\nmain thread has been stopped")複製程式碼

執行緒、程式與協程
執行結果

  • 由圖可得每次輸入yes 則放行一個執行緒
  • cd = threading.Condition() 獲取條件鎖物件
  • cd.wait(1) 設定執行緒最多等待時間
  • cd.wait_for(condition) 設定放行的條件, 該方法接受condition函式的返回值

在python的queue模組中內建了一種特殊的資料結構, 即佇列. 這裡我們可以把佇列簡單的看作是規定順序執行的一組執行緒.

Queue 先進先出佇列的使用:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import queue

q=queue.Queue(10)

for tmp in range(10):
    q.put(tmp)

for tmp in range(10):
    print(q.get(),q.qsize())複製程式碼
  • q=queue.Queue(10) 生成佇列物件, 設定佇列最多存放的資料為10個
  • q.put(tmp) 往佇列中存入資料
  • q.get() 獲取佇列資料
  • q.qsize() 獲取當前佇列的大小

利用Queue實現生產者消費者模型

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import time, threading, queue


def productor(i):
    while True:
        q.put(i)
        time.sleep(1)


def consumer(i):
    while True:
        print("consumer-%s ate %s" % (i, q.get()))


if __name__ == '__main__':
    q = queue.Queue(10)
    for tmp in range(8):
        t = threading.Thread(target=productor, args=(tmp,))
        t.start()

    for tmp in range(5):
        t = threading.Thread(target=consumer, args=(tmp,))
        t.start()

    print("main has been stopped")複製程式碼

執行緒、程式與協程
執行結果

不斷的建立和銷燬執行緒是非常消耗CPU的, 因此我們會採取維護一個執行緒池來實現多執行緒. 但是python中並未提供執行緒池的模組, 這裡就需要我們自己來寫.

簡單版本的執行緒池實現:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import queue, threading


class ThreadPool(object):
    def __init__(self, max_num=5):
        self.queue = queue.Queue(max_num)
        for i in range(max_num):
            self.queue.put(threading.Thread)

    def get_thread(self):
        return self.queue.get()

    def add_thread(self):
        self.queue.put(threading.Thread)


def test(pool, i):
    tm = __import__("time")
    tm.sleep(1)
    print("current thread is: ", i)
    pool.add_thread()


if __name__ == '__main__':
    p = ThreadPool()
    for tmp in range(20):
        thread = p.get_thread()
        t = thread(target=test, args=(p, tmp))
        t.start()
    print("main thread has been stopped")複製程式碼

執行緒、程式與協程
執行結果

  • 這裡實現執行緒池的主要思想是維護一個指定大小的佇列, 佇列中的每一個元素就是threading.Thread類. 每當需要執行緒時候, 直接獲取該類並建立執行緒, 使用完畢則返回執行緒池中
  • 缺點就是沒有回撥函式, 不能重複使用執行緒, 每當自己使用完執行緒需要自己將執行緒放回執行緒池, 且需要手動啟動執行緒

健壯版本的執行緒池:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import queue, threading, contextlib

stopFlag = object()


class ThreadPool(object):
    def __init__(self, max_num):
        self.queue = queue.Queue()
        self.max_num = max_num

        self.terminal = False
        self.queue_real_list_list = []
        self.queue_free_list = []

    def run(self, target, args, callback):
        task_tuple = (target, args, callback)
        self.queue.put(task_tuple)
        if len(self.queue_free_list) == 0 and len(self.queue_real_list_list) < self.max_num:
            self.add_thread()

    def add_thread(self):
        t = threading.Thread(target=self.fetch)
        t.start()

    def fetch(self):
        current_thread = threading.currentThread
        self.queue_real_list_list.append(current_thread)
        task_tuple = self.queue.get()
        while task_tuple != stopFlag:
            func, args, callback = task_tuple
            result_status = True
            try:
                result = func(*args)
            except Exception as e:
                result_status = False
                result = e
            if callback is not None:
                try:
                    callback(result_status, result)
                except Exception as e:
                    pass
            if not self.terminal:
                # self.queue_free_list.append(current_thread)
                # task_tuple = self.queue.get()
                # self.queue_free_list.remove(current_thread)
                with ThreadPool.queue_operate(self.queue_free_list,current_thread):
                    task_tuple = self.queue.get()
            else:
                task_tuple = stopFlag

        else:
            self.queue_real_list_list.remove(current_thread)

    def close(self):
        num = len(self.queue_real_list_list)
        while num:
            self.queue.put(stopFlag)
            num -= 1

    def terminate(self):
        self.terminal = True
        max_num = len(self.queue_real_list_list)
        while max_num:
            self.queue.put(stopFlag)
            max_num -= 1

    def terminate_clean_queue(self):
        self.terminal = True
        while self.queue_real_list_list:
            self.queue.put(stopFlag)
        self.queue.empty()

    @staticmethod
    @contextlib.contextmanager
    def queue_operate(ls, ct):
        ls.append(ct)
        try:
            yield
        finally:
            ls.remove(ct)


def callback_func(result_status, result):
    print(result_status, result)


def test(i):
    tm = __import__("time")
    tm.sleep(1)
    return "current thread is: {}".format(i)


if __name__ == '__main__':
    pool = ThreadPool(5)
    for tmp in range(20):
        pool.run(target=test, args=(tmp,), callback=callback_func)
    # pool.close()
    pool.terminate()複製程式碼
  • pool = ThreadPool(5) 生成執行緒池物件, 指定執行緒池最多執行緒數為5
    • __init__(self, max_num)被執行
    • self.queue = queue.Queue() 任務佇列
    • self.max_num = max_num 最多執行緒數
    • self.terminal = False 是否立即終止標誌
    • self.queue_real_list_list = [] 當前已經建立的執行緒物件列表
    • self.queue_free_list = [] 空閒的執行緒物件列表
  • pool.run(target=test, args=(tmp,), callback=callback_func) 執行執行緒池物件, target=test 執行緒執行的功能函式, args=(tmp,) 功能函式的引數, callback=callback_func 功能函式執行完畢之後呼叫的函式(即 回撥函式)
    • task_tuple = (target, args, callback) 將執行緒要執行的功能函式和回撥函式打包成任務元組
    • self.queue.put(task_tuple) 將任務元組加入到佇列中
    • if len(self.queue_free_list) == 0 and len(self.queue_real_list_list) < self.max_num:
              self.add_thread()複製程式碼
      判斷空閒列表是否為空且真實的執行緒列表數目是否小於最大執行緒數目, 若是則執行add_thread()函式新增執行緒
    • add_thread(self) 新增並啟動執行緒, 並將執行緒要執行的功能交給fetch(self)函式
    • current_thread = threading.currentThread 獲取當前執行緒, self.queue_real_list_list.append(current_thread) 將當前執行緒加入到真實執行緒列表中
    • task_tuple = self.queue.get() 從任務佇列中獲取任務元組
    • while task_tuple != stopFlag 該迴圈語句內容表示任務元組物件不是stopFlag結束標誌的時候執行其具體的功能和回撥函式
    • if not self.terminal 判斷是否立即終止當前執行緒(等待當前執行緒執行完任何立即結束)
  • pool.close() 根據當前真實執行緒列表新增對應的stopFlag終止符
  • pool.terminate() 此為不清空任務佇列的立即終止執行緒方法
  • terminate_clean_queue(self) 清空任務佇列的立即終止執行緒方法

在python中由multiprocess模組提供的Process類來實現程式相關功能(process與Process是不同的)

Process的使用:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from multiprocessing import Process


def test(pro):
    print("current process is: ",pro)


if __name__ == '__main__':
    for tmp in range(10):
        p = Process(target=test,args=(tmp,))
        p.start()複製程式碼

執行緒、程式與協程
執行結果

  • args=(tmp,) 這裡傳入的是元組, 不加逗號則表示整型資料
  • p = Process(target=test,args=(tmp,)) 建立程式物件

普通的資料共享在程式中的實現:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from multiprocessing import Process

ls = []


def test(i):
    ls.append(i)
    print("current process is: ", i, " and list is: ", ls)


if __name__ == '__main__':
    for tmp in range(10):
        p = Process(target=test, args=(tmp,))
        p.start()
        p.join()
    print("The final list is: ", ls)複製程式碼

執行緒、程式與協程
執行結果

  • 由圖可知, 程式之間預設是不能共享資料. 我們需要藉助python的multiprocess模組提供的類來實現資料共享

用Array共享資料

# -*- coding:utf-8 -*-
from multiprocessing import Process, Array


def test(i, ay):
    ay[i] += 10
    print('current process is: ', i)
    for tmp in ay:
        print(tmp)


if __name__ == '__main__':
    ay = Array('i', [1, 2, 3, 4, 5, 6])
    for tmp in range(5):
        p = Process(target=test, args=(tmp, ay))
        p.start()複製程式碼

執行緒、程式與協程
執行結果

  • ay = Array('i', [1, 2, 3, 4, 5, 6]) 建立整型的Array共享資料物件
  • p = Process(target=test, args=(tmp, ay)) 程式直接不能像執行緒之間共享資料, 故需要傳入ay物件

使用Manager共享資料:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from multiprocessing import Manager, Process


def test(i, dic):
    dic[i] = i + 10
    print('current process is: ', i)
    for k, v in dic.items():
        print(k, v)


if __name__ == '__main__':
    mg = Manager()
    dic = mg.dict()
    for tmp in range(10):
        p = Process(target=test, args=(tmp, dic))
        p.start()
        p.join()複製程式碼

執行緒、程式與協程
執行結果

  • mg = Manager() 初始化Manager物件
  • dic = mg.dict() 生成共享字典資料型別
  • p.join() 這裡需要保證每個程式執行完畢之後才能進行接下來的操作, 否則會報錯

使用queue共享資料:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from multiprocessing import Process,queues

import multiprocessing


def test(i,qu):
    qu.put(i+10)
    print("current process is: ",i," and zhe size of zhe queue is: ",qu.qsize())

if __name__ == '__main__':
    qu=queues.Queue(10,ctx=multiprocessing)
    for tmp in range(10):
        p=Process(target=test,args=(tmp,qu))
        p.start()複製程式碼

執行緒、程式與協程
執行結果

在程式中共享資料也會出現髒資料的問題, 比如用multiprocessing模組中的queue或者Queue共享資料時候就會出現髒資料. 此時我們往往需要設定程式鎖. 程式鎖的使用和執行緒鎖使用完全相同(Rlock, Lock, Semaphore, Event, Condition, 這些鎖均在multiprocess中)

在實際開發中我們並不會採取直接建立多程式來實現某些功能, 而是主動維護一個指定程式數的程式池來實現多程式. 因為不斷的建立程式和銷燬程式對CPU的開銷太大. python中內建了了程式池Pool 模組

程式池Pool的使用:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from multiprocessing import Pool
import time


def test(arg):
    time.sleep(1)
    return arg + 10


def call_end(arg):
    print(arg)


if __name__ == '__main__':
    p = Pool(5)
    for tmp in range(10):
        p.apply_async(func=test, args=(tmp,), callback=call_end)
    p.close()
    # p.terminate()
    p.join()複製程式碼

執行緒、程式與協程
執行結果

  • p.apply() 從程式池中取出一個程式執行其對應的功能
  • p.apply_async(func=test, args=(tmp,), callback=call_end)p.apply() 作用相同, p.apply_async() 可以呼叫回撥函式. callback=call_end 表明call_end是回撥函式, 當test執行完畢之後會將其返回值作為引數傳遞給該回撥函式
  • p.close() 等到所有程式結束後關閉程式池
  • p.join() 表明主程式必須等待所有子程式執行結束後方能結束(需要放在p.close()或者p.terminate()後面)

協成是python中特有的一個概念, 它是人為的利用單執行緒在操作某任務等待空閒的時間內, 通過yield來儲存當時狀態, 進而用該執行緒做其他的操作. 由此實現的併發操作, 本質上跟IO多路複用類似.

基礎版本協成的使用:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import greenlet


def f1():
    print('1111')
    gr2.switch()
    print('2222')
    gr2.switch()


def f2():
    print('3333')
    gr1.switch()
    print('4444')


if __name__ == '__main__':
    gr1 = greenlet.greenlet(f1)
    gr2 = greenlet.greenlet(f2)
    gr1.switch()複製程式碼
  • gr1 = greenlet.greenlet(f1) 建立f1函式的協成物件
  • gr1.switch() 由當前執行緒轉到到執行f1函式

封裝後的協成模組使用:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import gevent


def f1():
    print('this is f1 !!!')
    gevent.sleep(0)
    print('f1 after sleep')


def f2():
    print("this is f2 !!!")
    gevent.sleep(0)
    print('f2 after sleep')


if __name__ == '__main__':
    gevent.joinall([
        gevent.spawn(f1),
        gevent.spawn(f2),
    ])複製程式碼
  • gevent.joinall([
          gevent.spawn(f1),
          gevent.spawn(f2),
      ])複製程式碼
  • 等待f1f2執行完成再結束當前執行緒, 類似執行緒中的join()方法
  • gevent.sleep(0) 設定等待時間
  • 往往實際開發中並不需要設定從哪裡需要切換程式碼執行或者等待的

用協成訪問網頁簡單例子:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from gevent import monkey

monkey.patch_all()
import gevent, requests


def fetch(url):
    print('current url %s' % url)
    rp = requests.get(url)
    data = rp.text
    print(url, len(data))


if __name__ == '__main__':
    gevent.joinall([
        gevent.spawn(fetch, 'https://www.baidu.com'),
        gevent.spawn(fetch, 'https://www.sogou.com/'),
        gevent.spawn(fetch, 'http://www.jianshu.com'),
    ])複製程式碼

執行緒、程式與協程
執行結果

  • 由圖中可見, 執行第一個print('current url %s' % url)之後, 當前執行緒會處於等待請求狀態, 此時該執行緒會傳送第二個url, 依次類推. 直到最後請求資料獲取後, 才返回到第一次執行的函式中執行後續操作

相關文章