1. 程式人生 > 實用技巧 >三種直線段繪製方法:DDA演算法、B演算法和中點分割法

三種直線段繪製方法:DDA演算法、B演算法和中點分割法

一、協程

程序,執行緒,協程

面試機率極高

程序:資源分配的最小單位

執行緒:CPU排程的最小單位

協程:實現單執行緒下的併發,屬於執行緒下

協程介紹

協程是一種使用者態的輕量級執行緒,即協程是由使用者程式自己控制排程的。

在一個執行緒中可能有很多的函式,比如函式1執行時遇到了io,此時協程把當前狀態儲存,切到函式2,當函式2遇到了io,又儲存狀態,再切其他函式

協程需要做到:儲存狀態+切換,這就能讓人看起來是併發

我們利用yield來儲存狀態+切換。

yield:只要函式中有yield關鍵字,其返回值就是一個生成器。

如果是純計算的多個函式,單純的切換不但不會提高效率,反而會降低效率。

協程並不是真實存在的,而是程式設計師們臆想出來的。我們造出協程這個概念的目的,是因為cpu執行的執行緒的時候,遇到io就會跳到其他執行緒執行,那麼我們現在當我們的執行緒遇到了io時候,直接跳到下一個函式去接著計算,如此不斷的切函式。如此就偽裝了我們這個執行緒一直在執行,沒有遇到io,等到cpu的時間片到了,cpu才會去其他執行緒執行。

當協程沒有利用好的話,會很大影響我們的效率,如函式A與B都阻塞了,但是我們還是在函式A與B中反覆的切換,那麼cpu的效率就被犧牲了。

協程的優缺點(瞭解)

優點如下:

  1. 協程的切換開銷更小,屬於程式級別的切換,作業系統完全感知不到,因而更加輕量級
  2. 單執行緒內就可以實現併發的效果,最大限度地利用cpu

缺點如下:

  1. 協程的本質是單執行緒下,無法利用多核,可以是一個程式開啟多個程序,每個程序內開啟多個執行緒,每個執行緒內開啟協程
  2. 協程指的是單個執行緒,因而一旦協程出現阻塞,將會阻塞整個執行緒

總結協程特點:

  1. 必須在只有一個單執行緒裡實現併發
  2. 修改共享資料不需加鎖
  3. 使用者程式裡自己儲存多個控制流的上下文棧
  4. 附加:一個協程遇到IO操作自動切換到其它協程(如何實現檢測IO,yield、greenlet都無法實現,就用到了gevent模組(select機制))

二、greenlet模組(瞭解)

上述的介紹的yield不是很好,現在介紹一個也不是很好的模組,greenlet,遇到io不會自動切。

from greenlet import greenlet
import time
# 遇到io不會切,初級模組,gevent模組基於它寫的,處理io切換
def eat():
    print('我吃了一口')
    time.sleep(1)
    p.switch()
    print('我又吃了一口')
    p.switch()

def play():
    print('我玩了一會')
    e.switch()
    print('我又玩了一會')

if __name__ == '__main__':
    e = greenlet(eat)
    p = greenlet(play)
    e.switch()

三、gevent模組

基於greenlet寫的,實現了遇到了io自動切換

他開的不是執行緒,而是協程,為了實現單執行緒下實現併發。

猴子補丁,其實是一種替換的思想,擁有在模組執行時替換的功能,即動態替換

需要使用到猴子補丁,讓我們本身的模組方法被替換成gevent模組下的方法,不然的話使用該模組就沒有效果了

from gevent import monkey;monkey.patch_all()
import gevent
import time

def eat(name):
    print('%s 吃了一口' % name)
    time.sleep(1)  # io操作,被猴子補丁替換之後,gevent.sleep()
    print('%s 又吃了一口' % name)


def play(name):
    print('%s 玩了一會' % name)
    time.sleep(2)
    print('%s 又玩了一會' % name)


if __name__ == '__main__':
    ctim = time.time()
    e = gevent.spawn(eat,'lqz')
    p = gevent.spawn(play,'lqz')
    e.join() # 等待e執行完成
    p.join()
    print('主')
    print(time.time() - ctim)  #2.0165154933929443

