C#中Invoke和BeginInvoke实际应用详解

 更新时间:2023年12月13日 14:38:44   作者:微雨夏凉  
这篇文章主要给大家介绍了关于C#中Invoke和BeginInvoke实际应用的相关资料,Invoke是对象方法,BeginInvoke是静态方法,文中通过代码介绍的非常详细,需要的朋友可以参考下

前言

最近,在研究Invoke的使用,但是真的是一头雾水,网上看了很多资料,感觉还是看不懂,因为对于入门级的小白,想像不出Invoke的应用场景,更谈不上如何用了?

1、Invoke到底是什么?

Invoke的本质只是一个方法,方法一定是要通过对象来调用的。

一般来说,Invoke其实用法只有两种情况:

  • Control的Invoke
  • Delegate的Invoke

也就是说,Invoke前面要么是一个控件,要么是一个委托对象

2、什么时候用Invoke

2.1 Control的Invoke

Control的Invoke一般用于解决跨线程访问的问题,比如你想操作一个按钮button,你就要用button.Invoke,你想操作一个文本label,你就要用label.Invoke,但是大家会发现很麻烦,如果我想既操作button,又操作label,能不能写在一起呢?当然可以。
我们知道,主窗体是一个Form,Form自然也是继承Control的,所以Form也有Invoke的方法,可以直接调用Form.Invoke,这就是我们常见的this.Invoke。这就是为什么有的Invoke前面啥都没有的问题,其实前面是this,只不过省略了。

2.2 Delegate的Invoke

Delegate的Invoke其实就是从线程池中调用委托方法执行,Invoke是同步的方式,会卡住调用它的UI线程。很抽象吧。

3、实验

我们来做个简单的实验。

3.1 新建一个From ,来个Button,我想实现的功能是,点击Button时, Button 变成Disable,并开始显示计算1到8(每隔1秒加1),加到8后,跳出循环,然后把Button Enable,很简单吧。

小事情拉,于是开始行动,很快就搞定了,代码如下:

namespace InvokeTest1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        /// <summary>
        /// Butten 点击事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnAddFunction_Click_1(object sender, EventArgs e)
        {
            btnAddFunction.Enabled = false;
            for (int i = 1; i < 8; i++)
            {
                btnAddFunction.Text = i.ToString();
                Thread.Sleep(1000);
            }
            btnAddFunction.Text = "点击开始运行";
            btnAddFunction.Enabled = true;
        }
    }
}

开始运行,点击Button,控件是变成Disable,但是没有实现计数呀,而是一直就这样停止8秒,就像被卡住了一样。

 8秒后,Button控件直接变成Enable。没有1---8出现,逻辑不对啊,怎么回事?????

原因:直接主线程休眠是达不到效果的,此时桌面还处于假死状态,更新不了text值。代码放在了UI线程执行,阻塞了UI的显示,所以中间的结果你看不到。

3.2 找到了原因,那就好办了,既然代码放在了UI线程,那就新建个线程,在那里面更新UI控件好了。信心满满的开始行动。

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

        /// <summary>
        /// Butten 点击事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnAddFunction_Click_1(object sender, EventArgs e)
        {
            //启动一个线程,在这个线程里更新Button值
            new Thread(ThreadTask).Start();
        }

        /// <summary>
        /// 线程函数
        /// </summary>
        public void ThreadTask()
        {
            btnAddFunction.Enabled = false;
            for (int i = 1; i < 8; i++)
            {
                btnAddFunction.Text = i.ToString();
                Thread.Sleep(1000);
            }
            btnAddFunction.Text = "点击开始运行";
            btnAddFunction.Enabled = true;
        }

    }
}

真实现了数字的更新。

 功能是实现了,但没有达到我的目的,主角都没有登场,就谢幕了啊。网上查看了资料,说,这种方法,是不稳定的,特别是主窗口控件比较多的时候,很容易出错,造成画面混乱。为什么呢?因为控件是在主线程中创建的(比如this.Controls.Add(...);),进入控件的事件响应函数时,是在控件所在的线程,并不是主线程。在控件的事件响应函数中改变控件的状态,可能与主线程发生线程冲突。如果主线程正在重绘控件外观,此时在别的线程改变控件外观,就会造成画面混乱。

4、主角出场

4.1 C#的委托机制,一般有下面几种方式。

//第一种
btnAddFunction.Invoke(new EventHandler(delegate{button1.Text = "关闭";}));
//第二种
this.Invoke(new EventHandler(delegate{button1.Text = "关闭";}));
//第三种 网上说自C# 3.0开始就有了
this.Invoke(new Action(() =>{ button1.Text = "关闭";}));

