概述

C#Winform编程中,跨线程直接更新UI控件的做法是不正确的,会时常出现“线程间操作无效: 从不是创建控件的线程访问它”的异常。处理跨线程更新Winform UI控件常用的方法有4种:
1. 通过UI线程的SynchronizationContext的Post/Send方法更新;
2. 通过UI控件的Invoke/BeginInvoke方法更新;

3. 通过BackgroundWorker取代Thread执行异步操作;
4. 通过设置窗体属性,取消线程安全检查来避免"跨线程操作异常"(非线程安全,建议不使用)。
下文中对以上3种方法应用进行举例说明,希望能对初识C# Winform的同学们有些帮助。

正文

1. 通过UI线程的SynchronizationContext的Post/Send方法更新

用法:

//共分三步
//第一步:获取UI线程同步上下文(在窗体构造函数或FormLoad事件中)
/// <summary>
/// UI线程的同步上下文
/// </summary>
SynchronizationContext m_SyncContext = null;
public Form1()
{
InitializeComponent();
//获取UI线程同步上下文
m_SyncContext = SynchronizationContext.Current;
//Control.CheckForIllegalCrossThreadCalls = false;
}
//第二步:定义线程的主体方法
/// <summary>
/// 线程的主体方法
/// </summary>
private void ThreadProcSafePost()
{
//...执行线程任务 //在线程中更新UI(通过UI线程同步上下文m_SyncContext)
m_SyncContext.Post(SetTextSafePost, "This text was set safely by SynchronizationContext-Post."); //...执行线程其他任务
}
//第三步:定义更新UI控件的方法
/// <summary>
/// 更新文本框内容的方法
/// </summary>
/// <param name="text"></param>
private void SetTextSafePost(object text)
{
this.textBox1.Text = text.ToString();
}
//之后,启动线程
/// <summary>
/// 启动线程按钮事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void setSafePostBtn_Click(object sender, EventArgs e)
{
this.demoThread = new Thread(new ThreadStart(this.ThreadProcSafePost));
this.demoThread.Start();
}

说明:三处加粗部分是关键。该方法的主要原理是:在线程执行过程中,需要更新到UI控件上的数据不再直接更新,而是通过UI线程上下文的Post/Send方法,将数据以异步/同步消息的形式发送到UI线程的消息队列;UI线程收到该消息后,根据消息是异步消息还是同步消息来决定通过异步/同步的方式调用SetTextSafePost方法直接更新自己的控件了。

在本质上,向UI线程发送的消息并是不简单数据,而是一条委托调用命令。

//在线程中更新UI(通过UI线程同步上下文m_SyncContext)
m_SyncContext.Post(SetTextSafePost, "This text was set safely by SynchronizationContext-Post.");
可以这样解读这行代码:向UI线程的同步上下文(m_SyncContext)中提交一个异步消息(UI线程,你收到消息后以异步的方式执行委托,调用方法SetTextSafePost,参数是“this text was ....”).

2.通过UI控件的Invoke/BeginInvoke方法更新

用法:与方法1类似,可分为三个步骤。

// 共分三步

        // 第一步:定义委托类型
// 将text更新的界面控件的委托类型
delegate void SetTextCallback(string text); //第二步:定义线程的主体方法
/// <summary>
/// 线程的主体方法
/// </summary>
private void ThreadProcSafe()
{
//...执行线程任务 //在线程中更新UI(通过控件的.Invoke方法)
this.SetText("This text was set safely."); //...执行线程其他任务
}
//第三步:定义更新UI控件的方法
/// <summary>
/// 更新文本框内容的方法
/// </summary>
/// <param name="text"></param>
private void SetText(string text)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.textBox1.InvokeRequired)//如果调用控件的线程和创建创建控件的线程不是同一个则为True
{
while (!this.textBox1.IsHandleCreated)
{
//解决窗体关闭时出现“访问已释放句柄“的异常
if (this.textBox1.Disposing || this.textBox1.IsDisposed)
return;
}
SetTextCallback d = new SetTextCallback(SetText);
this.textBox1.Invoke(d, new object[] { text });
}
else
{
this.textBox1.Text = text;
}
}
//之后,启动线程
/// <summary>
/// 启动线程按钮事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void setTextSafeBtn_Click(
object sender,
EventArgs e)
{
this.demoThread =
new Thread(new ThreadStart(this.ThreadProcSafe)); this.demoThread.Start();
}

说明:这个方法是目前跨线程更新UI使用的主流方法,使用控件的Invoke/BeginInvoke方法,将委托转到UI线程上调用,实现线程安全的更新。原理与方法1类似,本质上还是把线程中要提交的消息,通过控件句柄调用委托交到UI线程中去处理。

解决窗体关闭时出现“访问已释放句柄“的异常部分代码参考博客园-事理同学的文章。

3.通过BackgroundWorker取代Thread执行异步操作

用法:

//共分三步

        //第一步:定义BackgroundWorker对象,并注册事件(执行线程主体、执行UI更新事件)
private BackgroundWorker backgroundWorker1 =null;
public Form1()
{
InitializeComponent(); backgroundWorker1 = new System.ComponentModel.BackgroundWorker();
//设置报告进度更新
backgroundWorker1.WorkerReportsProgress = true;
//注册线程主体方法
backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork);
//注册更新UI方法
backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
//backgroundWorker1.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.backgroundWorker1_RunWorkerCompleted);
} //第二步:定义执行线程主体事件
//线程主体方法
public void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
//...执行线程任务 //在线程中更新UI(通过ReportProgress方法)
backgroundWorker1.ReportProgress(50, "This text was set safely by BackgroundWorker."); //...执行线程其他任务
}
//第三步:定义执行UI更新事件
//UI更新方法
public void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.textBox1.Text = e.UserState.ToString();
}
//之后,启动线程
//启动backgroundWorker
private void setTextBackgroundWorkerBtn_Click(object sender, EventArgs e)
{
this.backgroundWorker1.RunWorkerAsync();
}

说明:C# Winform中执行异步任务时,BackgroundWorker是个不错的选择。它是EAP(Event based Asynchronous Pattern)思想的产物,DoWork用来执行异步任务,在任务执行过程中/执行完成后,我们可以通过ProgressChanged,ProgressCompleteded事件进行线程安全的UI更新。

需要注意的是://设置报告进度更新
            backgroundWorker1.WorkerReportsProgress = true;
默认情况下BackgroundWorker是不报告进度的,需要显示设置报告进度属性。

4. 通过设置窗体属性,取消线程安全检查来避免"线程间操作无效异常"(非线程安全,建议不使用)

用法:将Control类的静态属性CheckForIllegalCrossThreadCalls为false。

public Form1()
{
InitializeComponent();
//指定不再捕获对错误线程的调用
Control.CheckForIllegalCrossThreadCalls = false;
}

说明:通过设置CheckForIllegalCrossThreadCalls属性,可以指示是否捕获线程间非安全操作异常。该属性值默认为ture,即线程间非安全操作是要捕获异常的("线程间操作无效"异常)。通过设置该属性为false简单的屏蔽了该异常。Control.CheckForIllegalCrossThreadCalls的注释如下:

//
// 摘要:
// 获取或设置一个值,该值指示是否捕获对错误线程的调用,这些调用在调试应用程序时访问控件的 System.Windows.Forms.Control.Handle
// 属性。
//
// 返回结果:
// 如果捕获了对错误线程的调用,则为 true;否则为 false。
[EditorBrowsable(EditorBrowsableState.Advanced)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription("ControlCheckForIllegalCrossThreadCalls")]
[Browsable(false)]
public static bool CheckForIllegalCrossThreadCalls { get; set; }

综述:

文中介绍的4种方法,前三种是线程安全的 ,可在实际项目中因地制宜的使用。最后一种方法是非线程安全的,初学者可以实验体会但不建议使用它。

下面列表对比一下这四种方法

方法 线程安全 支持异步/同步 其他
UI SyncContext更新 是 Post/Send 尽量在窗体构造函数、FormLoad中获取同步上下文
控件Invoke 是 control.Invoke/BeginInvoke 注意检查控件句柄是否已释放
BackgroundWorker更新 是

ProgressChanged、RunWorkerCompleted

事件同步更新

报告进度

CheckForIllegalCrossThreadCalls

取消跨线程调用检查

否 同步更新 简单,不建议使用

参考资料:

瑞奇特(Jeffrey Richter),《clr var C#》

MSDN, How to: Make Thread-Safe Calls to Windows Forms Controls

事理,C#线程中安全访问控件(重用委托,避免繁复的delegate,Invoke)总结

原文链接:

[转]C# Winform 跨线程更新UI控件常用方法汇总的更多相关文章

  1. C# Winform 跨线程更新UI控件常用方法汇总(多线程访问UI控件)

    概述 C#Winform编程中,跨线程直接更新UI控件的做法是不正确的,会时常出现“线程间操作无效: 从不是创建控件的线程访问它”的异常.处理跨线程更新Winform UI控件常用的方法有4种:1. ...

  2. C# Winform 跨线程更新UI控件常用方法汇总

    https://www.cnblogs.com/marshal-m/p/3201051.html

  3. C# Winform 跨线程更新UI控件常用方法总结(转)

    出处:http://www.tuicool.com/articles/FNzURb 概述 C#Winform编程中,跨线程直接更新UI控件的做法是不正确的,会时常出现“线程间操作无效: 从不是创建控件 ...

  4. (转).NET 4.5中使用Task.Run和Parallel.For()实现的C# Winform多线程任务及跨线程更新UI控件综合实例

    http://2sharings.com/2014/net-4-5-task-run-parallel-for-winform-cross-multiple-threads-update-ui-dem ...

  5. WPF / Win Form:多线程去修改或访问UI线程数据的方法( winform 跨线程访问UI控件 )

    WPF:谈谈各种多线程去修改或访问UI线程数据的方法http://www.cnblogs.com/mgen/archive/2012/03/10/2389509.html 子线程非法访问UI线程的数据 ...

  6. Visual studio之C#跨线程调用UI控件

    背景 当前串口通讯项目,多个线程需要同时利用richTextBoxMsg控件打印信息,直接调用会造成线程不安全,严重的时候会直接导致UI线程挂掉,因此本篇就跨线程调用UI控件做个记录. 正文 定义控件 ...

  7. C#子线程更新UI控件的方法总结

    http://blog.csdn.net/jqncc/article/details/16342121 在winform C/S程序中经常会在子线程中更新控件的情况,桌面程序UI线程是主线程,当试图从 ...

  8. C# 后台线程更新UI控件

    /********************************************************************************* * C# 后台线程更新UI控件 * ...

  9. 跨线程访问UI控件时的Lambda表达式

    工作中经常会用到跨线程访问UI控件的情况,由于.net本身机制,是不允许在非UI线程访问UI控件的,实际上跨线程访问UI控件还是 将访问UI的操作交给UI线程来处理的, 利用Control.Invok ...

  10. 理解SynchronizationContext,如何在Winform里面跨线程访问UI控件

    SynchronizationContext 类是一个基类,可提供不带同步的自由线程上下文. 此类实现的同步模型的目的是使公共语言运行库内部的异步/同步操作能够针对不同的异步模型采取正确的行为.此模型 ...

随机推荐

  1. 云原生周刊:Harbor v2.11 版本发布 | 2024.6.17

    开源项目推荐 Descheduler Descheduler 是一个工具,可用于优化 Kubernetes 集群中 Pod 的部署位置.它可以找到可以移动的 Pod,并将其驱逐,让默认调度器将它们重新 ...

  2. KubeSphere Meetup 北京站火热报名中 | 搭载 CIC 2021 云计算峰会

    "CIC 2021 云计算峰会"是一场 Top 级行业盛会,将汇聚 800 家青云QingCloud 企业客户信息化负责人,采取线上线下相结合的方式,与会规模将超过 10000 人 ...

  3. 一文彻底弄懂并解决Redis的缓存雪崩,缓存击穿,缓存穿透

    缓存雪崩.缓存击穿.缓存穿透是分布式系统中使用缓存时,常遇到的三类问题,都会对系统性能和稳定性产生严重影响.下面将详细介绍这三者的定义.产生原因.危害以及常见的解决方案. 1. 缓存雪崩 1.1 定义 ...

  4. 高效求解 n 个点之间的最大曼哈顿距离

    平面上有 n 个点,如何求出任意两点的曼哈顿距离的最大值? 曼哈顿距离的公式为: \[d((x_1, y_1), (x_2, y_2)) = |x_1 - x_2| + |y_1 - y_2| \] ...

  5. Windows 如何启用 Administrator 账户

    Windows 默认安装的时候需要自己定义一个账户,不是管理员账户,不能操作某些目录,有时很不方便.不过我们可以手动打开,像盗版一样默认使用 Administrator  账户: 按下Win + R, ...

  6. Redhat 7安装Oracle 11.2.0.4 RAC 数据库软件中报错:Error in invoking target 'agent nmhs' of makefile

    环境:Redhat Linux 7.6 + Oracle 11.2.0.4 RAC 现象:图像化安装数据库软件过程中的86%时,报错. 1. 具体现象 2. 定位问题 3. 解决问题 1. 具体现象 ...

  7. Redis的发布订阅Pub/Sub

    发布订阅 Redis 发布订阅(publish/subscribe)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息. Redis 客户端可以订阅任意数量的频道. 下图展示了频道 ...

  8. 反射+特性实现 类和XML文档的序列化反序列化

    1.作用:将实体类的属性(字符串.日期.数值.布而.类)生成为xml文档中的结点,将xml文档的结点值或者属性值填充到实体类的属性值中2.思路:特性.反射.泛型:特性记录xml结点与实体属性的对应关系 ...

  9. Go语言GOPATH是什么

    一.GOPATH的概念 GOPATH 是 Go 语言中使用的一个环境变量,它使用绝对路径提供项目的工作目录(也称为工作区). 工作目录是一个工程开发的相对参考目录,好比当你要在公司编写一套服务器代码, ...

  10. 题解:【XR-3】核心城市

    题解:[XR-3]核心城市 思路一:考虑由特例推广到一般 1.很容易想到先考虑一个关键点的情况,然后再推广到一般情况. 2.一个点肯定选距离上最平衡的那个点,即树的中心.接着在中心周围贪心的选剩下的( ...