day22-多併發編程基礎(三)

今天學習了併發編程中的最後一部分,協程,也是python中區別於java,c等語言中很大不一樣的一部分html

1.協程產生的背景java

2.協程的概念python

3.yield模擬協程編程

4.協程中主要的倆個模塊安全

5.協程的應用網絡

 

開始今日份總結多線程

1.協程產生的背景併發

  以前咱們學習了線程、進程的概念,瞭解了在操做系統中進程是資源分配的最小單位,線程是CPU調度的最小單位。按道理來講咱們已經算是把cpu的利用率提升不少了。可是咱們知道不管是建立多進程仍是建立多線程來解決問題,都要消耗必定的時間來建立進程、建立線程、以及管理他們之間的切換。app

  隨着咱們對於效率的追求不斷提升,基於單線程來實現併發又成爲一個新的課題,即只用一個主線程(很明顯可利用的cpu只有一個)狀況下實現併發。這樣就能夠節省建立線進程所消耗的時間。異步

  爲此咱們須要先回顧下併發的本質:切換+保存狀態

  cpu正在運行一個任務,會在兩種狀況下切走去執行其餘的任務(切換由操做系統強制控制),一種狀況是該任務發生了阻塞,另一種狀況是該任務計算的時間過長

  ps:在介紹進程理論時,說起進程的三種執行狀態,而線程纔是執行單位,因此也能夠將上圖理解爲線程的三種狀態

  一:其中第二種狀況並不能提高效率,只是爲了讓cpu可以雨露均沾,實現看起來全部任務都被「同時」執行的效果,若是多個任務都是純計算的,這種切換反而會下降效率。

       二:第一種狀況的切換。在任務一遇到io狀況下,切到任務二去執行,這樣就能夠利用任務一阻塞的時間完成任務二的計算,效率的提高就在於此。

  對於單線程下,咱們不可避免程序中出現io操做,但若是咱們能在本身的程序中(即用戶程序級別,而非操做系統級別)控制單線程下的多個任務能在一個任務遇到io阻塞時就切換到另一個任務去計算,這樣就保證了該線程可以最大限度地處於就緒態,即隨時均可以被cpu執行的狀態,至關於咱們在用戶程序級別將本身的io操做最大限度地隱藏起來,從而能夠迷惑操做系統,讓其看到:該線程好像是一直在計算,io比較少,從而更多的將cpu的執行權限分配給咱們的線程。

  協程的本質就是在單線程下,由用戶本身控制一個任務遇到io阻塞了就切換另一個任務去執行,以此來提高效率。爲了實現它,咱們須要找尋一種能夠同時知足如下條件的解決方案:

#1. 能夠控制多個任務之間的切換,切換以前將任務的狀態保存下來,以便從新運行時,能夠基於暫停的位置繼續執行。
#2. 做爲1的補充:能夠檢測io操做,在遇到io操做的狀況下才發生切換

2.協程的概念

協程:在其餘語言中不多去用,在python中很是重要的點,對於操做系統來講,線程已是操做系統可以看到的最小單位,操做系統沒法感知協程

  • 協程的本質是,就是一條線程分紅多份,每一份執行一段代碼,多段代碼能夠在一個線程上來回切換
  • 若是能在一段代碼執行,在遇到I/O操做的時候,記錄此時的狀態,去執行另一段代碼,至關於完成利用協程完成了更加充分利用線程的目的

協程利用切換來規避I/O操做帶來的好處

  1. 一條線程能夠執行多個任務
  2. 減小了一個線程的阻塞,幫助線程最大程度的搶佔CPU資源
  3. 協程因爲操做系統不可見,不禁操做系統控制嗎,協程是用戶級,減小I/O操做,提升CPU的計算能力
  4. 協程之間永遠數據安全,----由於不少協程本質上就是一條線程

在pthon中,協程是很是重要的。

