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

news2024/11/18 10:21:02

目录

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)

界面及按钮实现如下:

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开发中做异步编程时最常遇到的问题了,就是下面这个报错。

简单的理解就是不能跨线程访问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,报错:

窗体程序的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);
    }

    /// <include file='doc\AsyncOperationManager.uex' path='docs/doc[@for="AsyncOperationManager.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;
 
        /// <summary>
        ///     Constructor. Protected to avoid unwitting usage - AsyncOperation objects
        ///     are typically created by AsyncOperationManager calling CreateOperation.
        /// </summary>
        private AsyncOperation(object userSuppliedState, SynchronizationContext syncContext)
        {
            this.userSuppliedState = userSuppliedState;
            this.syncContext = syncContext;
            this.alreadyCompleted = false;
            this.syncContext.OperationStarted();
        }
 
        /// <summary>
        ///     Destructor. Guarantees that sync context will always get notified of completion.
        /// </summary>
        ~AsyncOperation()
        {
            if (!alreadyCompleted && syncContext != null)
            {
                syncContext.OperationCompleted();
            }
        }
 
        public object UserSuppliedState
        {
            get { return userSuppliedState; }
        }
 
        /// <include file='doc\AsyncOperation.uex' path='docs/doc[@for="AsyncOperation.SynchronizationContext"]/*' />
        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");
            }
        }
 
        /// <summary>
        ///     Only for use by AsyncOperationManager to create new AsyncOperation objects
        /// </summary>
        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<TResult> 对象,而不是通过 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窗体的标题才更新完毕。

看下关于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)

按钮事件如下:

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<int> 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<int> 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执行完毕";
    }
}

运行后就是这样的结果。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/96175.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Jmeter(十八):硬件性能监控指标

硬件性能监控指标 一、性能监控初步介绍 性能测试的主要目标 1.在当前的服务器配置情况&#xff0c;最大的用户数 2.平均响应时间ART&#xff0c;找出时间较长的业务 3.每秒事务数TPS&#xff0c;服务器的处理能力 性能测试涉及的内容 1.客户端性能测试&#xff1a;web前…

【Redis】主从复制和哨兵模式

主从复制 主从复制&#xff1a;主机数据更新后根据配置和策略&#xff0c; 自动同步到备机的master/slaver机制&#xff0c;Master以写为主&#xff0c;Slave以读为主 作用&#xff1a; 读写分离&#xff0c;性能扩展容灾快速恢复 主从复制的配置 在我的/myredis目录中&…

第17章 前端之全局存储:Vuex=Store

17章 前端之全局存储&#xff1a;VuexStore Store用于对数据进行存储&#xff0c;并共享以为所有需要的Vue页面中的调用提供数据及其方法支撑&#xff1b;Vuex是Store的内置引用包&#xff0c;即如果想要前端Vue程序支撑Store必须在新建Vue程序&#xff1b;如果Vue程序没有引用…

【python】一文带你理解并解决conda activate虚拟环境后,pip安装的包没放在虚拟环境

太长不看版 环境变量有问题&#xff0c;查看环境变量&#xff0c;应该会发现&#xff0c;在你虚拟环境的地址之前&#xff0c;有其他的地址&#xff0c;比如/home/xxx/.local/bin:等&#xff0c;而且这个地址里面刚好有pip,python这些程序。 最简单的办法&#xff1a;去把/hom…

第十五章 Golang单元测试

1. 先看一个需求 在我们工作中&#xff0c;我们会遇到这样的情况&#xff0c;就是去确认一个函数&#xff0c;或者一个模块的结果是否正确。 func addUpper(n int) int{res : 0for i : 1;i<n;i{res i}return res }2.传统方法的优缺点 不方便&#xff0c;我们需要在main函…

java基于微信小程序的二手交易系统-计算机毕业设计

项目介绍 开发语言&#xff1a;Java 框架&#xff1a;ssm JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09; 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&a…

QCC51XX---RAM资源查看

QCC51XX---系统学习目录_嵌入式学习_force的博客-CSDN博客 想必大家有时会遇到一些memory的panic,就像下图的提示 这主要是内存溢出引起的。而内存溢出主要有内存池(memory pool)和RAM溢出两种,不管哪种溢出都是不规范使用内存或过度使用造成的。那具体有多少能用或怎么正确…

SpringCloud 学习笔记

❤ 作者主页&#xff1a;Java技术一点通的博客 ❀ 个人介绍&#xff1a;大家好&#xff0c;我是Java技术一点通&#xff01;(&#xffe3;▽&#xffe3;)~* &#x1f34a; 记得关注、点赞、收藏、评论⭐️⭐️⭐️ &#x1f4e3; 认真学习&#xff0c;共同进步&#xff01;&am…

SpringBoot 优雅地实现文件的上传和下载

2.技术栈mavenspringbootmybatis-plusmysqlthymeleafbootstrap3.数据库表 CREATE TABLE t_upload_file (id bigint(11) NOT NULL AUTO_INCREMENT,old_name varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,new_Name varchar(100) CHARACTER SET utf8 CO…

async和await随谈

以下只是个人观点&#xff0c;如果有出入或者错误之处&#xff0c;欢迎提出&#xff0c;你不需要纠结我的说法是否有一些小瑕疵&#xff0c;把其中你认为对的地方融入到自己的知识里去就好了&#xff0c;你学到的才是自己的。 在说async和await之前&#xff0c;先讲另一个东西迭…

ADI Blackfin DSP处理器-BF533的开发详解56:DSP控制ADV7180采集图像到LCD显示(含源码)

硬件准备 ADSP-EDU-BF533&#xff1a;BF533开发板 AD-HP530ICE&#xff1a;ADI DSP仿真器 软件准备 Visual DSP软件 硬件链接 代码实现功能 代码实现了采集一帧 720625 尺寸的 P 制 CVBS 信号源&#xff0c;以 YUYV422 的数据格式保存&#xff0c;通过 MDMA 将奇偶场数据交…

比较级和最高级

1. 单音节词和少数双音节词的比较级和最高级的构成 1)常见的单音节词和少数以-er和-ow结尾的双音节词&#xff0c;比较级在末尾加-er,最高级在末尾加-est 例如&#xff1a;单音节词&#xff1a;small-smaller-smallest, tall-taller-tallest; 双音节词&#xff1a;clever-cl…

第二十九章 linux-i2c子系统二

第二十九章 linux-i2c子系统二 文章目录第二十九章 linux-i2c子系统二linux-i2c数据结构分析linux-i2c驱动框架IIC核心IIC总线驱动IIC设备驱动linux-i2c设备实现硬件拓扑linux-i2c数据结构分析 driver 设备驱动模型 i2c_client来描述一个挂载在I2C总线上的I2C设备。 struc…

【图像处理】数字图像处理笔记

文章目录直方图处理滤波器图像复原形态学图像处理灰度形态学——多使用平坦结构元&#xff08;SE&#xff09;【数字图像处理P428图像分割1、canny边缘检测【数字图像处理P463】图像的表征特征描述子目标检测分类器AdaBoost算法——集成学习、迭代算法直方图处理 1、直方图均衡…

ARM处理器概论

目录 一、ARM处理器概述 1.ARM的含义 2.ARM公司 3.主流的两种处理器 RISC处理器&#xff08;精简指令集&#xff09; CISC处理器&#xff08;复杂指令集&#xff09; 4.SOC 二、ARM指令集概述 1.指令与指令集 指令 指令集 2.ARM指令集 ARM指令集 Thumb指令集 3.编…

vuex----的辅助函数mapState, mapActions, mapMutations用法和混入

Vuex的辅助函数mapState, mapActions, mapMutations用法和混入 爱学习的渣渣关注IP属地: 江苏 2022.03.28 00:14:13字数 287阅读 469 一.使用mapState来获取state里的值 第一步先解构 1.最简单的使用数组的形式来获取模块中state中的值 2.用对象的形式来获取模块中的state的…

面试官:Spring refresh过程是怎样的?

小熊学Java网站&#xff1a;https://javaxiaobear.gitee.io/&#xff0c;每周持续更新干货&#xff0c;建议收藏&#xff01; 1. Spring refresh 流程 refresh 是 AbstractApplicationContext 中的核心方法&#xff0c;负责初始化 ApplicationContext 容器&#xff0c;容器必须…

【算法】七月算法打卡

# 2022-07-11 深拷贝 const copyObject (obj {}) > {let newObj nullif (typeof (obj) object && obj ! null) {newObj obj instanceof Array ? [] : {}// 进入下一层进行递归for (let i in obj) newObj[i] copyObject(obj[i])} else {newObj obj}return …

手机(Android)刷NetHunter安装指南,无需ssh执行kali命令, NetHunter支持的无线网卡列表!

一、安装NetHunter 前提&#xff1a;确保手机已经root&#xff0c;已装上magisk。如果没有root&#xff0c;可用尝试magisk root 后执行此文 1、下载Nethunter&#xff1a;Get Kali | Kali Linux 然后push 到sdcard 里&#xff0c; 2、打开magisk&#xff0c;选择刚刚下好的…

krpt.dll丢失怎么安装?怎么修复快一点

krpt.dll丢失怎么安装&#xff1f;其实你只要记住一点&#xff0c;只要是dll文件丢失&#xff0c;那么就不外乎那几种修复方法&#xff0c;自己下载丢失的文件安装&#xff0c;或者使用dll修复工具&#xff0c;这些方法都是可以修复的。下面我们详细的说一说。 目录 一.krpt.…