四、asyncio(瞭解)

官方支援的庫

# 把普通函式變成協程函式
# 3.5以前這麼寫
import time
import asyncio

@asyncio.coroutine
def task():
    print('開始了')
    yield from asyncio.sleep(1)  #asyncio.sleep(1)模擬io
    print('結束了')


loop=asyncio.get_event_loop()  # 獲取一個時間迴圈物件#

# 協程函式加括號,並不會真正的去執行,它需要提交給loop,讓loop迴圈著去執行
# 協程函式列表

ctime=time.time()
t=[task(),task()]
loop.run_until_complete(asyncio.wait(t))
loop.close()
print(time.time()-ctime)




# 3.5以後
import time
import asyncio
from threading import current_thread
# 表示我是協程函式,等同於3.5之前的裝飾器
async def task():
    print('開始了')
    print(current_thread().name)
    await asyncio.sleep(3)  # await等同於原來的yield from
    print('結束了')

async def task2():
    print('開始了')
    print(current_thread().name)
    await asyncio.sleep(2)
    print('結束了')

loop=asyncio.get_event_loop()

ctime=time.time()
t=[task(),task2()]
loop.run_until_complete(asyncio.wait(t))
loop.close()
print(time.time()-ctime)

五、io模型(面試重點)

IO操作本質

1.資料複製的過程中不會消耗CPU

2.記憶體分為核心緩衝區和使用者緩衝區

3.應用程式不能直接操作記憶體緩衝區

3.各種資源包括網路下載的資源或者硬碟的資源載入到記憶體時候,先來到核心緩衝區,再copy到應用程式的緩衝區,應用程式才能用這個資料

io模型

阻塞io(BI/O)

在等待資料的時候,一直在等待,如果已經有資料,就等著從核心緩衝區複製到使用者緩衝區,如果沒資料,就等著cpu先去把資料獲取到核心緩衝區,再等著資料從核心緩衝區複製到使用者緩衝區。

'''
舉個例子:一個人去 商店買一把菜刀,
他到商店問老闆有沒有菜刀(發起系統呼叫)
如果有(表示在核心緩衝區有需要的資料)
老闆直接把菜刀給買家(從核心緩衝區拷貝到使用者緩衝區)
這個過程買家一直在等待

如果沒有,商店老闆會向工廠下訂單(IO操作,等待資料準備好)
工廠把菜刀運給老闆(進入到核心緩衝區)
老闆把菜刀給買家(從核心緩衝區拷貝到使用者緩衝區)
這個過程買家一直在等待
是同步io
'''

非阻塞io(NI/O)

在等待資料的時候,可以切到其他地方執行。即如果此時沒資料,就管自己做事情,且每隔一定時間傳送詢問是否有了資料。當多次詢問後,資料已經到了核心緩衝區。那麼就等著資料從核心緩衝區複製到使用者緩衝區(此個過程就不能去做其他事情了)

'''
舉個例子:一個人去 商店買一把菜刀,
他到商店問老闆有沒有菜刀(發起系統呼叫)
老闆說沒有,在向工廠進貨(返回狀態)
買家去別地方玩了會,又回來問,菜刀到了麼(發起系統呼叫)
老闆說還沒有(返回狀態)
買家又去玩了會(不斷輪詢)
最後一次再問,菜刀有了(資料準備好了)
老闆把菜刀遞給買家(從核心緩衝區拷貝到使用者緩衝區)

整個過程輪詢+等待:輪詢時沒有等待,可以做其他事,從核心緩衝區拷貝到使用者緩衝區需要等待
是同步io

同一個執行緒,同一時刻只能監聽一個socket,造成浪費,引入io多路複用,同時監聽讀個socket
'''

io多路複用(Multiplexing - I/O)

目前使用最多是就是這個

是阻塞式io,阻塞在資料從核心緩衝區複製到使用者緩衝區的地方,即這個過程還是得等待的。

