者:liamwang
在這篇文章中,我展示了如何使用 JavaScriptEngineSwitcher NuGet 包來在 .NET 應用程序中運行 JavaScript。這個包為許多不同的 JavaScript 引擎提供了一個一致的接口。
盡管我很喜歡 .NET 生態系統,但有些事情,JavaScript 生態系統做得更好。其中之一就是任何事情都能找到一個庫,特別是涉及到網絡時。
以語法高亮為例。這可以直接用 C# 來做,但這不是一個特別流暢的體驗。例如,TextMateSharp 項目為 TextMate 語法提供了一個解釋器。這些文件是 VS Code 用來為一種語言添加基本語法高亮的。然而,如果你想部署應用程序,它包裝了一個本地依賴,這就增加了一些復雜性。
相比之下,JavaScript 有大量成熟的語法高亮庫。僅舉幾例,有 highlight.js、Prism.js(在本博客中使用)和 shiki.js。尤其是前兩個,非常成熟,有多個插件和主題,而且有簡單的 API。
作為一個 .NET 開發者,JavaScript 的明顯問題是,你需要學習并選擇進入一個完整的獨立工具鏈,與 Node.js 和 NPM 一起工作。這似乎是一個很大的開銷,只是為了使用一個小功能。
因此,我們陷入了一個困境。我們要么走 C#(+ Native)路線,要么就得轉用 JavaScript。
或者......我們直接從我們的 .NET 應用程序中調用 JavaScript 。
一旦你決定在你的 .NET 代碼中運行 JavaScript,你就會考慮幾個選擇。你可以借用 JavaScript 引擎,讓它為你運行你的 JavaScript,但你并沒有真正解決問題,你仍然需要安裝 Node.js。
另一個選擇是在你的庫中直接捆綁 JavaScript 引擎。這并不像聽起來那么瘋狂,有幾個 NuGet 包采用了這種方法,然后暴露出一個 C# 層來與引擎進行交互。
下面是你可以使用的一些包的列表。
這個庫采取了上述的第一種方法。它不包括包中的 Node.js。相反,它為執行 JavaScript 代碼提供了一個 C# API,并調用了安裝在你機器上的 Node.js。這在你知道兩者都已安裝的環境中可能很有用,但它并沒有真正解決我想避免的問題。
ChakraCore 是 Edge 轉為基于 Chromium 引擎之前最初使用的 JavaScript 引擎。根據 GitHub 項目的介紹:
ChakraCore 是一個帶有 C 語言 API 的 JavaScript 引擎,你可以用它來為任何 C 語言或 C 語言兼容項目添加對 JavaScript 的支持。它可以在 Linux macOS 和 Windows 上針對 x64 處理器進行編譯。而 x86 和 ARM 只適用于 Windows。
因此,ChakraCore 包括一個本地依賴,但由于 C# 可以 P/Invoke 到本地庫,這本身并不是一個問題。但它會帶來一些部署方面的挑戰。
Node.JS、Chromium、Chrome 和最新的 Edge 使用的都是 V8 JavaScript 引擎。Microsoft.ClearScript 包為該庫提供了一個封裝,為調用 V8 庫提供了一個 C# 接口。就像 ChakraCore 一樣,V8 引擎本身是一個本地依賴。ClearScript 庫負責 P/Invoke 調用,提供了一個很好的 C# API,但你仍然要確保你在目標平臺上部署了正確的本地庫。
Jint 很有意思,因為它是一個完全在 .NET 中運行的 JavaScript 解釋器,沒有任何本地的依賴!它完全支持 ECMAScript 5.1 (ES5),并支持 .NET Standard 2.0,所以你可以在你的所有項目中使用它!
Jurassic 是另一個 JavaScript 引擎的 .NET 實現,類似于 Jint。也和 Jint 類似,它支持所有的 ES5,而且似乎也部分支持 ES6。與 Jint 不同的是,Jurassic 不是一個解釋器,它將 JavaScript 編譯成 IL,這使得它的速度非常快,而且它沒有本地的依賴性。
那么,在所有這些選擇中,你應該選擇哪一個?
還有一個偉大的項目可以讓你簡單地嘗試上面其中的任何一個庫。雖然所有的庫都允許你運行 JavaScript,但它們都有略微不同的 C# API 來與之交互。這可能會使比較它們變得有點痛苦,因為你必須為每個庫學習不同的 API。
JavaScriptEngineSwitcher 這個庫為我提到的所有庫和更多的庫提供了封裝:
每個庫都在一個單獨的包中(有本地依賴關系的引擎需要一個額外的本地包),還有一個 Core 包,它提供通用的 API。即使你不打算切換 JS 引擎,我也傾向于盡可能地使用 JavaScriptEngineSwitcher 封裝庫,這樣你就不必在以后需要切換引擎時弄清楚一個新的 API 了。
在 .NET 項目中改變使用的 JavaScript 引擎在我看來是完全可能的。例如,我開始使用 Jint,但當我需要執行更大的腳本時,我遇到了性能問題,于是換成了 Jurassic。JavaScriptEngineSwitcher 讓這一切變得很簡單,只需在我的項目中添加一個新的包并改變一些初始化代碼即可。
我最近才發現 JavaScriptEngineSwitcher 這個庫,但最新版本的下載量已接近一百萬,它被用于 .NET 靜態網站建設者 Statiq 中。在這篇文章的最后部分,我將舉一個最基本用法的例子。
在這篇文章的開頭,我討論了一個特定的場景--代碼塊的語法高亮。在本節中,我將展示如何使用 prism.js 高亮一小段代碼,并在一個控制臺應用程序中運行。
開始之前請添加 JavaScriptEngineSwitcher.Jurassic NuGet 包的引用。
dotnet add package JavaScriptEngineSwitcher.Jurassic1.
接下來,下載你想運行的 JavaScript 文件。例如,我從 Prism.js 的官網下載了 prism.js 文件,并將 C# 添加到默認支持高亮的語言集。在把文件放到項目文件夾的根目錄后,我把文件更新為嵌入資源。你可以在你的 IDE 中操作,也可以手動編輯項目文件:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JavaScriptEngineSwitcher.Jurassic" Version="3.17.4" />
</ItemGroup>
<!-- Make prism.js an embedded resource -->
<ItemGroup>
<None Remove="prism.js" />
<EmbeddedResource Include="prism.js" />
</ItemGroup>
</Project>1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.
剩下的就是編寫代碼,在我們的程序中運行腳本。下面的代碼段設置了 JavaScript 引擎,從程序集中加載嵌入的 prism.js 庫,并執行它。
using JavaScriptEngineSwitcher.Jurassic;
// Create an instance of the JavaScript engine
IJsEngine engine = new JurassicJsEngine();
// Execute the embedded resource called JsInDotnet.prism.js from the provided assembly
engine.ExecuteResource("JsInDotnet.prism.js", typeof(Program).Assembly);1.2.3.4.5.6.7.
現在我們可以在同一個上下文中運行我們自己的 JavaScript 命令。我們可以通過使用 SetVariableName、Execute 和 Evaluate 從 C# 向 JavaScript 引擎傳遞數值:
// This is the code we want to highlight
string code = @"
using System;
public class Test : ITest
{
public int ID { get; set; }
public string Name { get; set; }
}";
// set the JavaScript variable called "input" to the value of the c# variable "code"
engine.SetVariableValue("input", code);
// set the JavaScript variable called "lang" to the string "csharp"
engine.SetVariableValue("lang", "csharp");
// run the Prism.highlight() function, and set the result to the "highlighed" variable
engine.Execute($"highlighted = Prism.highlight(input, Prism.languages.csharp, lang)");
// "extract the value of "highlighted" from JavaScript to C#
string result = engine.Evaluate<string>("highlighted");
Console.WriteLine(result);1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.
當你把它們放在一起運行時,高亮的代碼會被打印到控制臺:
<span class="token keyword">using</span>
<span class="token namespace">System</span>
<span class="token punctuation">;</span>
<span class="token keyword">public</span>
<span class="token keyword">class</span>
<span class="token class-name">Test</span>
<span class="token punctuation">:</span>
<span class="token type-list">
<span class="token class-name">ITest</span>
</span>
<span class="token punctuation">{</span>
<span class="token keyword">public</span>
<span class="token return-type class-name">
<span class="token keyword">int</span>
</span>
ID
<span class="token punctuation">{</span>
<span class="token keyword">get</span>
<span class="token punctuation">;</span>
<span class="token keyword">set</span>
<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">public</span>
<span class="token return-type class-name">
<span class="token keyword">string</span>
</span>
Name
<span class="token punctuation">{</span>
<span class="token keyword">get</span>
<span class="token punctuation">;</span>
<span class="token keyword">set</span>
<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
渲染后,看起來像這樣:
我對整個過程的簡單程度感到驚訝。啟動一個 JavaScript 引擎,加載 prism.js 文件,并執行我們的自定義代碼是如此順利。這是我面臨問題的完美解決方案。
我顯然不建議所有的應用程序都這樣做。如果你需要運行大量的 JavaScript,那么直接使用 Node.js 生態系統及工具可能更容易。但如果你只是想利用一個小型的、獨立的工具(如 prims.js),那么這是一個不錯的選擇。
在這篇文章中,我展示了如何使用 JavaScriptEngineSwitcher NuGet 包來在 .NET 應用程序中運行 JavaScript。這個包為許多不同的 JavaScript 引擎提供了一個一致的接口。其中一些引擎(如 Chakra Core 和 V8)需依賴一個本地組件,而其他引擎(如 Jint 和 Jurassic)只使用托管代碼。最后,我展示了你如何使用 JavaScriptEngineSwitcher 在 .NET 應用程序內部運行 Prims.js 代碼高亮庫。
原文:bit.ly/38awq7W
作者:Andrew Lock
翻譯:精致碼農
來源: 精致碼農
述:本文將討論如何用最簡單的術語在網站上運行 C# 代碼。半技術講座我使用了 wasm-tools-net7,這是一個基于 wasm-tools 的工作負載,沒有包含任何額外的包。我的重點是簡單性和主要主題。徹底了解該主題可提供完成所有其他任務所需的信息。如何工作?WebAssembly 工作原理:序列圖創建演示創建項目我用的是net7,但這取決于你。Dotnet new console -o WASM_Demo cd WASM_Demo Dotnet workload install wasm-tools-net7此時,需要對 csproj 文件進行修改。Project Sdk=Mi
本文將討論如何用最簡單的術語在網站上運行 C# 代碼。
我使用了 wasm-tools-net7,這是一個基于 wasm-tools 的工作負載,沒有包含任何額外的包。我的重點是簡單性和主要主題。徹底了解該主題可提供完成所有其他任務所需的信息。
WebAssembly 工作原理:序列圖
Dotnet new console -o WASM_Demo
cd WASM_Demo
Dotnet workload install wasm-tools-net7
此時,需要對 csproj 文件進行修改。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<WasmMainJSPath>main.js</WasmMainJSPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<WasmExtraFilesToDeploy Include="index.html" />
<WasmExtraFilesToDeploy Include="main.js" />
</ItemGroup>
</Project>
我們添加了什么:
返回到program.cs文件,需要考慮某些規則。
讓我們舉個例子。
using System.Runtime.InteropServices.JavaScript;
namespace WASM_Demo;
public partial class Program
{
static void Main(string[] args) { }
[JSExport]
public static string Response()
{
return """
<h1>
Hello World
</h1>
""";
}
}
沒關系,但是我們如何在瀏覽器中運行此代碼?
運行這個程序的代碼是dotnet.js的,它自帶了wasm-tools,所以沒有必要擔心它。要使用此dotnet.js,我們只需使用一個名為 main.js 的文件。
import { dotnet } from './dotnet.js'
const is_browser = typeof window != "undefined";
if (!is_browser) throw new Error(`Expected to be running in a browser`);
const { setModuleImports, getAssemblyExports, getConfig, runMainAndExit } = await dotnet
.withDiagnosticTracing(false)
.withApplicationArgumentsFromQuery()
.create();
const config = getConfig();
const exports = await getAssemblyExports(config.mainAssemblyName);
const html =
exports
.WASM_Demo // Namespace
.Program // Class Name
.Response(); // Function Name
// Regular javascript code
document.getElementById("app").innerHTML = `${html}`;
await runMainAndExit(config.mainAssemblyName, [] /* Console App Args */);
index.html頁面的模板已經準備完畢。
<!DOCTYPE html>
<html lang="en">
<head>
<title>WASM Demo</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="modulepreload" href="./dotnet.js" />
</head>
<body>
<main id="app"></main>
<script type="module" src="./main.js"></script>
</body>
</html>
現在,讓我們再看一遍這個過程,
我們還有一件事要做,你需要打開一個名為 runtimeconfig.template.json 的文件,并將以下 JSON 數據放入其中。
{
"wasmHostProperties": {
"perHostConfig": [
{
"name": "browser",
"html-path": "index.html",
"Host": "browser"
}
]
}
}
我們已經到了盡頭,程序現在可以運行了。唯一需要的命令是:
Dotnet run -c Release
我可以托管所有文件而不是 wasm-tools 嗎?又是如何做到的呢?
當然,但它可能會變得有點復雜,你用 wasm-tools 制作的項目不能用于任何其他目的,即控制臺應用程序不起作用,wasm-tools 可以工作。因為我們選擇 browser-wasm 作為 RuntimeIdentifier,并且多個 RuntimeIdentifiers 在 .NET 中不可用。作為替代方法,您可以打開兩個項目,將第一個項目設置為 WASM 項目,然后在第二個項目中將其設置為控制臺應用程序,然后生成第一個項目并托管輸出文件夾,所有 DLL 和文件都將在那里。
這個演示只是索引文件,我可以做多頁嗎?又是如何做到的呢?
當然,但這比你想象的要難得多,因為這樣做的方法是一種叫做SPA(單頁應用程序)的方法,用戶總是在同一頁面上,只是內容發生了變化。有多種方法可以做到這一點。所以它可以用你的創造力來完成。
我可以像計數器一樣做動態代碼嗎?又是如何做到的呢?
_是的,我也這樣做了,你可以一遍又一遍地調用 C# 函數,如果你只是將導出綁定到 window 對象,你可以從每個 JavaScript 代碼中調用它。
最典型的比如通過前后線程 ID 來推斷其工作方式、在 async 方法中用 Thread.Sleep 來解釋 Task 機制而導出多線程模型的結論、在 Task.Run 中包含 IO bound 任務來推出這是開了一個多線程在執行任務的結論等等。
看上去似乎可以解釋的通,可是很遺憾,無論是從原理還是結論上看都是錯誤的。
要了解 .NET 中的 async / await 機制,首先需要有操作系統原理的基礎,否則的話是很難理解清楚的,如果沒有這些基礎而試圖向他人解釋,大多也只是基于現象得到的錯誤猜想。
說到異步大家應該都很熟悉了,2012 年 C# 5 引入了新的異步機制: Task ,并且還有兩個新的關鍵字 await 和 async ,這已經不是什么新鮮事了,而且如今這個異步機制已經被各大語言借鑒,如 JavaScript、TypeScript、Rust、C++ 等等。
下面給出一個簡單的對照:
語言調度單位關鍵字/方法
當然,這里這并不是本文的重點,只是提一下,方便大家在有其他語言經驗的情況下(如果有),可以認識到 C# 中 Task 和 async / await 究竟是一個和什么可以相提并論的東西。
在該異步編程模型誕生之前,多線程編程模型是很多人所熟知的。一般來說,開發者會使用 Thread 、 std::thread 之類的東西作為線程的調度單位來進行多線程開發,每一個這樣的結構表示一個對等線程,線程之間采用互斥或者信號量等方式進行同步。
多線程對于科學計算速度提升等方面效果顯著,但是對于 IO 負荷的任務,例如從讀取文件或者 TCP 流,大多數方案只是分配一個線程進行讀取,讀取過程中阻塞該線程:
void Main()
{
while (true)
{
var client = socket.Accept();
new Thread(() => ClientThread(client)).Start();
}
}
void ClientThread(Socket client)
{
var buffer = new byte[1024];
while (...)
{
// read and block
client.Read(buffer, 0, 1024);
}
}
上述代碼中, Main 函數在接收客戶端之后即分配了一個新的用戶線程用于處理該客戶端,從客戶端接收數據。 client.Read() 執行后,該線程即被阻塞,即使阻塞期間該線程沒有任何的操作,該用戶線程也不會被釋放,并被操作系統不斷輪轉調度,這顯然浪費了資源。
另外,如果線程數量多起來,頻繁在不同線程之間輪轉切換上下文,線程的上下文也不小,會浪費掉大量的性能。
因此我們在 Linux 上有了 epoll/io_uring 技術,在 Windows 上有了 IOCP 技術用以實現異步 IO 操作。
(這里插句題外話,吐槽一句,Linux 終于知道從 Windows 抄作業了。先前的 epoll 對比 IOCP 簡直不能打,被 IOCP 全面打壓,io_uring 出來了才好不容易能追上 IOCP,不過 IOCP 從 Windows Vista 時代開始每一代都有很大的優化,io_uring 能不能追得上還有待商榷)
這類 API 有一個共同的特性就是,在操作 IO 的時候,調用方控制權被讓出,等待 IO 操作完成之后恢復先前的上下文,重新被調度繼續運行。
所以表現就是這樣的:
假設我現在需要從某設備中讀取 1024 個字節長度的數據,于是我們將緩沖區的地址和內容長度等信息封裝好傳遞給操作系統之后我們就不管了,讀取什么的讓操作系統去做就好了。
操作系統在內核態下利用 DMA 等方式將數據讀取了 1024 個字節并寫入到我們先前的 buffer 地址下,然后切換到用戶態將從我們先前讓出控制權的位置,對其進行調度使其繼續執行。
你可以發現這么一來,在讀取數據期間就沒有任何的線程被阻塞,也不存在被頻繁調度和切換上下文的情況,只有當 IO 操作完成之后才會被重新調度并恢復先前讓出控制權時的上下文,使得后面的代碼繼續執行。
說了這么久還是沒有解釋 Task 到底是個什么東西,從上面的分析就可以得出, Task 其實就是一個所謂的調度單位,每個異步任務被封裝為一個 Task 在 CLR 中被調度,而 Task 本身會運行在 CLR 中的預先分配好的線程池中。
總有很多人因為 Task 借助線程池執行而把 Task 歸結為多線程模型,這是完全錯誤的。
這個時候有人跳出來了,說:你看下面這個代碼
static async Task Main()
{
while (true)
{
Console.WriteLine(Environment.CurrentManagedThreadId);
await Task.Delay(1000);
}
}
輸出的線程 ID 不一樣欸,你騙人,這明明就是多線程!對于這種言論,我也只能說這些人從原理上理解的就是錯誤的。
當代碼執行到 await 的時候,此時當前的控制權就已經被讓出了,當前線程并沒有在阻塞地等待延時結束;待 Task.Delay() 完畢后,CLR 從線程池當中挑起了一個先前分配好的已有的但是空閑的線程,將讓出控制權前的上下文信息(寄存器值)恢復,使得該線程恰好可以從先前讓出的位置繼續執行下去。這個時候,可能挑到了先前讓出前所在的那個線程,導致前后線程 ID 一致;也有可能挑到了另外一個和之前不一樣的線程執行下面的代碼,使得前后的線程 ID 不一致。在此過程中并沒有任何的新線程被分配了出去。
當然,在 WPF 等地方,因為利用了 SynchronizedContext 對上下文行為有所設置,所以可以得到和上述不同的結論。
但是上面和經典的多線程編程的那一套有任何的關系嗎?完全沒有。
至于 ValueTask 是個什么玩意,官方發現, Task 由于本身是一個 class ,在運行時如果頻繁反復的分配和回收會給 GC 造成不小的壓力,因此出了一個 ValueTask ,這個東西是 struct ,分配在棧上,這樣的話就不會給 GC 造成壓力了,減輕了開銷。不過也正因為 ValueTask 是會在棧上分配的值類型結構,因此提供的功能也不如 Task 全面。
由于 .NET 是允許有多個線程的,因此也提供了 Task.Run 這個方法,允許我們將 CPU bound 的任務放在上述的線程池之中的某個線程上執行,并且允許我們將該負載作為一個 Task 進行管理,僅在這一點才和多線程的采用線程池的編程比較像。
對于瀏覽器環境(v8),這個時候是完全沒有多線程這一說的,因此你開的新的 Promise 其實是后面利用事件循環機制,將該微任務以異步的方式執行。
想一想在 JavaScript 中, Promise 是怎么用的:
let p = new Promise((resolve, reject) => {
// do something
let success = true;
let result = 123456;
if (success) {
resolve(result);
}
else {
reject("failed");
}
})
然后調用:
let r = await p;
console.log(r); // 輸出 123456
你只需要把這一套背后的驅動器:事件循環隊列,替換成 CLR 的線程池,就差不多是 .NET 的 Task 相對 JavaScript 的 Promise 的工作方式了。
如果你把 CLR 線程池線程數量設置為 1,那就和 JavaScript 這套幾乎差不多了(雖然實現上還是有差異)。
了解了上面的東西之后,相信對 .NET 中的異步機制應該理解得差不多了,可以看出來這一套是名副其實的 coroutine,并且在實現上是 stackless 的。至于有的人說的什么狀態機什么的,只是實現過程中利用的手段而已,并不是什么重要的東西。
那我們要怎么樣使用 Task 來編寫我們自己的異步代碼呢?
事件驅動其實也可以算是一種異步模型,例如以下情景:
A 函數調用 B 函數,調用后就立馬讓出控制權(例如: BeginInvoke ), B 函數執行完成后觸發事件執行 C 函數。
private event Action CompletedEvent;
void A()
{
CompletedEvent += C;
Console.WriteLine("begin");
((Action)B).BeginInvoke();
}
void B()
{
Console.WriteLine("running");
CompletedEvent?.Invoke();
}
void C()
{
Console.WriteLine("end");
}
那么我們現在想要做一件事,就是把上面的事件驅動改造為利用 async / await 的異步編程模型,改造后的代碼就是簡單的:
async Task A()
{
Console.WriteLine("begin");
await B();
Console.WriteLine("end");
}
Task B()
{
Console.WriteLine("running");
return Task.CompletedTask;
}
你可以看到,原本 C 函數的內容被放到了 A 調用 B 的下面,為什么呢?其實很簡單,因為這里 await B(); 這一行以后的內容,本身就可以立即為 B 函數的回調了,只不過在內部實現上,不是直接從 B 進行調用的回調,而是 A 先讓出控制權, B 執行完成后,CLR 切換上下文,將 A 調度回來繼續執行剩下的代碼。
如果事件相關的代碼已經確定不可改動(即不能改動 B 函數),我們想將其封裝為異步調用的模式,那只需要利用 TaskCompletionSource 即可:
private event Action CompletedEvent;
async Task A()
{
// 因為 TaskCompletionSource 要求必須有一個泛型參數
// 因此就隨便指定了一個 bool
// 本例中其實是不需要這樣的一個結果的
// 需要注意的是從 .NET 5 開始
// TaskCompletionSource 不再強制需要泛型參數
var tsc = new TaskCompletionSource<bool>();
// 隨便寫一個結果作為 Task 的結果
CompletedEvent += () => tsc.SetResult(false);
Console.WriteLine("begin");
await tsc.Task;
Console.WriteLine("end");
}
void B()
{
Console.WriteLine("running");
CompletedEvent?.Invoke();
}
順便提一句,這個 TaskCompletionSource<T> 其實和 JavaScript 中的 Promise<T> 更像。 SetResult() 方法對應 resove() , SetException() 方法對應 reject() 。.NET 比 JavaScript 還多了一個取消狀態,因此還可以 SetCancel() 表示任務被取消了。
說句真的,一般能有這個需求,都說明你的代碼寫的有問題,但是如果你無論如何都想以阻塞的方式去等待一個異步任務完成的話:
Task t = ...
t.GetAwaiter().GetResult();
祝你好運,這相當于, t 中的異步任務開始執行后,你將當前線程阻塞,然后等到 t 完成之后再喚醒,可以說是:毫無意義,而且很有可能因為代碼編寫不當而導致死鎖的發生。
最后有人會問了,函數可以寫 async Task Foo() ,還可以寫 async void Bar() ,這有什么區別呢?
對于上述代碼,我們一般調用的時候,分別這么寫:
await Foo();
Bar();
可以發現,誒這個 Bar 函數不需要 await 誒。為什么呢?
其實這和用以下方式調用 Foo 是一樣的:
_ = Foo();
換句話說就是調用后瞬間就直接拋掉不管了,不過這樣你也就沒法知道這個異步任務的狀態和結果了。
當然不是。
在 C# 中只要你的類中包含 GetAwaiter() 方法和 bool IsCompleted 屬性,并且 GetAwaiter() 返回的東西包含一個 GetResult() 方法、一個 bool IsCompleted 屬性和實現了 INotifyCompletion ,那么這個類的對象就是可以 await 的。
public class MyTask<T>
{
public MyAwaiter<T> GetAwaiter()
{
return new MyAwaiter<T>();
}
}
public class MyAwaiter<T> : INotifyCompletion
{
public bool IsCompleted { get; private set; }
public T GetResult()
{
throw new NotImplementedException();
}
public void OnCompleted(Action continuation)
{
throw new NotImplementedException();
}
}
public class Program
{
static async Task Main(string[] args)
{
var obj = new MyTask<int>();
await obj;
}
}
本文至此就結束了,感興趣的小伙伴可以多多學習一下操作系統原理,對 CLR 感興趣也可以去研究其源代碼: https://github.com/dotnet/runtime 。
從現象猜測本質是大忌,可能解釋的通但是終究只是偶然現象,而且從原理上看也是完全錯誤的,甚至官方的實現代碼稍微變一下可能立馬就無法解釋的通了。
總之,通過本文希望大家能對異步和 .NET 中的異步有一個更清晰的理解。
作者:博客園-hez2010
原文地址:https://www.cnblogs.com/hez2010/p/async-in-dotnet.html
*請認真填寫需求信息,我們會在24小時內與您取得聯系。