大白話五種IO模型

来源:https://www.cnblogs.com/nickchen121/archive/2019/07/07/11145098.html
-Advertisement-
Play Games

[TOC] 一、I/O模型介紹 為了更好地瞭解I/O模型,我們需要事先回顧下:同步、非同步、阻塞、非阻塞 同步(synchronous) I/O和非同步(asynchronous) I/O,阻塞(blocking) I/O和非阻塞(non blocking)I/O分別是什麼,到底有什麼區別?這個問題其實 ...


目錄

一、I/O模型介紹

為了更好地瞭解I/O模型,我們需要事先回顧下:同步、非同步、阻塞、非阻塞

同步(synchronous) I/O和非同步(asynchronous) I/O,阻塞(blocking) I/O和非阻塞(non-blocking)I/O分別是什麼,到底有什麼區別?這個問題其實不同的人給出的答案都可能不同,比如wiki,就認為asynchronous I/O和non-blocking I/O是一個東西。這其實是因為不同的人的知識背景不同,並且在討論這個問題的時候上下文(context)也不相同。所以,為了更好的回答這個問題,我先限定一下本文的上下文。

本文討論的背景是Linux環境下的network I/O。本文最重要的參考文獻是Richard Stevens的“UNIX® Network Programming Volume 1, Third EditI/On: The Sockets Networking ”,6.2節“I/O Models ”,Stevens在這節中詳細說明瞭各種I/O的特點和區別,如果英文夠好的話,推薦直接閱讀。Stevens的文風是有名的深入淺出,所以不用擔心看不懂。本文中的流程圖也是截取自參考文獻。

Stevens在文章中一共比較了五種I/O Model

英文 中文
blocking I/O 阻塞I/O
nonblocking I/O 非阻塞I/O
I/O multiplexing I/O多路復用
signal driven I/O 信號驅動I/O
asynchronous I/O 非同步I/O

再說一下I/O發生時涉及的對象和步驟。對於一個network I/O (這裡我們以read舉例),它會涉及到兩個系統對象,一個是調用這個I/O的process (or thread),另一個就是系統內核(kernel)。當一個read操作發生時,該操作會經歷兩個階段:

  1. 等待數據準備 (Waiting for the data to be ready)
  2. 將數據從內核拷貝到進程中(Copying the data from the kernel to the process)

記住這兩點很重要,因為這些I/O模型的區別就是在兩個階段上各有不同的情況。

在網路環境下,再通俗的講,將I/O分為兩步:

  1. 等;
  2. 數據搬遷。

如果要想提高I/O效率,需要將等的時間降低。

五種I/O模型包括:阻塞I/O、非阻塞I/O、信號驅動I/O、I/O多路轉接、非同步I/O。其中,前四個被稱為同步I/O。

在介紹五種I/O模型時,我會舉生活中老王買車票的例子,加深理解。

二、阻塞I/O模型

以買票的例子舉例,該模型小結為:

# 老王去火車站買票,排隊三天買到一張退票。

# 耗費:在車站吃喝拉撒睡 3天,其他事一件沒乾。

在linux中,預設情況下所有的socket都是blocking,一個典型的讀操作流程大概是這樣:

188-大白話五種IO模型-01.png?x-oss-process=style/watermark

當用戶進程調用了recvfrom這個系統調用,kernel就開始了I/O的第一個階段:準備數據。對於network I/O來說,很多時候數據在一開始還沒有到達(比如,還沒有收到一個完整的UDP包),這個時候kernel就要等待足夠的數據到來。

而在用戶進程這邊,整個進程會被阻塞。當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶記憶體,然後kernel返回結果,用戶進程才解除block的狀態,重新運行起來。所以,blocking I/O的特點就是在I/O執行的兩個階段(等待數據和拷貝數據兩個階段)都被block了。

幾乎所有的程式員第一次接觸到的網路編程都是從listen()、send()、recv() 等介面開始的,使用這些介面可以很方便的構建伺服器/客戶機的模型。然而大部分的socket介面都是阻塞型的。如下圖

ps:所謂阻塞型介面是指系統調用(一般是I/O介面)不返回調用結果並讓當前線程一直阻塞,只有當該系統調用獲得結果或者超時出錯時才返回。

188-大白話五種IO模型-02.png?x-oss-process=style/watermark

