¿Qué hace SynchronizationContext?

En el libro Programming C #, tiene un código de ejemplo sobre SynchronizationContext :

 SynchronizationContext originalContext = SynchronizationContext.Current; ThreadPool.QueueUserWorkItem(delegate { string text = File.ReadAllText(@"c:\temp\log.txt"); originalContext.Post(delegate { myTextBox.Text = text; }, null); }); 

Soy un principiante en los hilos, así que por favor responda en detalle. Primero, no sé qué significa el contexto, ¿qué guarda el progtwig en el contexto originalContext ? Y cuando se Post método Post , ¿qué hará el subproceso de la interfaz de usuario?
Si le pregunto cosas tontas, por favor corrígeme, ¡gracias!

EDITAR: Por ejemplo, ¿qué myTextBox.Text = text; si solo escribo myTextBox.Text = text; en el método, ¿cuál es la diferencia?

¿Qué hace SynchronizationContext?

En pocas palabras, SynchronizationContext representa una ubicación donde se puede ejecutar el código. Los delegates que pasan a su método Send o Post se invocarán en esa ubicación. (La Post es la versión no bloqueante / asincrónica de Send ).

Cada hilo puede tener su propia instancia SynchronizationContext asociada. El hilo en ejecución se puede asociar con un contexto de sincronización llamando al método estático SynchronizationContext.SetSynchronizationContext , y el contexto actual del hilo en ejecución se puede consultar a través de la propiedad SynchronizationContext.Current .

A pesar de lo que acabo de escribir (cada hilo tiene un contexto de sincronización asociado), un SynchronizationContext no necesariamente representa un hilo específico ; también puede reenviar la invocación de los delegates pasados ​​a cualquiera de varios hilos (por ejemplo, a un hilo de trabajo ThreadPool ), o (al menos en teoría) a un núcleo de CPU específico, o incluso a otro host de red . Donde sus delegates terminan ejecutándose depende del tipo de SynchronizationContext utilizado.

Windows Forms instalará un WindowsFormsSynchronizationContext en el hilo en el que se crea el primer formulario. (Este subproceso se denomina comúnmente “subproceso de la interfaz de usuario”). Este tipo de contexto de sincronización invoca a los delegates que se le pasan exactamente en ese subproceso. Esto es muy útil ya que Windows Forms, al igual que muchas otras estructuras de interfaz de usuario, solo permite la manipulación de controles en el mismo hilo en el que se crearon.

¿Qué pasa si solo escribo myTextBox.Text = text; en el método, ¿cuál es la diferencia?

El código que ha pasado a ThreadPool.QueueUserWorkItem se ejecutará en un hilo de trabajo del grupo de subprocesos. Es decir, no se ejecutará en el hilo en el que se creó su myTextBox , por lo que Windows Forms tarde o temprano (especialmente en versiones de lanzamiento) lanzará una excepción, indicándole que no puede acceder a myTextBox desde otro hilo.

Esta es la razón por la cual tiene que “cambiar de nuevo” desde el hilo del trabajador al “hilo de la interfaz de usuario” (donde se creó myTextBox ) antes de esa asignación en particular. Esto se hace de la siguiente manera:

  1. Mientras todavía está en el hilo de la interfaz de usuario, capture el SynchronizationContext Windows Forms allí y almacene una referencia en una variable ( originalContext ) para usarlo más adelante. Debe consultar SynchronizationContext.Current en este punto; si lo consultó dentro del código pasado a ThreadPool.QueueUserWorkItem , puede obtener el contexto de sincronización asociado con el subproceso de trabajo del grupo de subprocesos. Una vez que haya almacenado una referencia al contexto de Windows Forms, puede usarlo en cualquier lugar y en cualquier momento para “enviar” código al hilo de la interfaz de usuario.

  2. Siempre que necesite manipular un elemento UI (pero ya no esté, o no esté, en el hilo de UI), acceda al contexto de sincronización de Windows Forms a través de originalContext y transfiera el código que manipulará la interfaz de usuario para Send o Post .


Comentarios finales y consejos:

  • Lo que los contextos de sincronización no harán por usted es decirle qué código debe ejecutarse en una ubicación / contexto específico, y qué código se puede ejecutar normalmente, sin pasarlo a un SynchronizationContext . Para decidirlo, debe conocer las reglas y los requisitos del marco contra el que está progtwigndo: Windows Forms en este caso.

    Así que recuerde esta regla simple para Windows Forms: NO acceda a controles o formularios desde un hilo que no sea el que los creó. Si debe hacer esto, use el mecanismo SynchronizationContext como se describe arriba, o Control.BeginInvoke (que es una forma específica de Windows Forms de hacer exactamente lo mismo).

  • Si está progtwigndo contra .NET 4.5 o posterior, puede hacer que su vida sea mucho más fácil al convertir su código que usa explícitamente SynchronizationContext , ThreadPool.QueueUserWorkItem , control.BeginInvoke , etc. en las nuevas palabras clave async / await y la Tarea paralela Biblioteca (TPL) , es decir, la API que rodea las clases Task y Task . En gran medida, estos se encargarán de capturar el contexto de sincronización del subproceso de interfaz de usuario, iniciar una operación asincrónica y luego volver al subproceso UI para que pueda procesar el resultado de la operación.

