WPF 与 WinForm 多线程实战:避免卡顿、安全更新 UI

182 阅读4分钟

前言

在开发过程中,线程问题一直是困扰许多开发者的难题。尤其在群里,经常看到小伙伴们询问关于线程切换、UI线程操作等相关问题。尽管市面上有很多视频教程,但不少人反映难以从中找到清晰、直接的解决方案。因此,决定结合自己的实践经验,写一篇关于UI线程切换的总结,帮助大家更好地理解和掌握这一关键技术。

UI线程切换的核心思路

1、直接修改UI的代码必须放在UI线程

这是UI线程操作的基本原则。在WinForms中,你可以通过将Control类的静态属性CheckForIllegalCrossThreadCalls设为false来关闭线程检查,但前提是你必须清楚自己在做什么,否则可能导致不可预知的错误。

2、WPF中的线程检查

与WinForms不同,WPF无法关闭线程检查。但WPF的ViewModel不需要UI线程进行更新,因为它通过内部通知机制来实现数据绑定和更新,这使得WPF在处理UI线程时更加灵活。

开启一个新的任务

在不同的.NET版本中,开启新任务的方式有所不同。以下是几种常见的方法:

Net4.5及以后版本

var param = 123;
Task.Run(() =>
{
    DoSomthing(param);
});

Task.Run(async () =>
{
    await DoSomthingAsync(param);
});

Net 4.0

Task.Factory.StartNew(delegate ()
{
    DoSomthing(param);
});

Net 3.5

Thread t = new Thread((ThreadStart)delegate ()
{
    DoSomthing(param);
});
t.Start();

// 使用线程池
ThreadPool.QueueUserWorkItem((WaitCallback)delegate (Object obj)
{
    DoSomthing(param);
});

为了简化说明,后续示例将主要使用Task

回到UI线程

在完成后台任务后,经常需要将结果返回到UI线程进行更新。以下是WinForm和WPF中回到UI线程的方法:

WinForm

Task.Run(() =>
{
    // 类似SendMessage,等待发送结束
    this.Invoke((Action)delegate ()
    {
        DoSomthing(param);
    });

    // 类似PostMessage,发了就跑
    this.BeginInvoke((Action)delegate ()
    {
        DoSomthing(param);
    });
});

WPF

Task.Run(async () =>
{
    this.Dispatcher.Invoke(delegate ()
    {
        DoSomthing(param);
    });

    // 使用异步等待任务结束
    await this.Dispatcher.BeginInvoke((Action)delegate ()
    {
        DoSomthing(param);
    });

    // 使用抛弃返回值的方式,直接过,不等待
    _ = this.Dispatcher.BeginInvoke((Action)delegate ()
    {
        DoSomthing(param);
    });
});

高级方法

使用SynchronizationContext

// 在UI线程时记录下上下文
var syncContext = SynchronizationContext.Current;
Task.Run(() =>
{
    // 使用UI线程
    syncContext.Post(o =>
    {
        DoSomthing(param);
    }, null);
});

await一个SynchronizationContext

// 在UI线程时记录下上下文
var syncContext = SynchronizationContext.Current;
Task.Run(() =>
{
    // 回到UI线程
    await syncContext;
    DoSomthing(param); // 在UI线程中执行
});

如何await一个SynchronizationContext

// 写个Awaiter类
public sealed class SynchronizationContextAwaiter : INotifyCompletion
{
    private readonly SynchronizationContext _context;
    public SynchronizationContextAwaiter(SynchronizationContext context) => 
        _context = context ?? throw new ArgumentNullException("context");

    public bool IsCompleted => SynchronizationContext.Current == _context;
    public void OnCompleted(Action action) => _context.Post(x => action(), null);
    public void GetResult() { }
}

// 写个扩展方法
public static SynchronizationContextAwaiter GetAwaiter(this SynchronizationContext context)
{
    return new SynchronizationContextAwaiter(context);
}

将高级方法整合到一个类中

为了更好地管理和使用UI线程切换,我们可以将上述高级方法整合到一个类中。

以下是一个完整的示例:

初始化

// 需要在UI线程初始化
UIThreadContext.Init();

UIThreadContext类

using Ruifei.Common.UIThread;
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;

public static class UIThreadContext
{
    public static SynchronizationContext Context;
    public static Thread UIThread => uiThread;
    static Thread uiThread = null;
    public static void SendUIThread(Action action)
    {
        if (Context != null)
            Context.Send(x => action(), null);
        else
            action();
    }

    public static void SendUIThreadIfRequired(Action action)
    {
        Thread crt = Thread.CurrentThread;
        if (uiThread != crt)
            SendUIThread(action);
        else
            action();
    }

    public static void PostUIThread(Action action)
    {
        if (Context != null)
            Context.Post(x => action(), null);
        else
            action();
    }

    public static void PostUIThread(Func<Task> action)
    {
        if (Context != null)
            Context.Post(x => action(), null);
        else
            action();
    }

    public static void PostUIThreadIfRequired(Action action)
    {
        if (IsInUIThread())
            action();
        else
            PostUIThread(action);
    }

    public static void PostUIThreadIfRequired(Func<Task> action)
    {
        if (IsInUIThread())
            action();
        else
            PostUIThread(action);
    }

    public static bool IsInUIThread()
    {
        return uiThread == Thread.CurrentThread;
    }

    static Dispatcher dispatcher = (Application.Current != null && Application.Current.Dispatcher != null) ? Application.Current.Dispatcher : Dispatcher.CurrentDispatcher;
    public async static void InvokeOnUIThread(Func<Task> action)
    {
        if (dispatcher == null || dispatcher.CheckAccess())
            await action();
        else
            await dispatcher.BeginInvoke(action);
    }

    public static void InvokeOnUIThread(Action action)
    {
        if (dispatcher == null || dispatcher.CheckAccess())
            action();
        else
            dispatcher.BeginInvoke(action);
    }

    public static void Init()
    {
        Context = SynchronizationContext.Current;
        uiThread = Thread.CurrentThread;
    }

    public static SynchronizationContextAwaiter GetAwaiter(this SynchronizationContext context)
    {
        return new SynchronizationContextAwaiter(context);
    }

    public static SynchronizationContext SwitchToUIThread()
    {
        return UIThreadContext.Context;
    }

    public static WaitForSwitchToNewTask SwitchToNewTask()
    {
        return new WaitForSwitchToNewTask(false);
    }
}

namespace Ruifei.Common.UIThread
{
    public class WaitForSwitchToNewTask
    {
        bool withContext = false;
        public WaitForSwitchToNewTask(bool _with)
        {
            withContext = _with;
        }
        public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter()
        {
            return Task.Run(() => { }).ConfigureAwait(withContext).GetAwaiter();
        }
    }

    public sealed class SynchronizationContextAwaiter : INotifyCompletion
    {
        private readonly SynchronizationContext _context;
        public SynchronizationContextAwaiter(SynchronizationContext context) =>
            _context = context ?? throw new ArgumentNullException("context");

        public bool IsCompleted => SynchronizationContext.Current == _context;
        public void OnCompleted(Action action)
        {
            if (_context == null)
            {
                action();
            }
            else
            {
                _context.Send(x => action(), null);
            }
        }

        public void GetResult() { }
    }
}

总结

UI线程切换是多线程编程中的重要环节,掌握它对于开发高效、稳定的用户界面至关重要。

本文从UI线程切换的核心思路出发,详细介绍了在不同.NET版本中开启新任务、回到UI线程的方法,并深入探讨了使用SynchronizationContextawait进行高级线程切换的技巧。

最后,通过整合这些方法到一个类中,提供了更加便捷、统一的线程管理方式。希望本文能帮助大家更好地理解和掌握UI线程切换技术,提升开发效率和质量。

关键词

UI线程切换、多线程编程、WPF、SynchronizationContext、Task、线程切换、UI线程、Invoke、Dispatcher、Task、BeginInvoke、C#、多线程、WinForm

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

作者:咖喱gg

出处:cnblogs.com/gxrsprite/p/12321759.html

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!