協程

這是我在論壇看到的一篇文章,寫的確實很是不錯。很受用。因此拿出來分享給你們。若是有地方沒作好,還但願你們多多包含。在分享以前呢。我給你們推薦一下我本身弄的python羣:595266089 無論是大牛仍是小白我都很是歡迎。羣裏有些學習資料。適合小白相對來說多些。羣裏也有人解答問題。你們能夠一塊兒交流。大牛作的小項目。源碼也是有一部分的。歡迎初學者和進階者還有大牛者進羣,進入一個你們庭!python

協程linux

協程,又稱微線程,纖程。英文名Coroutine。一句話說明什麼是協程,協程是一種用戶態的輕量級線程。git

協程擁有本身的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其餘地方,在切換回來的時候,恢復先前保存的寄存器上下文和棧。所以,協程能保留上一次調用的狀態(即全部局部狀態的一個特定組合),每次過程重入時,就至關於進入上一次調用的狀態,換種說法,進入上一次離開時所處邏輯流的位置。程序員

子程序,或者稱爲函數,在全部語言中都是層級調用,好比A調用B,B在執行過程當中又調用了C,C執行完畢返回,B執行完畢返回,最後A執行完畢。github

因此子程序調用時經過棧實現的,一個線程就是執行一個子程序。子程序調用老是一個入口,一次返回,調用順序是明確的。而協程的調用和子程序不一樣。編程

協程看上去也是子程序,但執行過程當中,在子程序內部可中斷,而後轉而執行別的子程序,在適當的時候再返回來接着執行。windows

注意,在一個子程序中中斷,去執行其餘子程序,不是函數調用,有點相似CPU的中斷。好比子程序A、B:數組

def a():緩存

print("1")安全

print("2")

print("3")

def b():

print("x")

print("y")

print("z")

假設由程序執行,在執行A的過程當中,能夠隨時中斷,去執行B,B也可能在執行過程當中中斷再去執行A,結果多是:

1

2

x

y

3

z

可是在A中是沒有調用B的,因此協程的調用比函數調用理解起來要難一些。看起來A、B的執行有點像多線程,但協程的特色在是一個線程執行,和多線程比協程有何優點?

最大的優點就是協程極高的執行效率。由於子程序切換不是線程切換,而是有程序自身控制,所以,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優點就越明顯。

第二大優點就是不須要多線程的鎖機制,由於只有一個線程,也不存在同時寫變量衝突,在協程中控制共享資源不加鎖,只須要判斷狀態就行了,因此執行效率比多線程高不少。

由於協程是一個線程執行,那麼怎麼利用多核CPU呢?最簡單的方法是多進程加協程,既充分利用多核,有充分發揮協程的高效率,可得到極高的性能。

協程的優勢:

無需線程上下文切換的開銷。

無需原子操做鎖定及同步的開銷。原子操做(atomic operation)是不須要synchronized,所謂原子操做是指不會被線程調度機制打斷的操做;這種操做一旦開始,就一直運行到結束,中間不會有任何context switch(切換到另外一個線程)。原子操做能夠是一個步驟,也能夠是多個操做步驟,可是其順序是不能夠被打亂,或者切割掉只執行部分。視做總體是原子性的核心。

方便切換控制流,簡化編程模型。

高併發+高擴展性+低成本。一個CPU支持上萬的協程都不是問題,因此很適合用於高併發處理。

協程的缺點:

沒法利用多核資源。協程的本質是個單線程,它不能同時將單個CPU的多個核用上,協程須要和進程配合才能運行在多CPU上。固然咱們平常所編寫的絕大部分應用都沒有這個必要,除非是CPU密集型應用。

進行阻塞(Blocking)操做(如IO時)會阻塞掉整個程序。

使用yield實現協程操做。

import time,queue

def consumer(name):

print("-->starting eating xoxo")

while True:

new_xo = yield

print("%s is eating xoxo %s"%(name,new_xo))

def producer():

r = con.__next__()

r = con2.__next__()

n = 0

while n < 5:

n += 1

con.send(n)

con2.send(n)

