搞懂 SynchronizationContext(第一部分)【翻译】

SynchronizationContext -MSDN 很让人失望

我不知道为什么,目前在.Net下关于这个类只有很少的资料。MSDN文档也只有很少的关于如何使用SynchronizationContext的资料。最开始的时候,我不得不说我在理解为什么有这个类以及怎么使用这个类上经历了一段困难的时间。通过阅读大量的相关资料,我最终搞明白了这个类的目的以及它应该如何去使用。我决定写这篇文章来帮助其他开发者理解如何使用这个类,这个类能干嘛以及它不能干嘛。

使用SynchronizationContext来封装一段来自一个线程的代码到另一个线程执行

让我们先来了解一些不常见的技术点以帮助我们展示如何使用这个类。SynchronizationContext可以使一个线程与另一个线程进行通信。假设你又两个线程,Thead1和Thread2。Thread1做某些事情,然后它想在Thread2里面执行一些代码。一个可以实现的方式是请求Thread得到SynchronizationContext这个对象,把它给Thread1,然后Thread1可以调用SynchronizationContext的send方法在Thread2里面执行代码。听起来很绕口...但是这就是你需要了解的东西。不是每一个线程都有一个SynchronizationContext对象。一个总是有SynchronizationContext对象的是UI线程。

谁把SynchronizationContext对象放到UI线程里的?有没有可以猜一下的?放弃思考了?好吧,我来告诉你答案吧。答案是在这个线程里的一个控件(control)被创建的时候会把SynchronizationContext对象放到这个线程里。一般来说,第一次会是form第一次创建的时候。我怎么知道的?好吧,我一会就给你证明。

因为我的代码用了SynchronizationContext.Current,所以就让我先来解释下这个静态属性到底能干嘛吧。SynchronizationContext.Current可以使我们得到一个当前线程的SynchronizationContext的对象。我们必须清楚如下问题:SynchronizationContext.Current对象不是一个AppDomain一个实例的,而是每个线程一个实例。这就意味着两个线程在调用Synchronization.Current时将会拥有他们自己的SynchronizationContext对象实例。如果你好奇这个context上下文对象怎么存储的,那么答案就是它存储在线程data store(就像我之前说的,不是在appDomain的全局内存空间)。

来吧,让我们看下在我们UI线程中使用的SynchronizationContext的代码吧。

[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    // let's check the context here
    var context = SynchronizationContext.Current;
    if (context == null)
        MessageBox.Show("No context for this thread");
    else
        MessageBox.Show("We got a context");

    // create a form
    Form1 form = new Form1();

    // let's check it again after creating a form
    context = SynchronizationContext.Current;

    if (context == null)
        MessageBox.Show("No context for this thread");
    else
        MessageBox.Show("We got a context");

    if (context == null)
        MessageBox.Show("No context for this thread");

    Application.Run(new Form1());
}

正如你所见,有如下几个点需要注意:

  1. 第一个messagebox将会显示这个线程没有context。因为.Net都不知道这个线程就会做什么,因此没有一个运行时类来在为这个线程初始化sync Context对象。
  2. 在form创建之后,我们就会发现context已经被设置了。Form类负责了这件事情。它会检测sync Context是否已经有了,如果没有,它就会给线程设置一个。记住context对象在一个线程里面是一样的,所以任何UI控件都可以访问它,因为所有的UI操作都必须在UI线程里面执行。再通俗点说,创建window的线程都可以与window通信。在我们的场景下,这个线程就是应用程序的主线程。

怎么使用它?

既然UI线程已经足够nice了,它给了我们一个Sync Context来使我们在UI线程下执行代码,那么我们如何写呢?

第一步,我们要确定我们真有需要给UI线程封送的代码么?答案肯定是“是”。如果你在一个不是UI线程的线程里面执行代码,但你不得不去更新UI。要不要成为一个尝试下的英雄?可惜的是,你会得到一个异常(在.net1.0都没有引发异常,它只会让你的程序挂掉,在.net2.0中,就会给你引发一个很恶心的异常)。

公平的说,我得说你不是必须得用这个类来与UI线程进行通信。你可以使用InvokeRequired属性(在每个UI control里面都有)来封送你的代码。如果你通过InvokeRequired得到一个“true”,你就可以使用Control.Invoke方法来封送代码到UI线程。非常好!那为什么还要继续读我的文章呢?但是,这个技术一个问题,你必须得有一个Control你才能调用invoke方法。在UI线程里面这没有什么,但是如果在非UI的线程里面,你如果还想封送代码,你就只能在你的非UI线程里面增加一个control了。从设计的角度来说,在业务逻辑层应该永远都没有一个UI的引用。所以,你将所有的同步代码都放到了UI类里面来让IU保证封送。但是,这将会增加UI的功能复杂度,使UI完成比我们希望的多得多的功能。我必须说,让一个没有任何Control或者Form引用的业务逻辑层来负责封送代码到UI层是一个更好的选择。