'''
舉個例子:多個人去 一個商店買菜刀,
多個人給老闆打電話,說我要買菜刀(發起系統呼叫)
老闆把每個人都記錄下來(放到select中)
老闆去工廠進貨(IO操作)
有貨了,再挨個通知買到的人,來取刀(通知/返回可讀條件)
買家來到商店等待,老闆把到給買家(從核心緩衝區拷貝到使用者緩衝區)

多路複用:老闆可以同時接受很多請求(select模型最大1024個,epoll模型),
但是老闆把到給買家這個過程,還需要等待,
是同步io
'''

強調:
 1. 如果處理的連線數不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server效能更好,可能延遲還更大。select/epoll的優勢並不是對於單個連線能處理得更快,而是在於能處理更多的連線。
 2. 在多路複用模型中,對於每一個socket,一般都設定成為non-blocking,但是,如上圖所示,整個使用者的process其實是一直被block的。只不過process是被select這個函式block,而不是被socket IO給block。
 結論: select的優勢在於可以處理多個連線,不適用於單個連線

關於多路複用中的select,poll,epoll
# 1 select poll 和epoll都是io多路複用技術
select, poll , epoN都是io多路複用的機制。I/O多路複用就是通過一種機 制個程序可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。但select, poll , epoll本質上都是同步I/O ,因為他們都需要在讀寫事件就緒後自己負責進行讀寫, 也就是說這個讀寫過程是阻塞的,而非同步I/O則無需自己負責進行讀寫,異 步I/O的實現會負責把資料從核心拷貝到使用者空間。

# 2 select
select函式監視的檔案描述符分3類,分別是writefds、readfds、和 exceptfds。呼叫後select函式會阻塞,直到有描述副就緒(有資料可讀、 可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回 設為null即可),函式返回。當select函式返回後,可以通過遍歷fdset,來 找到就緒的描述符。
select目前幾乎在所有的平臺上支援,其良好跨平臺支援也是它的一個 優點。select的一個缺點在於單個程序能夠監視的檔案描述符的數量存在最大限制,在Linux上一般為1024 ,可以通過修改巨集定義甚至重新編譯核心的 方式提升這一限制,但是這樣也會造成效率的降低。
# 3 poll
不同於select使用三個點陣圖來表示三個fdset的方式,poll使用一個 pollfd的指標實現。
pollfd結構包含了要監視的event和發生的event,不再使用select '引數-值'傳遞的方式。同時,pollfd並沒有最大數量限制(但是數量過大後 效能也是會下降)。和select函式一樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符。
從上面看,select和poll都需要在返回後,通過遍歷檔案描述符來獲取 已經就緒的socket。事實上,同時連線的大量客戶端在一時刻可能只有很少的處於就緒狀態,因此隨著監視的描述符數量的增長,其效率也會線性下降

# 4 epoll
epoll是在linux2.6核心中提出的,是之前的select和poll的增強版本。相對 於select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個文 件描述符管理多個描述符,將使用者關係的檔案描述符的事件存放到核心的一個事件表中,這樣在使用者空間和核心空間的copy只需一次。

# 5 更好的例子理解
老師檢查同學作業,一班50個人,一個一個問,同學,作業寫完了沒?select,poll
老師檢查同學作業,一班50個人,同學寫完了主動舉手告訴老師,老師去檢查 epoll

# 6 總結
在併發高的情況下,連線活躍度不高,epoll比select好,網站http的請求,連了就斷掉
併發性不高,同時連線很活躍,select比epoll好,websocket的連線,長連線,遊戲開發

select(windows支援,windows不支援epoll,官方不提供redis的window版本),poll(linux支援),epoll(linux支援)

