「C#」异步编程玩法笔记-WinForm中的常见问题

目录

1、异步更新界面

1.1、问题

1.2、解决问题

1.3、AsyncOperationManager和AsyncOperation

1.4、Invoke、BeginInvoke、EndInvoke及InvokeRequired

Invoke

InvokeRequired

BeginInvoke

EndInvoke

2、死锁

2.1、问题

2.2、 解决方法

2.2.1、不要await

2.2.2、用await代替Wait()/Result

2.2.3、使用新的异步方法中转

2.2.4、ConfigAwaiter(false)

3、ConfigAwaiter(false)


1、异步更新界面

1.1、问题

先新建个简单winform窗体程序(取名WinFormsTPL)

「C#」异步编程玩法笔记-WinForm中的常见问题_第1张图片

界面及按钮实现如下:

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

        private void btnAsyncUpdate_Click(object sender, EventArgs e)
		{
            Task.Factory.StartNew(() =>
            {    
                this.lbText.Text = "你好,世界!";
            });
        }
    }
}

然后运行,就能得到WinForm开发中做异步编程时最常遇到的问题了,就是下面这个报错。

「C#」异步编程玩法笔记-WinForm中的常见问题_第2张图片

简单的理解就是不能跨线程访问UI。因为UI的变更绘制有专门的线程。

但是深究这个问题,法相想理解清楚似乎有点难度。看了很多资料,总是逃不过两个主要的动东西:UI线程和同步上下文(SynchronizationContext)。

具象化一点,打个可能不恰当的比喻,公司里面办事的员工相当于线程,部门以及办公室相当于同步上下文。员工(线程)的工作需要办公场所(同步上下文)。但员工可以在不同办公场所穿行走动去完成他的工作,例如去装配间组转设备然后去厂房调试设备,然后去办公室写ppt……

看一下巨硬家大佬的文章怎么说的(似乎有点久远):

MSDN 杂志:并行计算 - SynchronizationContext 综述 | Microsoft Learn

SynchronizationContext 的实际“上下文”并没有明确的定义。不同的框架和主机可以自行定义自己的上下文。通过了解这些不同的实现及其限制,可以清楚了解 SynchronizationContext 概念可以和不可以实现的功能。我将简单讨论部分实现。

WindowsFormsSynchronizationContext(System.Windows.Forms.dll:System.Windows.Forms)Windows 窗体应用程序会创建并安装一个 WindowsFormsSynchronizationContext 作为创建 UI 控件的任意线程的当前上下文。这一 SynchronizationContext 使用 UI 控件的 ISynchronizeInvoke 方法,该方法将委托传递给基础 Win32 消息循环。WindowsFormsSynchronizationContext 的上下文是一个单独的 UI 线程。

在 WindowsFormsSynchronizationContext 列队的所有委托一次一个地执行;它们通过一个特定 UI 线程执行以便列队。当前实现为每个 UI 线程创建一个 WindowsFormsSynchronizationContext。

DispatcherSynchronizationContext(WindowsBase.dll:System.Windows.Threading)WPF 和 Silverlight 应用程序使用 DispatcherSynchronizationContext,这样,委托按“常规”优先级在 UI 线程的调度程序中列队。当一个线程通过调用 Dispatcher.Run 开始其调度程序时,这一 SynchronizationContext 作为当前上下文安装。DispatcherSynchronizationContext 的上下文是一个单独的 UI 线程。

在 DispatcherSynchronizationContext 列队的所有委托一次一个地执行;它们通过一个特定 UI 线程执行以便列队。当前实现为每个顶层窗口创建一个 DispatcherSynchronizationContext,即使它们都使用相同的基础调度程序也是如此。

本人WPF不熟,单说Winform的SynchronizationContext也就是WindowsFormsSynchronizationContext

,作为创建 UI 控件的任意线程的当前上下文。那就是说一个窗体程序(winform)只能有一个同步上下文。

那么能不能在一个同步上下文里启动另一个winfom程序呢?

在窗体上bia一个按钮,按钮事件中调用

Application.Run(new Form2());

ok,报错:

「C#」异步编程玩法笔记-WinForm中的常见问题_第3张图片

窗体程序的UI线程底层就是消息循环机制,一个线程上只能有一个消息循环。(没有找到比较明确的官方文档说明)

那么对于一个winform程序,其UI线程是单一线程,与其对应的同步上下文(SynchronizationContext)也只有一个。

不过也不是不能有多UI线程的窗体程序,比如这样写就不会报错:

private void btn_Click(object sender, EventArgs e)
{
    var thread = new Thread(() =>
    {
        Form f = new Form();
        Application.Run(f);
    });
    thread.SetApartmentState(ApartmentState.STA);
    thread.Start();
}

这样即在新线程里启动新窗体,但新的窗体也会有新的同步上下文。

在之前提到的官方文档MSDN 杂志:并行计算 - SynchronizationContext 综述 | Microsoft Learn 中也能看到说明

SynchronizationContext 实例和线程之间没有 1:1 的对应关系。WindowsFormsSynchronizationContext 确实 1:1 映射到一个线程(只要不调用 SynchronizationContext.CreateCopy),但任何其他实现都不是这样。一般而言,最好不要假设任何上下文实例将在任何指定线程上运行。

回过头来再看一下最初的报错信息:“从不是创建控件“xxx”的线程访问它”。即一个控件、一个窗体的同步上下文是在new它的时候确定的,如果在新线程中,则也会有新的同步上下文。

1.2、解决问题

大致了解清楚原有后,解决这个问题的方式就明确了,无非就是两条路,一是回到创建它的线程,二是回到它的同步上下文。

先看第一种

private void btnAsyncUpdate_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew(() =>
    {
    });
    this.lbText.Text = "你好,世界!";
}

emmm……“避免bug的最好方式就是不写代码!”避免异步报错的方式就是不要异步!

看过废话文学后看第二种方法:

private async void btnAsyncUpdate_Click(object sender, EventArgs e)
{
    SynchronizationContext currentContext = SynchronizationContext.Current;
    await Task.Factory.StartNew((c) =>
    {
        SendOrPostCallback sendCallback = (o) =>
        {
            this.lbText.Text = "你好,世界!";
        };
        if (c is WindowsFormsSynchronizationContext context)
        {
            context.Send(sendCallback, null);
        }

    }, currentContext);           
}

即使用SynchronizationContext.Send()方法。将界面操作封送会原有的同步上下文,执行时对控件的赋值自然在原有的同步上下文对应的线程上执行了,就不会报错。

SynchronizationContext有Send和Post()两个常用方法,有很多文章来详细介绍两者不同,总结的说,Send()是封送到同步执行,Post()是异步执行。具体看下源码,结合之前对线程和线程池的说明就很好理解了:

public virtual void Send(SendOrPostCallback d, Object state)
{
    d(state);
}
 
public virtual void Post(SendOrPostCallback d, Object state)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(d), state);
}

1.3、AsyncOperationManager和AsyncOperation

在VS中编写1.2中方法二的代码时,可以看到VS的一个提示:

即是说SynchronizationContext.Current是可能未空的,实际上控制台程序中该项即默认为空的。

更加推荐使用AsyncOperationManager和AsyncOperation

.NET Framework 中的 AsyncOperationManager 和 AsyncOperation 类是 SynchronizationContext 抽象的轻型包装。AsyncOperationManager 在第一次创建 AsyncOperation 时捕获当前 SynchronizationContext,如果当前 SynchronizationContext 为 null,则使用默认 SynchronizationContext。AsyncOperation 将委托异步发布到捕获的 SynchronizationContext。

最新的.Net7中也是有这两个类的。

public static class AsyncOperationManager
{
    public static AsyncOperation CreateOperation(object userSuppliedState)
    {
        return AsyncOperation.CreateOperation(userSuppliedState, SynchronizationContext);
    }

    /// 
    [EditorBrowsable(EditorBrowsableState.Advanced)]
    public static SynchronizationContext SynchronizationContext
    {
        get
        {
            if (SynchronizationContext.Current == null)
            {
                SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
            }

            return SynchronizationContext.Current;
        }

#if SILVERLIGHT
        // a thread should set this to null  when it is done, else the context will never be disposed/GC'd
        [SecurityCritical]
        [FriendAccessAllowed]
        internal set {
            SynchronizationContext.SetSynchronizationContext(value);
        }
#else
        // a thread should set this to null  when it is done, else the context will never be disposed/GC'd
        [PermissionSetAttribute(SecurityAction.LinkDemand, Name = "FullTrust")]
        set
        {
            SynchronizationContext.SetSynchronizationContext(value);
        }
#endif
    }
}

即使用AsyncOperationManager.CreateOperation()实例化AsyncOperation对象时是会判断有没有SynchronizationContext,没有则会创建一个SynchronizationContext,以确保其不为空。

再来看AsyncOperation,源码如下:

namespace System.ComponentModel
{
    using System.Security.Permissions;
    using System.Threading;
    
    [HostProtection(SharedState = true)]
    public sealed class AsyncOperation
    {
        private SynchronizationContext syncContext;
        private object userSuppliedState; 
        private bool alreadyCompleted;
 
        /// 
        ///     Constructor. Protected to avoid unwitting usage - AsyncOperation objects
        ///     are typically created by AsyncOperationManager calling CreateOperation.
        /// 
        private AsyncOperation(object userSuppliedState, SynchronizationContext syncContext)
        {
            this.userSuppliedState = userSuppliedState;
            this.syncContext = syncContext;
            this.alreadyCompleted = false;
            this.syncContext.OperationStarted();
        }
 
        /// 
        ///     Destructor. Guarantees that sync context will always get notified of completion.
        /// 
        ~AsyncOperation()
        {
            if (!alreadyCompleted && syncContext != null)
            {
                syncContext.OperationCompleted();
            }
        }
 
        public object UserSuppliedState
        {
            get { return userSuppliedState; }
        }
 
        /// 
        public SynchronizationContext SynchronizationContext
        {
            get
            {
                return syncContext;
            }
        }
 
        public void Post(SendOrPostCallback d, object arg)
        {
            VerifyNotCompleted();
            VerifyDelegateNotNull(d);
            syncContext.Post(d, arg);
        }
 
        public void PostOperationCompleted(SendOrPostCallback d, object arg)
        {
            Post(d, arg);
            OperationCompletedCore();
        }
 
        public void OperationCompleted()
        {
            VerifyNotCompleted();
            OperationCompletedCore();
        }
 
        private void OperationCompletedCore()
        {
            try
            {
                syncContext.OperationCompleted();
            }
            finally
            {
                alreadyCompleted = true;
                GC.SuppressFinalize(this);
            }
        }
 
        private void VerifyNotCompleted()
        {
            if (alreadyCompleted)
            {
                throw new InvalidOperationException(SR.GetString(SR.Async_OperationAlreadyCompleted));
            }
        }
 
        private void VerifyDelegateNotNull(SendOrPostCallback d)
        {
            if (d == null)
            {
                throw new ArgumentNullException(SR.GetString(SR.Async_NullDelegate), "d");
            }
        }
 
        /// 
        ///     Only for use by AsyncOperationManager to create new AsyncOperation objects
        /// 
        internal static AsyncOperation CreateOperation(object userSuppliedState, SynchronizationContext syncContext)
        {
            AsyncOperation newOp = new AsyncOperation(userSuppliedState, syncContext); 
            return newOp;
        }
    }
}

从源码看,一方面可以获取到不为空的SynchronizationContext,并且可以直接使用Post()方式进行调用,Post()内部处理前做了校验,一个委托只能在OperationCompleted()之前调用,使用PostOperationCompleted()即调用一次边关闭,Completed之后内部会调用GC回收这个AsyncOperation对象。

还有一点要说,就是更多的是在基于事件的异步编程中使用的,基于事件的异步编程已经不被推荐,更多的使用基于任务的异步编程。

新组件不应使用基于事件的异步模式。Visual Studio 异步社区技术预览 (CTP) 包含一篇描述基于任务的异步模式的文档,在这种模式下,组件返回 Task 和 Task 对象,而不是通过 SynchronizationContext 引发事件。基于任务的 API 是 .NET 中异步编程的发展方向。

1.4、Invoke、BeginInvoke、EndInvoke及InvokeRequired

如1.1和1.2中所说,如果有窗体或者控件(假设Form2)是在新的线程中(new Thread)创建,但是又想在主界面的UI线程(From1)中去操作这个窗体(Form2)的更新。例如下面的代码,应该怎么改更合适呢?

private void btnAsyncUpdate_Click(object sender, EventArgs e)
{
    Form2 form2 = null;
    Task.Factory.StartNew(() =>
    {
        form2 = new Form2();
        form2.ShowDialog();
    });
    Thread.Sleep(1000);//确保form2被实例化了
    form2.Text = "新窗体";//会报跨线程访问的错误
}

按前文的方法们就得在Form2中添加公共的SynchronizationContext或AsyncOperation属性,然后在form1中再去定义委托,再用form2的这个属性去传递这个委托,就会很麻烦。

WinForm中实际上已经封装了更为直接的方法,即Invoke

Invoke的注释翻译过来大概如下:

在拥有此控件的基础窗口句柄的线程上执行给定的委托。在该控件所属的同一线程上调用此方法是错误的。如果控件的句柄尚不存在,这将跟随控件的父链,直到找到确实具有窗口句柄的控件或窗体。如果找不到合适的句柄,Invoke将引发异常。在调用期间引发的异常将被传递回调用方。

从任何线程都可以安全地调用控件上的五个函数:GetInvokeRequired、Invoke、BeginInvoke、EndInvoke和CreateGraphics。对于所有其他方法调用,应使用其中一个Invoke方法来封送对控件线程的调用。

GetInvokeRequired、Invoke、BeginInvoke、EndInvoke和CreateGraphics都是System.Windows.Forms.Control下的方法。

GetInvokeRequired应该是较旧的方法,最新的与之对应的应该是InvokeRequired

InvokeRequired、Invoke、BeginInvoke、EndInvoke都是借口ISynchronizeInvoke所规定的。

Invoke

在拥有此控件的基础窗口句柄的线程上执行委托。

因此前面的例子可以直接改写为:

private void btnAsyncUpdate_Click(object sender, EventArgs e)
{
    Form2 form2 = null;
    Task.Factory.StartNew(() =>
    {
        form2 = new Form2();
        form2.ShowDialog();
    });
    Thread.Sleep(1000);//确保form2被实例化了
    form2.Invoke(() =>
        {
            form2.Text = "新窗体"
        });
}

在创建控件的线程上使用Invoke会报错。并且是根据控件或控件的父级中存在的窗体控件句柄(Handle)去查找底层的消息循环线程做处理的,所以控件或其父级必须具有实例的句柄,否则会抛异常。

InvokeRequired

获取一个bool值,该值指示调用方在对控件进行方法调用时是否必须调用 Invoke 方法,因为调用方位于创建控件所在的线程以外的线程中。

在创建控件的线程中使用Invoke会报错,所以当代码比较复杂时,提前做判断还是必要的:

private void btnAsyncUpdate_Click(object sender, EventArgs e)
{
    Form2 form2 = null;
    Task.Factory.StartNew(() =>
    {
        form2 = new Form2();
        form2.ShowDialog();
    });
    Thread.Sleep(1000);
    if (form2.InvokeRequired)
    {
        form2.Invoke(() =>
        {
            form2.Text = "新窗体";
        });
    }
}

BeginInvoke

先参考下源码:

public IAsyncResult BeginInvoke(Delegate method, params Object[] args) 
{
    using (new MultithreadSafeCallScope()) 
    {
        Control marshaler = FindMarshalingControl();
        return(IAsyncResult)marshaler.MarshaledInvoke(this, method, args, false);
    }
}

返回值是IAsyncResult类型的,有点熟悉哎,因为Task就是继承自IAsyncResult的。

BeginInvoke是异步的方法。即当需要Invoke一个比较耗时的任务时,可以考虑使用BeginInvoke,并不是要在这个方法中传入异步任务,而是它本身就是以异步方式执行。这样防止某个控件或窗体长时间的更新而对调用窗体造成假死状态。

比如:

private void btnAsyncUpdate_Click(object sender, EventArgs e)
{
    Form2 form2 = null;
    Task.Factory.StartNew(() =>
    {
        form2 = new Form2();
        form2.ShowDialog();
    });
    Thread.Sleep(500);
    if (form2.InvokeRequired)
    {
        form2.BeginInvoke(() =>
        {
            Thread.Sleep(1000);
            form2.Text = "新窗体";
        });
    }
    this.lbText.Text = "按钮事件执行完毕";
}

运行效果如下:先弹出form2窗体,然后form1主窗体中的label更新,然后form2窗体的标题才更新完毕。

「C#」异步编程玩法笔记-WinForm中的常见问题_第4张图片

看下关于BeginInvoke的官方注解

委托以异步方式调用,此方法会立即返回。 你可以从任何线程调用此方法,即使是拥有控件句柄的线程。 如果控件的句柄尚不存在,此方法将搜索控件的父链,直到找到具有窗口句柄的控件或窗体。 如果未找到适当的句柄, BeginInvoke 将引发异常。 委托方法中的异常被视为未捕获,并将发送到应用程序的未捕获异常处理程序。

这里隐藏了一些坑,控件不一定有句柄,如果按父链找到句柄就是调用BeginInvoke的窗体,这到底会怎样呢。

比如下面的操作

private void btnAsyncUpdate_Click(object sender, EventArgs e)
{    
    this.lbText.BeginInvoke(() =>
    {
        this.lbText.Text = "BeginInvoke正在执行";
        Thread.Sleep(2000);
        this.lbText.Text = "BeginInvoke执行完毕";
    });
    this.lbText.Text = "按钮事件执行完毕";
}

按异步的原理,应该会先看到"按钮事件执行完毕",然后"BeginInvoke正在执行"等待2秒后看到"BeginInvoke执行完毕"。然而实际上只看到最后加一句。

也就是说BeginInvoke的时候实际上时将对应句柄的窗体控件挂起,知道异步方法执行结束后再绘制界面。label控件没有句柄,会找到父窗体的句柄在其上执行,所以即使上面的例子中,窗体中引入其他控件,最终也是等BeginInvoke的内容全部执行完后才更新整个界面。

所以在使用BeginInvoke时还是要多注意,尽量是跨窗体使用。

EndInvoke

BeginInvoke官方注解中的另一段话是这个:

可以调用 EndInvoke 以从委托中检索返回值(如果为 neccesary),但这不是必需的。 EndInvoke 将阻止,直到可以检索返回值。

即EndInvoke是将异步边同步,类似于Task的Wait()方法。

使用方式是将BeginInvoke返回的IAsyncResult对象传入。

private void btnAsyncUpdate_Click(object sender, EventArgs e)
{
    Form2 form2 = null;
    Task.Factory.StartNew(() =>
    {
        form2 = new Form2();
        form2.ShowDialog();
    });
    Thread.Sleep(500);
    if (form2.InvokeRequired)
    {
        IAsyncResult asyncResult = form2.BeginInvoke(() =>
        {
            Thread.Sleep(1000);
            form2.Text = "新窗体";
        });
        form2.EndInvoke(asyncResult);
    }
    this.lbText.Text = "按钮事件执行完毕";
}

2、死锁

2.1、问题

死锁是擦winform等界面编程中比较常见又很诡异的情况。

前文的例子中,在form1中加一个按钮(btnAsyncFunc)

「C#」异步编程玩法笔记-WinForm中的常见问题_第5张图片

按钮事件如下:

private void btnAsyncFunc_Click(object sender, EventArgs e)
{
    AsyncFunc().Wait();
    this.lbText.Text = "AsyncFunc执行完毕";
}
private async Task AsyncFunc()
{
    await Task.Run(() =>
    {
        Thread.Sleep(100);
    });
}

看似再简单不过的一段代码了,但是点击按钮时,界面会完美卡死,无法操作。

为什么?

参考之前的笔记:「C#」异步编程玩法笔记-async、await_Raink_LH的博客-CSDN博客

里面有说明async和await的执行顺序。异步方法AsyncFunc会同步运行到await处,然后运行Task并把Task返回,返回后发现是Wait(),就得等待Task执行完毕,Task执行完毕后,Task语句之后的代码是同步执行的,这里的同步执行是在创建Task的线程,例子中也就是UI线程。而UI线程只有一个,此时线程中正在运行的是调用方的Wait()方法,Wait()没有执行完毕就不会运行到Task之后的语句,所以AsyncFunc方法中Task.Run(...);之后的代码不会执行(虽然末尾没有代码了,但方法末尾的后大括号也可以认为是代码),但是AsyncFunc方法不执行到后大括号,AsyncFunc方法就不能结束,从而不能让Wait()结束。互相等,从而死锁。

由此也可以知道,在调用异步方法时使用Wait()、Result等阻塞方法时都有可能出现这种情况。

但这样的死锁不会在控制台程序中出现。且最大并发数没有做限制,await之后的代码与Wait()/Result的执行是在不同线程上发生的,两者是可以同时运行的,所以不会有影响。比如这个就不会有问题。

static void Main(string[] args)
{
    LockTest();
    Console.WriteLine("程序结束");
    Console.ReadLine();
}
private static void LockTest()
{
    Console.WriteLine("调用并等待异步方法");
    ConeoleAsyncFunc().Wait();
    Console.WriteLine("异步方法结束");
}
private static async Task ConeoleAsyncFunc()
{
    await Task.Run(() =>
    {
        Thread.Sleep(100);
        Console.WriteLine("ConeoleAsyncFunc"); 
    });
}

但如果我们使用自定义的任务调度器,限定最大并发数为1,且拒绝内联的方式执行任务(重写TaskScheduler中的TryExecuteTaskInline方法直接return false),如下,程序就会自锁卡住。

static void Main(string[] args)
{
    TaskScheduler scheduler = new LimitedConcurrencyTaskScheduler(1);
    TaskFactory factory = new TaskFactory(scheduler);
    var t = factory.StartNew(LockTest);
    t.Wait();
    Console.WriteLine("程序结束");
    Console.ReadLine();
}

private static void LockTest()
{
    Console.WriteLine("调用并等待异步方法");
    ConeoleAsyncFunc().Wait();
    Console.WriteLine("异步方法结束");
}
private static async Task ConeoleAsyncFunc()
{
    await Task.Run(() =>
    {
        Thread.Sleep(100);
        Console.WriteLine("ConeoleAsyncFunc"); 
    });
}

2.2、 解决方法

2.2.1、不要await

既然要Wait(),要阻塞,那就最好把原方法改成同步的,不要有async,不要有await。

private void btnAsyncFunc_Click(object sender, EventArgs e)
{
    AsyncFunc().Wait();
    this.lbText.Text = "AsyncFunc执行完毕";
}
private Task AsyncFunc()
{
    var t =Task.Run(() =>
    {
        Thread.Sleep(100);
    });
    t.Wait();
}

嗯..........我承认这样有点脱裤子放屁,总之意思就是如果需要阻塞,就尽量不要用异步方法。

2.2.2、用await代替Wait()/Result

既然必须要异步,那么就异步到底,异步方法的调用者也使用async await。即:

private async void btnAsyncFunc_Click(object sender, EventArgs e)
{
    await AsyncFunc();
    this.lbText.Text = "AsyncFunc执行完毕";
}
private async Task AsyncFunc()
{
    await Task.Run(() =>
    {
        Thread.Sleep(100);
    });
}

对于异步方法有返回值TResult的,也已用await

private async void btnAsyncFunc_Click(object sender, EventArgs e)
{
    int num = await AsyncFunc();
    this.lbText.Text = "AsyncFunc执行完毕";
}
private async Task AsyncFunc()
{
    await Task.Run(() =>
    {
        Thread.Sleep(100);
    });
    return 0;
}

2.2.3、使用新的异步方法中转

即再加一个异步方法,新的异步方法像2.2.1中说的,不要用async、await,而完全用Wait()/Result

private void btnAsyncFunc_Click(object sender, EventArgs e)
{
    RunAsyncFunc();
    this.lbText.Text = "AsyncFunc执行完毕";
}
private void RunAsyncFunc()
{
    var t = Task.Run(() =>
    {
        AsyncFunc().Wait();
    });
    t.Wait();
}
private async Task AsyncFunc()
{
    await Task.Run(() =>
    {
        Thread.Sleep(100);
        int a = 0;
    });
}

虽然异步方法AsyncFunc()后面还是用了Wait(),但是这个Wait()是在另一个线程中发生的,即AsyncFunc()中await之后的代码是在另一个线程中发生,而不是界面的UI主线程,所以不会造成死锁。

2.2.4、ConfigAwaiter(false)

这个是Task的一个公共方法。官方的注解如下:

异步方法直接等待 Task 时,延续任务通常会出现在创建任务的同一线程中,具体取决于异步上下文。 此行为可能会降低性能,并且可能会导致 UI 线程发生死锁。 若要避免这些问题,请调用 Task.ConfigureAwait(false)。

也就是说,ConfigAwaiter()传入false时,是不要将延续任务安排在创建任务的同一线程中。

按照第一节中的问题和本节死锁的问题分析,死锁根源是await结束后返回了UI线程,UI线程由呗占用。

所以如果将await之后的续接任务,安排在别的线程中,就不会死锁了。

比如这样

private async void btnAsyncFunc_Click(object sender, EventArgs e)
{
    int num = await AsyncFunc();
    this.lbText.Text = "AsyncFunc执行完毕";
}
private async Task AsyncFunc()
{
    await Task.Run(() =>
    {
        Thread.Sleep(100);
    }).ConfigAwaiter(false);//使用ConfigAwaiter(false)
    return 0;
}

也可以解决死锁的问题。

3、ConfigAwaiter(false)

为什么把这个方法单独又列出来作为一节内容呢。再看一眼官方注解:

异步方法直接等待 Task 时,延续任务通常会出现在创建任务的同一线程中,具体取决于异步上下文。 此行为可能会降低性能,并且可能会导致 UI 线程发生死锁。 若要避免这些问题,请调用 Task.ConfigureAwait(false)。

似乎在UI编程中基于任务的多线程处理都应添加这个方法以避免死锁。

然后无脑使用这个方法,在避免第二节的死锁问题时,就很容易引发第一节的跨线程调用UI的错误。

ConfigureAwait(false)之后,也就是await的后续任务代码会在Task的上下文中执行,而如果后续任务是操作UI空间,则会触发“线程间操作无效……”的异常。

比如将上面的示例稍作调整,如下:

private void btnAsyncFunc_Click(object sender, EventArgs e)
{
    AsyncFunc().Wait();
}
private async Task AsyncFunc()
{
    await Task.Run(() =>
    {
        Thread.Sleep(100);
        int a = 0;
    }).ConfigureAwait(false);
    
    //异步完成后更新界面
    this.lbText.Text = "AsyncFunc执行完毕";
    }
}

运行后就是这样的结果。

「C#」异步编程玩法笔记-WinForm中的常见问题_第6张图片

所以ConfigAwaiter(false)不能无脑用,使用时一定主要,其后面不能有对UI界面的操作。

你可能感兴趣的:(假装会写C#,c#,ui,.net)