那到底怎么做呢?

简单来说,创建一个线程,给他sync context对象,然后就可以用这个对象给UI线程封送代码了。让我们看个例子吧。

在接下来的例子中,有一个list box在工作线程调用。这个线程完成了一些计算然后写到UI的listbox里面。这个线程通过mToolStripButtonThreads_Click事件响应来更新UI。

首先,让我们先看下form:

private void InitializeComponent()
    {
        System.ComponentModel.ComponentResourceManager resources =
          new System.ComponentModel.ComponentResourceManager(typeof(Form1));
        this.mListBox = new System.Windows.Forms.ListBox();
        this.toolStrip1 = new System.Windows.Forms.ToolStrip();
        this.mToolStripButtonThreads = new System.Windows.Forms.ToolStripButton();
        this.toolStrip1.SuspendLayout();
        this.SuspendLayout();
        //
        // mListBox
        //
        this.mListBox.Dock = System.Windows.Forms.DockStyle.Fill;
        this.mListBox.FormattingEnabled = true;
        this.mListBox.Location = new System.Drawing.Point(0, 0);
        this.mListBox.Name = "mListBox";
        this.mListBox.Size = new System.Drawing.Size(284, 264);
        this.mListBox.TabIndex = 0;
        //
        // toolStrip1
        //
        this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
        this.mToolStripButtonThreads});
        this.toolStrip1.Location = new System.Drawing.Point(0, 0);
        this.toolStrip1.Name = "toolStrip1";
        this.toolStrip1.Size = new System.Drawing.Size(284, 25);
        this.toolStrip1.TabIndex = 1;
        this.toolStrip1.Text = "toolStrip1";
        //
        // mToolStripButtonThreads
        //
        this.mToolStripButtonThreads.DisplayStyle =
          System.Windows.Forms.ToolStripItemDisplayStyle.Text;
        this.mToolStripButtonThreads.Image = ((System.Drawing.Image)
            (resources.GetObject("mToolStripButtonThreads.Image")));
        this.mToolStripButtonThreads.ImageTransparentColor =
             System.Drawing.Color.Magenta;
        this.mToolStripButtonThreads.Name = "mToolStripButtonThreads";
        this.mToolStripButtonThreads.Size = new System.Drawing.Size(148, 22);
        this.mToolStripButtonThreads.Text = "Press Here to start threads";
        this.mToolStripButtonThreads.Click +=
          new System.EventHandler(this.mToolStripButtonThreads_Click);
        //
        // Form1
        //
        this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.ClientSize = new System.Drawing.Size(284, 264);
        this.Controls.Add(this.toolStrip1);
        this.Controls.Add(this.mListBox);
        this.Name = "Form1";
        this.Text = "Form1";
        this.toolStrip1.ResumeLayout(false);
        this.toolStrip1.PerformLayout();
        this.ResumeLayout(false);
        this.PerformLayout();
    }

    #endregion

    private System.Windows.Forms.ListBox mListBox;
    private System.Windows.Forms.ToolStrip toolStrip1;
    private System.Windows.Forms.ToolStripButton mToolStripButtonThreads;
}

现在让我们看这个例子:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void mToolStripButtonThreads_Click(object sender, EventArgs e)
    {
        // let's see the thread id
        int id = Thread.CurrentThread.ManagedThreadId;
        Trace.WriteLine("mToolStripButtonThreads_Click thread: " + id);

        // grab the sync context associated to this
        // thread (the UI thread), and save it in uiContext
        // note that this context is set by the UI thread
        // during Form creation (outside of your control)
        // also note, that not every thread has a sync context attached to it.
        SynchronizationContext uiContext = SynchronizationContext.Current;

        // create a thread and associate it to the run method
        Thread thread = new Thread(Run);

        // start the thread, and pass it the UI context,
        // so this thread will be able to update the UI
        // from within the thread
        thread.Start(uiContext);
    }

    private void Run(object state)
    {
        // lets see the thread id
        int id = Thread.CurrentThread.ManagedThreadId;
        Trace.WriteLine("Run thread: " + id);

        // grab the context from the state
        SynchronizationContext uiContext = state as SynchronizationContext;

        for (int i = 0; i < 1000; i++)
        {
            // normally you would do some code here
            // to grab items from the database. or some long
            // computation
            Thread.Sleep(10);

            // use the ui context to execute the UpdateUI method,
            // this insure that the UpdateUI method will run on the UI thread.

            uiContext.Post(UpdateUI, "line " + i.ToString());
        }
    }

    /// <summary>
    /// This method is executed on the main UI thread.
    /// </summary>
    private void UpdateUI(object state)
    {
        int id = Thread.CurrentThread.ManagedThreadId;
        Trace.WriteLine("UpdateUI thread:" + id);
        string text = state as string;
        mListBox.Items.Add(text);
    }
}