Me gustaría agregar a otras respuestas, SynchronizationContext.Post simplemente pone en cola una callback para su posterior ejecución en el hilo objective (normalmente durante el siguiente ciclo del ciclo de mensajes del hilo objective), y luego la ejecución continúa en el hilo llamante. Por otro lado, SynchronizationContext.Send intenta ejecutar la callback en el subproceso objective de inmediato, lo que bloquea el subproceso de llamada y puede provocar un interlocking. En ambos casos, existe la posibilidad de reentrada de código (ingresando un método de clase en el mismo hilo de ejecución antes de que la llamada anterior al mismo método haya retornado).

Si está familiarizado con el modelo de progtwigción Win32, una analogía muy cercana serían las API PostMessage y SendMessage , a las que puede llamar para enviar un mensaje desde un hilo diferente al de la ventana de destino.

Aquí hay una muy buena explicación de los contextos de sincronización: se trata del SynchronizationContext .

Almacena el proveedor de sincronización, una clase derivada de SynchronizationContext. En este caso, probablemente sea una instancia de WindowsFormsSynchronizationContext. Esa clase utiliza los métodos Control.Invoke () y Control.BeginInvoke () para implementar los métodos Send () y Post (). O puede ser DispatcherSynchronizationContext, usa Dispatcher.Invoke () y BeginInvoke (). En una aplicación Winforms o WPF, ese proveedor se instala automáticamente tan pronto como crea una ventana.

Cuando ejecuta código en otro subproceso, como el subproceso de grupo de subprocesos utilizado en el fragmento, debe tener cuidado de no utilizar directamente objetos que no son seguros para el subproceso. Al igual que cualquier objeto de interfaz de usuario, debe actualizar la propiedad TextBox.Text del subproceso que creó el TextBox. El método Post () asegura que el objective delegado se ejecuta en ese hilo.

Tenga en cuenta que este fragmento es un poco peligroso, solo funcionará correctamente cuando lo llame desde el hilo de la interfaz de usuario. SynchronizationContext.Current tiene diferentes valores en diferentes subprocesos. Solo el subproceso de interfaz de usuario tiene un valor utilizable. Y es la razón por la cual el código tuvo que copiarlo. Una forma más legible y segura de hacerlo, en una aplicación de Winforms:

  ThreadPool.QueueUserWorkItem(delegate { string text = File.ReadAllText(@"c:\temp\log.txt"); myTextBox.BeginInvoke(new Action(() => { myTextBox.Text = text; })); }); 

Que tiene la ventaja de que funciona cuando se llama desde cualquier hilo. La ventaja de usar SynchronizationContext.Current es que todavía funciona si el código se usa en Winforms o WPF, es importante en una biblioteca. Este ciertamente no es un buen ejemplo de dicho código, siempre sabes qué tipo de TextBox tienes aquí, así que siempre sabes si usar Control.BeginInvoke o Dispatcher.BeginInvoke. En realidad, usar SynchronizationContext.Current no es tan común.

El libro trata de enseñarte sobre enhebrar, así que usar este ejemplo defectuoso es bueno. En la vida real, en los pocos casos en los que podría considerar utilizar SynchronizationContext.Current, aún debería dejarlo en las palabras clave async / await de C # o TaskScheduler.FromCurrentSynchronizationContext () para hacerlo por usted. Pero tenga en cuenta que todavía se portan mal de la forma en que lo hace el fragmento cuando los usa en el hilo equivocado, por la misma razón. Una pregunta muy común por aquí, el nivel extra de abstracción es útil, pero hace que sea más difícil averiguar por qué no funcionan correctamente. Esperemos que el libro también te diga cuándo no usarlo 🙂

El propósito del contexto de sincronización aquí es asegurarse de que myTextbox.Text = text; recibe un llamado en el hilo principal de UI.

Windows requiere que solo se acceda a los controles de la GUI mediante el hilo con el que se crearon. Si intenta asignar el texto en una secuencia de fondo sin primera sincronización (a través de varios medios, como este o el patrón Invoke), se lanzará una excepción.

Lo que hace es guardar el contexto de sincronización antes de crear el hilo de fondo, luego el hilo de fondo usa el contexto. El método Post ejecuta el código GUI.

Sí, el código que has mostrado es básicamente inútil. ¿Por qué crear un hilo de fondo, solo para volver inmediatamente al hilo principal de la interfaz de usuario? Es solo un ejemplo.

