是指Action方法。如:

public ActionResult PushFileData([FromBody] Web_PushFileData file) //同步

public async ActionResult PushFileData([FromBody] Web_PushFileData file) //非同步

疑問:對於同步方法,每個請求都是使用同個線程嗎?如客戶A請求同步Action,還未執行完畢時,客戶B請求會阻塞。

對於非同步方法,每個請求都是從線程池拿空閑線程出來執行方法?也就是客戶A和客戶B請求方法,都是在不同子線程里分別執行的。


1.如果該方法是一個非同步方法 返回類型應當為Task& 或者ValueTask& 用於返回該非同步任務執行狀態用於後續處理

2.不要認為async加上就是非同步,async只是用來告知編譯器,該方法內會有await等待語句,也包含非同步操作,這也是為什麼非同步編程會出現async大範圍傳染的一個情況

3.對於同步方法,本質就是執行到對應方法後,立即處理該方法內內容並阻塞,只有當目標方法執行完畢才會繼續執行下一步的語句,整體上來看最簡單理解就是這個流程是完全線性的

4.對於非同步方法,有個核心概念是叫做掛起與控制權轉移,簡單說對於非同步方法,他並不會導致阻塞情況發生,出現類似需要等待處理的操作或delay等行為時不是阻塞而是將該方法掛起並交出控制權交由其他方法繼續處理,在自己await的或者delay的方法完成後,將會恢復到那個方法交出控制權的時候的狀態繼續執行

5.由於恢復控制權的時候,具體由哪個線程來繼續運行是由CLR從線程池拿空閑線程進行處理,由於.net默認線程池是多線程,所以會出現發現非同步方法中出現了線程ID不同的情況,但其實如果你使用自定義的線程調度器並限制線程數為1那就能看出來其實非同步是與多線程沒有強關聯性

自定義線程調度器可以看看微軟寫的樣例

TaskScheduler 類 (System.Threading.Tasks)?

docs.microsoft.com圖標

6.因此對於類似web請求來說,如果你能保證每次請求都能已足夠快的速度完成,那麼你的所有請求其實都是可以達成理想的單線程全非同步,但是如果不能,那麼其實就是以多線程執行滿足的非同步

不過同理 如果同一時刻非同步任務數量超過了線程池空閑線程數量,那多的非同步任務還是得乖乖排隊等分配線程執行

建議直接閱讀以下內容:

C# 中的非同步編程?

docs.microsoft.com圖標非同步編程模式?

docs.microsoft.com圖標

然後隨便翻找了下其他老哥的理解見解

.NET 非同步詳解 - hez2010 - 博客園?

www.cnblogs.com


同步指的是代碼順序執行,執行上一行再執行下一行

非同步代碼指的是,代碼執行沒有順序.

如果還是不能理解,可以私聊我,我這邊有免費的這些視頻學習筆記,可以更深入的學習了解一下


非同步方法和同步方法哪裡不同呢?

首先非同步 != 多線程 , 這是兩個層次的概念。多線程是實現非同步的基礎,單線程也一樣可以實現非同步。比如node.js。

C#中更重要的是await,而不是async 。 async只是給編譯器的提示標記。所以我們具體看看同步/非同步函數內部的區別。

public ActionResult PushFileData(FileData file){
db.Save(file);
Log("done");
}

public async Task& PushFileData(FileData file){
await db.SaveAsync(file);
Log("done");
}

同步:

線程A開始執行代碼,之後同步執行db.Save方法。 同步執行意味著,將數據發送到資料庫,再到數返回寫入成功,這一段時間,線程A是被阻塞的。也就是白白浪費了CPU的時間片。以及線程池中的一個線程對象。

Save方法執行完之後仍然由線程A執行Log方法。

非同步:

線程A開始執行代碼,之後非同步執行SaveAsync方法。 非同步執行意味著,將數據發送並等待的這段時間,線程A被放回了線程池,換言之,沒有佔用任何線程。既然沒有佔用任何線程也就不存在佔用CPU時間片。