先浏览一遍这个代码。你应该注意到我将每个方法的线程ID都打印出来来方便我们一会回顾。

比如:

// let's see the thread id
int id = Thread.CurrentThread.ManagedThreadId;
Trace.WriteLine("mToolStripButtonThreads_Click thread: " + id);

当点击toolstrip button的时候,一个线程将会执行Run方法,需要注意的是我给这个线程传了一个state进去。我在调用的时候传入了UI线程的sync context对象。

SynchronizationContext uiContext = SynchronizationContext.Current;

因为我在toolstrip button的事件响应中执行,所以我知道我在UI线程中。通过调用SynchronizationContext.Current,我可以从UI线程得到sync context对象。

Run 将从它的state里面得到SynchronizationContext对象,这样它就有了像UI线程封送的代码的能力。

// grab the context from the state
SynchronizationContext uiContext = state as SynchronizationContext;

Run方法将会在listbox里面写1000行。怎么办呢?需要先用SynchronizationContext中的send方法:

public virtual void Send(SendOrPostCallback d, object state);

SynchronizationContext.Send方法有两个参数,一个是指向一个方法的委托,一个是"state"对象。在我们的例子里是这样的:

uiContext.Send(UpdateUI, "line " + i.ToString());

UpdateUI就是我们给这个委托传的方法,“state”是我们想给listbox增加的string。在UpdateUI中的代码是在UI线程中执行而不是在调用的线程。

private void UpdateUI(object state)
{
    int id = Thread.CurrentThread.ManagedThreadId;
    Trace.WriteLine("UpdateUI thread:" + id);
    string text = state as string;
    mListBox.Items.Add(text);
}

注意到这个代码直接在UI线程中执行。这里没有检查InvokerRequired,因为我知道由于使用了UI线程中的SynchronizationContext的send方法,它就会运行在UI线程中。

让我们来看下线程的id:

mToolStripButtonThreads_Click thread: 10
Run thread: 3
UpdateUI thread:10
UpdateUI thread:10
UpdateUI thread:10
UpdateUI thread:10
UpdateUI thread:10
UpdateUI thread:10
UpdateUI thread:10
UpdateUI thread:10
UpdateUI thread:10
UpdateUI thread:10
UpdateUI thread:10
UpdateUI thread:10
UpdateUI thread:10
UpdateUI thread:10
UpdateUI thread:10
... (x1000 times)

上面可以看到UI线程是10,工作线程(Run)是3,当我们更新UI的时候,我们又回到了线程10(UI线程)。这样所有的事情就像我们想的一样。

错误处理

很好,我们已经有能力像UI线程封送代码了,但是当我们封送的代码有引发异常的时候会怎么样?谁来捕获它呢?UI线程还是工作线程?

private void Run(object state)
{
    // let's see the thread id
    int id = Thread.CurrentThread.ManagedThreadId;
    Trace.WriteLine("Run thread: " + id);

    // grab the context from the state
    SynchronizationContext uiContext = state as SynchronizationContext;

    for (int i = 0; i < 1000; i++)
    {
        Trace.WriteLine("Loop " + i.ToString());
        // normally you would do some code here
        // to grab items from the database. or some long
        // computation
        Thread.Sleep(10);

        // use the ui context to execute the UpdateUI method, this insure that the
        // UpdateUI method will run on the UI thread.

        try
        {
            uiContext.Send(UpdateUI, "line " + i.ToString());
        }
        catch (Exception e)
        {
            Trace.WriteLine(e.Message);
        }
    }
}

/// <summary>
/// This method is executed on the main UI thread.
/// </summary>
private void UpdateUI(object state)
{
    throw new Exception("Boom");
}

我修改了UpdateUI方法来抛出一个异常:

throw new Exception("Boom");

当然,我也同时修改了Run方法在Send方法增加了try/catch。

try
{
    uiContext.Send(UpdateUI, "line " + i.ToString());
}
catch (Exception e)
{
    Trace.WriteLine(e.Message);
}

当执行这个代码的时候,我发现异常在Run方法中被捕获而不是在UI线程中。这很有趣,因为我们本以为会是UI线程由于没有异常处理而挂掉。

综上,Send方法施展了一个小魔法:它让我们的代码在别的线程执行,但是在当前线程引发异常。

Send 还是 Post

Send只是我们可以向UI线程封送代码的一种方式。另一种是Post。两者之间有什么不同呢?很多!
可能现在需要更多的了解这个类的细节了,我们先来看下SynchronizationContext的接口:

// Summary:
//     Provides the basic functionality for propagating a synchronization context
//     in various synchronization models.
public class SynchronizationContext
{
    // Summary:
    //     Creates a new instance of the System.Threading.SynchronizationContext class.
    public SynchronizationContext();