print("\033[32;1mproducer\033[0m is making xoxo %s"%n)

if __name__ == "__main__":

con = consumer("c1")

con2 = consumer("c2")

p = producer()

輸出:

-->starting eating xoxo

c1 is eating xoxo 1

c2 is eating xoxo 1

producer is making xoxo 1

c1 is eating xoxo 2

c2 is eating xoxo 2

producer is making xoxo 2

c1 is eating xoxo 3

c2 is eating xoxo 3

producer is making xoxo 3

c1 is eating xoxo 4

c2 is eating xoxo 4

producer is making xoxo 4

c1 is eating xoxo 5

c2 is eating xoxo 5

producer is making xoxo 5

協程的特色:

一、必須在只有一個單線程裏實現併發。

二、修改共享數據不需加鎖。

三、用戶程序裏本身保持多個控制流的上下文棧。

四、一個協程遇到IO操做自動切換到其它協程。

剛纔yield實現的不能算是合格的協程。

Python對協程的支持是經過generator實現的。在generator中,咱們不但能夠經過for循環來迭代,還能夠不斷調用next()函數獲取由yield語句返回到下一個值。可是python的yield不但能夠返回一個值,它能夠接收調用者發出的參數。

Greenlet

greenlet是一個用C實現的協程模塊,相比於Python自帶的yield,它能夠在任意函數之間隨意切換,而不需把這個函數聲明爲generator。

from greenlet import greenlet

def f1():

print(11)

gr2.switch()

print(22)

def f2():

print(33)

gr1.switch()

print(44)

gr1 = greenlet(f1)

gr2 = greenlet(f2)

11

33

22

44

以上例子還有一個問題沒有解決,就是遇到IO操做自動切換。

Gevent

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

import gevent

def foo():

print("Running in foo")

gevent.sleep()

print("Explicit contenxt switch to foo agin")

def bar():

print("Explicit context to bar")

gevent.sleep(1)

print("Implict context switch back to bar")

def func3():

print("running func3")

gevent.sleep(0)

print("running func3 again")

gevent.joinall([

gevent.spawn(foo),

gevent.spawn(bar),

gevent.spawn(func3),

])

Running in foo

Explicit context to bar

running func3

Explicit contenxt switch to foo agin

running func3 again

Implict context switch back to bar

同步與異步的性能區別

def f1(pid):

gevent.sleep(0.5)

print("F1 %s done"%pid)

for i in range(10):

f1(i)

def f3():

threads = [gevent.spawn(f1,i) for i in range(10)]

gevent.joinall(threads)

print("f2")

f2()

print("f3")

f3()

f2

F1 0 done

F1 1 done

F1 2 done

F1 3 done

F1 4 done

F1 5 done

F1 6 done

F1 7 done

F1 8 done

F1 9 done

f3

上面程序的重要部分是將f1函數封裝到Greenlet內部線程的gevent.spawn。初始化的greenlet列表存放在數組threads中,此數組被傳給gevent.joinall函數,後者阻塞當前流程,並執行全部給定的greenlet。執行流程只會在全部greenlet執行完後纔會繼續向下走。

IO阻塞自動切換任務

from urllib import request

import gevent,time

from gevent import monkey

# 把當前程序的全部的id操做給單獨的作上標記

monkey.patch_all()

def f(url):

print("GET:%s"%url)

resp = request.urlopen(url)

data = resp.read()

f = open("load.txt","wb")

f.write(data)

f.close()

print("%d bytes received from %s."%(len(data),url))

urls = ['https://www.python.org/',

'http://www.cnblogs.com/yinshoucheng-golden/',

'https://github.com/']

time_start = time.time()

for url in urls:

f(url)

print("同步cost",time.time() - time_start)

async_time_start = time.time()

gevent.spawn(f,'https://www.python.org/'),

gevent.spawn(f,'http://www.cnblogs.com/yinshoucheng-golden/'),

gevent.spawn(f,'https://github.com/'),

print("異步cost",time.time() - async_time_start)

經過gevent實現單線程下的多socket併發

server side

import sys,socket,time,gevent

from gevent import socket,monkey

def server(port):

