python基礎9-進程、線程、守護線程、全局解釋器鎖、生產者消費者模型

1、Python GIL(Global Interpreter Lock)

全局解釋器鎖
python(在CPython執行環境下)同一時間只有一個線程在運行,不管你的機器是幾核的cpu都一樣

首先需要明確的一點是GIL並不是Python的特性,它是在實現Python解析器(CPython)時所引入的一個概念。就好比C++是一套語言(語法)標準,但是可以用不同的編譯器來編譯成可執行代碼。有名的編譯器例如GCC,INTEL C++,Visual C++等。Python也一樣,同樣一段代碼可以通過CPython,PyPy,Psyco等不同的Python執行環境來執行。像其中的JPython就沒有GIL。然而因爲CPython是大部分環境下默認的Python執行環境。所以在很多人的概念裏CPython就是Python,也就想當然的把GIL歸結爲Python語言的缺陷。所以這裏要先明確一點:GIL並不是Python的特性,Python完全可以不依賴於GIL

2、進程(process)

進程是對各種資源管理的集合,比如說,qq,就是一個進程,qq以一個整體的形式暴露給操作系統管理,裏面包含對各種資源的調用、內存的管理,網絡接口的調用等

每一個程序(進程)的內存是獨立的

進程的缺陷:

  • 進程只能在一個時間幹一件事,如果想同時幹兩件事或多件事,進程就無能爲力了。
  • 進程在執行的過程中如果阻塞,例如等待輸入,整個進程就會掛起,即使進程中有些工作不依賴於輸入的數據,也將無法執行。

補充一個概念:
上下文切換
單核機器一次只能幹一件事情,由於cpu運行速度快,每秒可以計算幾億次,速度太快,給我們造成了我們的機器很多任務在併發運行的錯覺。

多進程multiprocessing

語法

# Author: 73

from multiprocessing import Process
import time, threading


def thread_run():
    print(threading.get_ident()) # 線程id

def run(name):
    time.sleep(2)
    print("hello", name)
    t = threading.Thread(target=thread_run, ) # 進程裏面再啓一個線程
    t.start()

for i in range(10):
    p = Process(target=run, args=("seth%s" % i, ))
    p.start()
#p.join()

通過進程id來看看進程與子進程之間的關係

# Author: 73

from multiprocessing import Process
import os

def info(title):
    print(title)
    print('module name:', __name__)
    print('parent process:', os.getppid())
    print('process id:', os.getpid())
    print("\n\n")


def f(name):
    info('\033[31;1mfunction f\033[0m')
    print('hello', name)


if __name__ == '__main__':
    info('\033[33;1mmain process line\033[0m')
    p = Process(target=f, args=('bob',))
    p.start()
    p.join()

進程間通訊-Queues/Pipes/Managers

不同進程間內存是不共享的,要想實現兩個進程間的數據交換,可以用以下方法:
Queues
兩個進程之間的數據傳遞

from multiprocessing import Process, Queue
 
def f(q):
    q.put([42, None, 'hello'])
 
if __name__ == '__main__':
    q = Queue()
    p = Process(target=f, args=(q,))
    p.start()
    print(q.get())    # prints "[42, None, 'hello']"
    p.join()

Pipes
兩個進程之間的數據傳遞

# Author: 73

from multiprocessing import Process, Pipe

def f(conn):
    conn.send("hello father")
    print(conn.recv())
    conn.close()

parent_conn, child_conn = Pipe()
p = Process(target=f, args=(child_conn, ))
p.start()
print(parent_conn.recv())
parent_conn.send("hello child")
p.join()

Managers
實現進程與進程之間的數據共享和操作

from multiprocessing import Process, Manager
 
def f(d, l):
    d[1] = '1'
    d['2'] = 2
    d[0.25] = None
    l.append(1)
    print(l)
 
if __name__ == '__main__':
    with Manager() as manager:
        d = manager.dict()
 
        l = manager.list(range(5))
        p_list = []
        for i in range(10):
            p = Process(target=f, args=(d, l))
            p.start()
            p_list.append(p)
        for res in p_list:
            res.join()
 
        print(d)
        print(l)

進程鎖

保證在屏幕上輸出信息的時候,不會亂(比如說在打印第一條信息,第一條信息還沒打完,第二條信息就開始打印了)

from multiprocessing import Process, Lock
 
def f(l, i):
    l.acquire()
    try:
        print('hello world', i)
    finally:
        l.release()
 
if __name__ == '__main__': # 是否手動執行,通過模塊導入就不會執行
    lock = Lock()
 
    for num in range(10):
        Process(target=f, args=(lock, num)).start()

進程池