    // Summary:
    //     Gets the synchronization context for the current thread.
    //
    // Returns:
    //     A System.Threading.SynchronizationContext object representing the current
    //     synchronization context.
    public static SynchronizationContext Current { get; }

    // Summary:
    //     When overridden in a derived class, creates a copy of the synchronization
    //     context.
    //
    // Returns:
    //     A new System.Threading.SynchronizationContext object.
    public virtual SynchronizationContext CreateCopy();
    //
    // Summary:
    //     Determines if wait notification is required.
    //
    // Returns:
    //     true if wait notification is required; otherwise, false.
    public bool IsWaitNotificationRequired();
    //
    // Summary:
    //     When overridden in a derived class, responds to the notification that an
    //     operation has completed.
    public virtual void OperationCompleted();
    //
    // Summary:
    //     When overridden in a derived class, responds to the notification that an
    //     operation has started.
    public virtual void OperationStarted();
    //
    // Summary:
    //     When overridden in a derived class, dispatches an asynchronous message to
    //     a synchronization context.
    //
    // Parameters:
    //   d:
    //     The System.Threading.SendOrPostCallback delegate to call.
    //
    //   state:
    //     The object passed to the delegate.
    public virtual void Post(SendOrPostCallback d, object state);
    //
    // Summary:
    //     When overridden in a derived class, dispatches a synchronous message to a
    //     synchronization context.
    //
    // Parameters:
    //   d:
    //     The System.Threading.SendOrPostCallback delegate to call.
    //
    //   state:
    //     The object passed to the delegate.
    public virtual void Send(SendOrPostCallback d, object state);
    //
    // Summary:
    //     Sets the current synchronization context.
    //
    // Parameters:
    //   syncContext:
    //     The System.Threading.SynchronizationContext object to be set.
    public static void SetSynchronizationContext(SynchronizationContext syncContext);
    //
    // Summary:
    //     Sets notification that wait notification is required and prepares the callback
    //     method so it can be called more reliably when a wait occurs.
    protected void SetWaitNotificationRequired();
    //
    // Summary:
    //     Waits for any or all the elements in the specified array to receive a signal.
    //
    // Parameters:
    //   waitHandles:
    //     An array of type System.IntPtr that contains the native operating system
    //     handles.
    //
    //   waitAll:
    //     true to wait for all handles; false to wait for any handle.
    //
    //   millisecondsTimeout:
    //     The number of milliseconds to wait, or System.Threading.Timeout.Infinite
    //     (-1) to wait indefinitely.
    //
    // Returns:
    //     The array index of the object that satisfied the wait.
    [PrePrepareMethod]
    [CLSCompliant(false)]
    public virtual int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout);
    //
    // Summary:
    //     Helper function that waits for any or all the elements in the specified array
    //     to receive a signal.
    //
    // Parameters:
    //   waitHandles:
    //     An array of type System.IntPtr that contains the native operating system
    //     handles.
    //
    //   waitAll:
    //     true to wait for all handles; false to wait for any handle.
    //
    //   millisecondsTimeout:
    //     The number of milliseconds to wait, or System.Threading.Timeout.Infinite
    //     (-1) to wait indefinitely.
    //
    // Returns:
    //     The array index of the object that satisfied the wait.
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
    [PrePrepareMethod]
    [CLSCompliant(false)]
    protected static int WaitHelper(IntPtr[] waitHandles,
                     bool waitAll, int millisecondsTimeout);
}

下面是Post方法的注释:

//
// Summary:
//     When overridden in a derived class, dispatches an asynchronous message to
//     a synchronization context.
//
// Parameters:
//   d:
//     The System.Threading.SendOrPostCallback delegate to call.
//
//   state:
//     The object passed to the delegate.
public virtual void Post(SendOrPostCallback d, object state);

这里的关键字是asynchronous,这意味着Post将不会等委托方法的执行完成。Post将会对委托的代码执行然后忘记。这同时也意味着你不能像我们之前用Send方法一样捕获异常。假设有一个异常被抛出了,它将在UI线程捕获;如果不处理就会停掉UI线程。

不管怎么说,Post也好,Send也好,都将在当前的线程里执行委托。用Post替换掉Send,你就可以得到在UI线程里执行的正确的线程id。

这样的话,我就可以用SynchronizationContext来进行任意我想的线程同步了么?不能!

现在,你可能会在任何线程中用SynchronizationContext。但是,你很快就会发现不是在每次用SynchronizationContext.Current的时候都会有SynchronizationContext实例,它经常会返回null。不用你说,你可以很简单地在没有Sync Context的时候创建一个。这确实很简单,但也确实没用。

让我们看一个与刚才的UI线程例子很类似的代码:

class Program
{
    private static SynchronizationContext mT1 = null;