# 本部分為了解內容,感興趣可看
IO複用:為了解釋這個名詞,首先來理解下複用這個概念,複用也就是共用的意思,這樣理解還是有些抽象,為此,咱們來理解下複用在通訊領域的使用,在通訊領域中為了充分利用網路連線的物理介質,往往在同一條網路鏈路上採用時分複用或頻分複用的技術使其在同一鏈路上傳輸多路訊號,到這裡我們就基本上理解了複用的含義,即公用某個“介質”來儘可能多的做同一類(性質)的事,那IO複用的“介質”是什麼呢?為此我們首先來看看伺服器程式設計的模型,客戶端發來的請求服務端會產生一個程序來對其進行服務,每當來一個客戶請求就產生一個程序來服務,然而程序不可能無限制的產生,因此為了解決大量客戶端訪問的問題,引入了IO複用技術,即:一個程序可以同時對多個客戶請求進行服務。也就是說IO複用的“介質”是程序(準確的說複用的是select和poll,因為程序也是靠呼叫select和poll來實現的),複用一個程序(select和poll)來對多個IO進行服務,雖然客戶端發來的IO是併發的但是IO所需的讀寫資料多數情況下是沒有準備好的,因此就可以利用一個函式(select和poll)來監聽IO所需的這些資料的狀態,一旦IO有資料可以進行讀寫了,程序就來對這樣的IO進行服務。


理解完IO複用後,我們在來看下實現IO複用中的三個API(select、poll和epoll)的區別和聯絡

select,poll,epoll都是IO多路複用的機制,I/O多路複用就是通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知應用程式進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而非同步I/O則無需自己負責進行讀寫,非同步I/O的實現會負責把資料從核心拷貝到使用者空間。三者的原型如下所示:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

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



 1.select的第一個引數nfds為fdset集合中最大描述符值加1,fdset是一個位數組,其大小限制為__FD_SETSIZE(1024),位陣列的每一位代表其對應的描述符是否需要被檢查。第二三四引數表示需要關注讀、寫、錯誤事件的檔案描述符位陣列,這些引數既是輸入引數也是輸出引數,可能會被核心修改用於標示哪些描述符上發生了關注的事件,所以每次呼叫select前都需要重新初始化fdset。timeout引數為超時時間,該結構會被核心修改,其值為超時剩餘的時間。

 select的呼叫步驟如下:

(1)使用copy_from_user從使用者空間拷貝fdset到核心空間

(2)註冊回撥函式__pollwait

