【问题标题】:How can I realize async GUI operations with async and await?如何使用 async 和 await 实现异步 GUI 操作?
【发布时间】:2020-01-29 20:05:04
【问题描述】:

通过单击一个按钮,我需要异步调用一个函数 f1() 来启动一个长操作。 当 f1() 运行时,我想用另一个按钮启动另一个函数 f2()。 当函数 f1() 完成后,我想认识到这一点。

我将整个事情分解为一个非常简单的示例,其中一个按钮启动一个长函数 writeStars(),该函数每 200 毫秒将“*”写入一个字符串,总共 20 次。 使用另一个按钮,我可以将“-”异步写入同一字符串。

        private async void btnStartThread1_Click(object sender, EventArgs e)
        {
            Thread thread1 = new Thread(writeStars);
            thread1.Start();
        }

        private void btnWriteMinus_Click(object sender, EventArgs e)
        {
            _Str += "-";
        }

        async void writeStars()
        {
            int count = 20;
            do
            {
                _Str += "*";
                Thread.Sleep(200);
            } while (count-- > 0);
        }

上面的代码给了我这样的结果:'**-****-*****-****'。

或者当更快地点击第二个按钮时:'**--*-**************'。

这真的是异步的 :-)

我想使用异步任务,以便能够在 writeStars() 结束时等待,然后也许启用一个按钮。我试过了,但我没有办法用 Task、async 和 await 编写这个简单的示例......

【问题讨论】:

  • writeStars的签名改为async Task writeStars()。然后,您可以执行以下操作:await Task.Run(() => writeStars()); DoSomethingAfterCompletion();。如需参考,请参阅:when to return a Task vs void

标签: c# asynchronous async-await task


【解决方案1】:

您可能应该使用asyncawait。基本上,假设您没有主动阻止 GUI,这两个功能(和两个按钮)彼此无关。

例如,您可以制作一个带有 4 个控件的简单 UI(我是在 WPF 中制作的)。两个按钮,每个按钮调用一个异步方法,两个文本块,将在异步方法之后更新。

WPF 代码非常简单。注意点击事件的名称。

<UniformGrid Columns="2">
    <Button x:Name="UpdateSlow" Content="Wait 10 seconds" Click="UpdateSlow_Click" />
    <Button x:Name="UpdateFast" Content="Wait 3 seconds" Click="UpdateFast_Click" />
    <TextBlock x:Name="LongWaitTime" Text="Waiting for text" />
    <TextBlock x:Name="ShortWaitTime" Text="Waiting for text" />
</UniformGrid>

还有后面的代码,里面包含了点击事件。

private async void UpdateSlow_Click(object sender, RoutedEventArgs e)
{
    await Task.Delay(10000);
    LongWaitTime.Text = "I finished in 10 seconds";
}

private async void UpdateFast_Click(object sender, RoutedEventArgs e)
{
    await Task.Delay(3000);
    ShortWaitTime.Text = "I finished in 3 seconds";
}

如果您单击“UpdateSlow”按钮,在“LongWaitTime”文本块更改之前会有 10 秒的延迟。同时,您可以单击“UpdateFast”按钮,该按钮将在 3 秒后更新“ShortWaitTime”文本块。最有可能在 10 秒等待时间结束之前完成(当然取决于您点击的时间)。

希望这是有道理的......

修改文本块:

我无法准确确定您的目标是什么,因此很难提出真正好的解决方案。如果只是为了好玩,下面的代码应该可以工作。但是,我不确定从两种不同的方法访问相同的字符串构建有任何可能的副作用,因此我不建议将其用于生产。

<UniformGrid Columns="1">
    <Button x:Name="AddStar" Content="Add star to text" Click="AddStar_Click" />
    <TextBlock x:Name="Dots" Text="." Margin="20 0" VerticalAlignment="Center" FontSize="24" />
</UniformGrid>

public MainWindow()
{
    InitializeComponent();
    PrintDots();
}

public async void PrintDots()
{
    // This will run forever, printing a dot every 0.2 seconds, clearing the string, when it reaches 50 characters.
    while (true)
    {
        _sb.Append(".");
        await Task.Delay(200);

        if (_sb.Length == 50) { _sb.Clear(); }

        Dots.Text = _sb.ToString();
    }
}