作用:允許一次最多多少個進程一起運行
進程池內部維護一個進程序列,當使用時,則去進程池中獲取一個進程,如果進程池序列中沒有可供使用的進進程,那麼程序就會等待,直到進程池中有可用進程爲止。

進程池中有兩個方法:

  • apply #同步執行/串行
  • apply_async # 異步執行/並行
# Author: 73

from multiprocessing import Process, Pool
import time, os


def Foo(i):
    time.sleep(2)
    print("hello ", os.getpid())
    return i + 100


def Bar(arg): # 主進程執行的這個回調函數
    print('-->exec done:', arg, os.getpid())


pool = Pool(5)
print("主進程: ", os.getpid())
for i in range(10):
    pool.apply_async(func=Foo, args=(i,), callback=Bar)
    #pool.apply(func=Foo, args=(i,)) # 同步執行
    #pool.apply_async(func=Foo, args=(i,))  # yi步執行

print('end')
pool.close()
pool.join()  # 進程池中進程執行完畢後再關閉,如果註釋,那麼程序直接關閉。

兩個坑:

  • 必須先close再join
  • 異步執行時,必須join,否則程序直接關閉

3、線程(thread)

線程是一串指令的集合,是操作系統能夠進行運算調度的最小單位。

多線程不適合cpu密集操作型任務,適合io操作密集型任務
*注:IO操作(如讀取數據、socketserver)不佔用CPU,計算佔用CPU

語法

直接調用

import threading
import time
 
def run(num): #定義每個線程要運行的函數
 
    print("running on number:%s" % num)
 
    time.sleep(3)
 
if __name__ == '__main__':
 
    t1 = threading.Thread(target=run,args=(1,)) #生成一個線程實例
    t2 = threading.Thread(target=run,args=(2,)) #生成另一個線程實例
 
 	# 併發執行
    t1.start() #啓動線程
    t2.start() #啓動另一個線程

	#run(1)
	#run(2)
 
    print(t1.getName()) #獲取線程名
    print(t2.getName())

繼承式調用

import threading
import time
  
class MyThread(threading.Thread):
    def __init__(self,num):
        super(MyThread, self).__init__()
        #threading.Thread.__init__(self)
        self.num = num
 
    def run(self):#定義每個線程要運行的函數
        print("running on number:%s" %self.num)
        time.sleep(3)
 
if __name__ == '__main__':
 
    t1 = MyThread(1)
    t2 = MyThread(2)
    t1.start()
    t2.start()

join函數

主線程卡住,等待調用join的子線程執行完畢

import threading
import time

class MyThread(threading.Thread):
    def __init__(self, num, t):
        super(MyThread, self).__init__()
        # threading.Thread.__init__(self)
        self.num = num
        self.t = t

    def run(self):  # 定義每個線程要運行的函數
        print("running on number:%s" % self.num)
        time.sleep(self.t)
        print('task done...')

if __name__ == '__main__':
    start_time = time.time()
    t1 = MyThread(1,2)
    t2 = MyThread(2,3)
    t1.start()
    #t1.join() # wait, 加在這裏,就把並行的變成了串行的
    t2.start()
    t1.join()
    t2.join() # 這句不加,打印的時間2秒多一點;加上,打印的時間3秒多一點
    stop_time = time.time()
    print(stop_time-start_time)

daemon(守護線程)

程序的主線程運行結束之後,會等待非守護線程運行結束再結束整個程序,當我們把一些線程設置爲守護線程之後,如果主線程運行結束,那麼守護線程就會同時結束,不管有沒有運行完

import threading
import time

class MyThread(threading.Thread):
    def __init__(self, num, t):
        super(MyThread, self).__init__()
        # threading.Thread.__init__(self)
        self.num = num
        self.t = t

    def run(self):  # 定義每個線程要運行的函數
        print("running on number:%s" % self.num)
        time.sleep(self.t)
        print('task done...')

if __name__ == '__main__':
    start_time = time.time()
    t1 = MyThread(1,2)
    t2 = MyThread(2,3)
    t1.setDaemon(True) # t1設置爲Daemon線程,它做爲程序主線程的守護線程,當主線程退出時,t1線程也會退出,由t1啓動的其它子線程會同時退出,不管是否執行完任務
    t2.setDaemon(True)
    t1.start()
    t2.start()
    stop_time = time.time()
    print(stop_time-start_time)

運行結果顯示,打印窗口不再打印“task done…”

線程鎖之Lock(互斥鎖mutex)/RLock(遞歸鎖)/Semaphore(信號量)

互斥鎖
也叫線程鎖
一個進程下可以啓動多個線程,多個線程共享父進程的內存空間,也就意味着每個線程可以訪問同一份數據,此時,如果2個線程同時要修改同一份數據,會出現什麼狀況?

# Author: 73

import threading, time

