首页 > 代码库 > SynchronizationContext是什么?

SynchronizationContext是什么?

使用场景, 例如有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

完!

SynchronizationContext是什么?