现在应用最多就是第3种了,因为现在版本基本上都是4.0以上了,所以线程函数改为如下的:

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

        /// <summary>
        /// Butten 点击事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnAddFunction_Click_1(object sender, EventArgs e)
        {
            //启动一个线程,在这个线程里更新Button值
            new Thread(ThreadTask).Start();
        }

        /// <summary>
        /// 线程函数
        /// </summary>
        public void ThreadTask()
        {
            //首先将button对象禁用
            this.Invoke(new Action(() =>
            {
                btnAddFunction.Enabled = false;
            }));
            for (int i = 0; i < 10; i++)
            {
                this.Invoke(new Action(() =>
                {
                    btnAddFunction.Text = i.ToString();
                }));
                Thread.Sleep(1000);
            }
            //虽然不是循环内,请不要忘记,你的调用依然在辅助线程中,所以,还是需要invoke的。
            this.Invoke(new Action(() =>
            {
                btnAddFunction.Text = "点击开始运行";
                btnAddFunction.Enabled = true;
            }));

        }

    }
}

4.2 Control的Invoke标准用法

其实,对于Control的Invoke,更标准的用法是先加判断,再调用。

if (this.lbl_Value.InvokeRequired)
{
    this.lbl_Value.Invoke(new Action(() =>
    {
        this.lbl_Value.Text = "Invoke功能测试";
    }));
}
else
{
    this.lbl_Value.Text = "Invoke测试失效";
}

InvokeRequired是Control的一个属性,官方解释为:获取一个值,该值指示调用方在对控件进行方法调用时是否必须调用 Invoke 方法,因为调用方位于创建控件所在的线程以外的线程中。如果控件的 Handle 是在与调用线程不同的线程上创建的(说明您必须通过 Invoke 方法对控件进行调用),则为 true;否则为 false。

简单来说,就是如果通过多线程去操作这个控件,那么这个属性则为True,否则为False。

实例化一个 this invoke的用法

​
using System.Threading;
public delegate void MyInvoke(string str);//invoke方法创建委托
private void btnStartThread_Click(object sender, EventArgs e)
{
    Thread thread = new Thread(new ThreadStart(DoWord));
    thread.Start();
}
public void DoWord()
{            
     MyInvoke mi = new MyInvoke(SetTxt);//实例化一个委托,并且指定委托方法
     BeginInvoke(mi,new object[]{"abc"}); //调用invoke方法           
}
public void SetTxt(string str)//委托对应的方法
{
     txtReceive.Text += str;
}
​

4.3 Delegate的Invoke 标准写法

对于Delegate的Invoke,我们一般判断这个方法之前,也是做个判断,判断这个委托对象是否为Null,所以更标准的写法如下:

DelegateInvokeFun testDelegate = new DelegateInvokeFun(DelegateInvokeMethod);
testDelegate?.Invoke();

5、 Invoke和BeginInvoke

Control.Invoke 和 Control.BeginInvoke

5.1 测试实例1 利用 控件中的Invoke 和 BeginInvoke 方法

作用1:在线程中执行访问和修改UI内容

作用2:Invoke可以阻塞线程,等待UI操作返回

作用3:BeginInvoke不阻塞线程,后台刷新UI,提高程序的流畅性
 

public partial class frmMain : Form
{
      public frmMain ()
      {
          InitializeComponent();
      }
 
      public void ThreadRun()
      {
          while (true)
          {
             Thread.Sleep(1);
 
                this.Invoke(new Action(() => 
                { 
                    MessageBox.Show("Invoke的方法");
                    // 在该this(Form)控件的线程中执行Action中的委托
                    // 你可以在此获取UI变量 或者 改变UI变量
                    // 但是 ThreadRun线程会被阻塞 等待 Action 执行完成
                }));
 
                this.BeginInvoke(new Action(() =>
                {
                    MessageBox.Show("BeginInvoke的方法");
                    // 在该this(Form)控件的线程中执行Action中的委托
                    // 你可以在此获取UI变量 或者 改变UI变量
                    // 但是 ThreadRun线程不会被阻塞继续向下执行
                }));
          }
      }
}

5.2 测试实例2 Action 等delegate 中Invoke和BeginInvoke的作用

作用1.Invoke在当前函数中立即执行,相当于直接调用该Action所注册的所有函数,阻塞当前函数帧

作用2.BeginInvoke在当前函数帧中开辟这个线程去执行Action所注册的函数,不阻塞当前函数帧

作用3.BeginInvoke 中有2额外两个参数 (arg1 回调委托,任意参数)用于beginInvoke完成后,执行该某些动作,

注意:结束完回调的函数 所在线程  为调用beginInvoke 的线程
 

 private void Form1_Load(object sender, EventArgs e)
        {
            // 在当前函数所在线程中执行,当前函数线程阻塞
            ShowMsg += (str) => { Debug.WriteLine(str); };
            ShowMsg.Invoke("This is Invoke test!");

            // 开辟一个线程执行 BeginInvoke Test,当前函数线程不阻塞
            ShowMsg.BeginInvoke(" BeginInvoke Test", null, null);


            // AsyncCallback 回调委托
            // IAsyncResult 异步执行的结果,可以自我继承,添加自定义参数 用于显示异步执行的状态
            // index(object类型) 外部传参,存储在IAsyncResult.AsyncState中
            AsyncCallback asyncCallback = ar => { Debug.WriteLine($"beginInvoke 执行完成 回调{ar.AsyncState}"); };
            int index = 0;
            ShowMsg.BeginInvoke(" BeginInvoke Test", asyncCallback, index);
        }