s = socket.socket()

s.bind(("0.0.0.0",port))

s.listen(500)

cli,addr = s.accept()

gevent.spawn(handle_request,cli)

def handle_request(conn):

try:

data = conn.recv(1024)

print("recv:",data)

if not data:

conn.shutdown(socket.SHUT_WR)

conn.send(data)

except Exception as ex:

print(ex)

finally:

conn.close()

server(6969)

client side

import socket

HOST = "localhost"

PORT = 6969

s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

s.connect((HOST,PORT))

msg = bytes(input(">>:"),encoding="utf8")

s.sendall(msg)

data = s.recv(1024)

# print(data)

print("Received",repr(data))

s.close()

socket併發

import socket,threading

def sock_conn():

client = socket.socket()

client.connect(("localhost",6969))

count = 0

client.send(("hello %s"%count).encode("utf-8"))

data = client.recv(1024)

print("%s from server:%s"%(threading.get_ident(),data.decode()))

count += 1

client.close()

for i in range(100):

t = threading.Thread(target=sock_conn)

t.start()

事件驅動與異步IO

寫服務器處理模型的程序時,有一下幾種模型:

(1)每收到一個請求,建立一個新的進程,來處理該請求。

(2)每收到一個請求,建立一個新的線程,來處理該請求。

(3)每收到一個請求,放入一個事件列表,讓主程序經過非阻塞I/O方式來處理請求。

上面的幾種方式,各有千秋。

第一種方法,因爲建立新的進程,內存開銷比較大。因此,會致使服務器性能比較差,但實現比較簡單。

第二種方法,因爲要涉及到線程的同步,有可能會面臨死鎖等問題。

第三種方法,在寫應用程序代碼時,邏輯比前面兩種都複雜。

綜合考慮各方面因素,通常廣泛認爲第三種方式是大多數網絡服務器採用的方式。

在UI編程中,經常要對鼠標點擊進行相應響應,首先如何得到鼠標點擊呢?

方式一:建立一個線程,該線程一直循環檢測是否有鼠標點擊,那麼這個方式有如下幾個缺點。

一、CPU資源浪費,可能鼠標點擊的頻率很是小,可是掃描線程仍是會一直循環檢測,這會形成不少的CPU資源浪費;若是掃描鼠標點擊的接口是阻塞的呢?

二、若是是阻塞的,又會出現下面這樣的問題。若是咱們不但要掃描鼠標點擊,還要掃描鍵盤是否按下,因爲掃描鼠標時被阻塞了,那麼可能永遠不會去掃描鍵盤。

三、若是一個循環須要掃描的設備很是多,這又會引發響應時間的問題。

因此,這種方式很是很差。

方式二:事件驅動模型

目前大部分的UI編程都是事件驅動模型。如不少UI平臺都會提供onClick()事件,這個事件就表明鼠標點擊事件。事件驅動模型大致思路以下。

一、有一個事件(消息)隊列。

二、鼠標按下時,往這個隊列中增長一個點擊事件(消息)。

三、有一個循環,不斷從隊列取出事件。根據不一樣的事件,調出不一樣的函數,如onClick()、onKeyDown()等。

四、事件(消息)通常都各自保存各自的處理函數指針,這樣每一個消息都有獨立的處理函數。

事件驅動編程是一種編程範式,這裏程序的執行流由外部事件來決定。它的特色是包含一個事件循環,當外部事件發生時使用回調機制來觸發相應的處理。另外兩個常見的編程範式是同步(單線程)以及多線程編程。

對比單線程、多線程以及事件驅動編程模型。下圖表示隨着時間的推移,這三種模式下程序所作的工做。這個程序有3個任務須要完成,每一個任務都在等待I/O操做時阻塞自身。阻塞在I/O操做上所花費的時間用灰色框表示。

在單線程同步模型中,任務按照順序執行。若是某個任務由於I/O而阻塞,其餘全部的任務必須等待,直到它完成以後才能依次執行其餘操做。這種明確的執行順序和串行化處理的行爲能夠看出,若是各任務之間並無相互依賴的關係,但各任務執行仍然須要互相等待,就使得程序總體運行速度下降了。