    static void Main(string[] args)
    {
        // log the thread id
        int id = Thread.CurrentThread.ManagedThreadId;
        Console.WriteLine("Main thread is " + id);

        // create a sync context for this thread
        var context = new SynchronizationContext();
        // set this context for this thread.
        SynchronizationContext.SetSynchronizationContext(context);

        // create a thread, and pass it the main sync context.
        Thread t1 = new Thread(new ParameterizedThreadStart(Run1));
        t1.Start(SynchronizationContext.Current);
        Console.ReadLine();
    }

    static private void Run1(object state)
    {
        int id = Thread.CurrentThread.ManagedThreadId;
        Console.WriteLine("Run1 Thread ID: " + id);

        // grab  the sync context that main has set
        var context = state as SynchronizationContext;

        // call the sync context of main, expecting
        // the following code to run on the main thread
        // but it will not.
        context.Send(DoWork, null);

        while (true)
            Thread.Sleep(10000000);
    }

    static void DoWork(object state)
    {
        int id = Thread.CurrentThread.ManagedThreadId;
        Console.WriteLine("DoWork Thread ID:" + id);
    }
}

这个简单的控制台程序你就不用在家试了。这个程序是不会符合预期的,但同时也证明了之前说的那点。注意到我给主线程设置了一个Sync Context的对象。我创建了一个Sync Context实例,然后把它设置给当前的线程。这跟UI线程在创建form时所做的事情非常相像(不完全一样,我稍后会解释。)。然后,我创建了一个线程Run1,并把主线程的sync context对象传递给它。当我尝试去调用Send的时候,我发现Send是在Run1线程里被调用而不是如我们期待的一样在主线程调用。下面是输出:

Main thread is 10
Run1 Thread ID: 11
DoWork Thread ID:11

DoWork在线程11中被执行,这与线程Run1一样。没有SynchronizationContext到主线程。为什么?到底发生了什么?通过这件事情,你应该意识到生活中没有什么是免费的。线程之间不能随意的切换,他们为了达到切换还需要一个基础设施。比如,UI线程用了一个message pump在它的SynchronizationContext对象中,它使消息同步到UI线程中。

因此,UI线程拥有自己的SynchronizationContext类,这个类集成于SynchronizationContext,叫System.Windows.Forms.WindowsFormsSynchronizationContext。这个类是一个与SynchronizationContext不同的实现。UI版本重写了Post和Send方法,提供了一个关于这些方法的"message pump"版本(我尽力去找了这些类的实现,但是没有找到)。那么这个单纯的SynchronizationContext到底做了什么呢?

代码如下:(原作者卖了会萌,然后说他修改了下代码的格式,此处就不一一翻译了)

namespace System.Threading
{
    using Microsoft.Win32.SafeHandles;
    using System.Security.Permissions;
    using System.Runtime.InteropServices;
    using System.Runtime.CompilerServices;
    using System.Runtime.ConstrainedExecution;
    using System.Reflection;

    internal struct SynchronizationContextSwitcher : IDisposable
    {
        internal SynchronizationContext savedSC;
        internal SynchronizationContext currSC;
        internal ExecutionContext _ec;

        public override bool Equals(Object obj)
        {
            if (obj == null || !(obj is SynchronizationContextSwitcher))
                return false;
            SynchronizationContextSwitcher sw = (SynchronizationContextSwitcher)obj;
            return (this.savedSC == sw.savedSC &&
                    this.currSC == sw.currSC && this._ec == sw._ec);
        }

        public override int GetHashCode()
        {
            return ToString().GetHashCode();
        }

        public static bool operator ==(SynchronizationContextSwitcher c1,
                                       SynchronizationContextSwitcher c2)
        {
            return c1.Equals(c2);
        }

        public static bool operator !=(SynchronizationContextSwitcher c1,
                                       SynchronizationContextSwitcher c2)
        {
            return !c1.Equals(c2);
        }

        void IDisposable.Dispose()
        {
            Undo();
        }

        internal bool UndoNoThrow()
        {
            if (_ec  == null)
            {
                return true;
            }

            try
            {
                Undo();
            }
            catch
            {
                return false;
            }
            return true;
        }

        public void Undo()
        {
            if (_ec  == null)
            {
                return;
            }

            ExecutionContext  executionContext =
              Thread.CurrentThread.GetExecutionContextNoCreate();
            if (_ec != executionContext)
            {
                throw new InvalidOperationException(Environment.GetResourceString(
                          "InvalidOperation_SwitcherCtxMismatch"));
            }
            if (currSC != _ec.SynchronizationContext)
            {
                throw new InvalidOperationException(Environment.GetResourceString(
                          "InvalidOperation_SwitcherCtxMismatch"));
            }
            BCLDebug.Assert(executionContext != null, " ExecutionContext can't be null");
            // restore the Saved Sync context as current
            executionContext.SynchronizationContext = savedSC;
            // can't reuse this anymore
            _ec = null;
        }
    }

    public delegate void SendOrPostCallback(Object state);

