1、简介
-
invoke:在拥有此控件的基础窗口句柄的现呈上同步执行指定的委托(同步)。Invoke() 调用时,Invoke会阻止当前主线程的运行,等到 Invoke() 方法返回才继续执行后面的代码,表现出“同步”的概念。
-
beginInvoke:在创建控件的基础句柄所在线程上异步执行的委托(异步)。BeginInvoke() 调用时,当前线程会启用线程池中的某个线程来执行此方法,BeginInvoke不会阻止当前主线程的运行,而是等当前主线程做完事情之后再执行BeginInvoke中的代码内容,表现出“异步”的概念。在想获取 BeginInvoke() 执行完毕后的结果时,调用EndInvoke() 方法来获取。而这两个方法中执行的是一个委托。
invoke和begininvoke方法的初衷是为了解决在某个非某个控件创建的线程中刷新该控件可能会引发异常的问题。所以不管是Invoke还是beginInvoke大多数都是在支线程使用。如果你要更新一个label,那程序会自动跳到你拥有(invoke)或创建(begininvoke)这个label的线程上去执行
2、区别
如果还是不理解,那么我用两段代码调试,大家看一下打印顺序和线程名就清楚了
Invoke
Console.WriteLine("111111-{0}", Thread.CurrentThread.ManagedThreadId);
Task.Run(() =>
{
Console.WriteLine("222222-{0}", Thread.CurrentThread.ManagedThreadId);
Invoke(new Action(() =>
{
Console.WriteLine("333333-{0}", Thread.CurrentThread.ManagedThreadId);
}));
Console.WriteLine("444444-{0}", Thread.CurrentThread.ManagedThreadId);
});
// 以下是打印顺序和线程
// 111111 - 1
// 222222 - 3
// 333333 - 1
// 444444 - 3
BeginInvoke
Console.WriteLine("111111-{0}", Thread.CurrentThread.ManagedThreadId);
Task.Run(() =>
{
Console.WriteLine("222222-{0}", Thread.CurrentThread.ManagedThreadId);
BeginInvoke(new Action(() =>
{
Console.WriteLine("333333-{0}", Thread.CurrentThread.ManagedThreadId);
}));
Console.WriteLine("444444-{0}", Thread.CurrentThread.ManagedThreadId);
});
// 以下是打印顺序和线程
// 111111-1
// 222222-3
// 444444-3
// 333333-1
3、使用
-
使用情况
一般来说,Invoke其实用法只有两种情况:
Control的Invoke
Delegate的Invoke
也就是说,Invoke前面要么是一个控件,要么是一个委托对象。 -
Delegate的Invoke也可以不用,直接去调用委托,但那样就无法做null的判断了。
Delegate的Invoke其实就是从线程池中调用委托方法执行,Invoke是同步的方法,会卡住调用它的UI线程。代码如下:
{
public event EventHandler EventArgs;
public void PerformOperation(int value, EventArgs e)
{
Task.Run(() =>
{
Console.WriteLine(value);
Task.Delay(8000).Wait();// 模拟操作延时
// 调用代理方法
if (EventArgs != null)
{
//EventArgs("返回值", e);
EventArgs?.Invoke("返回值", e);//委托的调用不是必须要用Invoke方法的,直接使用上面那行代码调用委托对象也可以,但是就无法做非空判断了,所以尽量还是使用EventArgs?.Invoke为好。
}
});
}
}
- Control的Invoke
windows GUI编程有一个规则,就是只能通过创建控件的线程来操作控件的数据,否则就可能产生不可预料的结果。
也就是说我们的控件都是在主线程创建的,如果想要修改控件的数据,也只能在主线程操作,如果我们在一个子线程里去操作控件数据,就会发生类似下图的报错。这里我继续用delegate博客学习里面的代码做的演示,这个MyCallbackFunction是在子线程回调进来的。
image.png
正确写法应该是:
public FrmMain()
{
InitializeComponent();
var test = new TestClass();
test.EventArgs += MyCallbackFunction;
test.EventArgs += MyCallbackFunction2;
test.PerformOperation(5, EventArgs.Empty);
Console.WriteLine("当前主线程:{0}", Thread.CurrentThread.ManagedThreadId);
button1.Text = "first按钮";
}
void MyCallbackFunction(object sender, EventArgs e)
{
Console.WriteLine("当前线程1:{0}", Thread.CurrentThread.ManagedThreadId);// 这里子线程
//this.button1.Invoke
//this.Invoke
Invoke(new Action(UpdateButton)); //在C# 3.0及以后的版本中有了Lamda表达式,NET Framework 3.5及以后版本更能用Action封装方法
//Invoke(new Action(() =>
//{
// Thread.Sleep(1000);
// this.button1.Text = "按钮";
// Console.WriteLine("按钮已改");
// Console.WriteLine("当前线程2:{0}", Thread.CurrentThread.ManagedThreadId);// 这里是主线程了
//}));
Console.WriteLine("回调函数被调用:" + sender.ToString());
}
void MyCallbackFunction2(object sender, EventArgs e)
{
Console.WriteLine("回调函数被调用2:" + sender.ToString());
}
void UpdateButton()
{
Thread.Sleep(1000);
this.button1.Text = "按钮";
Console.WriteLine("按钮已改");
Console.WriteLine("当前线程2:{0}", Thread.CurrentThread.ManagedThreadId);// 这里是主线程了
}
这里button1属于,主窗体Form,Form是继承了Control的,所以Form也有Invoke的方法,所以我们可以直接调用Form.Invoke,这就是常见的this.Invoke,this省略后就是直接Invoke即可。
我们来看一下control.Invoke定义,有两个重载
image.png image.png
第一个是委托类型的对象,第二个是委托object类型对象的参数数组(如果有参数),比如我们这样改一下:
//private Action<string> MyAction;//不加event说明仅用于代表方法,没有事件发布的概念
public event Action<string> MyAction;// 加Event用于将事件与订阅器连接起来
//加不加event关键字,都可以定义委托,实现事件机制。但定义使用event修饰的委托,在类外部是不能把该委托当做方法直接调用的,这就是用不用event的区别。
void MyCallbackFunction(object sender, EventArgs e)
{
MyAction += UpdateButton;
Console.WriteLine("当前线程1:{0}", Thread.CurrentThread.ManagedThreadId);// 这里子线程
//Invoke(MyAction, new object[] { "按钮" });// 这样写也可以
Invoke(MyAction, "按钮"); //在C# 3.0及以后的版本中有了Lamda表达式,NET Framework 3.5及以后版本更能用Action封装方法
//Invoke(new Action(() =>
//{
// Thread.Sleep(1000);
// this.button1.Text = "按钮";
// Console.WriteLine("按钮已改");
// Console.WriteLine("当前线程2:{0}", Thread.CurrentThread.ManagedThreadId);// 这里是主线程了
//}));
Console.WriteLine("回调函数被调用:" + sender.ToString());
}
void MyCallbackFunction2(object sender, EventArgs e)
{
Console.WriteLine("回调函数被调用2:" + sender.ToString());
}
void UpdateButton(string str)
{
Thread.Sleep(1000);
this.button1.Text = str;
Console.WriteLine("按钮已改");
Console.WriteLine("当前线程2:{0}", Thread.CurrentThread.ManagedThreadId);// 这里是主线程了
}
当然也可以传入一个我们自定义对象或者多个参数,以下是多个参数的实例
//private Action<string> MyAction;//不加event说明仅用于代表方法,没有事件发布的概念
public event Action<string, int> MyAction;// 加Event用于将事件与订阅器连接起来
//加不加event关键字,都可以定义委托,实现事件机制。但定义使用event修饰的委托,在类外部是不能把该委托当做方法直接调用的,这就是用不用event的区别。
void MyCallbackFunction(object sender, EventArgs e)
{
MyAction += UpdateButton;
Console.WriteLine("当前线程1:{0}", Thread.CurrentThread.ManagedThreadId);// 这里子线程
//Invoke(MyAction, new object[] { "按钮" });// 这样写也可以
Invoke(MyAction, "按钮", 10); //在C# 3.0及以后的版本中有了Lamda表达式,NET Framework 3.5及以后版本更能用Action封装方法
//Invoke(new Action(() =>
//{
// Thread.Sleep(1000);
// this.button1.Text = "按钮";
// Console.WriteLine("按钮已改");
// Console.WriteLine("当前线程2:{0}", Thread.CurrentThread.ManagedThreadId);// 这里是主线程了
//}));
Console.WriteLine("回调函数被调用:" + sender.ToString());
}
void MyCallbackFunction2(object sender, EventArgs e)
{
Console.WriteLine("回调函数被调用2:" + sender.ToString());
}
void UpdateButton(string str, int age)
{
Thread.Sleep(1000);
this.button1.Text = str + age;
Console.WriteLine("按钮已改");
Console.WriteLine("当前线程2:{0}", Thread.CurrentThread.ManagedThreadId);// 这里是主线程了
}
网友评论