在多線程版本中,這3個任務分別在獨立的線程中執行。這些線程由操做系統來管理,在多處理器系統上能夠並行處理,或者在單處理器系統上交替執行。這使得當某個線程阻塞在某個資源的同時其餘線程得以繼續執行。多線程程序更加難以判斷,由於這類程序不得不經過線程同步機制加鎖、可重入函數、線程局部存儲或者其餘機制來處理線程安全問題,若是實現不當就會致使出現微妙且使人痛不欲生的BUG。

在事件驅動版本的程序中,3個任務交錯執行,但仍然在一個單獨的線程控制中。當處理I/O或其餘等待操做時,註冊一個回調到事件循環中,而後當I/O操做完成時繼續執行。回調描述了該如何處理某個事件。事件循環輪詢全部的事件,當事件到來時將它們分配給等待處理事件的回調函數。這種方式讓程序儘量的得以執行而不須要用到額外的線程。事件驅動型程序比多線程程序更容易推斷出行爲,由於程序員不須要關心線程安全問題。

I/O多路複用

同步I/O和異步I/O,阻塞I/O和非阻塞I/O分別是什麼,到底有什麼區別?本文討論的背景是Linux環境下的network I/O。

概念說明

用戶空間與內核空間

如今操做系統都是採用虛擬存儲器,那麼對32位操做系統而言,它的尋址空間(虛擬存儲空間)爲4G(2的32次方)。操做系統的核心是內核,獨立於普通的應用程序,能夠訪問受保護的內存空間,也有訪問底層硬件設備的全部權限。爲了保證用戶進程不能直接操做內核(kernel),保證內核的安全,操做系統將虛擬空間劃分爲兩部分,一部分爲內核空間,一部分爲用戶空間。針對Linux操做系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱爲內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱爲用戶空間。

進程切換

爲了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復之前掛起的某個進程的執行。這種行爲被稱爲進程切換。所以能夠說,任何進程都是在操做系統內核的支持下運行的,是與內核緊密相關的。

從一個進程的運行轉到另外一個進程上運行,這個過程當中通過下面過程:

一、保存處理機上下文,包括程序計數器和其餘寄存器。

二、更新PCB信息。

三、把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。

四、選擇另外一個進程執行,並更新其PCB。

五、更新內存管理的數據結構。

六、恢復處理機上下文。

進程控制塊(Processing Control Block),是操做系統核心中一種數據結構,主要表示進程狀態。其做用是使一個在多道程序環境下不能獨立運行的程序(含數據),成爲一個能獨立運行的基本單位或與其它進程併發執行的進程。或者說,操做系統OS是根據PCB來對併發執行的進程進行控制和管理的。PCB一般是系統內存佔用區中的一個連續存放區,它存放着操做系統用於描述進程狀況及控制進程運行所需的所有信息。

進程的阻塞

正在執行的進程,因爲期待的某些事件未發生,如請求系統資源失敗、等待某種操做的完成、新數據還沒有到達或無新任務執行等,則由系統自動執行阻塞(Block),使本身由運行狀態變爲阻塞狀態。可見,進程的阻塞是進程自身的一種主動行爲,也所以只有處於運行狀態的進程(得到CPU),才能將其轉爲阻塞狀態。當進程進入阻塞狀態,是不佔用CPU資源的。

文件描述符fd

文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。

文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者建立一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些設計底層的程序編寫每每會圍繞着文件描述符展開。可是文件描述符這一律念每每只適用於UNIX、Linux這樣的操做系統。

緩存I/O

緩存I/O又被稱做標準I/O,大多數文件系統的默認I/O操做都是緩存I/O。在Linux的緩存I/O機制中,操做系統會將I/O的數據緩存在文件系統的頁緩存(page cache)中,也就是說,數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。

緩存I/O的缺點:

數據在傳輸過程當中須要在應用程序地址空間和內核進行屢次數據拷貝操做,這些數據拷貝操做所帶來的CPU以及內存開銷是很是大的。

IO模式

