使用场景, 例如有Thread1和Thread2两个线程,如果需要在Thread2执行的时候在Thread1线程中执行一些代码(UI线程的更新)那么需要Thread2需要做的是拿到Thread1 的SynchronizationContext对象,然后把要执行的代码利用委托的方式通过SynchronzationContext的Post()或者Send()方法来传递给Thread1执行。线程的SynchronizationContext 都是通过SynchronizationContext.Current 这个静态属性获得。 那么是不是每个线程默认都有SynchronizationContext呢? 并不是, 目前我只知道Winfrom 和WPF 创建Control的时候才会 给主线程的SynchronizationContext赋值。如果线程中没有类似的操作 那就SynchronizationContext是Null。当然我们可以手动给线程设置一个SynchronizationContext,然而我们自己手动设置的并不能起到跨线程调用的神奇效果。原因后面讲先看代码:
【代码1】:线程中的SynchronizationContext 何时会赋值?
class Program { static void Main(string[] args) { Console.WriteLine(SynchronizationContext.Current==null); TextBox tb = new TextBox(); Console.WriteLine("创建Control后"); Console.WriteLine(SynchronizationContext.Current==null); Console.WriteLine(SynchronizationContext.Current); Console.ReadKey(); } }
这是一段控制台代码,从执行结果可以看看出在创建Control之前主线程的Synchronization对象是空的,在创建Control之后Synchronization存在了说明Control在创建的时候给Synchronization赋值了。有细心的朋友可能会发打印出来的类名是WindowsFormsSynchronizationContext而不是SynchronizationContext后面会解释这一状况。 |
【代码2】:我们如何使用SynchronizationContext来实现神奇的跨线程调用委托的功能呢?
class Program { static void Main(string[] args) { Console.WriteLine("主线程:ID=" + Thread.CurrentThread.ManagedThreadId); TextBox tb = new TextBox(); //模拟UI线程获取SynchronizationContext对象 Thread t1 = new Thread(Run); t1.Start(SynchronizationContext.Current); Console.ReadKey(); } static void Run(object obj) { Console.WriteLine("线程t1:ID=" + Thread.CurrentThread.ManagedThreadId); SynchronizationContext sc = obj as SynchronizationContext; sc.Send(Callback, null); //sc.Post(Callback, null); Console.WriteLine("线程t1完。"); } static void Callback(object obj) { Console.WriteLine("回调:ID=" + Thread.CurrentThread.ManagedThreadId); } }
这里有个问题就是不管是Send还是Post 两个都没有执行CallBack这个回调,不同的是Post打印了完成,而Send没有打印完成。那我们就知道了Send同步调用的 代码被阻塞了,Post是异步调用的 没有阻塞线程。其实吧,不管是Send还是Post都是异步调用的区别是有没有用WaitHandle机制来阻塞而已。那么他们到底为什么没有执行回调函数呢?来看看他的源代码。由【代码1】我们可以看出Control创建的时候给主线程赋值了WindowsFormsSynchronizationContext,这个类是SynchronizationContext的子类。
【代码3】
public sealed class WindowsFormsSynchronizationContext : SynchronizationContext, IDisposable { private Control controlToSendTo; private WindowsFormsSynchronizationContext(Control marshalingControl, Thread destinationThread) { controlToSendTo = marshalingControl; this.DestinationThread = destinationThread; Debug.Assert(controlToSendTo.IsHandleCreated, "Marshaling control should have created its handle in its ctor."); } //省略若干代码。。。。。。。 public override void Send(SendOrPostCallback d, Object state) { Thread destinationThread = DestinationThread; if (destinationThread == null || !destinationThread.IsAlive) { throw new InvalidAsynchronousStateException(SR.GetString(SR.ThreadNoLongerValid)); } Debug.Assert(controlToSendTo != null, "Should always have the marshaling control by this point"); if (controlToSendTo != null) { controlToSendTo.Invoke(d, new object[] { state }); //Send直接调用Control的Invoke } } public override void Post(SendOrPostCallback d, Object state) { Debug.Assert(controlToSendTo != null, "Should always have the marshaling control by this point"); if (controlToSendTo != null) { controlToSendTo.BeginInvoke(d, new object[] { state }); //Post直接调用Control的BeginInvoke } } //省略若干代码。。。。。。。 }
说重点:从代码中可以看出 WindowsFormsSynchronizationContext 继承了SynchronizationContext 并且重写了Post()和Send()方法,而这两个方法的代码也不难懂 Post()方法就是直接调用Control 的BeginInvoke() 而Send就是直接调用了Contorl的Invoke方法。
Control的BeginInvoke 和Invoke我们 都知道 我们跨线程来更新UI的时候就会用到这两个方法,要不然抛异常。他们两个的背后又有着怎样的秘密呢?让我们来继续深挖。
【代码4】
//Control的BeginInvoke方法 都调用了 MarshaledInvoke 只是 最后一个参数不同 [EditorBrowsable(EditorBrowsableState.Advanced)] [SuppressMessage("Microsoft.Security", "CA2123:OverrideLinkDemandsShouldBeIdenticalToBase")] public IAsyncResult BeginInvoke(Delegate method, params Object[] args) { using (new MultithreadSafeCallScope()) { Control marshaler = FindMarshalingControl(); return (IAsyncResult)marshaler.MarshaledInvoke(this, method, args, false); } } //Control的BeginInvoke方法 都调用了 MarshaledInvoke 只是 最后一个参数不同 public Object Invoke(Delegate method, params Object[] args) { using (new MultithreadSafeCallScope()) { Control marshaler = FindMarshalingControl(); return marshaler.MarshaledInvoke(this, method, args, true); } } //Control的BeginInvoke和Invoke最后都调用了MarshaledInvoke private Object MarshaledInvoke(Control caller, Delegate method, Object[] args, bool synchronous) { //省略若干。。。。 UnsafeNativeMethods.PostMessage(new HandleRef(this, Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero); //PostMessage又是什么东东呢 //省略若干。。。。 if (synchronous) { if (!tme.IsCompleted) { WaitForWaitHandle(tme.AsyncWaitHandle); //Control 的BeginInvoke 方法和Invoke方法 的区别就在这里了 //其实他们的委托都是在拥有Control的线程上执行的也就是UI线程 //只是Invoke用到了WaitForWaitHandle 来阻塞调用Invoke的线程 而BeginInvoke没有阻塞 //从这个就可以看出Begininvoke的异步 并没有创建新的线程,并不是我们想象中的那种异步!!! } if (tme.exception != null) { throw tme.exception; } return tme.retVal; } else { return (IAsyncResult)tme; } } //PostMessage又是什么东东呢?没错他就是WindowsAPI 哈哈 这下算是挖到低了 //是一个用来发送消息到窗口消息队列的api函数,但这个方法是非阻塞的。也就是它会马上返回,而不管消息是否真的发送到目的地,也就是调用者不会被阻塞。 [DllImport(ExternDll.User32, CharSet = CharSet.Auto)] [ResourceExposure(ResourceScope.None)] public static extern bool PostMessage(HandleRef hwnd, int msg, IntPtr wparam, IntPtr lparam); }
那么什么是消息队列呢?
Windows GUI程序是基于消息机制的,有个主线程维护着一个消息泵。这个消息泵让windows程序生生不息。
Windows GUI程序的消息循环
Windows程序有个消息队列,窗体上的所有消息是这个队列里面消息的最主要来源。这里的while循环使用了GetMessage()这个方法,这是个阻塞方法,也就是队列为空时方法就会被阻塞,从而这个while循环停止运动,这避免了一个程序把cpu无缘无故地耗尽,让其它程序难以得到响应。当然在某些需要cpu最大限度运动的程序里面就可以使用另外的方法,例如某些3d游戏或者及时战略游戏中,一般会使用PeekMessage()这个方法,它不会被windows阻塞,从而保证整个游戏的流畅和比较高的帧速。
这个主线程维护着整个窗体以及上面的子控件。当它得到一个消息,就会调用DispatchMessage方法派遣消息,这会引起对窗体上的窗口过程的调用。窗口过程里面当然是程序员提供的窗体数据更新代码和其它代码。
【代码5】
public static void Main(string[] args) { Form f = new Form(); Application.Run(f); } //Dotnet窗体程序封装了上述的while循环,这个循环就是通过Application.Run方法启动的。
现在知道我们【代码2】为什么没有执行回调函数了吧,是的我们就只是创建了一个Control而已 并没有让这个这个程序的消息循环跑起来。那现在让我们对代码而做一些改动,看看是不是就可以执行回掉函数了。
【代码6】
class Program { static void Main(string[] args) { Console.WriteLine("我是主线程我的ID是:" + Thread.CurrentThread.ManagedThreadId); Application.SetCompatibleTextRenderingDefault(false); // TextBox tb = new TextBox(); //单来这个 不行的 , 毕竟到最后还是得依赖消息队列 句柄什么的 Form form = new Form(); Button btn = new Button(); btn.Text = "测试"; btn.Click += Btn_Click; form.Controls.Add(btn); Application.Run(form);//模拟UI线程获取SynchronizationContext Console.ReadKey(); } private static void Btn_Click(object sender, EventArgs e) { Thread t1 = new Thread(run); t1.Start(SynchronizationContext.Current); } static void run(object obj) { Console.WriteLine("我是T1线程我的ID是:" + Thread.CurrentThread.ManagedThreadId); SynchronizationContext sc = obj as SynchronizationContext; sc.Post(doWork, null); Console.WriteLine("执行完成"); } static void doWork(object obj) { Console.WriteLine("我是t1中使用SynchronizationContext的回调我的ID是:" + Thread.CurrentThread.ManagedThreadId); } }
果然没错。 回调的线程ID 和主线程的线程ID一致,证明回调是在主线程中执行的。
那么对于两个都不是UI线程的情况呢? 能不能做到跨线程委托的效果呢? 由【代码1】我们知道非UI线程中的SynchronizationContext如果不赋值的话那就是null。
所以在调用前要给SynchronizationContext赋值。
【代码7】
class Program { static void Main(string[] args) { Console.WriteLine("主线程:ID=" + Thread.CurrentThread.ManagedThreadId); //设置这个线程的SynchronizationContext SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); Thread t1 = new Thread(Run); t1.Start(SynchronizationContext.Current); Console.ReadKey(); } static void Run(object obj) { Console.WriteLine("线程t1:ID=" + Thread.CurrentThread.ManagedThreadId); SynchronizationContext sc = obj as SynchronizationContext; sc.Send(Callback, null); SendOrPostCallback cc = new SendOrPostCallback(Callback); Console.WriteLine("线程t1完。"); } static void Callback(object obj) { Console.WriteLine("回调:ID=" + Thread.CurrentThread.ManagedThreadId); } }
从执行结果可以看出回调函数打印的线程ID和线程t1的打印的线程ID 一致 ,说明回调并没有在主线程中执行。那这是为啥呢?
看看SynchronizationContext源代码。
【代码8】
public class SynchronizationContext { //省略若干代码。。。。。。 //直接调用回调 public virtual void Send(SendOrPostCallback d, Object state) { d(state); } //线程池调用回调 public virtual void Post(SendOrPostCallback d, Object state) { ThreadPool.QueueUserWorkItem(new WaitCallback(d), state); } //省略若干代码。。。。。。 }
看完代码你会发现 它就是什么都没干!!! Send就直接调用了。这也就解释了 回调函数打印的线程ID和线程t1的打印的线程ID 一致。
结语:
SynchronizationContext 只是一个父类而已,从它的Send和Post方法都标记为virtual 可以看出,它仅仅只是作为一个父类而存在,所以直接使用SynchronizationContext是毫无意义的。想要实现一些神奇的功能,你还要自己去重写它。目前MSDN 可以查到的 SynchronizationContext子类有俩个都是和GUI程序有关的看下图
WindowsFormsSynchronizationContext 我们见识过了,DispatcherSynchronizationContext至于这个 是WPF中的 和WindowsFormsSynchronizationContext 功能类似。
参考:
http://www.cnblogs.com/fuchongjundream/p/3939298.html
http://www.cnblogs.com/lzxianren/p/SynchronizationContext.html
完!