實際上,除非特別指定,幾乎所有的I/O介面 ( 包括socket介面 ) 都是阻塞型的。這給網路編程帶來了一個很大的問題,如在調用recv(1024)的同時,線程將被阻塞,在此期間,線程將無法執行任何運算或響應任何的網路請求。

2.1 一個簡單的解決方案

在伺服器端使用多線程(或多進程)。多線程(或多進程)的目的是讓每個連接都擁有獨立的線程(或進程),這樣任何一個連接的阻塞都不會影響其他的連接。

2.2 該方案的問題

開啟多進程或都線程的方式,在遇到要同時響應成百上千路的連接請求,則無論多線程還是多進程都會嚴重占據系統資源,降低系統對外界響應效率,而且線程與進程本身也更容易進入假死狀態。

2.3 改進方案

很多程式員可能會考慮使用“線程池”或“連接池”。“線程池”旨在減少創建和銷毀線程的頻率,其維持一定合理數量的線程,並讓空閑的線程重新承擔新的執行任務。“連接池”維持連接的緩存池,儘量重用已有的連接、減少創建和關閉連接的頻率。這兩種技術都可以很好的降低系統開銷,都被廣泛應用很多大型系統,如websphere、tomcat和各種資料庫等。

2.4 改進後方案的問題

“線程池”和“連接池”技術也只是在一定程度上緩解了頻繁調用I/O介面帶來的資源占用。而且,所謂“池”始終有其上限,當請求大大超過上限時,“池”構成的系統對外界的響應並不比沒有池的時候效果好多少。所以使用“池”必須考慮其面臨的響應規模,並根據響應規模調整“池”的大小。

對應上例中的所面臨的可能同時出現的上千甚至上萬次的客戶端請求,“線程池”或“連接池”或許可以緩解部分壓力,但是不能解決所有問題。總之,多線程模型可以方便高效的解決小規模的服務請求,但面對大規模的服務請求,多線程模型也會遇到瓶頸,可以用非阻塞介面來嘗試解決這個問題。

三、非阻塞I/O模型

以買票的例子舉例,該模型小結為

# 老王去火車站買票,隔12小時去火車站問有沒有退票,三天後買到一張票。

# 耗費:往返車站6次,路上6小時,其他時間做了好多事。

Linux下,可以通過設置socket使其變為non-blocking。當對一個non-blocking socket執行讀操作時,流程是這個樣子:

188-大白話五種IO模型-03.png?x-oss-process=style/watermark

從圖中可以看出,當用戶進程發出read操作時,如果kernel中的數據還沒有準備好,那麼它並不會block用戶進程,而是立刻返回一個error。從用戶進程角度講 ,它發起一個read操作後,並不需要等待,而是馬上就得到了一個結果。用戶進程判斷結果是一個error時,它就知道數據還沒有準備好,於是用戶就可以在本次到下次再發起read詢問的時間間隔內做其他事情,或者直接再次發送read操作。一旦kernel中的數據準備好了,並且又再次收到了用戶進程的system call,那麼它馬上就將數據拷貝到了用戶記憶體(這一階段仍然是阻塞的),然後返回。

也就是說非阻塞的recvform系統調用調用之後,進程並沒有被阻塞,內核馬上返回給進程,如果數據還沒準備好,此時會返回一個error。進程在返回之後,可以乾點別的事情,然後再發起recvform系統調用。重覆上面的過程,迴圈往複的進行recvform系統調用。這個過程通常被稱之為輪詢。輪詢檢查內核數據,直到數據準備好,再拷貝數據到進程,進行數據處理。需要註意,拷貝數據整個過程,進程仍然是屬於阻塞的狀態。所以,在非阻塞式I/O中,用戶進程其實是需要不斷的主動詢問kernel數據準備好了沒有。

3.1 非阻塞I/O實例

#服務端
from socket import *
import time
s=socket(AF_INET,SOCK_STREAM)
s.bind(('127.0.0.1',8080))
s.listen(5)
s.setblocking(False) #設置socket的介面為非阻塞
conn_l=[]
del_l=[]
while True:
    try:
        conn,addr=s.accept()
        conn_l.append(conn)
    except BlockingI/OError:
        print(conn_l)
        for conn in conn_l:
            try:
                data=conn.recv(1024)
                if not data:
                    del_l.append(conn)
                    continue
                conn.send(data.upper())
            except BlockingI/OError:
                pass
            except ConnectI/OnResetError:
                del_l.append(conn)

        for conn in del_l:
            conn_l.remove(conn)
            conn.close()
        del_l=[]

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

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