    [Flags]
    enum SynchronizationContextProperties
    {
        None = 0,
        RequireWaitNotification = 0x1
    };

    public class SynchronizationContext
    {
        SynchronizationContextProperties _props = SynchronizationContextProperties.None;

        public SynchronizationContext()
        {
        }

        // protected so that only the derived sync
        // context class can enable these flags
        protected void SetWaitNotificationRequired()
        {
            // Prepare the method so that it can be called
            // in a reliable fashion when a wait is needed.
            // This will obviously only make the Wait reliable
            // if the Wait method is itself reliable. The only thing
            // preparing the method here does is to ensure there
            // is no failure point before the method execution begins.

            RuntimeHelpers.PrepareDelegate(new WaitDelegate(this.Wait));
            _props |= SynchronizationContextProperties.RequireWaitNotification;
        }

        public bool IsWaitNotificationRequired()
        {
            return ((_props &
              SynchronizationContextProperties.RequireWaitNotification) != 0);
        }

        public virtual void Send(SendOrPostCallback d, Object state)
        {
            d(state);
        }

        public virtual void Post(SendOrPostCallback d, Object state)
        {
            ThreadPool.QueueUserWorkItem(new WaitCallback(d), state);
        }

        public virtual void OperationStarted()
        {
        }

        public virtual void OperationCompleted()
        {
        }

        // Method called when the CLR does a wait operation
        public virtual int Wait(IntPtr[] waitHandles,
                       bool waitAll, int millisecondsTimeout)
        {
            return WaitHelper(waitHandles, waitAll, millisecondsTimeout);
        }

        // Static helper to which the above method
        // can delegate to in order to get the default
        // COM behavior.
        protected static extern int WaitHelper(IntPtr[] waitHandles,
                         bool waitAll, int millisecondsTimeout);

        // set SynchronizationContext on the current thread
        public static void SetSynchronizationContext(SynchronizationContext syncContext)
        {
            SetSynchronizationContext(syncContext,
              Thread.CurrentThread.ExecutionContext.SynchronizationContext);
        }

        internal static SynchronizationContextSwitcher
          SetSynchronizationContext(SynchronizationContext syncContext,
          SynchronizationContext prevSyncContext)
        {
            // get current execution context
            ExecutionContext ec = Thread.CurrentThread.ExecutionContext;
            // create a switcher
            SynchronizationContextSwitcher scsw = new SynchronizationContextSwitcher();

            RuntimeHelpers.PrepareConstrainedRegions();
            try
            {
                // attach the switcher to the exec context
                scsw._ec = ec;
                // save the current sync context using the passed in value
                scsw.savedSC = prevSyncContext;
                // save the new sync context also
                scsw.currSC = syncContext;
                // update the current sync context to the new context
                ec.SynchronizationContext = syncContext;
            }
            catch
            {
                // Any exception means we just restore the old SyncCtx
                scsw.UndoNoThrow(); //No exception will be thrown in this Undo()
                throw;
            }
            // return switcher
            return scsw;
        }

        // Get the current SynchronizationContext on the current thread
        public static SynchronizationContext Current
        {
            get
            {
                ExecutionContext ec = Thread.CurrentThread.GetExecutionContextNoCreate();
                if (ec != null)
                    return ec.SynchronizationContext;
                return null;
            }
        }

        // helper to Clone this SynchronizationContext,
        public virtual SynchronizationContext CreateCopy()
        {
            // the CLR dummy has an empty clone function - no member data
            return new SynchronizationContext();
        }

        private static int InvokeWaitMethodHelper(SynchronizationContext syncContext,
            IntPtr[] waitHandles,
            bool waitAll,
            int millisecondsTimeout)
        {
            return syncContext.Wait(waitHandles, waitAll, millisecondsTimeout);
        }
    }
}

让我们看下Send和Post的实现:

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方法只是简单的在当前线程调用了委托,而不是切换到另一个线程,Post也是做了同样的事情,只是用了一个TreadPool来实现异步而已。我认为,这个类应当被定义为抽象的,这个默认的实现让人费解而且也没用。这也是我决定写这篇文章的原因之一。

结论

我希望你现在对这个class能够有了足够的了解,你可以弄懂怎么使用。在.net里面,我发现有两个类提供一般的同步功能。一个是Winform的线程上下文,另一个是WPF的。我相信肯定还有,但我目前只找到了这两个。这个类的默认实现没有实现从一个线程切换到另一个线程。这也是一个简单的线程在默认的情况下不能有这样效果的原因。另一方面,UI线程有"message pump"和windows的api(比如SendMessage和PostMessage),因此我确信可以封送代码到UI线程。

然而,这不是对这个类的研究的重点。你可以自己实现一个SynchronizationContext类,这也很简单。实际上,我自己也写了一个。在我的工作中,我们必须让所有基于COM的调用全部在STA的方法里运行。因此,我决定自己也一个版本的SynchronizationContext,名字叫StaSynchronizationContext。我将会在Part II部分展示给大家。