Usted y su esposa están enviando regalos por separado para personas separadas. Estás enviando a buscar a tu padre y ella está enviando a buscar a su madre. Usted prepara sus paquetes y los pone en el porche. (Subproceso 0) Desea que se entregue a través de Fedex (Subproceso 1) y que desee a través de UPS (Subproceso 2). Ambos esperan avisos de entrega de la misma persona a su casa. (contexto de sincronización). Agarra paquetes y los envía a través de Fedex y UPS. Finalmente, esta persona no debe enviar avisos a la dirección de su empresa, porque no puede entrar en el edificio (violación de acceso, debe regresar a donde se llama). Una vez que se entregan sus paquetes, regresa a la dirección de su domicilio y deja una notificación donde se han entregado los paquetes.

El intercambio de recursos entre el Subproceso 1 y el Subproceso 2 es una analogía más compleja. El escenerio anterior es el uso más básico en llamadas de redes.

A la fuente

Cada hilo tiene un contexto asociado, también conocido como el contexto “actual”, y estos contextos se pueden compartir entre hilos. ExecutionContext contiene metadatos relevantes del entorno actual o contexto en el que el progtwig está en ejecución. SynchronizationContext representa una abstracción: denota la ubicación donde se ejecuta el código de la aplicación.

Un SynchronizationContext le permite poner en cola una tarea en otro contexto. Tenga en cuenta que cada hilo puede tener su propio SynchronizatonContext.

Por ejemplo: supongamos que tiene dos hilos, Thread1 y Thread2. Diga, Thread1 está haciendo algo de trabajo, y luego Thread1 desea ejecutar código en Thread2. Una forma posible de hacerlo es pedirle a Thread2 su objeto SynchronizationContext, dárselo a Thread1, y luego Thread1 puede llamar a SynchronizationContext.Send para ejecutar el código en Thread2.

Este ejemplo proviene de ejemplos de Linqpad de Joseph Albahari, pero realmente ayuda a comprender lo que hace el contexto de sincronización.

 void WaitForTwoSecondsAsync (Action continuation) { continuation.Dump(); var syncContext = AsyncOperationManager.SynchronizationContext; new Timer (_ => syncContext.Post (o => continuation(), _)).Change (2000, -1); } void Main() { Util.CreateSynchronizationContext(); ("Waiting on thread " + Thread.CurrentThread.ManagedThreadId).Dump(); for (int i = 0; i < 10; i++) WaitForTwoSecondsAsync (() => ("Done on thread " + Thread.CurrentThread.ManagedThreadId).Dump()); } 

SynchronizationContext nos proporciona una forma de actualizar una UI desde un hilo diferente (de forma síncrona a través del método Send o asíncronamente a través del método Post).

Eche un vistazo al siguiente ejemplo:

  private void SynchronizationContext SyncContext = SynchronizationContext.Current; private void Button_Click(object sender, RoutedEventArgs e) { Thread thread = new Thread(Work1); thread.Start(SyncContext); } private void Work1(object state) { SynchronizationContext syncContext = state as SynchronizationContext; syncContext.Post(UpdateTextBox, syncContext); } private void UpdateTextBox(object state) { Thread.Sleep(1000); string text = File.ReadAllText(@"c:\temp\log.txt"); myTextBox.Text = text; } 

SynchronizationContext.Current devolverá el contexto de sincronización del subproceso de la interfaz de usuario. ¿Cómo sé esto? Al inicio de cada formulario o aplicación WPF, el contexto se establecerá en el hilo de la interfaz de usuario. Si crea una aplicación WPF y ejecuta mi ejemplo, verá que cuando hace clic en el botón, duerme durante aproximadamente 1 segundo y luego muestra el contenido del archivo. Es de esperar que no lo sea porque quien llama al método UpdateTextBox (que es Work1) es un método que se pasa a un Thread, por lo tanto, debe suspender ese hilo, no el hilo principal de UI, ¡NOPE! Aunque el método Work1 se pasa a un hilo, observe que también acepta un objeto que es el SyncContext. Si lo miras, verás que el método UpdateTextBox se ejecuta mediante el método syncContext.Post y no el método Work1. Eche un vistazo a lo siguiente:

 private void Button_Click(object sender, RoutedEventArgs e) { Thread.Sleep(1000); string text = File.ReadAllText(@"c:\temp\log.txt"); myTextBox.Text = text; } 

El último ejemplo y este ejecuta el mismo. Ambos no bloquean la IU mientras lo hace.

En conclusión, piense en SynchronizationContext como un hilo. No es un hilo, define un hilo (Tenga en cuenta que no todos los hilos tienen un SyncContext). Cada vez que llamamos al método de envío o envío para actualizar una interfaz de usuario, es como actualizar la interfaz de usuario normalmente desde el hilo de la interfaz de usuario principal. Si, por alguna razón, necesita actualizar la UI desde un hilo diferente, asegúrese de que el hilo tenga el SyncContext de la hebra de la interfaz de usuario principal y simplemente llame al método Enviar o Enviar con el método que desea ejecutar y usted es todo conjunto.

Espero que esto te ayude, amigo!