(3)遍歷所有fd,呼叫其對應的poll方法(對於socket,這個poll方法是sock_poll,sock_poll根據情況會呼叫到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll為例,其核心實現就是__pollwait,也就是上面註冊的回撥函式。

(5)__pollwait的主要工作就是把current(當前程序)掛到裝置的等待佇列中,不同的裝置有不同的等待佇列,對於tcp_poll 來說,其等待佇列是sk->sk_sleep(注意把程序掛到等待佇列中並不代表程序已經睡眠了)。在裝置收到一條訊息(網路裝置)或填寫完檔案數 據(磁碟裝置)後,會喚醒裝置等待佇列上睡眠的程序,這時current便被喚醒了。

(6)poll方法返回時會返回一個描述讀寫操作是否就緒的mask掩碼,根據這個mask掩碼給fd_set賦值。

(7)如果遍歷完所有的fd,還沒有返回一個可讀寫的mask掩碼,則會呼叫schedule_timeout是呼叫select的程序(也就是 current)進入睡眠。當裝置驅動發生自身資源可讀寫後,會喚醒其等待佇列上睡眠的程序。如果超過一定的超時時間(schedule_timeout 指定),還是沒人喚醒,則呼叫select的程序會重新被喚醒獲得CPU,進而重新遍歷fd,判斷有沒有就緒的fd。

(8)把fd_set從核心空間拷貝到使用者空間。

總結下select的幾大缺點:

(1)每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,這個開銷在fd很多時會很大

(2)同時每次呼叫select都需要在核心遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大

(3)select支援的檔案描述符數量太小了,預設是1024

 

2.  poll與select不同,通過一個pollfd陣列向核心傳遞需要關注的事件,故沒有描述符個數的限制,pollfd中的events欄位和revents分別用於標示關注的事件和發生的事件,故pollfd陣列只需要被初始化一次。

 poll的實現機制與select類似,其對應核心中的sys_poll,只不過poll向核心傳遞pollfd陣列,然後對pollfd中的每個描述符進行poll,相比處理fdset來說,poll效率更高。poll返回後,需要對pollfd中的每個元素檢查其revents值,來得指事件是否發生。

 

3.直到Linux2.6才出現了由核心直接支援的實現方法,那就是epoll,被公認為Linux2.6下效能最好的多路I/O就緒通知方法。epoll可以同時支援水平觸發和邊緣觸發(Edge Triggered,只告訴程序哪些檔案描述符剛剛變為就緒狀態,它只說一遍,如果我們沒有采取行動,那麼它將不會再次告知,這種方式稱為邊緣觸發),理論上邊緣觸發的效能要更高一些,但是程式碼實現相當複雜。epoll同樣只告知那些就緒的檔案描述符,而且當我們呼叫epoll_wait()獲得就緒檔案描述符時,返回的不是實際的描述符,而是一個代表就緒描述符數量的值,你只需要去epoll指定的一個數組中依次取得相應數量的檔案描述符即可,這裡也使用了記憶體對映(mmap)技術,這樣便徹底省掉了這些檔案描述符在系統呼叫時複製的開銷。另一個本質的改進在於epoll採用基於事件的就緒通知方式。在select/poll中,程序只有在呼叫一定的方法後,核心才對所有監視的檔案描述符進行掃描,而epoll事先通過epoll_ctl()來註冊一個檔案描述符,一旦基於某個檔案描述符就緒時,核心會採用類似callback的回撥機制,迅速啟用這個檔案描述符,當程序呼叫epoll_wait()時便得到通知。

 

epoll既然是對select和poll的改進,就應該能避免上述的三個缺點。那epoll都是怎麼解決的呢?在此之前,我們先看一下epoll 和select和poll的呼叫介面上的不同,select和poll都只提供了一個函式——select或者poll函式。而epoll提供了三個函 數,epoll_create,epoll_ctl和epoll_wait,epoll_create是建立一個epoll控制代碼;epoll_ctl是注 冊要監聽的事件型別;epoll_wait則是等待事件的產生。

  對於第一個缺點,epoll的解決方案在epoll_ctl函式中。每次註冊新的事件到epoll控制代碼中時(在epoll_ctl中指定 EPOLL_CTL_ADD),會把所有的fd拷貝進核心,而不是在epoll_wait的時候重複拷貝。epoll保證了每個fd在整個過程中只會拷貝 一次。

  對於第二個缺點,epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的裝置等待佇列中,而只在 epoll_ctl時把current掛一遍(這一遍必不可少)併為每個fd指定一個回撥函式,當裝置就緒,喚醒等待佇列上的等待者時,就會呼叫這個回撥 函式,而這個回撥函式會把就緒的fd加入一個就緒連結串列)。epoll_wait的工作實際上就是在這個就緒連結串列中檢視有沒有就緒的fd(利用 schedule_timeout()實現睡一會,判斷一會的效果,和select實現中的第7步是類似的)。

  對於第三個缺點,epoll沒有這個限制,它所支援的FD上限是最大可以開啟檔案的數目,這個數字一般遠大於2048,舉個例子, 在1GB記憶體的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統記憶體關係很大。

總結:

(1)select,poll實現需要自己不斷輪詢所有fd集合,直到裝置就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要呼叫 epoll_wait不斷輪詢就緒連結串列,期間也可能多次睡眠和喚醒交替,但是它是裝置就緒時,呼叫回撥函式,把就緒fd放入就緒連結串列中,並喚醒在 epoll_wait中進入睡眠的程序。雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的 時候只要判斷一下就緒連結串列是否為空就行了,這節省了大量的CPU時間,這就是回撥機制帶來的效能提升。

(2)select,poll每次呼叫都要把fd集合從使用者態往核心態拷貝一次,並且要把current往裝置等待佇列中掛一次,而epoll只要 一次拷貝,而且把current往等待佇列上掛也只掛一次(在epoll_wait的開始,注意這裡的等待佇列並不是裝置等待佇列,只是一個epoll內 部定義的等待佇列),這也能節省不少的開銷。
###############
這三種IO多路複用模型在不同的平臺有著不同的支援,而epoll在windows下就不支援,好在我們有selectors模組,幫我們預設選擇當前平臺下最合適的
##############

