[英]Executing multiple tasks asynchronously in C#
我正在嘗試構建一個應用來評估公司產品。 為了安全起見,以下描述在某種程度上被抽象化。
該應用程序的作用是更改產品中的某個參數,並查看產品的特定值如何變化。 所以我需要做兩件事。
該圖是這樣的。
應該重復執行這些任務,直到按下取消按鈕。
UI具有以下控件:
這是我編寫的代碼。
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
CancellationTokenSource cts = new CancellationTokenSource();
private async void button1_Click(object sender, EventArgs e)
{
await Task1();
await Task2();
}
private async Task Task1()
{
while (!cts.IsCancellationRequested)
{
Thread.Sleep(500);
ChangeParameter(0);
Thread.Sleep(1000);
ChangeParameter(10);
Thread.Sleep(500);
ChangeParameter(0);
}
}
private void ChangeParameter(double param)
{
// change device paremeter
Console.WriteLine("devicep parameter changed : " + param);
}
private async Task Task2()
{
while (!cts.IsCancellationRequested)
{
Thread.Sleep(100);
int data = GetDataFromDevice();
UpdateTextBoxWithData(data);
}
cts.Token.ThrowIfCancellationRequested();
}
private int GetDataFromDevice()
{
//pseudo code
var rnd = new Random();
return rnd.Next(100);
}
private void UpdateTextBoxWithData(int data)
{
textBox1.AppendText(data.ToString() + "\n");
// debug
Console.WriteLine("data : " + data);
}
private void button2_Click(object sender, EventArgs e)
{
cts.Cancel();
}
}
但是,此代碼中有兩個問題。
第二個問題來自await
因為它一個接一個地執行任務。 我本可以使用Task.Run()
但這不允許向Task.Run()
添加值,因為它與UI線程不同。
我該如何解決這些問題? 任何幫助,將不勝感激。
問題是您沒有在任務中使用await
,因此它們可以同步執行。
您應該使用類似的方法來保持UI響應( 注意這不是生產代碼,我只是在說明一個想法):
private void button1_Click(object sender, EventArgs e)
{
try
{
await Task.WhenAll(Task1(cts.Token), Task2(cts.Token));
}
catch (TaskCancelledException ex)
{
}
}
private async Task Task1(CancellationToken token)
{
while (true)
{
token.ThrowIfCancellationRequested();
await Task.Delay(500, token); // pass token to ensure delay canceled exactly when cancel is pressed
ChangeParameter(0);
await Task.Delay(1000, token);
ChangeParameter(10);
await Task.Delay(500, token);
ChangeParameter(0);
}
}
private async Task Task2(CancellationToken token)
{
while (true)
{
token.ThrowIfCancellationRequested();
await Task.Delay(100, token);
int data = await Task.Run(() => GetDataFromDevice()); //assuming this could be long running operation it shouldn't be on ui thread
UpdateTextBoxWithData(data);
}
}
基本上,當您需要在后台運行某些內容時,應將其包裝在Task.Run()
,然后await
結果。 只是將async
添加到您的方法不會使此方法異步。
為了使代碼更清晰,建議您將GetDataFromDevice
或ChangeParameter
類的方法移至服務層。 另外,請看一下IProgress
因為注釋建議根據某些過程的進度更新您的UI。
此代碼有很多問題:
async/await
不會自動使代碼異步。 它使您可以等待已經異步操作的結果。 如果要在后台運行尚未異步的內容,則需要使用Task.Run或類似的方法來啟動Task。 await
將執行返回到原始同步上下文。 在這種情況下,UI線程。 通過使用Thread.Sleep,您將凍結UI線程 Maxim Kosov已經清理了代碼,並演示了如何正確使用async/await
和Task.Run,因此,我將僅發布如何使用IProgress <T>及其實現( Progress)T <T>
IProgress用於通過IProgress <T> .Report方法公開進度更新。 它的默認實現Progress引發UI線程上的ProgressChanged事件和/或調用傳遞給其構造函數的Action<T>
。 具體來說,在創建類時捕獲的同步上下文上。
您可以在構造函數或按鈕單擊事件中創建進度對象,例如
private async void button1_Click(object sender, EventArgs e)
{
var progress=new Progress<int>(data=>UpdateTextBoxWithData(data));
//...
//Allow for cancellation of the task itself
var token=cts.Token;
await Task.Run(()=>MeasureInBackground(token,progress),token);
}
private async Task MeasureInBackground(CancellationToken token,IProgress<int> progress)
{
while (!token.IsCancellationRequested)
{
await Task.Delay(100,token);
int data = GetDataFromDevice();
progress.Report(data);
}
}
請注意,在任務中使用Thread.Sleep
並不是一個好主意,因為它浪費了線程池線程無所事事。 最好使用await Task.Delay()
,它要求方法的簽名更改為async Task
。 為此有一個Task.Run(Func)重載。
該方法與Maxim Kosov的代碼略有不同,它表明IProgress實際上是跨線程通信的。 IProgress可以處理復雜的類,因此您可以同時返回進度百分比和一條消息,例如:
private async Task MeasureInBackground(CancellationToken token,IProgress<Tuple<int,string>> progress)
{
while(!token.IsCancellationRequested)
{
await Task.Delay(100,token);
int data = GetDataFromDevice();
progress.Report(Tuple.Create(data,"Working"));
}
progress.Report(Tuple.Create(-1,"Cancelled!"));
}
在這里,我只是很懶,返回一個Tuple<int,string>
。 專門的進度課程在生產代碼中更為合適。
使用Action的好處是您不需要管理事件處理程序,並且對象是異步方法的本地對象。 清除由.NET本身執行。
如果您的設備API提供了真正的異步調用,則不需要Task.Run
。 這意味着您不必在緊縮循環中浪費任務,例如:
private async Task MeasureInBackground(CancellationToken token,IProgress<Tuple<int,string>> progress)
{
while(!token.IsCancellationRequested)
{
await Task.Delay(100, token);
int data = await GetDataFromDeviceAsync();
progress.Report(Tuple.Create(data,"Working"));
}
progress.Report(Tuple.Create(-1,"Cancelled!"));
}
大多數驅動程序使用OS功能(稱為完成端口)執行IO任務,本質上是在驅動程序完成操作時調用的回調。 這樣,他們在等待網絡,數據庫或文件系統響應時就無需阻塞。
編輯
在最后一個示例中,不再需要Task.Run
。 僅使用await
就足夠了:
await MeasureInBackground(token,progress);
首先, async
方法可能是虛幻的,因為它們不會神奇地使您的方法異步。 相反,您可以將異步方法視為狀態機的設置(請參見此處的詳細說明),在此您可以通過await
調用來調度操作鏈。
因此,您的異步方法必須盡快執行。 在這種設置方法中請勿執行任何阻止操作。 如果您有要在async方法中執行的阻塞操作,請通過await Task.Run(() => MyLongOperation());
計划它await Task.Run(() => MyLongOperation());
呼叫。
因此,例如,這將立即返回:
private async Task Task1()
{
await Task.Run(() =>
{
while (!cts.IsCancellationRequested)
{
Thread.Sleep(500);
ChangeParameter(0);
Thread.Sleep(1000);
ChangeParameter(10);
Thread.Sleep(500);
ChangeParameter(0);
}
}
}
一句話:其他人可能建議使用Task.Delay
而不是Thread.Sleep
。 我要說的是,僅當Task.Delay
是狀態機配置的一部分時才使用。 但是,如果打算將延遲用作持久操作的一部分,而又不想將其拆分,則可以只停留在Thread.Sleep
。
最后,這部分的說明:
private async void button1_Click(object sender, EventArgs e)
{
await Task1();
await Task2();
}
這會將您的任務配置為互相執行。 如果要並行執行它們,請執行以下操作:
private async void button1_Click(object sender, EventArgs e)
{
Task t1 = Task1();
Task t2 = Task2();
await Task.WhenAll(new[] { t1, t2 });
}
編輯 :關於持久任務的附加說明:默認情況下, Task.Run
在池線程上執行任務。 安排太多並行和長期任務可能會導致飢餓,並且整個應用程序可能凍結很長時間。 因此,為了進行長期操作,您可能需要將Task.Factory.StartNew
與TaskCreationOptions.LongRunning
選項一起使用,而不是Task.Run
。
// await Task.Run(() => LooongOperation(), token);
await Task.Factory.StartNew(() => LooongOperation(), token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.