亚洲乱色熟女一区二区三区丝袜,天堂√中文最新版在线,亚洲精品乱码久久久久久蜜桃图片,香蕉久久久久久av成人,欧美丰满熟妇bbb久久久

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

【C#】為什么選擇 async/await 而不是線程?

admin
2024年4月25日 18:46 本文熱度 2335

一個常見的說法是,線程可以做到 async/await 所能做的一切,且更簡單。那么,為什么大家選擇 async/await 呢?

Rust 是一種低級語言,它不會隱藏協(xié)程的復(fù)雜性。這與像 Go 這樣的語言相反,在 Go 中,異步是默認發(fā)生的,程序員甚至不需要考慮它。

聰明的程序員試圖避免復(fù)雜性。因此,他們看到 async/await 中的額外復(fù)雜性,并質(zhì)疑為什么需要它。當考慮到存在一個合理的替代方案——操作系統(tǒng)線程時,這個問題尤其相關(guān)。

讓我們通過 async 來進行一次思維之旅吧。

背景概覽

通常,代碼是線性的;一件事情在另一件事情之后運行。它看起來像這樣:

fn main() {
    foo();
    bar();
    baz();
}

很簡單,對吧?然而,有時你會想同時運行很多事情。這方面的典型例子是 web 服務(wù)器。考慮以下用線性代碼編寫的:

fn main() -> io::Result<()> {
    let socket = TcpListener::bind("0.0.0.0:80")?;
    loop {
        let (client, _) = socket.accept()?;
        handle_client(client)?;
    }
}

想象一下,如果 handle_client 需要幾毫秒,并且兩個客戶端同時嘗試連接到你的 web 服務(wù)器。你會遇到一個嚴重的問題!

  • 客戶端 #1 連接到 web 服務(wù)器,并被 accept() 函數(shù)接受。它開始運行 handle_client()。

  • 客戶端 #2 連接到 web 服務(wù)器。然而,由于 accept() 當前沒有運行,我們必須等待 handle_client() 完成客戶端 #1 的運行。

  • 幾毫秒后,我們回到 accept()??蛻舳?#2 可以連接。

現(xiàn)在想象一下,如果有兩百萬個同時客戶端。在隊列的末尾,你必須等待幾分鐘,web 服務(wù)器才能幫助你。它很快就會變得不可擴展。

顯然,初期的 web 試圖解決這個問題。最初的解決方案是引入線程。通過將一些寄存器的值和程序的棧保存到內(nèi)存中,操作系統(tǒng)可以停止一個程序,用另一個程序替換它,然后再后繼續(xù)運行那個程序。本質(zhì)上,它允許多個例程(或“線程”,或“進程”)在同一個 CPU 上運行。

使用線程,我們可以將上述代碼重寫如下:

fn main() -> io::Result<()> {
    let socket = TcpListener::bind("0.0.0.0:80")?;
    loop {
        let (client, _) = socket.accept()?;
        thread::spawn(move || handle_client(client));
    }
}

現(xiàn)在,客戶端由一個與處理新連接等待不同的線程處理。太棒了!通過允許并發(fā)線程訪問,這避免了問題。

  • 客戶端 #1 被服務(wù)器接受。服務(wù)器生成一個調(diào)用 handle_client 的線程。

  • 客戶端 #2 嘗試連接到服務(wù)器。

  • 最終,handle_client 在某處阻塞。操作系統(tǒng)保存處理客戶端 #1 的線程,并將主線程帶回來。

  • 主線程接受客戶端 #2。它生成一個單獨的線程來處理客戶端 #2。在只有幾微秒的延遲后,客戶端 #1 和客戶端 #2 并行運行。

線程在考慮到生產(chǎn)級 web 服務(wù)器擁有幾十個 CPU 核心時特別好用。不僅僅是操作系統(tǒng)可以給人一種所有這些線程同時運行的錯覺;實際上,操作系統(tǒng)可以讓它們真正同時運行。

最終,出于我稍后將詳細說明的原因,程序員希望將這種并發(fā)性從操作系統(tǒng)空間帶到用戶空間。用戶空間并發(fā)性有許多不同的模型。有事件驅(qū)動編程、actor 和協(xié)程。Rust 選擇的是 async/await。