但是非阻塞I/O模型絕不被推薦。

我們不能否則其優點:能夠在等待任務完成的時間里乾其他活了(包括提交其他任務,也就是 “後臺” 可以有多個任務在“”同時“”執行)。

但是也難掩其缺點:

  1. 迴圈調用recv()將大幅度推高CPU占用率;這也是我們在代碼中留一句time.sleep(2)的原因,否則在低配主機下極容易出現卡機情況
  2. 任務完成的響應延遲增大了,因為每過一段時間才去輪詢一次read操作,而任務可能在兩次輪詢之間的任意時間完成。這會導致整體數據吞吐量的降低。

此外,在這個方案中recv()更多的是起到檢測“操作是否完成”的作用,實際操作系統提供了更為高效的檢測“操作是否完成“作用的介面,例如select()多路復用模式,可以一次檢測多個連接是否活躍。

四、多路復用I/O模型

4.1 select/poll模型

以買票的例子舉例,select/poll模型小結為:

1.
# 老王去火車站買票,委托黃牛,然後每隔6小時電話黃牛詢問,黃牛三天內買到票,然後老王去火車站交錢領票。

# 耗費:往返車站2次,路上2小時,黃牛手續費100元,打電話17次

I/O multiplexing這個詞可能有點陌生,但是如果我說select/poll,大概就都能明白了。有些地方也稱這種I/O方式為事件驅動I/O(event driven I/O)。我們都知道,select/poll的好處就在於單個process就可以同時處理多個網路連接的I/O。它的基本原理就是select/poll這個functI/On會不斷的輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程。它的流程如圖:

188-大白話五種IO模型-04.png?x-oss-process=style/watermark

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

這個圖和blocking I/O的圖其實並沒有太大的不同,事實上還更差一些。因為這裡需要使用兩個系統調用(select和recvfrom),而blocking I/O只調用了一個系統調用(recvfrom)。但是,用select的優勢在於它可以同時處理多個connectI/On。

強調:

  1. 如果處理的連接數不是很高的話,使用select/poll的web server不一定比使用multi-threading + blocking I/O的web server性能更好,可能延遲還更大。select/poll的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。
  2. 在多路復用模型中,對於每一個socket,一般都設置成為non-blocking,但是,如上圖所示,整個用戶的process其實是一直被block的。只不過process是被select這個函數block,而不是被socket I/O給block。

    結論: select的優勢在於可以處理多個連接,不適用於單個連接

4.1.1 select網路I/O模型

#服務端
from socket import *
import select

s=socket(AF_INET,SOCK_STREAM)
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind(('127.0.0.1',8081))
s.listen(5)
s.setblocking(False) #設置socket的介面為非阻塞
read_l=[s,]
while True:
    r_l,w_l,x_l=select.select(read_l,[],[])
    print(r_l)
    for ready_obj in r_l:
        if ready_obj == s:
            conn,addr=ready_obj.accept() #此時的ready_obj等於s
            read_l.append(conn)
        else:
            try:
                data=ready_obj.recv(1024) #此時的ready_obj等於conn
                if not data:
                    ready_obj.close()
                    read_l.remove(ready_obj)
                    continue
                ready_obj.send(data.upper())
            except ConnectI/OnResetError:
                ready_obj.close()
                read_l.remove(ready_obj)

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

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

4.1.2 select監聽fd變化的過程分析

  1. 用戶進程創建socket對象,拷貝監聽的fd到內核空間,每一個fd會對應一張系統文件表,內核空間的fd響應到數據後,就會發送信號給用戶進程數據已到;

  2. 用戶進程再發送系統調用,比如(accept)將內核空間的數據copy到用戶空間,同時作為接受數據端內核空間的數據清除,這樣重新監聽時fd再有新的數據又可以響應到了(發送端因為基於TCP協議所以需要收到應答後才會清除)。

4.1.3 該模型的優點

相比其他模型,使用select() 的事件驅動模型只用單線程(進程)執行,占用資源少,不消耗太多 CPU,同時能夠為多客戶端提供服務。如果試圖建立一個簡單的事件驅動的伺服器程式,這個模型有一定的參考價值。