4PN6G06[)]EF97D5}Q6B7`R

3.yield模擬協程

  那麼如今就用yield來模擬協程,畢竟yield也是能夠在代碼級別記錄狀態

#代碼以下,yield本質是保存如今的狀態,send是調用其餘函數
def pro():
    print(1)
    n = yield 'a'
    print(n)
    yield 'b'

def com():
    g = pro()
    a = next(g)
    print(a)
    b = g.send(2)
    print(b)

com()

  代碼執行順序以下

TIM圖片20190212174933

  相比於串行的去執行,單純的用yield只會讓時間更長

  下面用yield測試一下以前用到的生產者消費者模型

#單純的生產者,消費者模型
import time

def consumer(res):
    '''單純的處理數據'''
    pass

def producer():
    res =[]
    for i in range(10000000):
        pass
    return res

start = time.time()
res = producer()
consumer(res)
end = time.time()
print(end-start)
#結果
0.347031831741333

#用yield模式嘗試

import time

def consumer():
    while True:

        x = yield

def producer():
    g = consumer()
    next(g)
    for i in range(10000000):
        g.send(i)
start = time.time()
#併發的執行任務
producer()
end = time.time()

print(end-start)

#結果
1.8232519626617432

  能夠看出來,單純線程之間倆個任務的切換時很可浪費時間的,若是數據量大存儲數據也是很須要時間的,每一次切換都須要記住當前的狀態,切換回去須要讀取以前的狀態。

  若是咱們遇到I/0操做的時候能夠自動切換,而且I/O阻塞時間能夠和執行代碼共享這段時間,纔是真正的提升了程序的執行率,yield只是保存了狀態。

  能夠用yield實現一個協程的操做。

4.協程中主要的倆個模塊

  協程中的主要有倆個模塊,倆個模塊都是第三方模塊,既然是第三方模塊那就先說明一下,第三方模塊的導入方法

  1. 方法一:在pycharm中,file—settings—project’xxx’—Project Interpreter—‘+’—搜索要安裝的包—InstallPackage
  2. 方法二:在cmd中 pip install ‘gevent’ 。pip list 查看已經安裝的包目錄。pip unistalled ‘’ 卸載已經安裝的包

  這個時候須要倆個第三方模塊,一個是gevent,一個是greenlet,不過gevent是greenlet的上層模塊,,gevent規避I/O操做,判斷程序中的I/O操做,遇到I/O就切換到另外一個任務去執行。greenlet主要是倆個任務之間的切換,狀態的保存以及讀取

4.1 greenlet模塊

  安裝 :pip3 install greenlet

查看代碼

import greenlet

def eat():
    print('eat1')
    g2.switch()
    print('eat2')
    g2.switch()

def sleep():
    print('sleep1')
    g1.switch()
    print('sleep2')

g1 = greenlet.greenlet(eat)
g2 = greenlet.greenlet(sleep)
g1.switch()
#結果
eat1
sleep1
eat2
sleep2

  greenlet 模塊只是記錄了狀態而且在切換回去的是讀取了狀態,並無真正意思的自動規避I/O操做

4.2 gevent模塊

  這個時候就須要了gevent模塊了

  安裝:pip3 install gevent

  Gevent 是一個第三方庫,能夠輕鬆經過gevent實現併發同步或異步編程,在gevent中用到的主要模式是Greenlet, 它是以C擴展模塊形式接入Python的輕量級協程。 Greenlet所有運行在主程序操做系統進程的內部,但它們被協做式地調度。

#gevent模塊的使用方法
g1=gevent.spawn(func,1,,2,3,x=4,y=5)建立一個協程對象g1,spawn括號內第一個參數是函數名,如eat,後面能夠有多個參數,能夠是位置實參或關鍵字實參,都是傳給函數eat的

g2=gevent.spawn(func2)

g1.join() #等待g1結束

g2.join() #等待g2結束

#或者上述兩步合做一步:gevent.joinall([g1,g2])

g1.value#拿到func1的返回值

先運用最基本的協程函數

import gevent
def eat():
    print('eat1')
    gevent.sleep(1)
    print('eat2')

def sleep():
    print('sleep1')
    gevent.sleep(1)
    print('sleep2')
g1 = gevent.spawn(eat)#實例化一個gevent對象
g2 = gevent.spawn(sleep)#實例化一個gevent對象

gevent.joinall([g1,g2])#監測到有I/O就切換

  上例gevent.sleep(2)模擬的是gevent能夠識別的io阻塞,而time.sleep(2)或其餘的阻塞,gevent是不能直接識別的須要用下面一行代碼,打補丁,就能夠識別了from gevent import monkey;monkey.patch_all()必須放到被打補丁者的前面,如time,socket模塊以前,或者咱們乾脆記憶成:要用gevent,須要將from gevent import monkey;monkey.patch_all()放到文件的開頭。

查看更改後的代碼

from gevent import monkey
monkey.patch_all()#用來匹配全部的I/O操做
import gevent
import time
def eat():
    print('eat1')
    time.sleep(1)
    print('eat2')

def sleep():
    print('sleep1')
    time.sleep(1)
    print('sleep2')
g1 = gevent.spawn(eat)#實例化一個gevent對象
g2 = gevent.spawn(sleep)#實例化一個gevent對象

gevent.joinall([g1,g2])#監測到有I/O就切換

  最後咱們來看一下協程的id號,代碼以下

from gevent import monkey
monkey.patch_all()
import gevent
import time
from threading import currentThread

def eat():
    print('eat:',currentThread())
    print('eat1')
    time.sleep(1)
    print('eat2')

def sleep():
    print('sleep:',currentThread())
    print('sleep1')
    time.sleep(1)
    print('sleep2')

g1 = gevent.spawn(eat)
g2 = gevent.spawn(sleep)

gevent.joinall([g1,g2])
#結果以下
eat: <_DummyThread(DummyThread-1, started daemon 53379528)>
eat1
sleep: <_DummyThread(DummyThread-2, started daemon 53380480)>
sleep1
eat2
sleep2

 

  咱們能夠用threading.current_thread().getName()來查看每一個g1和g2,查看的結果爲DummyThread-n,即假線程

5.協程的應用

  對於協程通常使用比較多的地方爲網絡I/O以及sleep操做,不過通常咱們程序代碼基本是不會去使用sleep操做,因此平常用的比較多的就是網絡爬蟲以及socket.server

5.1 網絡爬蟲簡易

  看代碼

#普通打開方式
import time
from urllib import request

def func(name,url):
    ret =  request.urlopen(url)#獲取網頁
    with open(name+'.html','wb') as f:
        f.write(ret.read())
url_lst = [
    ('python','https://www.python.org/'),
    ('blog','http://www.cnblogs.com/Eva-J/articles/8324673.html'),
    ('pypi','https://pypi.org/project/pip/'),
    ('blog2','https://www.cnblogs.com/z-x-y/p/9237706.html'),
    ('douban','https://www.douban.com/')
]
start = time.time()
for url_item in url_lst:
    func(*url_item)
end = time.time()
print('普通打開方式',end-start)

#協程打開方式
from gevent import monkey
monkey.patch_all()
import gevent
from urllib import request
import time

def func(name,url):
    ret = request.urlopen(url)
    with open(name+'2.html','wb')as f:
        f.write(ret.read())

url_lst = [
    ('python','https://www.python.org/'),
    ('blog','http://www.cnblogs.com/Eva-J/articles/8324673.html'),
    ('pypi','https://pypi.org/project/pip/'),
    ('blog2','https://www.cnblogs.com/z-x-y/p/9237706.html'),
    ('douban','https://www.douban.com/')
]
start = time.time()
g_list =[]
for url_item in url_lst:
    g = gevent.spawn(func,*url_item)
    g_list.append(g)
gevent.joinall(g_list)
end = time.time()
print('協程打開方式',end-start)

看結果

  普通打開方式 6.35495924949646
  協程打開方式 1.931349754333496

  咱們會發現如今在少許的url情況下是這樣,若是在大量的代碼下,這個時間就會縮減的更多。

補充:這個是我在測試的時候發現的情況,在已有文件,打開文件並從新寫入文件內容,耗費的時間會高不少!

在爬蟲的時候仍是用協程,這樣會更快的拿到咱們須要的數據並對其做出分析!

5.2 用協程實現socket.server

看代碼

#服務端

#服務端
import socket
from gevent import monkey
monkey.patch_all()
import gevent

def talk(conn):
    while True:
        msg = conn.recv(1024).decode()
        conn.send(msg.upper().encode('utf-8'))

sk =socket.socket()
sk.bind(('127.0.0.1',8500))
sk.listen()

while True:
    conn,addr = sk.accept()
    gevent.spawn(talk,conn)

#客戶端

import socket
sk = socket.socket()
sk.connect(('127.0.0.1',8500))

while True:
    msg = input('--->').encode('utf-8')
    sk.send(msg)
    recv_msg = sk.recv(1024).decode('utf-8')
    print(recv_msg)

sk.close()

任何基礎知識都是看着簡單,運用難,多練習就好啦!

相關文章
相關標籤/搜索