簡體   English   中英

在C#中異步執行多個任務

[英]Executing multiple tasks asynchronously in C#

環境

  • Windows 7的
  • 視覺工作室
  • C#

我想做什么

我正在嘗試構建一個應用來評估公司產品。 為了安全起見,以下描述在某種程度上被抽象化。

該應用程序的作用是更改產品中的某個參數,並查看產品的特定值如何變化。 所以我需要做兩件事。

  1. 每隔一定時間更改參數
  2. 以一定的時間間隔在文本框中顯示值

該圖是這樣的。

在此處輸入圖片說明

應該重復執行這些任務,直到按下取消按鈕。

UI具有以下控件:

  • button1:開始按鈕
  • button2:取消按鈕
  • textbox1:顯示從設備獲得的值

這是我編寫的代碼。

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();
    }


}

問題

但是,此代碼中有兩個問題。

  1. UI凍結。
  2. Task2從不執行。

第二個問題來自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添加到您的方法不會使此方法異步。

為了使代碼更清晰,建議您將GetDataFromDeviceChangeParameter類的方法移至服務層。 另外,請看一下IProgress因為注釋建議根據某些過程的進度更新您的UI。

此代碼有很多問題:

  1. async/await不會自動使代碼異步。 它使您可以等待已經異步操作的結果。 如果要在后台運行尚未異步的內容,則需要使用Task.Run或類似的方法來啟動Task。
  2. await將執行返回到原始同步上下文。 在這種情況下,UI線程。 通過使用Thread.Sleep,您將凍結UI線程
  3. 您不能從另一個線程更新UI,這也適用於Tasks。 您可以使用IProgress界面來報告進度。 許多BCL類都使用此接口,就像CancellationToken一樣

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.StartNewTaskCreationOptions.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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM