27

我在带有一个按钮和一个标签的 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";
            });    
        }
    }
}

这是我正在开发的真实应用程序的简化版本。我的印象是通过在我的中使用 async/awaitTask.Run可以设置label1.Text属性。但是,在运行此代码时,我收到我不在 UI 线程上并且无法访问控件的错误。

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

4

6 回答 6

31

当您使用 时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";
}
于 2013-09-26T13:13:39.080 回答
19

尝试这个:

代替

label1.Text = "test";

SetLabel1Text("test");

并将以下内容添加到您的课程中:

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

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

于 2014-09-18T02:29:17.053 回答
16

尝试这个

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

或者

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

或者

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        
}

async/await 不保证它会在 UI 线程中运行。await一旦任务完成,将捕获当前SynchronizationContext并使用捕获的上下文继续执行。

因此,在您的情况下,您有一个嵌套await在内部,Task.Run因此 secondawait将捕获不会出现的上下文,UiSynchronizationContext因为它是由WorkerThreadfrom执行的ThreadPool

这能回答你的问题吗?

于 2013-09-26T13:00:41.437 回答
6

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

你可能应该这样做:

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

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

于 2013-09-26T12:36:21.930 回答
1

因为它在不同的线程上,不允许跨线程调用。

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

于 2013-09-26T12:35:28.010 回答
-1

我将为您提供我为异步理解而给出的最新答案。

解决方案如您所知,当您调用异步方法时,您需要作为任务运行。

这是一个快速控制台应用程序代码,您可以使用它作为参考,它会让您更容易理解这个概念。

using System;
using System.Threading;
using System.Threading.Tasks;

public class Program
{
    public static void Main()
    {
        Console.WriteLine("Starting Send Mail Async Task");
        Task task = new Task(SendMessage);
        task.Start();
        Console.WriteLine("Update Database");
        UpdateDatabase();

        while (true)
        {
            // dummy wait for background send mail.
            if (task.Status == TaskStatus.RanToCompletion)
            {
                break;
            }
        }

    }

    public static async void SendMessage()
    {
        // Calls to TaskOfTResult_MethodAsync
        Task<bool> returnedTaskTResult = MailSenderAsync();
        bool result = await returnedTaskTResult;

        if (result)
        {
            UpdateDatabase();
        }

        Console.WriteLine("Mail Sent!");
    }

    private static void UpdateDatabase()
    {
        for (var i = 1; i < 1000; i++) ;
        Console.WriteLine("Database Updated!");
    }

    private static async Task<bool> MailSenderAsync()
    {
        Console.WriteLine("Send Mail Start.");
        for (var i = 1; i < 1000000000; i++) ;
        return true;
    }
}

在这里,我正在尝试启动名为发送邮件的任务。临时我想更新数据库,而后台正在执行发送邮件任务。

一旦数据库更新发生,它正在等待发送邮件任务完成。但是,通过这种方法,很明显我可以在后台运行任务,并且仍然继续使用原始(主)线程。

于 2014-09-18T02:33:10.480 回答