private void AddStar_Click(object sender, RoutedEventArgs e)
{
    // On click a star (asterix) will be appended to the string
    _sb.Append("*");
    Dots.Text = _sb.ToString();
}

我决定将字符串保留在 StringBuilder 中,因为这样可以提高性能,并通过“Append”方法提高可读性。

一个公平的警告:

通常async void 应该只用在用户界面中(例如在按钮点击事件上)。在大多数其他情况下,这不是进行异步编程的好方法。一旦启动,程序将无法跟踪方法进度。这可能会导致各种奇怪的情况,您不知道哪些方法已完成,哪些未完成。相反,您应该使用可以返回和等待的async Task

【讨论】:

  • 非常感谢您的回答。我尝试了您的示例,并且 Task.Delay() 确实异步运行,因为我可以在 UpdateSlow 结束之前单击 UpdateFast。但我想让异步任务做一些事情,比如用星星填充一个字符串,每 200 毫秒一颗星。在此期间,我想通过单击另一个按钮来添加哈希。这适用于我的示例,但我找不到任务/等待的方法。
  • @tom2051 查看添加的示例。这应该可以解决问题(如果只是为了好玩)。
  • 谢谢,这与我的示例非常接近,不同之处在于我明确使用了一个调用函数 writeStars 的新线程。我可以使用我的示例/您的示例中的代码在我的真实应用程序中解决问题。我的意思是我可以在没有等待的情况下解决这个问题。我仍然理解它应该像......启动异步任务,启用按钮,等待任务结束,禁用按钮,在任务运行期间我可以通过单击按钮添加哈希。我无法让它工作......
  • 我现在运行它,我犯的错误是将等待放在异步函数之外。当我将 await 放在 async 函数中时,我可以启动该函数,单击一些按钮,在 await 之后我可以做其他事情,这就是我想要的 :-)
【解决方案2】:

我有点不清楚你想要什么 - 你开始说你想在 f1 运行时启动 f2,但随后你要求在 f1 结束时等待并启用 @ 的按钮987654324@。我将回答第二种情况,您不想f1 完成之前运行f2

您可以使用这样的设计(请原谅语法或格式怪异,我已经很长时间没有接触 C# 领域了):

async void btnWriteStars_Click() {
  try {
    btnWriteMinus.Enabled = false;
    await writeStars();
  } finally {
    btnWriteMinus.Enabled = true;
  }
}

Task WriteStars() {
  return Task.Run(() => {
    // much stringy goodness here
  });
}

策略是“给我写星”事件处理程序禁用您不想中断异步处理的按钮,然后等待异步处理完成,然后再次启用该按钮。

请注意,仅使 WriteStars 异步是不够的 - 它仍然会在调用线程上运行直到第一个 await,当然您的 WriteStars 没有没有 await。因此,我们使用Task.Run 或类似函数来生成一个异步任务,在该任务中运行同步代码块。根据您的实际方法的作用,您可能可以将其写为 async 而不是显式干预 Task 工厂 - 在任何情况下,关键是 WriteStars 为按钮事件处理程序返回一个 Task 对象等待

如果您的要求更复杂——比如你想启用button2,但要排队等待它的“附加减号”操作,直到写入星星完成——那么你的设计也需要更复杂。您可能能够在表单级别存储 Task 实例并让 button2 在其上执行 ContinueWith,但对于真正复杂的场景,您可能需要实现生产者-消费者方法,其中 UI 按钮将操作放在队列和工作线程将它们取下来。但是对于简单的“在 Y 运行时不要执行 X”场景,disable-await-enable 就足够了。

【讨论】:

  • 非常感谢您的回答。问题是我想通过单击 btn1 一次来运行 async writeStars() 。虽然此函数异步运行,在 200 毫秒内添加一个星号,但我希望能够在每次单击 btn2 时在同一个字符串之间写入哈希值。使用我的示例可以,但我不知道如何使用任务/等待来执行此操作。也许这不是任务/等待的工作。据我了解等待,通常我会启动一个异步任务,然后做其他事情,然后在任务上放置一个等待。但我无法以这种方式工作。
猜你喜欢
  • 2012-03-22
  • 2018-02-15
  • 2019-01-17
  • 2016-09-03
  • 1970-01-01
  • 2019-10-12
  • 2013-04-11
  • 2013-05-14
  • 1970-01-01
相关资源
最近更新 更多