def run(n):
    global num
    num -= 1
    time.sleep(2)

num = 100

threading_list = []
for i in range(100):
    t = threading.Thread(target=run, args=(i,))
    t.start()
    threading_list.append(t)

for t in threading_list:
   t.join()
   
print(num)

正常來講,這個num結果應該是0, 但在python 2.7上多運行幾次,會發現,最後打印出來的num結果不總是0,爲什麼每次運行的結果不一樣呢?很簡單,假設你有A,B兩個線程,此時都 要對num 進行減1操作, 由於2個線程是併發同時運行的,所以2個線程很有可能同時拿走了num=100這個初始變量交給cpu去運算,當A線程去處完的結果是99,但此時B線程運算完的結果也是99,兩個線程同時CPU運算的結果再賦值給num變量後,結果就都是99。那怎麼辦呢? 很簡單,每個線程在要修改公共數據時,爲了避免自己在還沒改完的時候別人也來修改此數據,可以給這個數據加一把鎖, 這樣其它線程想修改此數據時就必須等待你修改完畢並把鎖釋放掉後才能再訪問此數據。

*注:不要在3.x上運行,不知爲什麼,3.x上的結果總是正確的,可能是自動加了鎖

# Author: 73

import threading, time

def addNum(n):
    global num
    mutex.acquire() # 修改前對數據加鎖
    num -= 1
    mutex.release() # 修改後釋放
    #time.sleep(2)

num = 100

mutex = threading.Lock()

threading_list = []
for i in range(100):
    t = threading.Thread(target=addNum, args=(i,))
    t.start()
    threading_list.append(t)

for t in threading_list:
   t.join()

print(num)

Python已經有一個GIL來保證同一時間只能有一個線程來執行了,爲什麼這裏還需要lock? 注意啦,這裏的lock是用戶級的lock,跟那個GIL沒關係 ,如下圖
在這裏插入圖片描述
既然用戶程序已經自己有鎖了,那爲什麼C python還需要GIL呢?加入GIL主要的原因是爲了降低程序的開發的複雜度,比如現在的你寫python不需要關心內存回收的問題,因爲Python解釋器幫你自動定期進行內存回收,你可以理解爲python解釋器裏有一個獨立的線程,每過一段時間它起wake up做一次全局輪詢看看哪些內存數據是可以被清空的,此時你自己的程序 裏的線程和 py解釋器自己的線程是併發運行的,假設你的線程刪除了一個變量,py解釋器的垃圾回收線程在清空這個變量的過程中的clearing時刻,可能一個其它線程正好又重新給這個還沒來及得清空的內存空間賦值了,結果就有可能新賦值的數據被刪除了,爲了解決類似的問題,python解釋器簡單粗暴的加了鎖,即當一個線程運行時,其它人都不能動,這樣就解決了上述的問題, 這可以說是Python早期版本的遺留問題。

遞歸鎖
在一個大鎖中包含子鎖

# Author: 73
import threading, time


def run1():
    print("grab the first part data--")
    lock.acquire()
    global num
    num += 1
    lock.release()
    return num


def run2():
    print("grab the second part data-")
    lock.acquire()
    global num2
    num2 += 1
    lock.release()
    return num2


def run3():
    lock.acquire()
    res = run1()
    print('--------between run1 and run2-----')
    res2 = run2()
    lock.release()
    print(res, res2)



num, num2 = 0, 0
lock = threading.RLock()
for i in range(10):
    t = threading.Thread(target=run3)
    t.start()

while threading.active_count() != 1:
    print(threading.active_count())
else:
    print('----all threads done---')
    print(num, num2)

Semaphore(信號量)
互斥鎖 同時只允許一個線程更改數據,而Semaphore是同時允許一定數量的線程更改數據 ,比如廁所有3個坑,那最多隻允許3個人上廁所,後面的人只能等裏面有人出來了才能再進去

# Author: 73

import threading, time


def run(n):
    semaphore.acquire()
    time.sleep(1)
    print("run the thread: %s\n" % n)
    semaphore.release()

semaphore = threading.BoundedSemaphore(5)  # 最多允許5個線程同時運行
for i in range(20):
    t = threading.Thread(target=run, args=(i,))
    t.start()

while threading.active_count() != 1:
    pass  # print threading.active_count()
else:
    print('----all threads done---')

Event

通過Event來實現兩個或多個線程間的交互

event = threading.Event()

# a client thread can wait for the flag to be set
event.wait()

# a server thread can set or reset it
event.set()
event.clear()
If the flag is set, the wait method doesn’t do anything.
If the flag is cleared, wait will block until it becomes set again.
Any number of threads may wait for the same event.

下面是一個紅綠燈的例子,即起動一個線程做交通指揮燈,生成幾個線程做車輛,車輛行駛按紅燈停,綠燈行的規則。

