目录
三、Delegate的Invoke/BeginInvoke/EndInvoke
1.3、通过Delegate.BeginInvoke()将“耗时操作”放在子线程中运行
四、Control的Invoke/BeginInvoke/EndInvoke
1.3、Control.BeginInvoke(Delegate)
一、前言
此文章是我阅读了许多前人的文章、博客后,根据自己的理解做的总结、记录。有不正确的地方望各位多加指正,有侵权的地方望各位提醒我添加引用备注出处。
此文章迭代更新中(20221201-1619)。
对C#中的Delegate、Event、Thread,WinForm中的Control有基本了解。
二、背景
当运行一个WinForm窗体时,UI线程(主线程)上运行着一个消息循环,可以理解“消息循环”是一个死循环,每次循环都会从“消息队列”中取出一个“消息”(WinForm窗体中的用户操作,比如鼠标单击/双击/移入/移出操作,键盘输入等),然后处理这个“消息”,体现在WinForm窗体上就是实时地响应了用户的操作。
如果有个[需求1]:要在点击WinForm窗体中的一个Button后,执行一个非常耗时的操作(比如,sleep 10s)。如果直接在主线程中执行这个耗时操作,主线程因为不能进行消息循环导致WinForm窗体看起来卡死,无法响应用户操作。所以,此时应该创建一个子线程,然后将耗时操作放在子线程中执行,此时,会用到Thread,或者Delegate.BeginInvoke()来创建子线程。
如果有个[需求2]:在[需求1]的基础上,“耗时操作”在子线程中执行完后,需要获取“耗时操作”的执行结果。
如果有个[需求3]:在[需求1]、[需求2]的基础上,“耗时操作”在子线程中执行完后,需要刷新WinForm中的某个Control(比如,需要刷新TextBox文本框中的值。此时也可以理解为是[需求2]的一种特殊情况)。如果“刷新操作”也放在子线程中执行,会抛出运行时异常(跨线程访问控件,即控件是在主线程中创建的,但是却在子线程中对控件进行了刷新)。所以,此时应该将“刷新操作”整体封装成一个Delegate,然后通过Control.Invoke(Delegate)、Control.BeginInvoke(Delegate)将“刷新操作”交给主线程执行,就能解决“跨线程访问控件”的运行时异常。
三、Delegate对象的Invoke/BeginInvoke/EndInvoke
1、基于[需求1]
1.1、直接在主线程中运行“耗时操作”
public Form1()
{
InitializeComponent();
Debug.Listeners.Add(new ConsoleTraceListener());
}
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Begin Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Thread.Sleep(10000);
Console.WriteLine(string.Format("End Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return;
}
protected override void WndProc(ref Message m)
{
Debug.WriteLine(m.Msg.ToString());
base.WndProc(ref m);
return;
}
点击Button后,主线程Sleep 10s。此时WinForm看起来卡死,不响应用户在UI中的操作;Console输出的用户在UI中的操作信息看起来也卡住。
当主线程Sleep结束后,WinForm才响应用户在UI中的操作;Console才继续输出主线程Sleep期间用户在UI中的操作信息。
1.2、通过Thread将“耗时操作”放在子线程中运行
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Begin Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Thread thread = new Thread(new ThreadStart(SubThreadMethod));
thread.IsBackground = true;
thread.Start();
Console.WriteLine(string.Format("End Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return;
}
private void SubThreadMethod()
{
Console.WriteLine(string.Format("Begin SubThread ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Thread.Sleep(10000);
Console.WriteLine(string.Format("End SubThread ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return;
}
点击Button后,通过new Thread()创建一个子线程,子线程执行Thread封装的SubThreadMethod,即Sleep 10s;主线程上仍然运行着消息循环。此时WinForm仍然可以响应用户在UI中的操作。
1.3、通过Delegate对象.BeginInvoke()将“耗时操作”放在子线程中运行
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Begin Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
CalcDel myCalcDel = new CalcDel(ExeMethod);
Console.WriteLine(string.Format("Before BeginInvoke ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
myCalcDel.BeginInvoke(null, null);
Console.WriteLine(string.Format("End Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return;
}
public delegate void CalcDel();
public void ExeMethod()
{
Console.WriteLine(string.Format("Start ExeMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Thread.Sleep(10000);
Console.WriteLine(string.Format("End ExeMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return;
}
点击Button后,通过Delegate对象.BeginInvoke()创建一个子线程,子线程执行Delegate对象封装的ExeMethod,即Sleep 10s;主线程上仍然运行着消息循环。效果看起来等同于[1.2]。
1.4、总结
对比2、3发现,Delegate对象.BeginInvoke()实质上是Thread,创建了一个子线程,然后在子线程中运行Delegate对象封装的操作。
了解了Delegate对象.BeginInvoke()后,再详细对比下Delegate对象.BeginInvoke()、Delegate对象.EndInvoke()的若干用法。
2、基于[需求2]
此段内容,参考文章[WinForm二三事(二)异步操作 - 横刀天笑 - 博客园],并进行完善
2.1、一去不复返
1.3中展示的就是,子线程运行的“耗时操作”结束后,主线程不需要获取任何关于子线程结束的信息。
2.2、去了还要回->轮询
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Begin Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Func<int> myAction = new Func<int>(ExeMethod);
Console.WriteLine(string.Format("Begin BeginInvoke ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
IAsyncResult asynResult = myAction.BeginInvoke(null, null);
Console.WriteLine(string.Format("End BeginInvoke ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
DateTime lastTime = DateTime.MinValue;
while (!asynResult.IsCompleted)
{
DateTime curTime = DateTime.Now;
if((curTime - lastTime).TotalSeconds > 2){
Console.WriteLine(string.Format("NotComplete ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
lastTime = curTime;
}
}
Console.WriteLine(string.Format("Complete ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
int result = myAction.EndInvoke(asynResult);
Console.WriteLine(string.Format("End Click result:{0} ThreadId:{1} {2}", result, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return;
}
private int ExeMethod()
{
Console.WriteLine(string.Format("Begin ExeMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Thread.Sleep(10 * 1000);
Console.WriteLine(string.Format("End ExeMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return 25;
}
点击Button后,通过Delegate对象.BeginInvoke()创建一个子线程,子线程执行Delegate对象封装的ExeMethod,即Sleep 10s;主线程上运行着循环,监视子线程是否执行完毕。
如果子线程没有执行完毕,asynResult.IsCompleted始终为false,主线程中的循环一直进行;如果子线程执行完毕,asynResult.IsCompleted变成true,主线程跳出循环。
主线程在此while循环期间,因为主线程没有运行消息循环,所以WinForm看起来卡死,不响应用户在UI中的操作。
主线程、子线程分别在哪些时间段执行,是由调度算法分配的。比如,截图中,Delegate对象.BeginInvoke()语句执行后,并没有立即执行由Delegate对象.BeginInvoke()语句创建的子线程,而是由调度算法决定什么时候执行子线程中的语句:
1、先拥有控制权的主线程执行了一阵子while循环,然后把控制权交给子线程执行ExeMethod;
2、当子线程Sleep后,又将控制权交给主线程执行while循环;
3、主线程执行了一阵子while循环,等子线程Sleep结束后,将控制权交给子线程;
4、子线程执行完毕后,IAsyncResult对象.IsCompleted由false变为true,控制权交给主线程;
5、主线程跳出while循环。
IAsyncResult asynResult = myAction.BeginInvoke(null, null);
while(!asynResult.IsCompleted){}
会使执行该语句的当前线程一直循环,直到子线程运行完Delegate对象封装的操作。
2.3、去了还要回->WaitOne
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Begin Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Func<int> myAction = new Func<int>(ExeMethod);
Console.WriteLine(string.Format("Begin BeginInvoke ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
IAsyncResult asynResult = myAction.BeginInvoke(null, null);
Console.WriteLine(string.Format("End BeginInvoke ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
if (asynResult.AsyncWaitHandle.WaitOne(5 * 1000, true))
{
Console.WriteLine(string.Format("Begin wait ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
int result = myAction.EndInvoke(asynResult);
Console.WriteLine(string.Format("End wait result:{0} ThreadId:{1} {2}", result, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
}
Console.WriteLine(string.Format("End Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return;
}
private int ExeMethod()
{
Console.WriteLine(string.Format("Begin ExeMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Thread.Sleep(10 * 1000);
Console.WriteLine(string.Format("End ExeMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return 25;
}
1、点击Button后,主线程执行到IAsyncResult asynResult = myAction.BeginInvoke(null, null)时,会创建一个子线程跑ExeMethod,不过此时主线程还拥有控制权,所以主线程继续执行到asynResult.AsyncWaitHandle.WaitOne(5 * 1000, true),接着主线程会阻塞等待5s(因为子线程还没执行完毕,所以WaitOne让主线程阻塞等待),此时主线程的控制权被调度算法收回;
[主线程在此阻塞等待期间,因为主线程没有运行消息循环,所以WinForm看起来卡死,不响应用户在UI中的操作]
2、调度算法将控制权交给子线程,子线程Sleep 10s,此时子线程的控制权被调度算法收回;
3、5s后,主线程结束了阻塞等待,调度算法将控制权交给主线程,主线程跳过if(asynResult.AsyncWaitHandle.WaitOne(5 * 1000, true)){}花括号中的语句(因为子线程还没执行完毕,所以if条件不满足),直接执行if(){}之后的语句,然后主线程结束,主线程的控制权被调度算法收回;
4、又过了5s后,子线程结束了Sleep,调度算法将控制权交给子线程,然后子线程结束,子线程的控制权被调度算法收回。
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Begin Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Func<int> myAction = new Func<int>(ExeMethod);
Console.WriteLine(string.Format("Begin BeginInvoke ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
IAsyncResult asynResult = myAction.BeginInvoke(null, null);
Console.WriteLine(string.Format("End BeginInvoke ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
if (asynResult.AsyncWaitHandle.WaitOne(10 * 1000, true))
{
Console.WriteLine(string.Format("Begin wait ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
int result = myAction.EndInvoke(asynResult);
Console.WriteLine(string.Format("End wait result:{0} ThreadId:{1} {2}", result, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
}
Console.WriteLine(string.Format("End Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return;
}
private int ExeMethod()
{
Console.WriteLine(string.Format("Begin ExeMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Thread.Sleep(5 * 1000);
Console.WriteLine(string.Format("End ExeMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return 25;
}
1、点击Button后,主线程执行到IAsyncResult asynResult = myAction.BeginInvoke(null, null)时,会创建一个子线程跑ExeMethod,不过此时主线程还拥有控制权,所以主线程继续执行到asynResult.AsyncWaitHandle.WaitOne(10 * 1000, true),接着主线程会阻塞等待10s(因为子线程还没执行完毕,所以WaitOne让主线程阻塞等待),此时主线程的控制权被调度算法收回;
[主线程在此阻塞等待期间,因为主线程没有运行消息循环,所以WinForm看起来卡死,不响应用户在UI中的操作]
2、调度算法将控制权交给子线程,子线程Sleep 5s,此时子线程的控制权被调度算法收回;
3、5s后,子线程结束了Sleep,调度算法将控制权交给子线程,然后子线程结束,子线程的控制权被调度算法收回;
4、主线程直接结束了阻塞等待(不用再等5s,因为此时子线程已经结束了,WaitOne是最多等ns,而不是必须等ns),调度算法将控制权交给主线程,主线程执行if(asynResult.AsyncWaitHandle.WaitOne(5 * 1000, true)){}花括号中的语句(因为子线程已经执行完毕,所以if条件满足),接着执行if(){}之后的语句,然后主线程结束,主线程的控制权被调度算法收回。
IAsyncResult asynResult = myAction.BeginInvoke(null, null);
asynResult.AsyncWaitHandle.WaitOne(10 * 1000, true);
会使执行该语句的当前线程等待最多10s。
2.4、去了还要回->回调
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Begin Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Func<int> myAction = new Func<int>(ExeMethod);
Console.WriteLine(string.Format("Begin BeginInvoke ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
IAsyncResult asynResult = myAction.BeginInvoke(new AsyncCallback((result) =>
{
Console.WriteLine(string.Format("Begin wait ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
int res = myAction.EndInvoke(result);
Console.WriteLine(string.Format("End wait result:{0} ThreadId:{1} {2}", res, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
}), null);
Console.WriteLine(string.Format("End BeginInvoke ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Console.WriteLine(string.Format("End Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return;
}
private int ExeMethod()
{
Console.WriteLine(string.Format("Begin ExeMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Thread.Sleep(10 * 1000);
Console.WriteLine(string.Format("End ExeMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return 25;
}
1、点击Button后,主线程执行到IAsyncResult asynResult = myAction.BeginInvoke(new AsyncCallback((result) => {}), null)时,会创建一个子线程跑ExeMethod,不过此时主线程还拥有控制权,所以主线程继续执行直到主线程结束,此时主线程的控制权被调度算法收回;
2、调度算法将控制权交给子线程,子线程Sleep 10s,此时子线程的控制权被调度算法收回;
[子线程在Sleep期间,因为主线程有运行消息循环,所以WinForm看起来没有卡死,会时刻响应用户在UI中的操作]
3、10s后,子线程结束了Sleep,调度算法将控制权交给子线程,子线程执行完ExeMethod后,接着执行回调函数(result) => {},然后子线程结束,此时子线程的控制权被调度算法收回。
CalcDel myCalDel = null;
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Begin Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
myCalDel = new CalcDel(ExecuteTask);
for (int i = 3; i < 11; i++)
{
Console.WriteLine(string.Format("{0}-Begin BeginInvoke ThreadId:{1} {2}", i, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
myCalDel.BeginInvoke(10 * i, 1000 * i, new AsyncCallback(MyCallBack), i);
Console.WriteLine(string.Format("{0}-End BeginInvoke ThreadId:{1} {2}", i, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
}
Console.WriteLine(string.Format("End Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return;
}
public delegate int CalcDel(int num, int ms);
private int ExeMethod(int num, int ms)
{
Console.WriteLine(string.Format("Begin ExeMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Thread.Sleep(ms);
Console.WriteLine(string.Format("End ExeMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return num * num;
}
private void MyCallback(IAsyncResult result)
{
Console.WriteLine(string.Format("Begin {0}-Callback ThreadId:{1} {2}", result.AsyncState.ToString(), Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
int res = myCalDel.EndInvoke(result);
Console.WriteLine(string.Format("End {0}-Callback 结果:{1} ThreadId:{2} {3}", result.AsyncState.ToString(), res, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return;
}
了解了Delegate.BeginInvoke()、Delegate.EndInvoke()的若干用法,再对比下Delegate.Invoke()。
3、Delegate.Invoke()
Delegate.Invoke()不会创建子线程,而是直接在当前线程中运行Delegate封装的操作。
四、Control的Invoke/BeginInvoke/EndInvoke
1、基于[需求3]
此段内容,参考文章[浅谈Invoke 和 BegionInvoke的用法 - 大艺术家007 - 博客园]
1.1、禁用“检查是否跨线程访问控件”,这是不安全的做法
[待完善...]
1.2、Control.Invoke(Delegate)
private void button1_Click(object sender, EventArgs e)
{
Console.WriteLine(string.Format("Begin Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Thread subThread = new Thread(new ThreadStart(SubThreadMethod));
subThread.Start();
Console.WriteLine(string.Format("End subThread.Start ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
string a = "+";
for (int i = 0; i < 5; i++)
{
a = a + "A";
Console.WriteLine(string.Format("Loop-{0}-{1} ThreadId:{2} {3}", i, a, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Thread.Sleep(2000);
}
Console.WriteLine(string.Format("End Click ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return;
}
private void SubThreadMethod()
{
Console.WriteLine(string.Format("Begin SubThreadMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
button1.Invoke(new Del(invokeMethod));
//button1.BeginInvoke(new Del(DelMethod));
Console.WriteLine(string.Format("End BeginInvoke ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
string b = "-";
for (int i = 0; i < 7; i++)
{
b = b + "B";
Console.WriteLine(string.Format("Loop-{0}-{1} ThreadId:{2} {3}", i, b, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
Thread.Sleep(1300);
}
Console.WriteLine(string.Format("End SubThreadMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return;
}
public delegate void Del();
private void DelMethod()
{
Console.WriteLine(string.Format("Begin DelMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
this.textBox1.Text = DateTime.Now.ToString();
Console.WriteLine(string.Format("End DelMethod ThreadId:{0} {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
return;
}
在子线程中运行Control.Invoke(Delegate),会立即将Delegate封装的操作交给主线程运行。
此时,控制权会立即从子线程交给主线程,只有当主线程运行完Delegate封装的操作后,才会再接着运行子线程中Control.Invoke(Delegate)语句后面的操作;而在主线程运行完Delegate封装的操作之前,子线程(Control.Invoke(Delegate)语句后面的操作)会被阻塞。
1.3、Control.BeginInvoke(Delegate)
在子线程中运行Control.BeginBeginInvoke(Delegate),也会将Delegate封装的操作交给主线程运行。
此时,子线程(Control.Invoke(Delegate)语句后面的操作)不会被阻塞。至于主线程、子线程的执行顺序,是由调度算法控制的,在用户看来是不确定的。
1.4、总结
Control.Invoke(Delegate)、Control.BeginBeginInvoke(Delegate),都会将Delegate封装的操作交给主线程运行,而当Delegate封装的操作是刷新Control时,就不会抛出“跨线程访问控件”的运行时异常了。