簡單來說,你將程序編譯成一個狀態(tài)機的集合,這些狀態(tài)機可以獨立于彼此運行。Rust 本身提供了一種創(chuàng)建狀態(tài)機的機制;async 和 await 的機制。使用 smol 編寫的上述程序?qū)⑷缦滤荆?/p>

#[apply(smol_macros::main!)]
async fn main(ex: &smol::Executor) -> io::Result<()> {
    let socket = TcpListener::bind("0.0.0.0:80").await?;
    loop {
        let (client, _) = socket.accept().await?;
        ex.spawn(async move {
            handle_client(client).await;
        }).detach();
    }
}

主函數(shù)前面有 async 關(guān)鍵字。這意味著它不是一個傳統(tǒng)函數(shù),而是一個返回狀態(tài)機的函數(shù)。大致上,函數(shù)的內(nèi)容對應(yīng)于該狀態(tài)機。

await 包括另一個狀態(tài)機作為當前運行狀態(tài)機的一部分。對于 accept(),這意味著狀態(tài)機將把它作為一個步驟包含在內(nèi)。

最終,一個內(nèi)部函數(shù)將會產(chǎn)生結(jié)果,或者放棄控制。例如,當 accept() 等待新連接時。在這一點上,整個狀態(tài)機將把執(zhí)行權(quán)交給更高級別的執(zhí)行器。對我們來說,那是 smol::Executor。

一旦執(zhí)行被產(chǎn)生,執(zhí)行器將用另一個正在并發(fā)運行的狀態(tài)機替換當前狀態(tài)機,該狀態(tài)機是通過 spawn 函數(shù)生成的。

我們將一個異步塊傳遞給 spawn 函數(shù)。這個塊代表一個完全新的狀態(tài)機,獨立于由 main 函數(shù)創(chuàng)建的狀態(tài)機。這個狀態(tài)機所做的一切都是運行 handle_client 函數(shù)。

一旦 main 產(chǎn)生結(jié)果,就選擇一個客戶端來代替它運行。一旦那個客戶端產(chǎn)生結(jié)果,循環(huán)就會重復(fù)。

你現(xiàn)在可以處理數(shù)百萬的并發(fā)客戶端。

當然,像這樣的用戶空間并發(fā)性引入了復(fù)雜性的提升。當你使用線程時,你不必處理執(zhí)行器、任務(wù)和狀態(tài)機等。

如果你是一個理智的人,你可能會問:“我們?yōu)槭裁葱枰鏊羞@些事情?線程工作得很好;對于 99% 的程序,我們不需要涉及任何用戶空間并發(fā)性。引入新復(fù)雜性是技術(shù)債務(wù),技術(shù)債務(wù)會花費我們的時間和金錢?!?/p>

“那么,我們?yōu)槭裁床皇褂镁€程呢?”

超時問題

也許 Rust 最大的優(yōu)勢之一是可組合性。它提供了一組可以嵌套、構(gòu)建、組合和擴展的抽象。

我記得讓我堅持使用 Rust 的是 Iterator trait。它可以讓我將某個東西變成 Iterator,應(yīng)用一些不同的組合器,然后將結(jié)果 Iterator 傳遞給任何接受 Iterator 的函數(shù),這讓我大開眼界。

它繼續(xù)給我留下深刻印象。假設(shè)你想從另一個線程接收一列表整數(shù),只取那些立即可用的整數(shù),丟棄任何不是偶數(shù)的整數(shù),給它們?nèi)考右唬缓髮⑺鼈兺频揭粋€新列表上。

在某些其他語言中,這將是五十行代碼和一個輔助函數(shù)。在 Rust 中,可以用五行完成:

let (send, recv) = mpsc::channel();
my_list.extend(
    recv.try_iter()
        .filter(|x| x & 1 == 0)
        .map(|x| x + 1)
);

async/await 最好的事情是,它允許你將這種可組合性應(yīng)用于 I/O 限制函數(shù)。假設(shè)你有一個新的客戶端要求;你想在上面的函數(shù)中添加一個超時。假設(shè)我們的 handle_client 函數(shù)看起來像這樣:

async fn handle_client(client: TcpStream) -> io::Result<()> {
    let mut data = vec![];
    client.read_to_end(&mut data).await?;

    let response = do_something_with_data(data).await?;
    client.write_all(&response).await?;
    Ok(())
}

如果我們想添加一個三秒鐘的超時,我們可以組合兩個組合器來做到這一點:

race 函數(shù)同時運行兩個 future。

