原文地址: https://blog.csdn.net/fgf00/article/details/52790360 編輯:智能算法,歡迎關注! 上期我們一起學習了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()
聲明:本文系網絡轉載,版權歸原作者所有。如涉及版權,請聯繫刪除!