4.1.4 該模型的缺點

  1. 首先select()介面並不是實現“事件驅動”的最好選擇。因為當需要探測的句柄值較大時,select()介面本身需要消耗大量時間去輪詢各個句柄。
  2. 很多操作系統提供了更為高效的介面,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。
  3. 如果需要實現更高效的伺服器程式,類似epoll這樣的介面更被推薦。遺憾的是不同的操作系統特供的epoll介面有很大差異,
  4. 所以使用類似於epoll的介面實現具有較好跨平臺能力的伺服器會比較困難。
  5. 其次,該模型將事件探測和事件響應夾雜在一起,一旦事件響應的執行體龐大,則對整個模型是災難性的。

4.2 epoll模型(瞭解)

以買票的例子舉例,epoll模型小結為:

# 老王去火車站買票,委托黃牛,黃牛買到後即通知老王去領,然後老王去火車站交錢領票。

# 耗費:往返車站2次,路上2小時,黃牛手續費100元,無需打電話

I/O多路復用這個概念被提出來以後, select是第一個實現 (1983 左右在BSD裡面實現)。但是,select模型有著一個很大的問題,那就是它所支持的fd的數量是有限制的,從linux源碼中所看:

188-大白話五種IO模型-08.png?x-oss-process=style/watermark

它所需要的fd_set類型其實是一個__FD_SETSIZE長度bit的數組。也就是說通過FD_SET巨集來對它進行操作的時候,需要註意socket不能過大,否則可能出現數組寫越界的錯誤。

而linux在2.6 內核中引入的epoll模型,就徹底解決了這個問題,由於目前epoll模型只能在linux中使用,因此我們開發僅做瞭解即可。

epoll模型提供了三個介面:

188-大白話五種IO模型-09.png?x-oss-process=style/watermark

其使用流程基本與select一致,大致如下:

188-大白話五種IO模型-10.png?x-oss-process=style/watermark

epoll_create函數會為要監聽的fd分配記憶體。

epoll_ctl函數將要監聽的fd拷貝到內核空間,從而避免每次等待事件都要進行記憶體拷貝。同時,註冊一個回調函數到 fd的設備等待隊列中,這樣,當設備就緒的時候,驅動程式可以直接調用回調函數進行處理,從而避免了對所有監聽fd的輪循。

epoll_wait函數會檢查是否已經有fd就緒了,如果有則直接返回,如果沒有,則進入休眠狀態,直到被上述的回調函數喚醒或者超時時間到達。

五、信號驅動I/O模型(瞭解)

以買票的例子舉例,該模型小結為:

# 老王去火車站買票,給售票員留下電話,有票後,售票員電話通知老王,然後老王去火車站交錢領票。

# 耗費:往返車站2次,路上2小時,免黃牛費100元,無需打電話

188-大白話五種IO模型-07.png?x-oss-process=style/watermark

由於信號驅動I/O在實際中並不常用,所以我們只做簡單瞭解。

信號驅動I/O模型,應用進程告訴內核:當數據報準備好的時候,給我發送一個信號,對SIGI/O信號進行捕捉,並且調用我的信號處理函數來獲取數據報。

六、非同步I/O模型

以買票的例子舉例,該模型小結為:

# 老王去火車站買票,給售票員留下電話,有票後,售票員電話通知老王並快遞送票上門。

# 耗費:往返車站1次,路上1小時,免黃牛費100元,無需打電話

Linux下的asynchronous I/O其實用得不多,從內核2.6版本才開始引入。先看一下它的流程:

188-大白話五種IO模型-05.png?x-oss-process=style/watermark

用戶進程發起read操作之後,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當它受到一個asynchronous read之後,首先它會立刻返回,所以不會對用戶進程產生任何block。然後,kernel會等待數據準備完成,然後將數據拷貝到用戶記憶體,當這一切都完成之後,kernel會給用戶進程發送一個signal,告訴它read操作完成了。

七、I/O模型比較分析

到目前為止,已經將四個 I/O Model都介紹完了。現在回過頭來回答最初的那幾個問題:阻塞和非阻塞的區別在哪,同步I/O 和 非同步I/O 的區別在哪。

先回答最簡單的這個:阻塞 vs 非阻塞。前面的介紹中其實已經很明確的說明瞭這兩者的區別。調用 阻塞I/O 會一直block住對應的進程直到操作完成,而 非阻塞I/O 在kernel還準備數據的情況下會立刻返回。