之後等到SaveAsync方法執行完畢後,又從線程池中獲取一個線程(可能是A,也可能是其他線程),在執行Log方法。

所以,上面的非同步代碼也等價於

public Task& PushFileData(FileData file){
return Task.Run(() =&> db.SaveAsync(file))
.ContinueWith(lastTask =&> Log("done"));
}
//就和ES6的 .then()方法一樣

那麼你可能又會問,你噼里啪啦說了這麼多,同步方法和非同步方法對於http://APS.NET CORE好像並沒有什麼用啊? 對於GUI程序起碼還能讓主界面不至於卡死。

經過上面的一番分析之後,我們知道http://ASP.NET CORE在執行控制器方法的時候同樣依賴線程池。

當有大量並發的請求時。比如10個吧,而你線程池的線程只有5個可用線程。

那麼你同步方法就會在同一時刻佔用5個線程處理5個請求,剩下的5個請求就排隊等待了。

非同步方法用於只執行本機的代碼,不會阻塞等待網路IO,提前被放回線程池,那麼原本剩下的5個請求就能被更快的處理啦。


有了以上的認知我們來看一下題主的問題。

疑問:對於同步方法,每個請求都是使用同個線程嗎?如客戶A請求同步Action,還未執行完畢時,客戶B請求會阻塞。

無論是同步方法還是非同步方法,都會從線程池獲取線程獲取一個線程執行。

所以,「 使用同個線程?」 ,答案是不一定。

  1. 如果線程A執行完被放回了線程池,下一次請求過來仍然獲取的A線程,那麼兩次使用的是同一個線程。
  2. 如果線程A正在使用,則線程池中不存在A線程,可能獲取的B線程,則是不同線程。
  3. 如果線程A執行完被放回了線程池,下一次請求過來獲取的是B線程,則是不同線程。

「還未執行完畢時,客戶B請求會阻塞?」

  1. 如果線程池中還有剩餘線程時,那麼不會阻塞。
  2. 如果線程池中沒有剩餘線程時,那麼會嘗試創建一個新的線程。有兩個閾值,一個是單位時間內創建的線程數,另一個是線程池中的總數。
  3. 如果沒有到閾值,則創建線程。不會阻塞。
  4. 如果到達閾值,阻塞等待。

對於非同步方法,每個請求都是從線程池拿空閑線程出來執行方法?也就是客戶A和客戶B請求方法,都是在不同子線程里分別執行的

emmm... 之前說過,同步方法也是從線程池裡面拿線程。


1 .NET多線程是什麼?

1.1 進程與線程

進程是一種正在執行的程序。

線程是程序中的一個執行流。

多線程是指一個程序中可以同時運行多個不同的線程來執行不同的任務。

1.2 .NET中的線程

Thread是創建和控制線程的類。

ManagedThreadId是線程ID。

CurrentThread是獲取當前正在運行的線程。

1.3 同步與非同步

同步是調用一旦開始,調用者必須等到方法調用返回後,才能繼續後續的行為。(單線程)

非同步調用一旦開始,方法調用就會立即返回,調用者就可以繼續後續的操作。(多線程)

1.4 .NET中的多線程發展

主要有Thread,ThreadPool,Task

Thread就是線程,需要自己調度,直接跟系統對接,相對管理比較複雜及效率差。

ThreadPool是Thread的一個升級版,ThreadPool是從線程池中獲取線程,如果線程池中又空閑的元素,則直接調用,如果沒有才會創建,而Thread則是會一直創建新的線程,要知道開啟一個線程就算什麼事都不做也會消耗大約1m的內存,是非常浪費性能的。但是ThreadPool提供的介面比較少。

Task和ThreadPool是一樣的,都是從線程池中取空閑的線程。比ThreadPool調用介面更加豐富。目前.Net使用多線程管理,應該優先使用Task。

代碼:

/// &

/// 多線程發展歷史
/// &
/// &

& /// &