對於一次IO訪問(以read爲例),數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。當一個read操做發生時,會經歷兩個階段:

一、等待數據準備(waiting for the data to be ready)。

二、將數據從內核拷貝到進程中(Copying the data from the kernel to the process)。

正是由於這兩個階段,Linux系統產生了下面五種網絡模式的方案。

阻塞I/O(blocking IO)。

非阻塞I/O(nonblocking IO)

I/O多路複用(IO multiplexing)

信號驅動I/O(signal driven IO)

異步I/O(asynchronous IO)

因爲信號驅動I/O(signal driven IO)在實際中並不經常使用,因此只剩下四種IO模式。

阻塞I/O(blocking IO)

在Linux中,默認狀況下全部的Socket都是blocking,一個典型的讀操做流程以下:

當用戶進程調用了recvfrom,kernel就開始了IO的第一個階段,準備數據。對於網絡IO來講,不少時候數據在一開始尚未到達。好比尚未收到一個完整的UDP包,這個時候kernel就要等待足夠的數據到來。這個過程須要等待,也就是說數據被拷貝到操做系統內核的緩衝區中是須要一個過程的。而在用戶進程這邊,整個進程會被阻塞。當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶內存,而後kernel返回結果,用戶進程才解除block的狀態,從新運行起來。

因此,blocking IO的特色就是在IO執行的兩個階段都被block了。

Linux下,能夠經過設置Socket使其變爲non-blocking。當對一個non-blocking socket執行讀操做時,流程以下:

當用戶進程發出read操做時,若是kernel中的數據尚未準備好,那麼它並不會block用戶進程,而是馬上返回一個error。從用戶進程角度講,它發起一個read操做後,並不須要等待,而是立刻就獲得了一個結果。用戶進程判斷結果是一個error時,它就知道數據尚未準備好,因而它能夠再次發送read操做。一旦kernel中的數據準備好了,而且又再次收到了用戶進程的system call,那麼它立刻將數據拷貝到了用戶內存,而後返回。

因此,nonblocking IO的特色是用戶進程須要不斷的主動詢問kernel數據好了沒有。

IO multiplexing就是平時所說的select、poll、epoll,有些地方也稱這種IO方式爲event driven IO。select/epoll的好處就在於單個process就能夠同時處理多個網絡鏈接的IO。它的基本原理就是select、poll、epoll這個function會不斷的輪詢所負責的全部socket,當某個socket有數據到達了,就通知用戶進程。

當用戶進程調用了select,那麼整個進程會被block。而同時kernel會"監視"全部select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操做,將數據從kernel拷貝到用戶進程。

因此,I/O多了複用的特色是經過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函數就能夠返回。

這個圖和blocking IO的圖其實並無太大的不一樣。事實上還更差一些,由於這裏須要使用兩個system call(select和recvfrom),而blocking IO只調用了一個system call(recvfrom)。可是用select的優點在於它能夠同時處理多個connection。

實際在IO multiplexing Model中,對於每個socket通常都設置成爲non-blocking。可是如上圖所示整個用戶的process實際上是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。

Linux下的asynchronous IO其實用得不多。

用戶進程發起read操做以後,離開就能夠開始去作其它的事。而另外一個方面,從kernel的角度,當它受到一個asynchronous read以後,首先它會馬上返回,因此不會對用戶進程產生任何block。而後kernel會等待數據準備完成,而後將數據拷貝到用戶內存,當這一切都完成以後,kernel會給用戶進程發送一個signal,告訴它read操做完成了。

總結

blocking和non-blocking的區別

調用blocking IO會一直block,直到對應的進程操做完成。而non-blocking IO在kernel還在準備數據的狀況下就會馬上返回。

synchronous IO和asynchronous IO的區別

在說明synchronous IO和asynchronous IO的區別以前,須要先給出二者的定義。POSIX的定義:

synchronous IO會致使請求進程被阻塞,直到該輸I/O操做完成。

asynchronous IO不會致使請求進程被阻塞。

二者的區別就在於synchronous IO作"IO operation"的時候會將process阻塞。按照這個定義以前所述的blocking IO、non-blocking IO、IO multiplexing都屬於synchronous IO。