一些词语

"message pump":消息泵

后记

本篇翻译是起于在项目中要写到UI线程的回调,而又不想写Invoke,之前在别的项目中见到过这个写法,故拿来研究下,发现确实是个好东西,心动不已,so 给大家翻译下推广下此项技术。

原文链接

http://www.codeproject.com/Articles/31971/Understanding-SynchronizationContext-Part-I

转载于:https://www.cnblogs.com/lzxianren/p/SynchronizationContext.html

using CommunityToolkit.Mvvm.ComponentModel; using Liudin.Acs; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Media; namespace Liudin.App.ViewModels.bar { public partial class InfoBarViewModel : BaseViewModel { private readonly Singleton config; public InfoBarViewModel(Singleton _config) { config = _config; config._InfoBarContext = SynchronizationContext.Current ?? new SynchronizationContext(); ShowInfo(); } [ObservableProperty] private string infoText = "机器没有上电,请打开电源开关"; [ObservableProperty] private Brush? fontColor = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#006400")); /// <summary> /// 设置字体颜色 0-正常 1-警告 2-错误 /// </summary> /// <param name="lev"></param> private void SetMsgColor(int lev) { switch (lev) { case 0: FontColor = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#006400")); break; case 1: FontColor = Brushes.Orange; break; case 2: FontColor = Brushes.Red; break; } } private void ShowInfo() { try { Task.Run(async () => { while (true) { config._InfoBarContext.Post(_ => { InitInfo(); }, null); await Task.Delay(100); } }); } catch (TaskCanceledException) { /* 处理取消 */ } } private void InitInfo() { string msg = ""; if (JSGS.bPowerOn) { SetMsgColor(2); msg = "机器没有上电,请打开电源开关"; } else if (JSGS.bEStop) { SetMsgColor(2); msg = "急停开关已打开,处理好,关闭急停开关"; } else if (App.Current.m_bBgnHome) { SetMsgColor(1); msg = "机器启动,请回零"; } else if (JSGS.bToHome) { SetMsgColor(1); msg = "急停开关已关闭,请回零"; } else if (JSGS.bAlarm) { SetMsgColor(2); msg = "机器报警,请停止检查"; } else if (JSGS.bHoming) { SetMsgColor(1); msg = "机器正在回零..."; } else if (JLimit(ref msg)) { SetMsgColor(1); } else if (JSGS.bRunning) { SetMsgColor(0); msg = "系统正在自动运行中..."; } else { SetMsgColor(0); msg = "系统准备就绪,等待中..."; } } } }
最新发布
04-05
<think>好的,我现在需要帮助用户分析他们提供的C#代码,特别是使用CommunityToolkit.Mvvm的MVVM模式实现的InfoBarViewModel。首先,我要理解代码的结构和各个部分的作用。 首先,用户引用了CommunityToolkit.Mvvm.ComponentModel,这可能意味着他们使用了该库中的ObservableObject或ObservableProperty属性,用于数据绑定。代码中的[ObservableProperty]属性正说明了这一点,这些属性会自动生成INotifyPropertyChanged的实现,简化了ViewModel的编写。 接下来,代码定义了一个部分类InfoBarViewModel,继承自BaseViewModel。构造函数中注入了Singleton实例,这可能是一个全局配置或状态管理类。这里将当前线程的SynchronizationContext赋给了config的_InfoBarContext,可能是为了在UI线程上更新信息栏的内容,避免跨线程访问的问题。 然后,InfoText和FontColor是两个可观察属性,用于绑定到UI界面,显示信息和字体颜色。SetMsgColor方法根据传入的级别设置不同的颜色,这里用到了SolidColorBrush和颜色转换,可能需要检查颜色字符串是否正确,以及是否有潜在异常。 ShowInfo方法启动了一个后台任务,每隔100毫秒调用InitInfo方法。这里使用了Task.Run和async/await,但需要注意循环中的延迟是否合理,以及是否正确处理了取消操作。捕获了TaskCanceledException,但处理部分为空,可能需要进一步处理或记录。 InitInfo方法根据不同的条件设置信息内容和颜色。例如,当bPowerOn为true时显示机器未上电,使用红色字体。这里需要确认各个条件之间的优先级是否正确,比如多个条件同时满足时,是否按照正确的顺序处理。例如,bPowerOn的判断在bEStop之前,如果两者同时为true,会优先显示bPowerOn的信息,而可能忽略急停状态,这是否符合业务逻辑? 另外,JLimit(ref msg)的调用可能是一个方法,用于检查其他限制条件,并返回布尔值。需要确认这个方法是否正确处理了msg的赋值,以及在返回true时是否已经设置了msg内容,否则可能导致显示不正确。 在代码中,所有对FontColor和InfoText的赋值都是通过config._InfoBarContext.Post来执行的,这确保了在UI线程上更新属性,避免跨线程异常。但需要确认SynchronizationContext.Current是否在构造函数调用时正确捕获,尤其是在不同的初始化环境下是否可能为null,导致使用了new SynchronizationContext(),这可能不合适,因为不是所有的SynchronizationContext都能正确工作。 另外,使用ColorConverter.ConvertFromString可能会在无效颜色字符串时抛出异常,但代码中没有异常处理,这可能导致应用崩溃。建议检查颜色字符串的格式是否正确,或者在转换时添加try-catch块。 最后,整个ViewModel的设计符合MVVM模式,将UI逻辑与业务逻辑分离,通过数据绑定更新界面。但需要确保BaseViewModel是否正确实现了必要的基类功能,例如INotifyPropertyChanged或其他MVVM相关的功能,虽然CommunityToolkit的ObservableProperty可能已经处理了这部分。 总结可能的问题点:条件判断的顺序、颜色转换的异常处理、SynchronizationContext的正确获取、后台任务的取消处理,以及JLimit方法的行为是否正确。需要用户进一步确认这些部分的业务逻辑是否正确,或者是否存在潜在的错误。</think>以下是对你提供的 `InfoBarViewModel` 代码的逐步解析: --- ### **1. 整体结构** - **继承关系**:继承自 `BaseViewModel`(应实现 MVVM 基础功能)。 - **依赖注入**:通过构造函数注入 `Singleton` 实例(全局单例,可能用于共享状态)。 - **线程上下文**:捕获 `SynchronizationContext.Current` 用于 UI 线程更新。 --- ### **2. 核心属性** ```csharp [ObservableProperty] private string infoText = "机器没有上电,请打开电源开关"; [ObservableProperty] private Brush? fontColor; ``` - **功能**:通过 `CommunityToolkit.Mvvm` 的代码生成器自动生成 `INotifyPropertyChanged` 通知。 - **作用**:`InfoText` 显示状态信息,`FontColor` 控制文字颜色。 --- ### **3. 颜色设置方法** ```csharp private void SetMsgColor(int lev) { switch (lev) { case 0: // 正常 FontColor = new SolidColorBrush(...); break; case 1: // 警告 FontColor = Brushes.Orange; break; case 2: // 错误 FontColor = Brushes.Red; break; } } ``` - **颜色规则**: - 正常(绿色):`#006400` - 警告(橙色):系统预定义颜色 - 错误(红色):系统预定义颜色 --- ### **4. 后台消息循环** ```csharp private void ShowInfo() { Task.Run(async () => { while (true) { config._InfoBarContext.Post(_ => InitInfo(), null); await Task.Delay(100); } }); } ``` - **功能**:每 100ms 在 UI 线程调用 `InitInfo()` 更新状态。 - **注意点**: - 使用 `Post` 确保跨线程安全。 - 死循环需配合取消机制(例如 `CancellationToken`)。 --- ### **5. 状态判断逻辑 (`InitInfo`)** ```csharp private void InitInfo() { string msg = ""; if (JSGS.bPowerOn) { /* 未上电 */ } else if (JSGS.bEStop) { /* 急停打开 */ } else if (App.Current.m_bBgnHome) { /* 需回零 */ } else if (JSGS.bToHome) { /* 需回零 */ } else if (JSGS.bAlarm) { /* 报警 */ } else if (JSGS.bHoming) { /* 回零中 */ } else if (JLimit(ref msg)) { /* 其他限制条件 */ } else if (JSGS.bRunning) { /* 运行中 */ } else { /* 就绪 */ } } ``` - **优先级顺序**:条件判断有严格顺序,需确保业务逻辑正确性。 - **潜在问题**: - `JLimit(ref msg)` 需确保正确设置 `msg`。 - 多个条件可能同时为 `true`,但只会触发第一个匹配项。 --- ### **6. 关键改进建议** 1. **取消机制**: - `while (true)` 循环应支持取消(例如通过 `CancellationToken`)。 - 当前仅捕获 `TaskCanceledException`,但未主动触发。 2. **颜色安全**: - `ColorConverter.ConvertFromString("#006400")` 可能抛出异常,建议添加 `try-catch`。 3. **条件判断优化**: - 检查条件顺序是否符合业务优先级(例如急停 `bEStop` 是否应比电源未开启更重要)。 4. **线程上下文**: - `SynchronizationContext.Current` 可能为 `null`,需确认 `new SynchronizationContext()` 是否适用当前场景。 --- ### **7. 代码亮点** - **MVVM 范式**:通过数据绑定实现 UI 与逻辑解耦。 - **异步处理**:使用 `Task.Run` 和 `SynchronizationContext` 正确处理 UI 线程更新。 - **可维护性**:状态机逻辑集中管理,便于扩展新条件。 --- 如果需要更深入的优化分析或解决特定问题,请提供具体场景!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值