Python學習(十)---- python中的進程與協程

原文地址: https://blog.csdn.net/fgf00/article/details/52790360 編輯:智能算法,歡迎關注! 上期我們一起學習了python中的線程的相關知識

Python學習(九)---- python中的線程

今天我們繼續深入,一起學習python中的進程和協程相關知識。

目錄

1. 多進程

2. 協程

1 多進程

多進程:進程之間是獨立的, python的線程是用的操作系統的原生線程、python的進程也是用的操作系統的原生進程。 原生進程是由操作系統去維護的,python只是通過C代碼庫去起了一個進程,真正進程的管理還是通過操作系統去完成的。 操作系統的進程管理是沒有全局解釋器鎖的,進程只是是獨立的,根本不需要鎖的概念。

1.1 多進程的基本語法

進程:資源的集合,至少包含一個線程 python使用多核運算,使用python多進程

多進程和多線程的使用基本是一樣的

1import multiprocessing
2muitiprocessing.Process
1import multiprocessing
 2import threading
 3import time
 4
 5def thread_run(i,n):
 6    print("在進程%s的線程%s"%(i,n))
 7
 8def run(i):
 9    print("進程:%s "%i)
10    time.sleep(1)
11    for n in range(2):
12        t = threading.Thread(target=thread_run,args=(i,n))
13        t.start()
14
15if __name__ == '__main__':  # 這個必須要有
16    for i in range(4):
17        p = multiprocessing.Process(target=run,args=(i,))
18        p.start()

如果我想取我的進程號,怎麼去取呢?

1from multiprocessing import Process
 2import os
 3
 4def info(title):  # 打印進程信息
 5    print(title)
 6    print('module name:', __name__)  # 模塊名
 7    print('parent process:', os.getppid())  # 父進程ID
 8    print('process id:', os.getpid())  # 進程ID
 9    print("\n")
10
11def f(name):
12    info('\033[31;1mcalled from child process function f\033[0m')  # 打印子進程信息
13    print('hello', name)
14
15if __name__ == '__main__':
16    info('\033[32;1mmain process line\033[0m')  # 打印當前進程信息
17    p = Process(target=f, args=('FGF',))  # 子進程
18    p.start()
19    # p.join()

1.2 進程間數據交互

前面提到進程間內存是獨立的,但是想要訪問,怎麼辦呢? 有下面幾種方式:(萬變不離其宗,需要箇中間件(翻譯))

  • 隊列 Queues 使用方法跟threading裏的queue差不多
1from multiprocessing import Process, Queue
 2
 3def f(qq):
 4    qq.put([42, None, 'hello'])  # 子進程中放數據
 5
 6if __name__ == '__main__':
 7    q = Queue()  # 定義一個Queue
 8    p = Process(target=f, args=(q,))
 9    p.start()  # 啓動子進程
10    print(q.get())  # 主進程獲取數據並打印
11    p.join()

如果把線程queue傳給子進程,傳不了,那麼父進程的Queue是怎麼傳遞的? 看上去像數據共享,實際上是克隆了一個Queue,把自己的Queue克隆了一份交給了子進程。 但是爲了數據共享,子進程會把Queue pickle序列化到一箇中間的地方,中間位置再把數據反序列化給其他進程。

  • 管道 Pipes 類似socket、如電話線,一人在這頭,一人在那頭
1from multiprocessing import Process, Pipe
 2
 3def f(conn):
 4    conn.send([42, None, 'hello from child'])
 5    conn.send([42, None, 'hello from child2'])
 6    print("from parent:",conn.recv())
 7    conn.close()
 8
 9if __name__ == '__main__':
10    parent_conn, child_conn = Pipe()  # 名字自定義
11    p = Process(target=f, args=(child_conn,))
12    p.start()
13    print(parent_conn.recv())  # prints [42, None, 'hello from child']
14    print(parent_conn.recv())  # prints [42, None, 'hello from child2']
15    parent_conn.send("[42, None, 'hello']") # prints "[42, None, 'hello']"
16    p.join()
  • 數據共享 Managers 上面兩種方式只是實現了數據的傳遞,還沒有實現數據的共享,如實現數據共享,就要用到Managers。
1from multiprocessing import Process,Manager
 2import os
 3
 4def f(dict1,list1):
 5    dict1[os.getpid()] = os.getpid()  # 往字典裏放當前PID
 6    list1.append(os.getpid())  # 往列表裏放當前PID
 7    print(list1)
 8
 9if __name__ == "__main__":
10    with Manager() as manager:
11        d = manager.dict()  # 生成一個字典,可在多個進程間共享和傳遞
12        l = manager.list(range(5)) #生成一個列表,可在多個進程間共享和傳遞
13        p_list = []  # 存進程列表
14        for i in range(10):
15            p = Process(target=f,args=(d,l))
16            p.start()
17            p_list.append(p)
18        for res in p_list:  # 等待結果
19            res.join()
20        print('\n%s' %d)

要不要加鎖呢,不用加鎖,Managers默認就幫你處理了,內部有鎖控制。

  • 進程裏面也有一個鎖 進程不是內存獨立的麼,要鎖還有毛用?來看一下:
1from multiprocessing import Process, Lock
 2
 3def f(l, i):
 4    l.acquire()  # acquire一把鎖
 5    try:
 6        print('hello world', i)
 7    finally:
 8        l.release()
 9
10if __name__ == '__main__':
11    lock = Lock()  # 生成鎖實例
12    for num in range(10):
13        Process(target=f, args=(lock, num)).start()

因爲屏幕共享,會出現打印亂的問題。所以加鎖

1.3 進程池

創建一個子進程就是克隆一份父進程空間給子進程,開銷非常大。假如父進程空間1G,創建幾個子進程內存空間就佔滿了,所以有進程池的限制。 同一時間有多少進程在運行。 線程是沒有線程池的,(你可以自己搞:通過信號量搞線程池)

進程池內部維護一個進程序列,當使用時,則去進程池中獲取一個進程,如果進程池序列中沒有可供使用的進進程,那麼程序就會等待,直到進程池中有可用進程爲止。

進程池中有兩個方法:

  • apply: 同步、串行
  • apply_async: 異步、並行
1def Foo(i):
 2    time.sleep(2)
 3    print("\033[31min process %s\033[0m"%os.getpid())
 4    return i
 5
 6def Bar(arg):
 7    print("--> ecex done:", arg, os.getpid())  # 回調
 8    # 回調函數:通過PID,可見是主進程調用的,不是子進程調用的
 9
10if __name__ == "__main__":  # windows下面必須有這句
11    pool = Pool(processes=4)  # 允許進程池同時放入4個進程
12    print("主進程:%s\n%s"%(os.getpid(),'*'*22))
13
14    for i in range(10):
15        pool.apply_async(func=Foo, args=(i,), callback=Bar)  # 回調,參數爲前面函數的返回結果
16        # pool.apply(func=Foo, args=(i,))        串行
17        # pool.apply_async(func=Foo, args=(i,)) 並行
18    pool.close()  # 一定先關閉進程池再join等待已運行的結束,自己試試區別
19    pool.join()  # 進程池中進程執行完畢後在關閉。如果註釋,那麼程序直接關閉

2 協程

2.1 協程介紹

協程,又稱微線程,纖程。英文名Coroutine。協程是一種用戶態的輕量級線程。

協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。因此:

協程能保留上一次調用時的狀態(即所有局部狀態的一個特定組合),每次過程重入時,就相當於進入上一次調用的狀態,換種說法:進入上一次離開時所處邏輯流的位置。

線程的切換,會保存到CPU的寄存器裏。 CPU感覺不到協程的存在,協程是用戶自己控制的。

之前通過yield做的生產者消費者模型,就是協程,在單線程下實現併發效果。

協程的好處:

  • 無需線程上下文切換的開銷
  • 無需數據操作鎖定及同步的開銷
  • 方便切換控制流,簡化編程模型
  • 高併發+高擴展性+低成本:一個CPU支持上萬的協程都不是問題。所以很適合用於高併發處理。

缺點:

  • 無法利用多核資源:協程的本質是個單線程,它不能同時將 單個CPU 的多個核用上,協程需要和進程配合才能運行在多CPU上.當然我們日常所編寫的絕大部分應用都沒有這個必要,除非是cpu密集型應用。
  • 進行阻塞(Blocking)操作(如IO時)會阻塞掉整個程序

2.2 使用yield實現協程操作

1import time
 2
 3def consumer(name):
 4    print("--->starting eating baozi...")
 5    while True:
 6        new_baozi = yield
 7        print("[%s] is eating baozi %s" % (name,new_baozi))
 8        # time.sleep(1)
 9