#服務端
from socket import *
import selectors

sel=selectors.DefaultSelector()
def accept(server_fileobj,mask):
    conn,addr=server_fileobj.accept()
    sel.register(conn,selectors.EVENT_READ,read)

def read(conn,mask):
    try:
        data=conn.recv(1024)
        if not data:
            print('closing',conn)
            sel.unregister(conn)
            conn.close()
            return
        conn.send(data.upper()+b'_SB')
    except Exception:
        print('closing', conn)
        sel.unregister(conn)
        conn.close()

server_fileobj=socket(AF_INET,SOCK_STREAM)
server_fileobj.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
server_fileobj.bind(('127.0.0.1',8088))
server_fileobj.listen(5)
server_fileobj.setblocking(False) #設定socket的介面為非阻塞
sel.register(server_fileobj,selectors.EVENT_READ,accept) #相當於網select的讀列表裡append了一個檔案控制代碼server_fileobj,並且綁定了一個回撥函式accept

while True:
    events=sel.select() #檢測所有的fileobj,是否有完成wait data的
    for sel_obj,mask in events:
        callback=sel_obj.data #callback=accpet
        callback(sel_obj.fileobj,mask) #accpet(server_fileobj,1)

#客戶端
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',8088))

while True:
    msg=input('>>: ')
    if not msg:continue
    c.send(msg.encode('utf-8'))
    data=c.recv(1024)
    print(data.decode('utf-8'))

非同步io(AI/O)

發起請求立刻得到回覆,不用掛起等待; 資料會由核心程序主動完成拷貝

'''
舉個例子:還是買菜刀
現在是網上下單到商店(系統呼叫)
商店確認(返回)
商店去進貨(io操作)
商店收到貨把貨發個賣家(從核心緩衝區拷貝到使用者緩衝區)
買家收到貨(指定訊號)

整個過程無等待
非同步io

AIO框架在windows下使用windows IOCP技術,在Linux下使用epoll多路複用IO技術模擬非同步IO

市面上多數的高併發框架,都沒有使用非同步io而是用的io多路複用,因為io多路複用技術很成熟且穩定,並且在實際的使用過程中,非同步io並沒有比io多路複用效能提升很多,沒有達到很明顯的程度
並且,真正的AIO編碼難度比io多路複用高很多
'''

訊號驅動io

目前還在理論階段,不做討論

同步I/O與非同步I/O

  • 同步I/O
    • 概念:導致請求程序阻塞的I/O操作,直到I/O操作任務完成
    • 型別:BIO、NIO、IO Multiplexing
  • 非同步I/O
    • 概念:不導致程序阻塞的I/O操作
    • 型別:AIO

注意:

  • 同步I/O與非同步I/O判斷依據是,是否會導致使用者程序阻塞
  • BIO中socket直接阻塞等待(使用者程序主動等待,並在拷貝時也等待)
  • NIO中將資料從核心空間拷貝到使用者空間時阻塞(使用者程序主動詢問,並在拷貝時等待)
  • IO Multiplexing中select等函式為阻塞、拷貝資料時也阻塞(使用者程序主動等待,並在拷貝時也等待)
  • AIO中從始至終使用者程序都沒有阻塞(使用者程序是被動的)

六、併發-並行-同步-非同步-阻塞-非阻塞

1 併發
併發是指一個時間段內,有幾個程式在同一個cpu上執行,但是同一時刻,只有一個程式在cpu上執行
跑步,鞋帶開了,停下跑步,繫鞋帶
2 並行
指任意時刻點上,有多個程式同時執行在多個cpu上
跑步,邊跑步邊聽音樂
3 同步:
指程式碼呼叫io操作時,必須等待io操作完成才返回的呼叫方式
4 非同步
非同步是指程式碼呼叫io操作時,不必等io操作完成就返回呼叫方式
5 阻塞
指呼叫函式時候,當前執行緒別掛起
6 非阻塞
指呼叫函式時候,當前執行緒不會被掛起,而是立即返回

區別:
同步和非同步是訊息通訊的機制
阻塞和非阻塞是函式呼叫機制