& private void btnHistory_Click(object sender, EventArgs e)
{
Console.WriteLine($"Thread start id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
var threadStart = new ThreadStart(DoNothing);
var thread = new Thread(threadStart);
thread.Start();
Console.WriteLine($"Thread end id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");

Thread.Sleep(3000);
Console.WriteLine($"ThreadPool start id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
var callback = new WaitCallback(DoNothing);
ThreadPool.QueueUserWorkItem(callback);
Console.WriteLine($"ThreadPool end id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");

Thread.Sleep(3000);
Console.WriteLine($"Task start id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
Action action = DoNothing;
Task task = new Task(action);
task.Start();
Console.WriteLine($"Task end id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
}

2 為什麼需要多線程?

特點: 卡界面:單線程卡,多線程不卡 性能好:單線程差,多線程好(資源換性能) * 執行順序:單線程順序,多線程無序

代碼:

/// &

/// 同步(單線程)
/// &
/// &

& /// &

& private void btnSync_Click(object sender, EventArgs e)
{
Console.WriteLine($"btnSync_Click start id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
for (int i = 0; i &< 5; i++) { DoNothing(); } Console.WriteLine($"btnSync_Click end id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}"); } /// &

/// 非同步(多線程)
/// &
/// &

& /// &

& private void btnAsync_Click(object sender, EventArgs e)
{
Console.WriteLine($"btnAsync_Click start id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
for (int i = 0; i &< 5; i++) { var ts = new ThreadStart(DoNothing); var t = new Thread(ts); t.Start(); } Console.WriteLine($"btnAsync_Click end id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}"); } private void DoNothing() { Console.WriteLine($"DoNothing start id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}"); Thread.Sleep(2000); Console.WriteLine($"DoNothing end id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}"); }

3 如何使用.NET多線程?

3.1 Task

3.1.1 創建任務

1、通過調用任務類構造函數實例化,但通過調用其Start()啟動任務。

Task t1 = new Task(action, "alpha");
t1.Start();

2、通過調用TaskFactory.StartNew(Action &< Object&>,Object)方法在單個方法調用中實例化和啟動任務。

Task t2 = Task.Factory.StartNew(action, "beta");

3、通過調用Run(Action)方法在單個方法調用中實例化和啟動任務。

Task t3 = Task.Run(action);

3.1.2 從任務中返回值

Result 屬性將阻止調用線程,直到任務完成。

Task& task1 = Task&.Factory.StartNew(() =&> 1);
int i = task1.Resu<

3.1.3 等待任務完成

可以通過調用 Wait 方法來等待一個或多個任務完成,從而同步調用線程的執行以及它啟動的非同步任務。

調用無參數 Wait() 方法以無條件等待,直到任務完成。

調用Wait(Int32)和 Wait(TimeSpan) 方法會阻止調用線程,直到任務完成或超時間隔(以先達到者為準)為止。

調用WaitAny(Task[])方法等待一組任務中第一個任務完成。

調用WaitAll(Task[])方法來等待一系列任務全部完成。

3.1.4 異常處理

調用代碼可以通過使用 try/catch 塊中的以下任意方法來處理異常: await task task.Wait() task.Result task.GetAwaiter().GetResult()

代碼:

var task1 = Task.Run(() =&> { throw new Exception("This exception is expected!"); });
try
{
task1.Wait();
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.ReadKey();

3.1.5 取消任務

你可以使用 CancellationTokenSource 類在以後某一時間發出取消請求。

static void Main(string[] args)
{
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
var tasks = new ConcurrentBag&();

Task tc;
for (int i = 0; i &< 10; i++) { var k = i; tc = Task.Run(() =&> DoNothing(k, token), token);
tasks.Add(tc);
}

char ch = Console.ReadKey().KeyChar;
if (ch == c || ch == C)
{
tokenSource.Cancel();
Console.WriteLine("
開始取消任務.");
}

try
{
Task.WhenAll(tasks.ToArray());
}
catch (OperationCanceledException)
{
Console.WriteLine($"
{nameof(OperationCanceledException)} thrown
");
}
finally
{
tokenSource.Dispose();
}
Console.ReadKey();
}

private static void DoNothing(int i, CancellationToken ct)
{
Console.WriteLine($"DoNothing start index:{i} id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
Thread.Sleep(i * 1000);
if (ct.IsCancellationRequested)
{
Console.WriteLine($"任務已取消 index:{i} id:{Thread.CurrentThread.ManagedThreadId} ");
ct.ThrowIfCancellationRequested();
}
Console.WriteLine($"DoNothing end index:{i} id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
}

3.1.6 實際案例

代碼:

/// &

/// Task實際案例
/// &
/// &

& /// &

& private void btnTask_Click(object sender, EventArgs e)
{
Console.WriteLine($"項目組接到任務 id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
Console.WriteLine($"項目經理設計資料庫,設計原型,分配任務 id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
List& tasks = new List&();

tasks.Add(Task.Run(() =&> Coding("趙XX","前端頁面")));
tasks.Add(Task.Run(() =&> Coding("王XX", "IOS頁面")));
tasks.Add(Task.Run(() =&> Coding("黃XX", "後端介面")));
tasks.Add(Task.Run(() =&> Coding("杜XX", "後端介面")));

TaskFactory taskFactory = new TaskFactory();
taskFactory.ContinueWhenAll(tasks.ToArray(), t =&>
{
Console.WriteLine($"項目經理髮布,測試人員測試任務 id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");

});
}

private void Coding(string personName,string taskName)
{
Console.WriteLine($"{personName}開發{taskName} id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
Thread.Sleep(2000);
}

3.2 async和await

Async 和 Await幾乎與創建同步方法一樣創建非同步方法。

代碼:

/// &

/// Async和Await應用
/// &
/// &

& /// &

& private void btnAsyncAndAwait_Click(object sender, EventArgs e)
{
Console.WriteLine($"btnAsyncAndAwait_Click start id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
DoNothingAsync();
Console.WriteLine($"btnAsyncAndAwait_Click end id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");

}

private async Task DoNothingAsync()
{
Console.WriteLine($"DoNothingAsync start id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
await Task.Run(() =&> {
Console.WriteLine($"DoNothingAsync Task start id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
Thread.Sleep(2000);
Console.WriteLine($"DoNothingAsync Task end id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
});
Console.WriteLine($"DoNothingAsync end id:{Thread.CurrentThread.ManagedThreadId} time:{DateTime.Now}");
}


其實很簡單,最重要的是你要搞懂無阻塞非同步等待的概念,因為正確的webapi的async await的作用就是這一個,可以無阻塞非同步等待。

如果你要阻塞線程,那麼你也就不需要強行非同步了。如果你不需要等待,那用Task.Run那樣跑掉就完了,不需要去await。

也就是說一個是同步的,一個是加了async await的非同步代碼,他們在代碼結構上一模一樣,僅僅是多了無阻塞非同步等待。

那麼無阻塞非同步等待是什麼意思呢? 最基本的意思就是不佔用自己資源的去等待一個非同步跑完,比如基本不佔用任何線程

那麼這樣非同步之後的好處是什麼呢? 比如 你把線程數量限制在1,這樣就會非常明顯,你有一個action,裡面CPU密集的東西很少,加一個等待10秒鐘啥也不幹,一下子訪問10次, 非同步基本10秒後就同時完成了這個方法,他們在等待時就把這個線程空出來了。而阻塞版執行完需要100秒,阻塞時就真的阻塞了,把別的請求也阻塞了。 我們平時不會沒事兒等待10秒鐘,但是我們訪問網路等他回復,查詢資料庫等數據回來,訪問文件等數據時,其實都是啥也沒做的在那裡等待,而其實這些都是讓系統IO去做事情,我無所事事,所以windows下有了IOCP,其他平台下會用一個循環去處理。

最終的好處其實比較微妙,就是減少對線程池裡線程數量的佔用,聽起來貌似用處不大,但是博客園當時就遇到了線程猛增一時間創造不出那麼多線程導致卡頓宕機,之後用了async await後基本線程池沒壓力的事情。


推薦閱讀:
相关文章