Timer future 等待一段時間后返回。

最終的代碼看起來像這樣:

async fn handle_client(client: TcpStream) -> io::Result<()> {
    // 處理實際連接的 future
    let driver = async move {
        let mut data = vec![];
        client.read_to_end(&mut data).await?;

        let response = do_something_with_data(data).await?;
        client.write_all(&response).await?;
        Ok(())
    };
    // 處理等待超時的 future
    let timeout = async {
        Timer::after(Duration::from_secs(3)).await;
        // 我們剛剛超時了!返回一個錯誤。
        Err(io::ErrorKind::TimedOut.into())
    };
    // 并行運行兩者
    driver.race(timeout).await
}

我發(fā)現(xiàn)這是一個非常簡單的過程。你所要做的就是將你的現(xiàn)有代碼包裝在一個異步塊中,然后將其與另一個 future 競速。

這種方法的額外好處是,它適用于任何類型的流。在這里,我們使用 TcpStream。然而,我們可以很容易地將其替換為任何實現(xiàn) impl AsyncRead + AsyncWrite 的東西。async 可以輕松地適應(yīng)你需要的任何模式。

用線程實現(xiàn)

如果我們想在我們的線程示例中實現(xiàn)這一點呢?

fn handle_client(client: TcpStream) -> io::Result<()> {
    let mut data = vec![];
    client.read_to_end(&mut data)?;
    let response = do_something_with_data(data)?;
    client.write_all(&response)?;
    Ok(())
}

這并不容易。通常,你不能在阻塞代碼中中斷 read 或 write 系統(tǒng)調(diào)用,除非做一些災(zāi)難性的事情,比如關(guān)閉文件描述符(在 Rust 中無法做到)。

幸運的是,TcpStream 有兩個函數(shù) set_read_timeout 和 set_write_timeout,可以用來分別設(shè)置讀寫超時。然而,我們不能天真地使用它。想象一個客戶端每 2.9 秒發(fā)送一個字節(jié),只是為了重置超時。

所以我們需要在這里稍微防御性地編程。由于 Rust 組合器的強大,我們可以編寫自己的類型,包裝 TcpStream 來編程超時。

// `TcpStream` 的截止日期感知包裝器
struct DeadlineStream {
    tcp: TcpStream,
    deadline: Instant,
}

impl DeadlineStream {
    // 創(chuàng)建一個新的 `DeadlineStream`,經(jīng)過一段時間后過期
    fn new(tcp: TcpStream, timeout: Duration) -> Self {
        Self {
            tcp,
            deadline: Instant::now() + timeout,
        }
    }
}

impl io::Read for DeadlineStream {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        // 設(shè)置截止日期
        let time_left = self.deadline.saturating_duration_since(Instant::now());
        self.tcp.set_read_timeout(Some(time_left))?;
        // 從流中讀取
        self.tcp.read(buf)
    }
}

impl io::Write for DeadlineStream {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        // 設(shè)置截止日期
        let time_left = self.deadline.saturating_duration_since(Instant::now());
        self.tcp.set_write_timeout(Some(time_left))?;
        // 從流中讀取
        self.tcp.write(buf)
    }
}

// 創(chuàng)建包裝器
let client = DeadlineStream::new(client, Duration::from_secs(3));
let mut data = vec![];
client.read_to_end(&mut data)?;
let response = do_something_with_data(data)?;
client.write_all(&response)?;
Ok(())

一方面,可以認為這是優(yōu)雅的。我們使用 Rust 的能力用一個相對簡單的組合器解決了問題。我相信它會運行得很好。

另一方面,這絕對是 hacky。

我們鎖定了自己使用 TcpStream。Rust 中沒有特質(zhì)來抽象使用 set_read_timeout 和 set_write_timeout 類型。所以如果要使用任何類型的寫入器,需要額外的工作。

這涉及到設(shè)置超時的額外系統(tǒng)調(diào)用。

我認為這種類型對于 web 服務(wù)器要求的實際邏輯來說,使用起來要笨重得多。

異步成功案例

這就是為什么 HTTP 生態(tài)系統(tǒng)采用 async/await 作為其主要運行機制的原因,即使是客戶端也是如此。你可以取任何進行 HTTP 調(diào)用的函數(shù),并使其適應(yīng)你想要的任何用例。