10def producer():
11    r = con.__next__()
12    r = con2.__next__()
13    n = 0
14    while n < 5:
15        n +=1
16        print("\033[32;1m[producer]\033[0m is making baozi %s" %n )
17        con.send(n)
18        con2.send(n)
19        time.sleep(1)
20
21if __name__ == '__main__':
22    con = consumer("c1")  # 第一次調用只是生成器,next的時候纔回生成
23    con2 = consumer("c2")
24    p = producer()

爲了保證併發效果,在什麼時候切換呢?遇到IO操作就切換。 但什麼時候切回去呢?IO操作完了就切回去,但是程序是怎麼實現的呢?

2.3 Greenlet 一個封裝好的協程

1from greenlet import greenlet
 2
 3def test1():
 4    print(12)
 5    gr2.switch()
 6    print(34)
 7    gr2.switch()
 8
 9def test2():
10    print(56)
11    gr1.switch()
12    print(78)
13
14gr1 = greenlet(test1)
15gr2 = greenlet(test2)
16gr1.switch()

2.4 Gevent 自動切換

Greenlet 手動切換;Gevent 自動切換,封裝了Greenlet Gevent 是一個第三方庫,可以輕鬆通過gevent實現併發同步或異步編程,在gevent中用到的主要模式是Greenlet 它是以C擴展模塊形式接入Python的輕量級協程。 Greenlet全部運行在主程序操作系統進程的內部,但它們被協作式地調度。

1import gevent
 2
 3def foo():
 4    print("Running in foo")
 5    gevent.sleep(2)
 6    print("swich to foo again")
 7# 來回切換,直到sleep結束
 8def bar():
 9    print("Running in bar")
10    gevent.sleep(1)
11    print("swich back to bar")
12
13def func3():
14    print("Running in func3")
15    gevent.sleep(0)  # 只觸發一次切換操作
16    print("swich func3 again")
17
18gevent.joinall([
19    gevent.spawn(foo),  # 生成
20    gevent.spawn(bar),
21    gevent.spawn(func3),
22])
  • 協程gevent併發爬網頁
1from urllib import request
 2import gevent, time
 3# 注意!:Gevent檢測不到urllib的io操作,還是串行的,讓它知道就需要打補丁
 4from gevent import monkey
 5monkey.patch_all()  # 把當前程序的所有IO操作給我做單獨的做上標記
 6
 7def f(url):
 8    print("Get %s" %url)
 9    resp = request.urlopen(url)
10    data = resp.read()
11    # with open("url.html", 'wb') as f:
12    #     f.write(data)
13    print("%d bytes received from %s" %(len(data), url))
14
15print("異步時間統計中……")  # 協程實現
16async_start_time = time.time()
17gevent.joinall([
18    gevent.spawn(f, "https://www.python.org"),
19    gevent.spawn(f, "https://www.yahoo.com"),
20    gevent.spawn(f, "https://github.com"),
21])
22print("\033[32;1m異步cost:\033[0m",time.time()-async_start_time)
23#------------------------以下只爲對比效果---------------------------
24print("同步步時間統計中……")
25urls = [
26    "https://www.python.org",
27    "https://www.yahoo.com",
28    "https://github.com",
29]
30start_time = time.time()
31for url in urls:
32    f(url)
33print("\033[32;1m同步cost:\033[0m",time.time()-start_time)
  • 通過gevent實現單線程下的多socket併發 服務端:
1import sys
 2import socket
 3import time
 4import gevent
 5from gevent import socket,monkey
 6monkey.patch_all()
 7
 8def server(port):
 9    s = socket.socket()
10    s.bind(('0.0.0.0', port))
11    s.listen(500)
12    while True:
13        cli, addr = s.accept()  # 每個連接起一個協程
14        gevent.spawn(handle_request, cli)
15
16def handle_request(conn):
17    try:
18        while True:
19            data = conn.recv(1024)
20            print("recv:", data)
21            conn.send(data)
22            if not data:
23                conn.shutdown(socket.SHUT_WR)  # 類似break
24    except Exception as  ex:
25        print(ex)
26    finally:
27        conn.close()
28if __name__ == '__main__':
29    server(8001)

客戶端:

1import socket
 2
 3HOST = 'localhost'    # The remote host
 4PORT = 8001           # The same port as used by the server
 5s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 6s.connect((HOST, PORT))
 7while True:
 8    msg = bytes(input(">>> "),encoding="utf8")
 9    s.sendall(msg)
10    data = s.recv(1024)
11    #print(data)
12
13    print('Received', repr(data))  # 內置方法repr:格式化輸出
14s.close()

聲明:本文系網絡轉載,版權歸原作者所有。如涉及版權,請聯繫刪除!

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