6 应用 测速下载多个文件时耗用的时间 - 异步编程实现

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
 
namespace InvokeDemo
{
    public partial class 异步编程 : Form
    {
        public delegate string delegateObj(string paht);
        public 异步编程()
        {
            InitializeComponent();
            delobj = new delegateObj(copyFile);
        }
        delegateObj delobj=null;
        /// <summary>
        /// 同步拷贝文件,并输出文件名称
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button1_Click(object sender, EventArgs e)
        {
            String[] paths = Directory.GetFiles(@"F:\迅雷下载");
            for (int i = 0; i < paths.Length; i++)
            {
                this.listBox1.Items.Add(copyFile(paths[i]));
            }
        }
        /// <summary>异步拷贝文件,并输出文件名
        /// 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void button2_Click(object sender, EventArgs e)
        {
            
            String[] paths = Directory.GetFiles(@"F:\迅雷下载");
            for (int i = 0; i < paths.Length; i++)
            {
                delobj.BeginInvoke(paths[i], callback, Path.GetFileName(paths[i]));//最后一个参数是回调状态  ,如果不需要回调函数的话,直接null,这样就不需要调用EndInvoke,单纯的BeginInvoke即可
            }
        }
        public void callback(IAsyncResult result)
        {
            string filename = "";
            //if (this.listBox1.InvokeRequired)
            //{
            //    filename = this.listBox1.EndInvoke(result).ToString();
            //    this.listBox1.Items.Add(filename + "复制成功");
            //    this.listBox2.Items.Add(result.AsyncState.ToString() + "复制成功");
                
            //}
            filename = delobj.EndInvoke(result);
            //this.listBox1.Items.Add(filename + "复制成功");
            //this.listBox2.Items.Add(result.AsyncState.ToString() + "复制成功");
 
            Console.WriteLine(result.AsyncState.ToString() + "复制成功");
        }
        /// <summary>
        /// 拷贝文件用
        /// </summary>
        /// <param name="filename"></param>
        public string copyFile(string filename)
        {
            if (filename.EndsWith("desktop.ini"))
                return "desktop.ini";
            if (File.Exists(@"F:\迅雷下载2\" + Path.GetFileName(filename)))
            {
                File.Delete(@"F:\迅雷下载2\" + Path.GetFileName(filename));
            }
            File.Copy(filename, @"F:\迅雷下载2\" + Path.GetFileName(filename));
            return Path.GetFileName(filename);
        }
    }
}

总结 

到此这篇关于C#中Invoke和BeginInvoke实际应用详解的文章就介绍到这了,更多相关C# Invoke和BeginInvoke应用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C#内存管理CLR深入讲解(下篇)

    C#内存管理CLR深入讲解(下篇)

    本文详细讲解了C#内存管理CLR的内存分配和对大对象回收,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-01-01
  • Unity实现虚拟键盘

    Unity实现虚拟键盘

    这篇文章主要为大家详细介绍了Unity实现虚拟键盘,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-03-03
  • c# 计算时间间隔的简单方法(推荐)

    c# 计算时间间隔的简单方法(推荐)

    下面小编就为大家带来一篇c# 计算时间间隔的简单方法(推荐)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-08-08
  • C#实现appSettings节点读取与修改的方法

    C#实现appSettings节点读取与修改的方法

    这篇文章主要介绍了C#实现appSettings节点读取与修改的方法,是非常实用的技巧,需要的朋友可以参考下
    2014-10-10
  • c# Process.Start()找不到系统文件的解决方法

    c# Process.Start()找不到系统文件的解决方法

    vs1027在X64应用程序下执行process.start()时,OK;但是在X86应用程序下执行process.start(),报错:找不到系统文件,本文就详细的介绍一下解决方法,感兴趣的可以了解一下
    2023-09-09
  • C#类和结构详解

    C#类和结构详解

    本文详细讲解了C#中的类和结构,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • C#中实现可变参数实例

    C#中实现可变参数实例

    这篇文章主要介绍了C#中实现可变参数实例,本文演示使用params 实现可变数量的参数,并且这些参数的类型可以不同,需要的朋友可以参考下
    2015-01-01
  • 浅析C# 结构体struct

    浅析C# 结构体struct

    这篇文章主要介绍了C# 结构体struct 的相关资料,帮助大家更好的理解和学习c#,感兴趣的朋友可以了解下
    2020-08-08
  • C#适配器模式的使用

    C#适配器模式的使用

    本文主要介绍了C#适配器模式的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-06-06
  • c#实现图片二值化例子(黑白效果)

    c#实现图片二值化例子(黑白效果)

    这篇文章主要介绍了c#实现图片二值化例子,图像二值化就是将图像上的像素点的灰度值设置为0或255,也就是将整个图像呈现出明显的黑白效果,需要的朋友可以参考下
    2014-08-08

最新评论