tower 可能是我能想到的這種現(xiàn)象最好的例子,這也是讓我意識到 async/await 可以有多強大的東西。如果你將你的服務(wù)實現(xiàn)為一個異步函數(shù),你會得到超時、速率限制、負載均衡、對沖和背壓處理。所有這些都是無負擔實現(xiàn)的。

不管你使用的是什么運行時,或者你的服務(wù)實際上在做什么。你可以將它扔給 tower,使其更加健壯。

macroquad 是一個小型 Rust 游戲引擎,旨在使游戲開發(fā)盡可能簡單。它的主函數(shù)使用 async/await 來運行其引擎。這是因為 async/await 確實是在 Rust 中表達需要停下來等待其他事情的線性函數(shù)的最佳方式。

在實踐中,這可能非常強大。想象一下,同時輪詢你的游戲服務(wù)器和你的 GUI 框架的網(wǎng)絡(luò)連接,在同一線程上??赡苄允菬o限的。

提升異步的形象

我認為問題不在于有人認為線程比異步更好。我認為問題是異步的好處沒有被廣泛傳播。這導(dǎo)致一些人對異步的好處有誤解。

如果這是一個教育問題,我認為值得看一下教育材料。這是 Rust Async Book 在比較 async/await 和操作系統(tǒng)線程時所說的:

操作系統(tǒng)線程不需要對編程模型做任何改變,這使得并發(fā)表達非常容易。然而,線程間的同步可能會很困難,性能開銷也很大。線程池可以緩解這些成本,但不足以支持大規(guī)模的 I/O 密集型工作負載。

—— Rust Async Book

我認為這是整個異步社區(qū)的一個一貫問題。當有人問“為什么我們想用這個而不是操作系統(tǒng)線程”時,人們傾向于揮揮手說“異步開銷更小。除此之外,其他都一樣?!?/p>

這就是 web 服務(wù)器作者轉(zhuǎn)向 async/await 的原因。這就是他們?nèi)绾谓鉀Q C10k 問題的。但這不會是其他人轉(zhuǎn)向 async/await 的原因。

c10k 問題:https://en.wikipedia.org/wiki/C10k_problem

性能提升是不穩(wěn)定的,可能會在錯誤的情況下消失。有很多情況下,線程工作流程可以比等效的異步工作流程更快(主要是在 CPU 密集型任務(wù)的情況下)。可能以前我們過分強調(diào)了異步 Rust 的短暫性能優(yōu)勢,但低估了它的語義優(yōu)勢。

在最壞的情況下,這會導(dǎo)致人們對 async/await 置之不理,認為它是“你為小眾用例而求助的奇怪事物”。它應(yīng)該被視為一個強大的編程模型,讓你能夠簡潔地表達在同步 Rust 中無法表達的模式,而不需要數(shù)十個線程和通道。

有一種趨勢是試圖使異步 Rust “就像同步 Rust 一樣”,這種方式鼓勵了負面比較。當我說到“趨勢”時,我的意思是這是 Rust 項目的明確路線圖,即“編寫異步 Rust 代碼應(yīng)該像編寫同步代碼一樣容易,除了偶爾的 async 和 await 關(guān)鍵字。”

我拒絕這種框架,因為它根本不可能。這就像試圖在一個滑雪坡上舉辦披薩派對。我們不應(yīng)該試圖將我們的模型強行塞入不友好的慣用法,以迎合拒絕采用另一種模式的程序員。我們應(yīng)該努力突出 Rust 的 async/await 生態(tài)系統(tǒng)的優(yōu)勢;它的可組合性和它的能力。我們應(yīng)該努力使 async/await 成為程序員達到并發(fā)性時的默認選擇。我們不應(yīng)該試圖使同步 Rust 和異步 Rust 相同,我們應(yīng)該接受差異。


該文章在 2024/4/28 21:30:25 編輯過
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點晴ERP是一款針對中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國內(nèi)大量中小企業(yè)的青睞。
點晴PMS碼頭管理系統(tǒng)主要針對港口碼頭集裝箱與散貨日常運作、調(diào)度、堆場、車隊、財務(wù)費用、相關(guān)報表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點,圍繞調(diào)度、堆場作業(yè)而開發(fā)的。集技術(shù)的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點晴WMS倉儲管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質(zhì)期管理,貨位管理,庫位管理,生產(chǎn)管理,WMS管理系統(tǒng),標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務(wù)都免費,不限功能、不限時間、不限用戶的免費OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved