【c#异步使用过多】

电脑杂谈  发布时间:2020-07-02 22:16:14  来源:网络整理

c# filestream 异步_c filestream读取文件_linux c/c++异步io

C# 窗体加载假死,异步刷新总结

 littlewoodman 关注

http://blog.sina.com.cn/s/blog_621e24e201015r29.html

 

总结:

  1. control.Invoke 和 Control.BeginInvoke都是运行在UI线程下的,也就是主线程,与一般异步不同
  2. BeginInvoke的处理就是直接回调,Invoke却在等待异步函数执行完后,才继续执行,也就是假如在循环中调用,BeginInvoke会提前返回,继续异步循环,而Invoke会等待一个循环完成才进行下一个循环,相当于是同步
  3. 文中的高刷新的例子表明,界面从1到10000,用BeginInvoke执行飞快,但保证不了界面数据的连贯性,从1-10000不是连贯的,而且界面出现假死,而调用Invoke,从1-10000是连贯的,但是执行效率很慢,界面闪烁一直刷新,但是不会假死,滚动条还是可以滚动的
  4. 解决界面闪烁
// 打开控件的双缓冲
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);

文本如下:

异步调用是CLR为开发人员提供的一种重要编程方法,也是构建高性能,可扩展应用程序的关键.

随着多核CPU的日益普及,异步编程允许使用很少的线程来执行许多操作.

我们通常使用异步来完成许多计算和IO型复杂且耗时的操作,以获取应用程序所需的部分数据.

获取这些数据后,我们需要将它们绑定以呈现在UI中. 当数据量太大时,我们会发现该表单变成空白面板. 这时,如果单击鼠标,表单的标题将显示为“丢失响应”,但实际上UI线程仍在工作,这对于用户来说是非常糟糕的体验. 如果您想了解原因(不复杂: ))并完全解决问题,那么花时间阅读本文可能是个不错的选择.

通常来说,窗口阻塞分为两种情况. 一种是在UI线程上调用耗时的操作,例如访问. 这种阻塞是由UI线程的占用引起的,这可以通过委托的异步编程来解决. 另一种是该窗体加载大量数据,例如,将大量数据添加到诸如ListView和DataGridView的控件中. 本文重点介绍后一种类型的阻止.

基础理论

本节简要介绍CLR对跨线程UI访问的处理. 作为基本内容,我相信大多数.NET开发人员都不是新手,因此读者可以根据实际情况略过.

控件的线程安全检测

在传统的表单编程中,UI中的控件元素与其他工作线程是隔离的. 每次我们访问UI控件时,它实际上都是在UI线程中完成的. 如果尝试在另一个线程中访问控件,则CLR将针对不同的.NET Framework版本以不同的方式处理它. 在Framework 1.x中,CLR允许应用程序以跨线程方式运行,而在Framework 2.0和更高版本中,System.Windows.Form.Control添加了CheckForIllegalCrossThreadCalls属性,该属性是可读可写的bool常量c# filestream 异步,标记是否需要以检测非UI线程对控件的调用. 如果指定true,则从另一个线程访问UI时,CLR将耗尽“ InvalidOperationException: 线程间操作无效,并且创建控件的线程永远不会访问它”. 如果为假,则不会捕获对错误线程的调用,该应用程序仍在运行.

在Framework 1.x版本中,此值默认为false. 问什么更高版本将添加此属性来约束我们的UI?实际上,对此的官方解释是,当多个并发线程尝试读写UI时,很容易造成线程争用引起的死锁. 因此,默认情况下,CLR不允许非UI线程访问控件.

但是,我们经常需要使用异步线程的形式来处理某些操作,例如IO和Socket通信. 这时,也需要跨线程UI访问. 为此,.NET向我们提供的补充解决方案是Invoke和Control的BeginInvoke.

控制调用和BeginInvoke

对于这两种方法,我们必须首先具有以下知识:

linux c/c++异步io_c# filestream 异步_c filestream读取文件

Control.Invoke,Control.BeginInvoke和委托.Invoke,委托.BeginInvoke是不同的. Control.Invoke中的委托方法在主线程(即我们的UI线程)上执行. Control.BeginInvoke被异步命名(开始),但是它仍然在UI线程上执行. 如果直接在UI线程中调用Invoke和BeginInvoke,则当数据量太大时,仍然会导致UI的假死.

许多开发人员首次接触这两个功能时,很容易将它们与异步连接. 有些人认为他们是独立于UI线程的工作线程. 实际上,它们两者都是相同的. 此功能的命名是盲目的. 如果您以传统的异步调用方式直接调用Control.BeginInvoke,则与执行同步功能没有什么不同,UI线程仍将处理所有困难的操作,从而导致我们的应用程序阻塞.

Control.Invoke的调用模型非常清楚: 在UI线程中,代码序列是同步执行的. 因此,除了工作线程调用UI元素的干扰之外,我们还可以将Control.Invoke视为同步的. 本文不会介绍太多.

许多开发人员在联系异步后将处理表单死亡的问题. 将Control.BeginInvoke作为WinForm的异步形式很容易. 所以我们专注于这种方法.

体验BeginInvoke

如前所述,BeginInvoke的命名方式看起来像异步的. 实际上,很多时候我们称其为不具有异步“非阻塞”功能. 我使用以下示例尝试对BeginInvoke的简单调用.

如您所见,我现在创建了一个简单的窗体,其中包含一个Lable控件lable1和一个Button控件btn_Start. 在下面,开始代码:

private void btn_Start_Click(object sender, EventArgs e) { 
    // 储存UI线程的标识符 int curThreadID = Thread.CurrentThread.ManagedThreadId;
    new Thread((ThreadStart)delegate() { 
        PrintThreadLog(curThreadID); 
    }).Start(); 
}
private void PrintThreadLog(int mainThreadID) { 
    // 当前线程的标识符 
    // A代码块 
    int asyncThreadID = Thread.CurrentThread.ManagedThreadId;
    // 输出当前线程的扼要信息,及与UI线程的引用比对结果 
    // B代码块         
    label1.BeginInvoke((MethodInvoker)delegate() { 
    // 执行BeginInvoke内的方法的线程标识符 
    int curThreadID = Thread.CurrentThread.ManagedThreadId;
    label1.Text = string.Format("Async Thread ID:{0},Current Thread ID:{1},Is UI Thread:{2}", asyncThreadID, curThreadID, curThreadID.Equals(mainThreadID)); });
    // 挂起当前线程3秒,模拟耗时操作 // C代码块 Thread.Sleep(3000); 
}

此代码在新线程中访问UI,因此我们使用label1.BeginInvoke函数. 在新线程中,我们获得了当前工作线程的线程标识符,并且还在BeginInvoke函数中获得了该线程. 然后,将其与UI线程的标识符进行比较,并在Label1控件上输出结果. 最后,我们将当前工作线程暂停3秒钟,以模拟一些常见的耗时操作.

为了便于区分,我们将此代码分为三个代码块A,B和C.

运行结果:

我们可以得出以下结论:

PrintThreadLog函数(A,C代码块)的主体在新线程中执行,该新线程执行BeginInvoke中未包含的其他代码. 当我们调用Control.BeginInvoke时,线程调度权返回到UI线程. 换句话说,BeginInvoke内部的代码(B代码块)在UI线程上执行. 当UI线程执行封装在BeginInvok中的代码时,辅助线程中的其余代码(C代码块)将同时进行. 它与BeginInvoke中的UI线程并行执行,而不会互相干扰. 因为Thread.Sleep(3000)是与UI线程隔离的工作线程,所以由这一行代码引起的线程阻塞实际上会阻塞该工作线程,并且对UI没有任何影响.

Control.BeginInvoke的真实含义

由于Control.BeginInvoke中的委托函数仍在UI线程中执行,因此“异步”到底是什么意思?主题回到本文的开头: 上面提到了“控件的线程安全检测”的概念. 我相信每个人都已经熟悉在此工作线程中调用Control.BeginInvoke的方法. 我们还提到“ CLR不喜欢工作线程调用UI元素”. 微软的决心如此之大,以至于CLR团队在.NET Framework 2.0中添加了CheckForIllegalCrossThreadCalls和Control.Invoke,Control.BeginInvoke方法. 这是一项相当重大的改革,CLR团队希望达到这一效果:

c filestream读取文件_c# filestream 异步_linux c/c++异步io

如果不声明CheckForIllegalCrossThreadCalls = false;这样的“不安全”代码,您只能使用Control.Invoke和Control.BeginInvoke;并且只要使用后两个,无论它们的上下文运行环境是其他工作线程还是UI线程,它们封装的代码都将在UI线程中执行. 因此,msdn对Control.BeginInvoke进行了这样的解释: 指定的委托在创建控件的基本句柄的线程上异步执行.

它的真实含义是: BeginInvoke的所谓异步性是相对于调用线程,而不是UI线程.

CLR在UI的Control.BeginInvoke(delegate方法)中执行异步功能. 如果像我上面那样用新线程调用BeginInvoke,则该方法相对于该新线程中的其他函数是异步的. 毕竟,该方法是在UI线程上执行的,新线程将立即回调,而无需等待Control.BeginInvoke的完成. 因此,此后台线程完全享受“异步”的好处,不再阻塞,但我们看不到它;当然,如果您在BeginInvoke中执行耗时的代码,无论是获取信息,从远程服务器读取IO来获取还是在控件中加载大量数据,UI线程仍然会被阻塞.

由于传统的Delegate.BeginInvoke异步工作线程是从.NET线程池中提取的,所以Control.BeginInvoke的异步工作线程是UI线程.

您现在了解两个BeginInvoke之间的区别吗?

Control.Invoke,BeginInvoke和Windows消息

实际上,Invoke和BeginInvoke的原理是将调用的方法Marshal编组为消息,然后调用Win32Api的RegisterWindowMessage()将消息发送到UI. 我们使用Reflector,我们可以看到以下代码:

Control.Invoke:

公共对象Invoke(委托方法,params object [] args){使用(new MultithreadSafeCallScope()){返回this.FindMarshalingControl(). MarshaledInvoke(this,method,args,true);}}

Control.BeginInvoke:

[EditorBrowsable(EditorBrowsableState.Advanced)]公共IAsyncResult BeginInvoke(委托方法,params对象[] args){使用(new MultithreadSafeCallScope()){返回(IAsyncResult)this.FindMarshalingControl(). MarshaledInvoke(this,method,args ,false);}}

在上面的代码中,我们看到Control.Invoke和BeginInvoke之间的区别在于,当调用MarshaledInvoke时,Invoke将false传递给最后一个参数,而BeginInvoke为true.

MarshaledInvoke的结构如下:

私有对象MarshaledInvoke(控制调用方,委托方法,对象[] args,布尔同步)

很明显,最后一个参数sync是指示是否同步处理该参数. MarshaledInvoke像这样在内部处理此参数:

c# filestream 异步_c filestream读取文件_linux c/c++异步io

if(!synchronous){返回条目;} if(!entry.IsCompleted){this.WaitForWaitHandle(entry.AsyncWaitHandle); }

因此,BeginInvoke的处理是直接回调,但是Invoke等待异步函数的执行继续.

此时,Invoke和BeginInvoke的工作已经结束. 剩下的工作是通过UI处理消息,该过程由Control的WndProc(参考消息m)执行. 消息处理将对我们的UI产生什么影响?然后查看Application.DoEvents()函数.

Application.DoEvents

Application.DoEvents()函数在WinForm编程中是非常重要的函数,但是在实际编程中,大多数开发人员很少调用它. 如果您不了解此功能,则可能会对长期编程中的“形式死亡”现象感到困惑.

运行Windows窗体时,它将创建一个新窗体,然后该窗体等待处理事件. 表单每次处理事件时,都会处理与该事件关联的所有代码. 所有其他事件都在队列中等待. 当代码处理事件时,应用程序将不响应. 例如,如果将窗口A拖动到窗口B上,则不会重绘窗口B.

如果在代码中调用DoEvents,则您的应用程序可以处理其他事件. 例如,如果您有一个表单将数据添加到ListBox并将DoEvents添加到代码中,那么当您将另一个窗口拖到表单上时,表单将被重绘. 如果您从代码中删除DoEvents,则在按钮的click事件处理程序完成执行之前,您的表单不会重新绘制.

因此,如果在窗口执行事件时我们不处理消息队列中的Windows消息c# filestream 异步,则窗口将不可避免地丢失其响应. 如上所述,Control.Invoke和BeginInvoke都将消息发送到UI,导致UI处理消息. 因此,这为我们提供了一种在表单加载大量数据时解决假死的想法.

解决方案

尝试“没有死亡”

这一次,我们使用在开发中经常出现的ListView控件来体验理想的“异步刷新”. 表单中有一个名为listView1的ListView控件,并且View设置为Detail,并添加了两个ColumnHeader. 一个按钮名为btn_Start,设计视图如下:

起始代码:

private只读int Max_Item_Count = 10000;

private void button1_Click(对象发送者,EventArgs e){新线程((ThreadStart)(delegate(){对于(int i = 0; i

代码运行后,您将看到快速滚动的ListView列表. 在加载过程中,列表以令人眼花speed乱的速度添加数据. 这时,如果您尝试拉动滚动条或移动表单,您会发现这次的效果与之前的“白板”和“假死”完全不同!这是一个可喜的变化.

linux c/c++异步io_c filestream读取文件_c# filestream 异步

运行过程:

从我的屏幕快照中可以看出,在数据加载过程中该窗口仍在绘制界面,并且没有“假死”.

如果上面的代码调用Control.BeginInvoke,程序中将出现一些奇怪的现象. 想想为什么?

好的,现在我们终于可以松一口气了,接口响应的问题已经解决,一切都很好. 但是,这种形式仍然存在两个主要问题: 1.与传统装载相比,“无死角形式”的装载速度明显慢. 2.数据加载过程中,窗口剧烈闪烁.

问题分析

当我们调用Control.Invoke时,我们强制表单处理消息,以便接口获得响应,并且还具有一些副作用. 其中之一是消息处理使表单在循环中重新绘制. “闪烁”现象是由表单重绘引起的. 具有GDI +开发经验的开发人员应该更加熟悉. 同时,每次调用Invoke都会使UI处理消息,这也直接增加了控件处理数据的时间成本,从而导致性能问题.

对于“性能问题”,我没有任何解决方案(欢迎有自己意见的朋友提出). 某些控件(ListView,ListBox)具有BeginUpdate和EndUpdate函数,可以暂时挂起刷新以提高性能. 但是毕竟我们在这里创建了一个滚动界面,这种数据“动态加载”方式是前者无法比拟的.

对于“闪烁”,首先让我解释问题的原因. 通常,控件的绘制包括两个链接: 擦除原始对象和绘制新对象. Windows首先发送一条消息,通知控件删除原始图像然后绘制它. 如果要在控制面板上绘制SolidBrush,则控件将直接在其面板上绘制内容. 当用户更改控件的大小时,Windows将调用许多图形回收操作. 当发生每次回收和绘制时,由于“绘制”比“擦除”更延迟,因此会给用户带来“闪烁”的感觉. . 过去,为了解决这种问题,我们经常需要在Control.WndProc中进行复杂的处理. .NET Framework为我们提供了一个更优雅的解决方案,即双缓冲,我们可以直接调用它.

最终计划

1. 创建一个新的Windows组件DBListView.cs,并使其从ListView继承. 2.将以下代码添加到控件中: public DBListView(){

//打开控件SetStyle的双缓冲(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint,true); }

重新生成项目,然后将新创建的DBListView从工具箱中拖到名为dbListView1的窗体上,并执行以下代码: private void button1_Click(object sender,EventArgs e){new Thread((ThreadStart)(delegate( ){for(int i = 0; i

“闪烁”问题现在解决了吗?

对于DataGridView,它也是每行的加法,

for(int i = 0; i

///创建行DataGridViewRow dr = new DataGridViewRow(); foreach(dataGridViewAllInfo.Columns中的DataGridViewColumn c){dr.Cells.Add(c.CellTemplate.Clone()as DataGridViewCell);} //累积序列号dr.Cells [0] .Value = i ++;

try {dataGridViewAllInfo.Invoke((MethodInvoker)delegate(){dataGridViewAllInfo.Rows.Add(dr);});} catch(Exception ex){//如果插入时有异常,则直接返回以返回; }

}


本文来自电脑杂谈,转载请注明本文网址:
https://www.pc-fly.com/a/jisuanjixue/article-266254-1.html

    相关阅读
      发表评论  请自觉遵守互联网相关的政策法规,严禁发布、暴力、反动的言论

      • 裴瑶
        裴瑶

        好像“一妻多夫”“光棍”也不该属于经济问题啊

      • 刘凤翔
        刘凤翔

        强迫症

      热点图片
      拼命载入中...