SynchronizationContext — когда MSDN подводит

в 9:33, , рубрики: .net, synchronization, threads

Не знаю почему, но информации об этом новом класса в .NET Framework, действительно немного. Документация MSDN почти ничего не говорит о способах использования SynchronizationContext. Должен сказать, изначально я и сам плохо представлял назначение этого класса и как его использовать. После продолжительного изучения вопроса я наконец понял его назначение и решил написать эту статью чтобы помочь разобраться другим разработчикам.

Использование SynchronizationContext для проброса кода из одного потока в другой

Рассмотрим некоторые технические подробности общения потоков посредством SynchronizationContext. Предположим у вас есть два потока, t1 и t2. И t1 исполняет некоторую работу, и в какой-то момент желает передать выполнение кода потоку t2. Один из способов сделать это, запросить у t2 SynchronizationContext, передать его в t1, который вызовет метод Send для передачи кода в t2. Напоминает магию… Однако вы должны знать что не у каждого потока есть связанный с ним SynchronizationContext. Только один поток однозначно имеет SynchronizationContext, это UI поток.

Кто задает SynchronizationContext для UI потока? Есть предположения? Хорошо, вот вам ответ, первый созданный контрол в потоке помещает SynchronizationContext в этот поток. Как правило это первая созданная форма. Откуда я это узнал? Ну… я закодил проверку.

Поскольку мой код использует SynchronizationContext.Current, позвольте объяснить что дает это статическое свойство. SynchronizationContext.Current позволяет получить SynchronizationContext который присоединен к текущему потоку. Сразу проясним, SynchronizationContext.Current не синглтон в рамках AppDomain, но синглтон в рамках потока. Это значит что два разных потока могут получить разные экземпляры SynchronizationContext вызвав SynchronizationContext.Current. Если вас интересует где хранится актуальный SynchronizationContext, он хранится в хранилище данных потока (и как я сказал ранее, не в глобальной памяти домена приложения).

Хорошо, давайте посмотри на код, который задает SynchronizationContext в наш UI поток:

Пример

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

    // проверим наличие контекста синхронизации
    var context = SynchronizationContext.Current;
    if (context == null)
        MessageBox.Show("No context for this thread");
    else
        MessageBox.Show("We got a context");

    // создадим форму
    Form1 form = new Form1();

    // проверим наличие контекста синхронизации еще раз
    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());
}

Как вы видите есть пара моментов которые надо учесть:

  • Первый блок кода показывает что изначально нет SynchronizationContext'а прикрепленного к потоку. Это потому, что .NET не знает что произойдет в этом потоке, и нет исполняемого класса который бы инициализировал контекст синхронизации для этого потока.
  • Сразу после создания формы, мы видим что контекст установлен. За это несет ответственность класс Form, Он проверяет, если контекст синхронизации отсутствует, то следует его задать. Запомните, контекст всегда один в одном потоке, так что любой UI контрол может получить к нему доступ. Потому что все UI операции должны исполняться в UI потоке. Поток который создает окно, должен иметь возможность с этим окном общаться. В нашем случае это основной поток приложения.

И что мне с этим все теперь делать?

Теперь, когда UI поток задал контекст синхронизации, и мы можем запускать код в UI потоке, как мы можем это использовать?

Для начала, мы действительно можем прокидывать код в UI поток? Да. Если код исполняется в потоке отличном от UI потока, вы не можете воздействовать на пользовательский интерфейс. Хотите погеройствовать и попробовать сделать это? Вы получите исключение (в версии 1.0 исключения не будет, приложение просто упадет, но в версии 2.0 есть жирные уродливые исключения которые приложение выплюнет вам в лицо).

Справедливости ради, я скажу что вы не должны использовать контекст синхронизации в UI потоке. Вам нужно использовать свойство InvokeRequired(которое есть у каждого класса любого UI контрола) и смотреть, нужно ли вам прокидывать код. Если InvokeRequired вернет значение true, то задействуйте Control.Invoke для маршалинга в поток UI. Отлично! Но есть проблема с этой техникой. У вас должен быть контрол, на котором вы можете вызвать Invoke. Не имеет значение какой UI контрол это будет, но вам нужна по крайней мере одна доступная ссылка на контрол, в вашем не UI потоке, для проведения маршалинга.

С точки зрения дизайна, вам не нужны ссылки на UI в бизнес-слое. Тогда вы можете оставить все операции по синхронизации UI классу, и быть уверенным что UI самостоятельно несет ответственность за маршалинг(посмотрите мою статью по MVP). Тем не менее, это задает UI больше ответственности, и делает UI более нагруженным чем хотелось бы. Было бы хорошо на слое бизнес-логики иметь возможность маршалинга в пользовательский интерйфейс, не имея ссылок на контролы или на форму.

И как это делается?

Да примитивно, Создать поток, передать ему контекст синхронизации, и использовать этот поток как объект синхронизации для маршалинга в UI поток. Посмотрим пример.
В следующем примере, у меня есть listBox который заполняется из рабочего потока. Поток имитирует вычисления и выводит данные в listBox. Поток используемый для обновления пользовательского интерфейса запускается из обработчика mToolStripButtonThreads_Click.

Прежде всего посмотрим что на форме:

Посмотреть что на форме

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)
    {
        // посмотрим id потока
        int id = Thread.CurrentThread.ManagedThreadId;
        Trace.WriteLine("mToolStripButtonThreads_Click thread: " + id);

        // захватим контекст синхронизации ассоциированный с этим
        // потоком (UI поток), и сохраним его в uiContext
        // отметье что этот контекст устанавливается в UI потоке
        // во время создания формы (вне зоны вашего контроля)
        // также отметье, что не каждый поток имеет контекст синхронизации связанный с ним.
        SynchronizationContext uiContext = SynchronizationContext.Current;

        // Создадим поток и зададим ему метод Run для исполнения
        Thread thread = new Thread(Run);

        // Запустим поток и установим ему контекст синхронизации,
        // таким образом этот поток сможет обновлять UI
        thread.Start(uiContext);
    }

    private void Run(object state)
    {
        // смотри id потока
        int id = Thread.CurrentThread.ManagedThreadId;
        Trace.WriteLine("Run thread: " + id);

        // вытащим контекст синхронизации из state'а
        SynchronizationContext uiContext = state as SynchronizationContext;

        for (int i = 0; i < 1000; i++)
        {
			// Тут мог бы быть ваш код который обращается к базе
			// или выполняет какие-то вычисления
            Thread.Sleep(10);

            // испольуем UI контекст для обновления интерфейса, 
			// посредством исполнения метода UpdateUI, метод UpdateUI 
			// будет исполнен в UI потоке

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

    /// <summary>
    /// Этот метод исполняется в основном UI потоке
    /// </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 потока, чтобы мы могли посмотреть на него в дальнейшем.

При нажатии на кнопку ToolStrip, поток запускается с указателем на метод Run. В этот поток я передаю состояние, в котором содержится контекст синхронизации UI потока.

SynchronizationContext uiContext = SynchronizationContext.Current;

Я знаю что в SynchronizationContext.Current содержится контекст синхронизации UI потока, потому что код выполняется по нажатию на кнопку (UI контрол). Метод Run получает контекст синхронизации из переданного состояния, и теперь у него есть способ проброса кода в UI поток.

// Получение контекста синхронизации из состояния
SynchronizationContext uiContext = state as SynchronizationContext;

Метод Run 1000 раз выводит запись в listBox. Как? Он использует метод Send контекста синхронизации.

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

Метод Send принимает два аргумента, делегат на метод и состояние. В нашем примере…

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

...UpdateUI указатель на метод, в состояние содержит строку для вывода в listBox. Код из метода 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 поток потому что был использован метод 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)

Здесь мы видим что id UI потока равен 10, рабочий поток (Run) имеет id равный трем, и когда мы вызываем обновление пользовательского интерфейса, id потока в котором оно происходит равно 10. Все работает как и рекламировалось.

Обработка ошибок

Очень хорошо, мы способны прокидывать код в UI поток, но что случится если прокинутый код сгенерирует исключение? Кто несет ответственность за его перехват? UI поток или рабочий поток?

Пример выброса исключения

private void Run(object state)
{
    // смотрим id потока
    int id = Thread.CurrentThread.ManagedThreadId;
    Trace.WriteLine("Run thread: " + id);

    // захватываем контекст синхронизации
    SynchronizationContext uiContext = state as SynchronizationContext;

    for (int i = 0; i < 1000; i++)
    {
        Trace.WriteLine("Loop " + i.ToString());
        // симуляция вычислений
        Thread.Sleep(10);

        // прокидываем код в UI поток
        try
        {
            uiContext.Send(UpdateUI, "line " + i.ToString());
        }
        catch (Exception e)
        {
            Trace.WriteLine(e.Message);
        }
    }
}

/// <summary>
/// Метод исполняемый в UI потоке
/// </summary>
private void UpdateUI(object state)
{
    throw new Exception("Boom");
}

Я изменил метод UpdateUI, чтобы он выкидывал исключение. И добавил try/catch на методе Send контекста синхронизации.

При запуске этого кода я увидел что исключение появилось в потоке метода Run, а не в UI. Это интересно, т.к. можно было ожидать исключение в UI потоке, принимая во внимание отсутствие классов перехватывающих исключения в UI потоке.
Следовательно, есть в методе Send немного магии; он выполняет наш код синхронно и возвращает нам любое возникшее исключение.

Send vs. Post

Использование метода Send это один из двух возможных способов прикинуть код в UI поток. Второй способ — использование метода Post. Есть ли разница? Она огромна!

Пришло время рассмотреть более подробно контракт класса SynchronizationContext.

ISynchronizationContext

// 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);

Ключевое слово здесь: асинхронный. Это означает что метод Post не будет ждать исполнения делегата, для собственного завершения. «Выстрелил и забыл» об исполняемом коде. Также это означает что вы не сможете перехватить сообщение, как при вызове метода Send. И теперь исключение получит UI поток. Если это исключение не обработать, UI поток упадет.

Тем не менее, Post или Send вы выберете, исполняемый код всегда отработает в нужном потоке. Заменив Send на Post, вы все равно получите идентификатор UI потока в исполняемом коде.

Теперь я могу использовать SynchronizationContext для синхронизации любых потоков, верно? Неа!

В любой момент вы можете попробовать использовать SynchronizationContext из любого потока. Однако вы обнаружите что ваш поток при вызове SynchronizationContext.Current получит null. Ничего страшного — скажете вы, и зададите SynchronizationContext, если его нет. Примитивно. Но это не сработает.

Посмотрим на программу аналогичную используемой ранее.

Пример

class Program
{
    private static SynchronizationContext mT1 = null;

    static void Main(string[] args)
    {
        // запишем id потока
        int id = Thread.CurrentThread.ManagedThreadId;
        Console.WriteLine("Main thread is " + id);

        // создадим новый контекст синхронизации для текущего потока
        var context = new SynchronizationContext();
        // зададим контекст синхронизации текущему потоку
        SynchronizationContext.SetSynchronizationContext(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);

        // вытаскиваем контекст синхронизации из состояния
        var context = state as SynchronizationContext;

        // пробуем выполнить код в основном потоке используя контекст синхронизации
        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);
    }
}

Это простое консольное приложение показывает как вам не стоит делать. Эта программа не работает. Заметьте, я задаю контекст синхронизации в главном потоке консольного приложения. Я просто создаю новый экземпляр. И присоединяю его к текущему потоку. Это очень похоже на то, что делает UI поток когда создается форма (не совсем, объясню позже). Затем я создаю поток Run1, и посылаю ему контекст синхронизации основного потока. Когда я пытаюсь вызвать метод Send, то глядя на вывод, я вижу что метод вызывается в потоке Run1, а не в основном потоке, как ожидалось. Вот вывод:

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

Видите, DoWork исполняется в том же потоке что и Run1. А вовсе не в основном потоке. Почему? Что происходит?
Ну… В этой части вы поймете что нет ничего бесплатного в этой жизни. Потоки не могут просто так переключаться между контекстами, им нужна инфраструктура встроенная в них для проведения такой операции. Поток UI, например, использует очередь сообщений, и в своем контексте синхронизации он использует эту очередь для синхронизации в пользовательском интерфейсе.

Т.е. UI поток имеет собственный контекст синхронизации, но этот класс производный от SynchronizationContext, и называется он System.Windows.Forms.WindowsFormsSynchronizationContext. И этот класс имеет весьма значительные отличия от базовой реализации SynchronizationContext. Версия UI переопределяет вызовы методов Send и Post, и реализует концепцию очереди сообщений (я пытался найти исходный код этого класса но не нашел). Что же делает базовая реализация SynchronizationContext?

/*

от переводчика:

Исходный код WindowsFormsSynchronizationContext
Исходный код SynchronizationContext

Реализация InvokeRequired в WindowsFormsSynchronizationContext

public bool InvokeRequired 
{
    get 
	{

        using (new MultithreadSafeCallScope())
        {
            HandleRef hwnd;
            if (IsHandleCreated) {
                hwnd = new HandleRef(this, Handle);
            }
            else {
                Control marshalingControl = FindMarshalingControl();

                if (!marshalingControl.IsHandleCreated) {
                    return false;
                }

                hwnd = new HandleRef(marshalingControl, marshalingControl.Handle);
            }

            int pid;
            int hwndThread = SafeNativeMethods.GetWindowThreadProcessId(hwnd, out pid);
            int currentThread = SafeNativeMethods.GetCurrentThreadId();
            return(hwndThread != currentThread);
        }
    }
}

Реализация Invoke в WindowsFormsSynchronizationContext

private Object MarshaledInvoke(Control caller, Delegate method, Object[] args, bool synchronous) 
{
  // Marshaling an invoke occurs in three steps:
  //
  // 1.  Create a ThreadMethodEntry that contains the packet of information
  //     about this invoke.  This TME is placed on a linked list of entries because
  //     we have a gap between the time we PostMessage and the time it actually
  //     gets processed, and this gap may allow other invokes to come in.  Access
  //     to this linked list is always synchronized.
  //
  // 2.  Post ourselves a message.  Our caller has already determined the
  //     best control to call us on, and we should almost always have a handle.
  //
  // 3.  If we're synchronous, wait for the message to get processed.  We don't do
  //     a SendMessage here so we're compatible with OLE, which will abort many
  //     types of calls if we're within a SendMessage.
  //

  if (!IsHandleCreated) {
      throw new InvalidOperationException(SR.GetString(SR.ErrorNoMarshalingThread));
  }

  // We have to demand unmanaged code permission here for the control hosted in
  // the browser case. Without this check, we will expose a security hole, because
  // ActiveXImpl.OnMessage() will assert unmanaged code for everyone as part of
  // its implementation.
  // The right fix is to remove the Assert() on top of the ActiveXImpl class, and
  // visit each method to see if it needs unmanaged code permission, and if so, add
  // the permission just to that method(s).
  //
  ActiveXImpl activeXImpl = (ActiveXImpl)Properties.GetObject(PropActiveXImpl);
  if (activeXImpl != null) {
      IntSecurity.UnmanagedCode.Demand();
  }

  // We don't want to wait if we're on the same thread, or else we'll deadlock.
  // It is important that syncSameThread always be false for asynchronous calls.
  //
  bool syncSameThread = false;
  int pid; // ignored
  if (SafeNativeMethods.GetWindowThreadProcessId(new HandleRef(this, Handle), out pid) == SafeNativeMethods.GetCurrentThreadId()) {
      if (synchronous)
          syncSameThread = true;
  }

  // Store the compressed stack information from the thread that is calling the Invoke()
  // so we can assign the same security context to the thread that will actually execute
  // the delegate being passed.
  //
  ExecutionContext executionContext = null;
  if (!syncSameThread) {
      executionContext = ExecutionContext.Capture();
  }
  ThreadMethodEntry tme = new ThreadMethodEntry(caller, this, method, args, synchronous, executionContext);

  lock (this) {
      if (threadCallbackList == null) {
          threadCallbackList = new Queue();
      }
  }

  lock (threadCallbackList) {
      if (threadCallbackMessage == 0) {
          threadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage");
      }
      threadCallbackList.Enqueue(tme);
  }

  if (syncSameThread) {
      InvokeMarshaledCallbacks();
  }  else {
      // 

      UnsafeNativeMethods.PostMessage(new HandleRef(this, Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);
  }

  if (synchronous) {
      if (!tme.IsCompleted) {
          WaitForWaitHandle(tme.AsyncWaitHandle);
      }
      if (tme.exception != null) {
          throw tme.exception;
      }
      return tme.retVal;
  }
  else {
      return(IAsyncResult)tme;
  }
}

*/

Так или иначе я нашел исходный код SynchronizationContext, вот он (я удалил атрибуты и сделал незначительное форматирование):

Базовая реализация 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 делает то же самое, просто используя пул потоков для асинхронности. На мой взгляд этот класс должен быть абстрактным. Такая реализация только запутывает и, кроме того, бесполезна. Это одна из двух причин способствовавших написанию этой статьи.

В заключение

Я надеюсь вы почерпнули что-то новое для себя, о контексте синхронизации и способах его использования. В .NET я нашел два класса реализующих контекст синхронизации для пользовательского интерфейса, один для WinForms и один для WPF. Я уверен что их больше, но пока нашел только их. Базовая реализация, как я показал, не делает ничего для переключения потоков. UI поток, в свою очередь, использует очередь сообщений и Windows API (SendMessage и PostMessage), так что я уверен что код будет выполнен в UI потоке.

Тем не менее это не предел использования данного класса. Вы можете сделать свою реализацию SynchronizationContext, это не так и сложно. На самом деле я должен был написать одну такую. В моей работе требовалось чтобы все вызовы COM, выполнялись в STA потоке. Тем не менее наше приложение использует пул потоков и WCF, и было не просто сделать проброс кода в STA поток. Поэтому я решил сделать свою версию SynchronizationContext, под названием StaSynchronizationContext, о которой и пойдет речь во второй части статьи.

От переводчика

Контекст синхронизации заинтересовал меня когда я пытался решить задачу по написанию мультипоточного обработчика однотипных задач, т.е. что-то вроде

using(var processor = new Processor<int>(handler, exceptionHandler, completedHandler))
{
    for(int i=0;i<1000000; i++)
        processor.Push(i);
}

, и первое впечатление от найденных статей по контексту синхронизации, это то что нужно. Но после более детального изучения стало понятно что SynchronizationContext появился и развивался в рамках задачи взаимодействия с UI, и этими задачи его использование и ограничивается. Собственно в самом FCL всего два класса от него наследуются, один для WPF и второй для WinForms.

По простому всю эту модель можно представить как синглтон, управление состоянием которого ведется набором последовательных команд из одного конкретного потока, но сами команды могут быть поданы из разных потоков. Сам контекст синхронизации в данном случае можно представить как потокобезопасную очередь этих команд, в которую все пишут, и из которой один читает.

Т.е. решение как бы хорошее, но как сказано в статье, базовая реализация вовсе не реализует переключение потоков. А имеющиеся расширенные реализации узконаправлены на UI. Написание же своей реализации будет перегружено базовыми методами, 80% которых скорее всего не пригодятся. По итогу получается что для своих задач будет проще использовать TPL или собственную реализацию контекста для потоков (что по итогу и было сделано). Ну или как у автора статьи в специфических задачах.

Тем не менее в ряде случаев понимание работы SynchronizationContext может быть весьма полезно, например как показано выше, при управлении UI'ем из бизнес-слоя, без загрязнения кода на форме вызовами через BeginInvoke.

Автор: Ogoun

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js