# Author: 73

import threading, time

event = threading.Event()

def lighter():
    count = 0
    if not event.is_set():
        event.set()
    while 1:
        if count>5 and count<10: # red light
            if event.is_set():
                event.clear() # 標誌位清空,wait卡住
            print('\033[41;1m--red light on---\033[0m')
        elif count > 10: # red light over
            event.set()
            count = 0
        else:
            print('\033[42;1m--green light on---\033[0m')
        count += 1
        time.sleep(1)

def car(n):
    while 1:
        if event.is_set():
            print("%s is runing..." % n)
            time.sleep(1)
        else:
            print("%s sees red light..." % n)
            event.wait()
            print("%s ready to go..." % n)

light = threading.Thread(target=lighter, )
light.start()

car1 = threading.Thread(target=car, args=("bmw", ))
car1.start()

Queue

簡單的說,就是一個有順序的容器,它和列表最大的區別就是:
從列表中取一個數據之後,數據還在列表中;從隊列中取一個數據之後,數據就從隊列中沒了

隊列作用:

  1. 緊耦合 接耦成 鬆耦合
  2. 提高運行效率
class queue.Queue(maxsize=0) #先入先出
class queue.LifoQueue(maxsize=0) #last in fisrt out 
class queue.PriorityQueue(maxsize=0) #存儲數據時可設置優先級的隊列

Queue.qsize()
Queue.empty() #return True if empty  
Queue.full() # return True if full 

Queue.put(item, block=True, timeout=None) 
Queue.put_nowait(item) == Queue.put(item, Flase)

Queue.get(block=True, timeout=None)
Queue.get_nowait() == Queue.get(False)
# Author: 73

import queue

q1 = queue.Queue(maxsize=0) #先入先出
print("Queue")
q1.put(1)
q1.put(2)
q1.put(3)
print(q1.get())
print(q1.get())
print(q1.get())

q2 = queue.LifoQueue(maxsize=0) #last in fisrt out
print("LifoQueue")
q2.put(1)
q2.put(2)
q2.put(3)
print(q2.get())
print(q2.get())
print(q2.get())

q3 = queue.PriorityQueue(maxsize=0) #存儲數據時可設置優先級的隊列
print("PriorityQueue")
q3.put((5, 'x'))
q3.put((2, 't'))
q3.put((3, 'ds'))
print(q3.get())
print(q3.get())
print(q3.get())

生產者消費者模型

在併發編程中使用生產者和消費者模式能夠解決絕大多數併發問題。該模式通過平衡生產線程和消費線程的工作能力來提高程序的整體處理數據的速度。

生產者消費者模式是通過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通訊,而通過阻塞隊列來進行通訊,所以生產者生產完數據之後不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列裏取,阻塞隊列就相當於一個緩衝區,平衡了生產者和消費者的處理能力。

最基本的生產者消費者模型的例子1

# Author: 73

import threading, time, queue

q = queue.Queue(maxsize=10)

def producer():
    count = 1
    while 1:
        q.put("骨頭%s" % count)
        print("生產了骨頭%s" % count)
        count += 1
        time.sleep(1)

def consumer(name):
    while 1:
        print("%s拿到骨頭%s並吃了..." % (name, q.get()))
        time.sleep(1)

p = threading.Thread(target=producer)
c = threading.Thread(target=consumer, args=("dog1", ))
c2 = threading.Thread(target=consumer, args=("dog2", ))

p.start()
c.start()
c2.start()

最基本的生產者消費者模型的例子2

# Author: 73

import threading, queue

def producer():
    for i in range(10):
        q.put("骨頭 %s" % i)

    print("開始等待所有的骨頭被取走...")
    q.join()
    print("所有的骨頭被取完了...")


def consumer(n):
    while q.qsize() > 0:
        print("%s 取到" % n, q.get())
        q.task_done()  # 告知這個任務執行完了


q = queue.Queue()

p = threading.Thread(target=producer, )
p.start()

c1 = consumer("dog1")

4、進程和線程的關係區別

在這裏插入圖片描述

關係

  • 線程是進程的實際運行單位
  • 線程是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程併發執行不同的任務
  • 進程要操作cpu必須要先創建一個線程
  • 所有在一個進程裏的線程是共享同一塊內存空間

區別

  • 線程共享內存空間,進程的內存是獨立的
  • 同一個進程的線程之間可以直接交流,兩個進程想通信,必須通過一箇中間代理來實現
  • 創建新線程很簡單,創建新進程需要對其父進程進行一次克隆
  • 一個線程可以控制和操作同一個進程裏的其它線程,但是進程只能操作子進程
  • 線程的啓動速度比進程快
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章