Программист прикладного уровня
4,0
рейтинг
15 августа 2014 в 13:33

Разработка → SynchronizationContext — когда MSDN подводит перевод

Не знаю почему, но информации об этом новом класса в .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.
Перевод: mikeperetz
Ogoun @Ogoun
карма
59,0
рейтинг 4,0
Программист прикладного уровня
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (41)

  • –4
    Предположим у вас есть два потока, t1 и t2. И t1 исполняет некоторую работу, и в какой-то момент желает передать выполнение кода потоку t2.

    Зачем это делать кроме UI?
    • 0
      Ну если отойти от темы статьи, то вот пример, один поток выполняет прослушку сетевого протокола, и в момент подключения передает обработку другому потоку. Т.е. многопоточный клиент-сервер описывается данным предложением.
      Так же можно подвести очереди сигналов и сообщений, ну и вообще большое количество задач, где множество потоков могут писать, а один разгребает очередь.
      • +1
        Это прекрасно описывается формулировкой «ставит задачу», «передает сообщение». Прямой передачи выполнения кода лучше избегать. И особенно полезно избегать ситуации, когда во втором потоке зачем-то используется именно синхронизированная обработка.
  • –2
    | информации об этом новом класса в .NET Framework, действительно немного

    Новом? Ему сто лет в обед.

    Мне пока не попадалось задач, где нельзя было бы обойтись без него. На мой взгляд, если вы считаете, что вам необходимо воспользоваться его функциональностью, то это сигнал о том, что у вас что-то не в порядке с архитектурой.
    • 0
      >>если вы считаете, что вам необходимо воспользоваться его функциональностью, то это сигнал о том, что у вас что-то не в порядке с архитектурой

      абсолютно не согласен! SynchronizationContext — очень важная вещь в инфраструктуре .net, да и вообще если вы делаете что-то сложнее чем «hello, world!» для многопоточных приложений.

      WCF, WF и т.д. — лишь примеры использования. Если есть message loop — то SynchronizationContext подходит как ни что другое.

      очень хороший пост на эту тему — msdn magazine.
      • 0
        Есть подозрение, что товарищ aush не пользуется контекстом синхронизации, пользуясь асинхронным программированием и не зная, что он там есть (в чём большого ужаса я не вижу).

        В статье, что Вы привели (она от 2011 года, тут прямо не статья, а машина времени) внизу всё расписано о «ожидающихся» async/await и как они связаны с контекстом.

        На мой взгляд, вся польза от знаний о SynchronizationContext свелась к пониманию того, что не надо больше писать, используя await.
        • 0
          Не обратил внимания, что это перевод старой статьи. Конечно, я говорил именно о том, что сейчас нет причин лезть на этот уровень, т.к. имеющиеся высокоуровневые абстракции обеспечивают весь необходимый функционал.
          • 0
            Что за бред. Вы async/await в консольном приложении не сможете пользоваться не создав msg loop и syncContext.
            SynchronizationContext это элементарная и совершенно базовая штука. Для реализации того же ActiveObject очень удобно
            • 0
              Вы async/await в консольном приложении не сможете пользоваться не создав msg loop и syncContext.

              aush, как мне кажется, пишет о том, что в большей части случаев достаточно просто воспользоваться async/await, и предоставить компилятору и BCL думать о синхронизации и циклах. Повышение уровня абстракции.
              • 0
                Так await не будет работать в консольном приложении если не подумать о SynchronizationContext и не создать loop
                • 0
                  Что значит «не будет работать»? Просто каждый следующий кусок кода будет в произвольном потоке из тредпула.

                  Is it a problem then that using async like this in a console app might end up running continuations on ThreadPool threads? I can’t answer that, because the answer is entirely up to what kind of semantics you need in your application. For many applications, this will be perfectly reasonable behavior.

                  blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx
                  • 0
                    Для того поведения что вы описали есть .ConfigureAwait(false)
                    • 0
                      Спасибо, я в курсе. Более того, я в курсе, что это рекомендуемое поведение для библиотечного кода. Как это влияет на тот факт, что можно написать консольное приложение с async/await, не используя никакой контекст синхронизации?

                      (если честно, я вообще не уверен, что захват контекста синхронизации по умолчанию — это благо, особенно с тех пор, как я малость задолбался, отслеживая, что везде, где у меня используется await, проставлено «работать без синхронизации»)
                      • 0
                        Получить race condition становится элементарно в коде который на глаз (при отсутствии упоминания что выполнение идет на потоке без контекста) выглядит как абсолютно корретный и несодержащий проблем мультипоточной мутации.

                        Если вас смутила моя фраза «не будет работать», я перефразирую. Скорее всего будет работать неочевидно некорректно.
                        • 0
                          Получить race condition становится элементарно

                          Race condition между чем и чем, простите?

                          Скорее всего будет работать неочевидно некорректно.

                          Скажите мне, как именно «неочевидно некорректно» может работать код вида

                          async Do()
                          {
                              await Part1();
                              await Part2();
                              await Part3();
                              await Part4();
                              await Part5();
                          }
                          
                          • –1
                            Вы реально не понимаете или занимаетесь жирным троллингом?

                            Если троллите — переметнитесь на других пожалуйста.
                            Если не понимаете — вот код который падает без контекста.

                             class Program
                                {
                                    private static List<object> list = new List<object>();
                            
                                    static void Main(string[] args)
                                    {
                                        Run1();
                                        Run2();
                            
                                        Console.ReadLine();
                                    }
                            
                                    private static async void Run2()
                                    {
                                        while (true)
                                        {
                                            await Task.Yield();
                            
                                            list.Add(new object());
                                        }
                                    }
                            
                                    private static async void Run1()
                                    {
                                        while (true)
                                        {
                                            await Task.Yield();
                                            foreach (var l in list)
                                            {
                                            }
                                        }
                                    }
                                }
                            


                            Очевидно что код не из проекта, но у меня были семантически довольно близкие к этому примеры которые падали бы тоже.
                            В местах где нет явного маршалинга в другой поток, я не буду синхронизировать доступ к полям просто из за того что await гипотетически может работать иначе. Это разумное предположение которое нарушается в консольных приложениях.

                            В общем я хотел просто сказать что понимать что такое синк контекст необходимо. И никакие async/await не скроют то что под ними, потому как нижележащий слой очень сильно на них влияет.
                            • +1
                              Это какой-то очень сильно вымышленный пример. Я не очень понимаю, что и зачем он должен делать. Вы хотите, чтобы методы выполнялись последовательно? Сделайте их async Task и .Wait() на каждом вызове. Вы хотите, чтобы они выполнялись параллельно? Все равно нужно синхронизировать доступ к list.

                              async void — это вещь, которой надо очень сильно избегать.
                              • 0
                                Избегайте. Я же не испытываю никаких проблем с async void.

                                Пример такой чтобы вы могли скопировать, запустить и убедиться.
                                Если хотите более реального — представьте себе game loop без vsync который постоянно делает update/render. Вы находясь на потоке лупа запустили Task с созданием тяжелого объекта и сделали на нем await, в надежде потом воткнуть этот объект в иерархию сцены.

                                Если бы контекст был — вернулись бы в поток лупа, и как следствие втыкали бы между апдейтами.
                                Когда контекста нет — втыкаете на BG потоке, и вполне вероятно что сейчас на потоке лупа идет обход сцены и вы воткнете в коллекцию по которой топает итератор.

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

                                • 0
                                  Вот мы и вернулись к тому, что нам нужно синхронизироваться с каким-то конкретным объектом ради получения фиксированного поведения. Как мы оба прекрасно понимаем, есть больше одного способа это делать, главное — понимать, что именно и зачем делается.

                                  Ну и да, рекомендация делать ConfigureAwait(false) в максимально возможном числе мест — она не просто так появилась, контекст влечет за собой накладные расходы (а иногда — и дедлоки).
            • 0
              Вы async/await в консольном приложении не сможете пользоваться не создав msg loop и syncContext.
              Это с какой стати? Если отсутствует текущий контекст синхронизации, continuation будет вызвано в потоке из пула. Другое дело, что к тому моменту может завершиться главный поток вместе со всем приложением, но к работоспособности async/await это не имеет ни малейшего отношения.
              • 0
                Вы бы прочитали мой тред с liar…
    • 0
      А какие у него, собственно, альтернативы, позволяющие запускать фоновые задачи?

      BackgroundWorker — хороший инструмент, но только до тех пор, пока вы не запутаетесь в огромном количестве этих воркеров… Кроме того, он зачастую вынуждает писать логику в классе формы, чего в крупных проектах лучше не делать.

      Задачи (Task)? Да, задачи — это круто, а асинхронные методы — еще круче. До тех пор, пока в требованиях к приложению не значится совместимость со вторым фреймворком.

      Control.Invoke? Этот метод вылетает при закрытии формы. Видели приложения, которые не могут закрыться без ошибки? В половине случаев тут виноват Control.Invoke.
      • 0
        А какие у него, собственно, альтернативы, позволяющие запускать фоновые задачи?

        Тредпул? Автономный тред с очередью задач?

        До тех пор, пока в требованиях к приложению не значится совместимость со вторым фреймворком.

        К счастью, сейчас такие требования все-таки редкость.
        • 0
          Тредпул? Автономный тред с очередью задач?
          И как же тредпул решает проблему возвращения в поток UI после выполнения фоновой задачи? Уйти в фоновой поток действительно просто, я же перечислял способы отобразить результат вычислений на форме.
          • 0
            И как же тредпул решает проблему возвращения в поток UI после выполнения фоновой задачи?

            Никак не решает.

            Уйти в фоновой поток действительно просто, я же перечислял способы отобразить результат вычислений на форме.

            Вы спрашивали альтернативы, позволяющие запустить фоновые задачи, а не «запустить фоновую задачу и отобразить результат на форме». Для взаимодействия с UI разумных альтернатив контексту синхронизации нет — хотя бы потому, что большая часть из перечисленного вами внутри себя работает через него же.
            • 0
              Ну, я как-то неявно предполагал, что результат выполнения любой задачи должен быть как-то использован, а не позабыт :)
              • 0
                Во-первых, бывают не-UI-ные приложения. Во-вторых, бывают задачи, результат которых не потребляется напрямую UI. И, наконец, в-третьих, есть больше одного способа передать результат в UI, не обязательно напрямую в поток ходить.
              • 0
                Отображение на UI не подразумевает маршалинг в UI поток. Можно без проблем написать UI фреймворк в котором все взаимодействие с контролами будет thread-safe. И не только можно, а и пишут, и используют. Или же просто помнят какие вещи надо маршалить, а какие нет.

                На моем текущем проекте заменить текстовому полю текст можно безопасно из BG потока, причем даже синхронизаций внутри UI либы не написано никаких. Все из за того что присвоение ссылки в дотнете атомарно, а логика не обращается внутри логической еденицы к полю 2 раза.
      • +2
        До тех пор, пока в требованиях к приложению не значится совместимость со вторым фреймворком.
        У меня всё работает, что я делаю не так?
        • 0
          Этот способ не будет работать в 2012й студии, потому что компилятору не хватит еще десятка методов. А если взять их реализацию из BCL, то компилятор из 2010й студии начнет выдавать некорректный код. Но если решить эту проблему, то асинхронные методы действительно заработают.
          • 0
            Прекрасно работает в 2012 и 2013 студиях.
            • 0
              Ну, значит вы пропустили в своей статье кучу интересного. Потому что в вашей MonoLib я ясно вижу следы внутренних интерфейсов из 4.5 фреймворка, которые совершенно не нужны компилятору из 2010й студии, и в AsyncCTP их нет.

              Я не верю, что вы не проверяли этот код в 2012й студии :)
              • 0
                У меня этот код уже полтора года в продакшне и пережил переезд проекта сначала на 2012, затем на 2013. И я всё ещё не могу понять логики, по которой он не должен работать.
                • 0
                  В AsyncCTP не было, к примеру, интерфейса IAsyncStateMachine, и никаких намеков, что он будет нужен. Если бы библиотека делалась изначально под 2010ю студию (как аналогичная моя) — то в 2012й студии код упал бы с ошибкой компиляции из-за ненайденного интерфейса и нескольких методов.

                  С другой стороны, если взять библиотеку BCL (Backward Compatibility Library) — то в ней задача (Task, точнее TaskCompletionSource) создается по первому требованию, а не при создании AsyncTaskMethodBuilder. Смысл этого я не понимаю, если честно. Но при попытке использовать старый компилятор все асинхронные методы сразу же зависают, потому что задача создается уже после упаковки структуры (как результат — создаются две задачи, одна рабочая, а вторая возвращается).

                  PS более всего меня удивило, что в ваша библиотека работает, несмотря на отсутствие интерфейса IAsyncMethodBuilder и его метода PreBoxInitialization в билдерах. Видимо, компилятор несколько умнее, чем мне думалось…
                  • 0
                    UPD сам удивился — сам разобрался. В коде от Microsoft интерфейс IAsyncMethodBuilder — полностью внутренний, компилятор про него не знает. Он нужен в случае, если компилятор забудет вызвать метод SetStateMachine. Однако, компилятор почему-то этого никогда не забывает :)
  • –2
    --deleted--
  • 0
    Простите-извините, комментировать статью 2008 года в 2014 — это интересно.

    В .NET я нашел два класса реализующих контекст синхронизации для пользовательского интерфейса, один для WinForms и один для WPF.


    Сейчас контекст синхронизации неявно используется в асинхронном программировании (Task + async/await) и не зависит от WinForms / WPF / etc.

    Но если хочется явного SynchronizationContext можно сделать, например, так:

    // Suppose we are on a UI thread in a Windows Forms / WPF application:
    _uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
    Task.Run (() => Foo())
         .ContinueWith (ant => lblResult.Content = ant.Result, _uiScheduler);

  • 0
    Немного веселой магии:

    await Task.Delay(TimeSpan.FromTicks(9999));
    await Task.Delay(TimeSpan.FromTicks(10000));

    При вызове с UI потока какого-нибудь WPF или WinForms SynchronizationContext::Post будет вызван только для второго случая (с 10000) :-)
    • 0
      А что тогда будет вызвано для первого случая?.. Когда я декомпилировал реализацию await, логика выглядела довольно простой, и без всякой магии.
      • 0
        Ничего, выполнится синхронно. Всё из-за того, что после деления на TicksPerMiliseconds и прямого каста к инту из 9999 тиков получается 0мс. А 0мс — это вернуть управление синхронно (return CompletedTask) — просто особенности реализации Task.Delay.
  • 0
    И первое впечатление от найденных статей по контексту синхронизации, это то что нужно. Но после более детального изучения стало понятно что SynchronizationContext появился и развивался в рамках задачи взаимодействия с UI, и этими задачи его использование и ограничивается. Собственно в самом FCL всего два класса от него наследуются, один для WPF и второй для WinForms.

    Ну я не согласен: AspNetSynchronizationContextBase, WorkflowSynchronizationContext, ComPlusSynchronizationContext… Их довольно много только в стандартной поставке, не считая кастомных.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.