有人認爲non-blocking IO並無被block,這裏是很是容易誤解的地方。定義中所指的"IO operation"是指真實的IO操做,就是例子中的recvfrom這個system call。non-blocking IO在執行recvfrom這個system call的時候,若是kernel的數據沒有準備好,這時候不會block進程。可是當kernel中數據準備好的時候,recvfrom會將數據從kernel拷貝到用戶內存中,這個時候進程是被block了,這段時間內進程是被block的。

而asynchronous IO則不同,當進程發起IO操做以後,就直接返回不再理睬了,直到kernel發送一個信號,告訴進程說IO完成。在這整個過程當中進程徹底沒有被block。

各個IO model的比較以下圖:

經過上面的圖片能夠發現non-blocking IO和asynchronous IO的區別仍是很明顯的。在non-blocking IO中,雖然進程大部分時間都不會被block,可是它仍然要求進程主動的check,而且當數據準備完成以後,也須要進程主動的再次調用recvfrom來說數據拷貝到用戶內存。而asynchronous IO則徹底不一樣,它就像是用戶進程將整個IO操做交給了他人(kernel)完成,而後kernel作完後發信號通知。在此期間用戶進程不須要去檢查IO操做的狀態,也不須要主動的去拷貝數據。

I/O多路複用select、poll、epoll詳解

select、poll、epoll都是IO多路複用的機制。I/O多路複用就是經過一種機制,一個進程能夠監視多個描述符,一旦某個描述符就緒(通常是讀就緒或者寫就緒),可以通知程序進行相應的讀寫操做。但select、poll、epoll本質上都是同步I/O,由於他們都須要在讀寫事件就緒後本身負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需本身負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。

select

select(rlist,wlist,xlist,timeout=None)

select函數監視的文件描述符分3類,分別是writefds、readfds和execptfds。調用後select函數會阻塞,直到有描述符就緒(有數據可讀、可寫或有except)或者超時(timeout指定等待時間,若是當即返回設爲null便可)函數返回。當select函數返回後,能夠經過遍歷fdset,來找到就緒的描述符。

select目前幾乎在全部的平臺上支持,其良好跨平臺支持也是它的一個優勢。select的一個缺點在於單個進程可以監視的文件描述符的數量存在最大限制,在Linux上通常爲1024,能夠經過修改宏定義甚至從新編譯內核的方式提高這一限制,可是這樣也會形成效率的下降。

poll

int poll(struct pollfd *fds,unsigned,int nfds,int timeout)

select使用了三個位圖來表示三個fdset的方式,poll使用一個pollfd的指針實現。

struct pollfd{

int fd; # 文件描述符

short events; # 請求

short revents; # 響應

}

pollfd結構包含了要監視的event和發生的event,再也不使用select"參數-值"傳遞的方式。同時pollfd並無最大數量限制(可是數量過多後性能也是會降低)。和select函數同樣,poll返回後,須要輪詢pollfd來獲取就緒的描述符。

從上面能夠看出,select和poll都須要在返回後經過遍歷文件描述符來獲取已經就緒的socket。事實上,同時鏈接的大量客戶端在一時刻可能只有不多的處於就緒狀態,所以隨着監視的描述符數量的增加,其效率也會線性降低。

epoll

epoll是在2.6內核中提出的,是以前的select和poll的加強版本。相對於select和poll來講,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。

epoll操做過程須要三個接口。

int epoll_create(int size); # 建立一個epoll的句柄,size用來告訴內核監聽的數量

int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);

int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout);

int epoll_create(int size);

建立一個epoll的句柄,size用來告訴內核監聽的數量,這個參數不一樣於select()中的第一個參數,給出最大監聽的fd+1的值,參數size並非限制了epoll所能監聽的描述符最大個數,只是對內核初始分配內部數據結構的一個建議。

當建立好epoll句柄後,它就會佔用一個fd值,在linux下若是查看/proc/進程id/fd/,是可以看到這個fd的,因此在使用完epoll後,必須調用close()關閉,不然可能致使fd被耗盡。

函數是對指定描述符fd執行op操做。