再說明同步I/O 和 非同步I/O 的區別之前,需要先給出兩者的定義。Stevens給出的定義(其實是POSIX的定義)是這樣子的:

A synchronous I/O operatI/O n causes the requesting process to be blocked until that I/O operatI/O ncompletes;

An asynchronous I/O operatI/O n does not cause the requesting process to be blocked;

兩者的區別就在於 同步I/O 做”I/O operatI/O n”的時候會將process阻塞。按照這個定義,四個I/O模型可以分為兩大類,之前所述的 阻塞I/O , 非阻塞I/O ,I/O多路復用 都屬於 同步I/O 這一類,而 非同步I/O 屬於後一類。

有人可能會說, 非阻塞I/O 並沒有被block啊。這裡有個非常“狡猾”的地方,定義中所指的”I/O operatI/O n”是指真實的I/O操作,就是例子中的recvfrom這個system call。 非阻塞I/O 在執行recvfrom這個system call的時候,如果kernel的數據沒有準備好,這時候不會block進程。但是,當kernel中數據準備好的時候,recvfrom會將數據從kernel拷貝到用戶記憶體中,這個時候進程是被block了,在這段時間內,進程是被block的。而 非同步I/O 則不一樣,當進程發起I/O操作之後,就直接返回再也不理睬了,直到kernel發送一個信號,告訴進程說I/O完成。在這整個過程中,進程完全沒有被block。

各個I/O Model的比較如圖所示:

188-大白話五種IO模型-06.png?x-oss-process=style/watermark

經過上面的介紹,會發現 非阻塞I/O 和 非同步I/O 的區別還是很明顯的。在非阻塞I/O 中,雖然進程大部分時間都不會被block,但是它仍然要求進程去主動的check,並且當數據準備完成以後,也需要進程主動的再次調用recvfrom來將數據拷貝到用戶記憶體。而 非同步I/O 則完全不同。它就像是用戶進程將整個I/O操作交給了他人(kernel)完成,然後他人做完後發信號通知。在此期間,用戶進程不需要去檢查I/O操作的狀態,也不需要主動的去拷貝數據。

可以看出,以上五個模型的阻塞程度由低到高為:阻塞I/O>非阻塞I/O>多路轉接I/O>信號驅動I/O>非同步I/O,因此他們的效率是由低到高的。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 【本文為原創,轉載請註明出處】 技術【HTML】 佈局【Frameset】 無步驟 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>後臺管理系統框架</title> </head> <frameset row ...
  • 【本文為原創,轉載請註明出處】 技術【CSS+HTML】 佈局【Frameset】 步驟1 Frameset 佈局 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>視頻網站大雜燴</title> <link ...
  • 一、Web前端與後端(Python Web) 1.Web前端:網頁,包含HTML,CSS,JS。 靜態網頁:不能與伺服器交互的網頁 動態網頁:能夠與伺服器交互的網頁 2.Web後端:、Flask,Django、Ajax技術 二、伺服器 1.伺服器:為用戶提供服務的電腦,將數據抽象成URL,以供用戶 ...
  • 【本文為原創,轉載請註明出處】 技術【CSS+HTML】 佈局【Table】 圖片準備【百度圖標、10張不同類型圖】 步驟1 table 佈局 1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <titl ...
  • js中打開一個新視窗的方法: 1.window.location.href=“url” 2.jbox.win(); 3.window.open(); js無任何提示的關閉彈出的頁面: window.opener=null; window.open('','_self'); window.close( ...
  • 在安裝本框架的基礎必須安裝合適的Node.js和可以運行的npm包,安裝過程本書前面已經提到過,不再贅述。 ...
  • 有時我們需要統計自定義echarts圖,統計x軸區間的y軸數量。 思路是利用echarts的自定義配置:option.series[i].type='custom'中的renderItem(params, api)函數進行處理,這裡包括了兩個參數:params是對應每一個dataItem的數據信息; ...
  • 首先,在C 中,多態的體現是什麼?虛函數、抽象方法、介面。廢話不多說,直接上代碼: 我們抽象出來基類,將其改造為抽象類和抽象方法,使其在子類中重寫各自的邏輯,而後具體的選擇(也就是條件語句)交給客戶端來處理。 就這麼簡單,細心的我們可以發現,這種多態的寫法好像策略模式,沒錯!設計模式中絕大數的模式都 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...