SynchronizationContext是什么?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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是什么?的主要内容,如果未能解决你的问题,请参考以下文章

SynchronizationContext 有啥作用?

SynchronizationContext 和 TaskScheduler 之间的概念区别是啥

从给定线程获取 SynchronizationContext

为啥控制台应用程序中没有捕获默认的 SynchronizationContext?

SynchronizationContext.CreateCopy 的目的

SynchronizationContext笔记