在WinForms上使用async/await访问Task.Run中的UI控件

Wou*_*ort 23 c# winforms task-parallel-library async-await

我在WinForms应用程序中有以下代码,只有一个按钮和一个标签:

using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            await Run();
        }

        private async Task Run()
        {
            await Task.Run(async () => {
                await File.AppendText("temp.dat").WriteAsync("a");
                label1.Text = "test";
            });    
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我正在研究的实际应用程序的简化版本.我的印象是,通过在我使用async/await Task.Run我可以设置label1.Text属性.但是,当运行此代码时,我得到错误,我不在UI线程上,我无法访问该控件.

为什么我无法访问标签控件?

svi*_*ick 27

当你使用时Task.Run(),你希望代码在当前上下文中运行,所以这正是发生的事情.

但是不需要Task.Run()在代码中使用.正确编写的async方法不会阻塞当前线程,因此您可以直接在UI线程中使用它们.如果这样做,await将确保该方法在UI线程上恢复.

这意味着如果您编写这样的代码,它将起作用:

private async void button1_Click(object sender, EventArgs e)
{
    await Run();
}

private async Task Run()
{
    await File.AppendText("temp.dat").WriteAsync("a");
    label1.Text = "test";
}
Run Code Online (Sandbox Code Playgroud)

  • @Daniel那个引用是关于异步代码的同步等待(例如`Wait()`),我当然不是这么说的.来自同一篇文章:"Void-returns async方法有一个特定的目的:使异步事件处理程序成为可能."而这正是我在这里所做的. (11认同)
  • 您不应该使用GUI同步上下文中的async void方法.Cf http://msdn.microsoft.com/en-us/magazine/jj991977.aspx - "GUI和ASP.NET应用程序有一个SynchronizationContext,一次只能运行一个代码块.当等待完成时,它尝试在捕获的上下文中执行async方法的其余部分.但是该上下文已经有一个线程,它(同步)等待异步方法完成.它们每个都在等待另一个,导致死锁. " (3认同)
  • 值得注意的是,在未处理的`private async Task Run()`方法中发生的任何异常都会"冒泡"到异步事件处理程序本身. (2认同)

Sri*_*vel 14

试试这个

private async Task Run()
{
    await Task.Run(async () => {
       await File.AppendText("temp.dat").WriteAsync("a");
       });
    label1.Text = "test";
}
Run Code Online (Sandbox Code Playgroud)

要么

private async Task Run()
{
    await File.AppendText("temp.dat").WriteAsync("a");        
    label1.Text = "test";
}
Run Code Online (Sandbox Code Playgroud)

要么

private async Task Run()
{
    var task = Task.Run(async () => {
       await File.AppendText("temp.dat").WriteAsync("a");
       });
    var continuation = task.ContinueWith(antecedent=> label1.Text = "test",TaskScheduler.FromCurrentSynchronizationContext());
    await task;//I think await here is redundant        
}
Run Code Online (Sandbox Code Playgroud)

async/await不保证它将在UI线程中运行.await将捕获当前SynchronizationContext并在任务完成后继续执行捕获的上下文.

所以你的情况,你有一个嵌套await这里面Task.Run,因此第二await将捕获它不会是上下文UiSynchronizationContext,因为它正在被执行WorkerThreadThreadPool.

这是否回答了你的问题?


Met*_*tro 11

试试这个:

更换

label1.Text = "test";
Run Code Online (Sandbox Code Playgroud)

SetLabel1Text("test");
Run Code Online (Sandbox Code Playgroud)

并将以下内容添加到您的类:

private void SetLabel1Text(string text)
{
  if (InvokeRequired)
  {
    Invoke((Action<string>)SetLabel1Text, text);
    return;
  }
  label1.Text = text;
}
Run Code Online (Sandbox Code Playgroud)

如果您不在UI线程上,则InvokeRequired返回true.Invoke()方法获取委托和参数,切换到UI线程,然后递归调用该方法.在Invoke()调用之后返回,因为在Invoke()返回之前已经递归调用了该方法.如果在调用方法时恰好位于UI线程上,则InvokeRequired为false,并直接执行赋值.

  • 不管是不是旧式代码,这是我在更新控件之前确保我在 UI 线程上的方式。async await 并没有真正改变问题。 (2认同)

Per*_*rsi 6

为什么使用Task.Run?启动一个新的工作线程(cpu绑定),它会导致你的问题.

你应该这样做:

    private async Task Run()
    {
        await File.AppendText("temp.dat").WriteAsync("a");
        label1.Text = "test";    
    }
Run Code Online (Sandbox Code Playgroud)

等待确保您将继续使用相同的上下文,除非您使用.ConfigureAwait(false);


Ger*_*nck 1

因为它位于不同的线程上,并且不允许跨线程调用。

您需要将“上下文”传递给您正在启动的线程。请参阅此处的示例:http ://reedcopsey.com/2009/11/17/synchronizing-net-4-tasks-with-the-ui-thread/