epfd:epoll_create()的返回值。

op:op操做,用三個宏來表示,添加EPOLL_CTL_ADD,刪除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分別添加、刪除和修改對fd的監聽事件。

fd:須要監聽的fd(文件描述符)。

epoll_event:內核須要監聽的目標。

等待epfd上的io事件,最多返回maxevents個事件。

參數events用來從內核獲得事件的集合,maxevents告以內核這個events有多大,這個maxevents的值不能大於建立epoll_create()時的size,參數timeout是超時時間(毫秒,0會當即返回,-1將不肯定)。該函數返回須要處理的事件數目,如返回0表示已超時。

select、poll、epoll三者的區別

select最先於1983年出如今4.2BSD中,它經過一個select()系統調用來監視多個文件描述符的數組,當select()返回後,該數組中就緒的文件描述符便會被內核修改標誌位,使得進程能夠得到這些文件描述符從而進行後續的讀寫操做。

select目前幾乎在全部的平臺上支持,其良好跨平臺支持也是它的一個優勢,事實上從如今看來,這也是它所剩很少的優勢之一。

select的一個缺點在於單個進程可以監視的文件描述符的數量存在最大限制,在Linux上通常爲1024,不過能夠經過修改宏定義甚至從新編譯內核方式提高這一限制。

另外,select()所維護的存儲大量文件描述符的數據結構,隨着文件描述符數量的增大,其複製的開銷也線性增大。同時,因爲網絡響應時間的延遲使得大量TCP鏈接處於非活躍狀態,但調用select()會對全部socket進行一次線性掃描,因此這也浪費了必定的開銷。

poll在1986年誕生於System V Release 3,它和select在本質上沒有多大差異,可是poll沒有最大文件描述符數量的限制。

poll和select一樣存在一個缺點就是,包含大量文件描述符的數組被總體複製與用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨着文件描述符數量的增長而線性增大。

另外,select()和poll()將就緒的文件描述符告訴進程後,若是進程沒有對其進行IO操做,那麼下次調用select()和poll()的時候將再次報告這些文件描述符,因此它們通常不會丟失就緒的消息,這種方式稱爲水平觸發(Level Triggered)。

直到Linux 2.6纔出現了由內核直接支持的實現方法,那就是epoll,它幾乎具有了以前所說的一切優勢,被公認爲Linux 2.6下性能最好的多路I/O就緒通知方法。

epoll能夠同時支持水平觸發和邊緣觸發(Edge Triggered,只告訴進程哪些文件描述符剛剛變爲就緒狀態,它只說一遍,若是咱們沒有采起行動,那麼它就不會再次告知,這種方式稱爲邊緣觸發),理論上邊緣觸發的性能要更高一些,但代碼實現至關複雜。

epoll一樣只告知那些就緒的文件描述符,並且當咱們調用epoll_wait()得到就緒文件描述符時,返回的不是實際的描述符,而是一個表明就緒描述符數量的值,你只須要去epoll指定的一個數組中依次取得相應數量的文件描述符便可,這裏也使用了內存映射(mmap)技術,這樣便完全省掉了這些文件描述符在系統調用時複製的開銷。

另外一個本質的改進在於epoll採用基於事件的就緒通知方式。在select/poll中,進程只有在調用必定的方法後,內核纔對全部監視的文件描述符進行描述,而epoll事先經過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會採用相似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便獲得通知。

Python select

Python的select()方法直接調用操做系統的IO接口,它監控sockets、open files、pipes(全部帶fileno()方法的文件句柄)什麼時候變成readable和writeable或者通訊錯誤,select()使得同時監控多個鏈接變得簡單,而且這比寫一個長循環來等待和監控多客戶端鏈接要高效,由於select直接經過操做系統提供的C的網絡接口進行操做,而不是經過Python的解釋器。

注意:Using Python's file objects with select() works for Unix, but is not supported under Windows.

select_socket_server

__author__ = 'Golden'

#!/usr/bin/env python3

# -*- coding:utf-8 -*-

import select,socket,sys,queue

server = socket.socket()

server.setblocking(0)

server_addr = ('localhost',6969)

print('starting up on %s port %s'%server_addr)

server.bind(server_addr)

server.listen(5)

# 監測本身,由於server自己也是個fd

inputs = [server,]

outputs = []

message_queues = {}

print('waiting for next event...')

# 若是沒有任何fd就緒,程序會一直阻塞在這裏

readable,writeable,exeptional = select.select(inputs,outputs,inputs)

# 每一個s就是一個socket

for s in readable:

# 上面server本身也當作一個fd放在了inputs列表裏,傳給了select,若是s是server表明server這個fd就緒了,即新的鏈接進來

if s is server:

# 接收這個鏈接

conn,client_addr = s.accept()

print('new connection from',client_addr)

conn.setblocking(0)

"""

爲了避免阻塞整個程序,不會馬上在這裏開始接收客戶端發來的數據,把它放到inputs裏,下一次loop時,

這個新鏈接就會被交給select去監聽,若是這個鏈接的客戶端發來了數據,那麼這個鏈接的fd在server端就會變成就緒的,

select就會把這個數據返回到readable列表裏,而後就能夠loop readable列表,取出這個鏈接,開始接收數據

inputs.append(conn)

# 接收到客戶端的數據後,不馬上返回,暫存在隊列裏,之後發送

message_queues[conn] = queue.Queue()

# s不是server那就只會是一個與客戶端創建的鏈接的fd

else:

# 接收客戶端的數據

if data:

print('收到來自【%s】的數據:'%s.getpeername()[0],data)

# 收到的數據先放入queue裏,一會返回給客戶端

message_queues[s].put(data)

if s not in outputs:

# 爲了避免影響處理與其它客戶端的鏈接,這裏不馬上返回數據給客戶端

outputs.append(s)

# 若是收不到data,表明客戶端已斷開

print('客戶端已斷開...',s)

if s in outputs:

# 清理已斷開的鏈接

outputs.remove(s)

inputs.remove(s)

del message_queues[s]

for s in writeable:

next_msg = message_queues[s].get_nowait()

except queue.Empty:

print('client [%s]'%s.getpeername()[0],'queue is empty...')

print('sending msg to [%s]'%s.getpeername()[0],next_msg)

s.send(next_msg.upper())

for s in exeptional:

print('handling exception for',s.getpeername())

select_socket_client

import socket,sys

messages = [b'This is the message.',

b'It will be sent',

b'in parts.',

]

server_address = ('localhost',6969)

# 建立一個TCP/IP鏈接

socks = [socket.socket(socket.AF_INET,socket.SOCK_STREAM),

socket.socket(socket.AF_INET,socket.SOCK_STREAM),

socket.socket(socket.AF_INET,socket.SOCK_STREAM),]

print('connecting to %s port %s'%server_address)

for s in socks:

s.connect(server_address)

for message in messages:

# 發送數據

print('%s:sending "%s"'%(s.getsockname(),message))

s.send(message)

# 接收數據

print('%s:received "%s"'%(s.getsockname(),data))

print(sys.stderr,'closing socket',s.getsockname())

selectors

selectors模塊能夠實現IO多路複用,它具備根據平臺選出最佳的IO多路機制,例如在windows上默認是select模式,而在linux上默認是epoll。常分爲三種模式select、poll和epoll。

selector_socket_server:

import selectors,socket

sel = selectors.DefaultSelector()

def accept(sock,mask):

conn,addr = sock.accept()

print('accrpted',conn,'form',addr)

sel.register(conn,selectors.EVENT_READ,read)

def read(conn,mask):

print('echoing',repr(data),'to',conn)

print('closing',conn)

sel.unregister(conn)

sock = socket.socket()

sock.bind(('localhost',6969))

sock.listen(100)

sock.setblocking(0)

sel.register(sock,selectors.EVENT_READ,accept)

events = sel.select()

for key,mask in events:

callback = key.data

callback(key.fileobj,mask)

代碼方面還有些許粗糙。你們多多見諒,要改進的地方,歡迎你們在下方留言。小編必定謹記!!

相關文章
相關標籤/搜索