¿Qué hace SynchronizationContext?


135

En el libro Programming C #, tiene un código de muestra 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 hilos, así que por favor conteste en detalle. Primero, no sé qué significa el contexto, ¿qué guarda el programa en el originalContext? Y cuando Postse activa el método, ¿qué hará el hilo de la interfaz de usuario?
Si pregunto algunas tonterías, corrígeme, ¡gracias!

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


1
El excelente manual tiene esto que decir El propósito del modelo de sincronización implementado por esta clase es permitir que las operaciones internas asíncronas / sincronización del Common Language Runtime se comporten correctamente con diferentes modelos de sincronización. Este modelo también simplifica algunos de los requisitos que las aplicaciones administradas han tenido que seguir para funcionar correctamente en diferentes entornos de sincronización.
ta.speot.is

IMHO async aguarda ya hace esto
Royi Namir

77
@RoyiNamir: Sí, pero adivina qué: async/ awaitdepende de SynchronizationContextdebajo.
stakx - ya no contribuye el

Respuestas:


170

¿Qué hace SynchronizationContext?

En pocas palabras, SynchronizationContextrepresenta una ubicación "donde" podría ejecutarse el código. Los delegados que se pasan a su Sendo Postmétodo serán invocados en esa ubicación. ( Postes la versión no bloqueante / asíncrona de Send).

Cada hilo puede tener una SynchronizationContextinstancia asociada a él. El subproceso en ejecución se puede asociar con un contexto de sincronización llamando al método estáticoSynchronizationContext.SetSynchronizationContext , y el contexto actual del subproceso en ejecución se puede consultar a través de la SynchronizationContext.Currentpropiedad .

A pesar de lo que acabo de escribir (cada hilo tiene un contexto de sincronización asociado), a SynchronizationContextno representa necesariamente un hilo específico ; también puede reenviar la invocación de los delegados que se le pasaron a cualquiera de varios subprocesos (por ejemplo, a un ThreadPoolsubproceso de trabajo), o (al menos en teoría) a un núcleo de CPU específico , o incluso a otro host de red . El lugar donde sus delegados terminan funcionando depende del tipo de SynchronizationContextutilizado.

Windows Forms instalará un WindowsFormsSynchronizationContexten el hilo en el que se crea el primer formulario. (Este hilo se denomina comúnmente "el hilo de la interfaz de usuario"). Este tipo de contexto de sincronización invoca a los delegados que se le pasan exactamente en ese hilo. Esto es muy útil ya que Windows Forms, como muchos otros marcos de interfaz de usuario, solo permite la manipulación de controles en el mismo subproceso 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 al que ha pasado ThreadPool.QueueUserWorkItemse ejecutará en un subproceso de trabajo de grupo de subprocesos. Es decir, no se ejecutará en el subproceso en el que myTextBoxse creó, por lo que Windows Forms tarde o temprano (especialmente en las versiones de lanzamiento) arrojará una excepción, diciéndole que no puede acceder myTextBoxdesde otro subproceso.

Esta es la razón por la que tiene que "cambiar" de alguna manera el subproceso de trabajo al "subproceso de interfaz de usuario" (donde myTextBoxse creó) 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 los formularios de Windows SynchronizationContextallí y almacene una referencia a él en una variable ( originalContext) para su uso posterior. Debe consultar SynchronizationContext.Currenten este punto; si lo consulta dentro del código que se le pasa 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 los formularios de Windows, 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 de la interfaz de usuario (pero ya no esté, o podría no estar, en el hilo de la interfaz de usuario), acceda al contexto de sincronización de Windows Forms a través de originalContexty entregue el código que manipulará la interfaz de usuario a cualquiera Sendo Post.


Observaciones finales y sugerencias:

  • 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 puede ejecutarse normalmente, sin pasarlo a a SynchronizationContext. Para decidir eso, debe conocer las reglas y requisitos del marco contra el que está programando: Windows Forms en este caso.

    Por lo tanto, 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 SynchronizationContextmecanismo como se describe anteriormente, o Control.BeginInvoke(que es una forma específica de Windows Forms de hacer exactamente lo mismo).

  • Si está programando contra .NET 4.5 o posterior, puede hacer su vida mucho más fácil mediante la conversión de su código que explícitamente usos SynchronizationContext, ThreadPool.QueueUserWorkItem, control.BeginInvoke, etc a los nuevos async/ awaitpalabras clave y la tarea paralela Biblioteca (TPL) , es decir, que rodea la API el Tasky las Task<TResult>clases. Estos, en un grado muy alto, se encargarán de capturar el contexto de sincronización del subproceso de la interfaz de usuario, comenzar una operación asincrónica y luego volver al subproceso de la interfaz de usuario para que pueda procesar el resultado de la operación.


Usted dice que Windows Forms, como muchos otros marcos de IU, solo permite la manipulación de controles en el mismo hilo, pero todas las ventanas en Windows deben ser accedidas por el mismo hilo que lo creó.
user34660

44
@ user34660: No, eso no es correcto. Usted puede tener varios hilos que crean controles de Windows Forms. Pero cada control está asociado con el único hilo que lo creó, y solo ese hilo debe acceder a él. Los controles de diferentes hilos de IU también son muy limitados en la forma en que interactúan entre sí: uno no puede ser el padre / hijo del otro, el enlace de datos entre ellos no es posible, etc. Finalmente, cada hilo que crea controles necesita su propio mensaje loop (que comienza por Application.RunIIRC). Este es un tema bastante avanzado y no algo hecho casualmente.
stakx - ya no contribuye el

Mi primer comentario se debe a que usted dice "como muchos otros marcos de IU", lo que implica que algunas ventanas permiten la "manipulación de controles" desde un hilo diferente, pero ninguna ventana de Windows lo hace. No puede "tener varios subprocesos que crean controles de formularios Windows Forms" para la misma ventana y "debe tener acceso el mismo subproceso" y "solo debe tener acceso un subproceso" que dicen lo mismo. Dudo que sea posible crear "Controles desde diferentes hilos de IU" para la misma ventana. Todo esto no está avanzado para aquellos de nosotros que tenemos experiencia con la programación de Windows antes de .Net.
user34660

3
Toda esta charla sobre "Windows" y "Windows Windows" me está mareando bastante. ¿Mencioné alguna de estas "ventanas"? No lo creo ...
stakx - ya no contribuye el

1
@ibubi: No estoy seguro de entender tu pregunta. El contexto de sincronización de cualquier subproceso no es set ( null) o una instancia de SynchronizationContext(o una subclase de él). El punto de esa cita no era lo que obtienes, sino lo que no obtendrás: el contexto de sincronización del hilo de la interfaz de usuario.
stakx - ya no contribuye el

24

Me gustaría agregar a otras respuestas, SynchronizationContext.Postsolo pone en cola una devolución de llamada para su posterior ejecución en el hilo objetivo (normalmente durante el siguiente ciclo del bucle de mensajes del hilo objetivo), y luego la ejecución continúa en el hilo de llamada. Por otro lado, SynchronizationContext.Sendintenta ejecutar la devolución de llamada en el subproceso de destino inmediatamente, lo que bloquea el subproceso de llamada y puede provocar un punto muerto. En ambos casos, existe la posibilidad de reentrada de código (ingresar un método de clase en el mismo hilo de ejecución antes de que la llamada anterior al mismo método haya regresado).

Si está familiarizado con el modelo de programación Win32, una muy estrecha analogía sería PostMessagey SendMessageAPI, que se puede llamar para enviar un mensaje de un hilo diferente de uno de la ventana de destino.

Aquí hay una muy buena explicación de lo que son los contextos de sincronización: se trata del contexto de sincronización .


16

Almacena el proveedor de sincronización, una clase derivada de SynchronizationContext. En este caso, probablemente será una instancia de WindowsFormsSynchronizationContext. Esa clase usa 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 sean seguros para subprocesos. Al igual que cualquier objeto de interfaz de usuario, debe actualizar la propiedad TextBox.Text del hilo que creó el TextBox. El método Post () asegura que el objetivo delegado se ejecute 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 hilos. Solo el hilo de la interfaz de usuario tiene un valor utilizable. Y es la razón por la que el código tuvo que copiarlo. Una forma más legible y segura de hacerlo, en una aplicación Winforms:

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

Lo cual tiene la ventaja de que funciona cuando se llama desde cualquier hilo. La ventaja de usar SynchronizationContext.Current es que aún funciona si el código se usa en Winforms o WPF, es importante en una biblioteca. Ciertamente, este 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 está tratando de enseñarte sobre el enhebrado, por lo que está bien usar este ejemplo defectuoso. En la vida real, en los pocos casos en los que podría considerar usar SynchronizationContext.Current, aún lo dejaría a las palabras clave asíncronas / en espera de C # o TaskScheduler.FromCurrentSynchronizationContext () para que lo haga por usted. Pero tenga en cuenta que todavía se comportan mal como lo hace el fragmento cuando los usa en el hilo incorrecto, exactamente por la misma razón. Una pregunta muy común por aquí, el nivel adicional de abstracción es útil pero hace que sea más difícil descubrir por qué no funcionan correctamente. Esperemos que el libro también te diga cuándo no usarlo :)


Lo siento, ¿por qué dejar que el identificador de subproceso de interfaz de usuario sea seguro para subprocesos? es decir, creo que el hilo de la interfaz de usuario podría estar usando myTextBox cuando se activó Post (), ¿es seguro?
cloudyFan

44
Tu inglés es difícil de descifrar. Su fragmento original solo funciona correctamente cuando se llama desde el hilo de la interfaz de usuario. Que es un caso muy común. Solo entonces se volverá a publicar en el hilo de la interfaz de usuario. Si se llama desde un subproceso de trabajo, el destino delegado Post () se ejecutará en un subproceso de grupo de subprocesos. Kaboom Esto es algo que quieres probar por ti mismo. Inicie un hilo y deje que el hilo llame a este código. Lo hizo bien si el código se bloquea con una NullReferenceException.
Hans Passant

5

El propósito del contexto de sincronización aquí es asegurarse de que myTextbox.Text = text;se llame al hilo principal de la interfaz de usuario.

Windows requiere que los controles de la GUI sean accedidos solo por el hilo con el que fueron creados. Si intenta asignar el texto en un hilo de fondo sin sincronizar primero (por cualquiera de varios medios, como este o el patrón Invocar), se generará una excepción.

Lo que esto hace es guardar el contexto de sincronización antes de crear el subproceso de fondo, luego el subproceso 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 subproceso de fondo, solo para volver inmediatamente al subproceso principal de la interfaz de usuario? Es solo un ejemplo.


44
"Sí, el código que ha mostrado es básicamente inútil. ¿Por qué crear un subproceso en segundo plano, solo para volver inmediatamente al subproceso principal de la interfaz de usuario? Es solo un ejemplo". - Leer un archivo puede ser una tarea larga si el archivo es grande, algo que puede bloquear el hilo de la interfaz de usuario y dejarlo sin respuesta
Yair Nevet

Tengo una pregunta estúpida. Cada subproceso tiene un Id, y supongo que el subproceso de la interfaz de usuario también tiene un ID = 2, por ejemplo. Entonces, cuando estoy en el grupo de subprocesos, ¿puedo hacer algo así: var thread = GetThread (2); thread.Execute (() => textbox1.Text = "foo")?
John

@ John - No, no creo que funcione porque el hilo ya se está ejecutando. No puede ejecutar un hilo que ya se está ejecutando. Ejecutar solo funciona cuando un hilo no se está ejecutando (IIRC)
Erik Funkenbusch

3

A la fuente

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

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

Por ejemplo: supongamos que tiene dos hilos, Thread1 y Thread2. Digamos, Thread1 está haciendo algo de trabajo, y luego Thread1 desea ejecutar el 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.


2
Un contexto de sincronización no está necesariamente vinculado a un hilo particular. Es posible que varios subprocesos manejen solicitudes a un solo contexto de sincronización, y que un solo subproceso maneje solicitudes para múltiples contextos de sincronización.
Servicio

3

SynchronizationContext nos proporciona una forma de actualizar una IU desde un hilo diferente (sincrónicamente a través del método Send o asincrónicamente 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 hilo de la interfaz de usuario. ¿Cómo se esto? Al comienzo 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 aproximadamente 1 segundo, luego muestra el contenido del archivo. Es posible que espere que no lo haga porque la persona que llama del método UpdateTextBox (que es Work1) es un método pasado a un subproceso, por lo tanto, debe suspender ese subproceso, no el subproceso principal de la interfaz de usuario, ¡NO! Aunque el método Work1 se pasa a un subproceso, tenga en cuenta que también acepta un objeto que es el SyncContext. Si lo mira, verá que el método UpdateTextBox se ejecuta a través del método syncContext.Post y no el método Work1. Echa 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 se ejecuta igual. Ambos no bloquean la interfaz de usuario mientras hace trabajos.

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 Post o Send 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 interfaz de usuario desde un subproceso diferente, asegúrese de que ese subproceso tenga el SyncContext del subproceso de la interfaz de usuario principal y simplemente llame al método Enviar o Publicar con el método que desea ejecutar y ya está conjunto.

Espero que esto te ayude, amigo!


2

SynchronizationContext básicamente es un proveedor de ejecución de delegados de devolución de llamada, principalmente responsable de garantizar que los delegados se ejecuten en un contexto de ejecución dado después de que una parte particular del código (encapsulado en un obj de tarea de .Net TPL) de un programa haya completado su ejecución.

Desde el punto de vista técnico, SC es una clase simple de C # que está orientada a admitir y proporcionar su función específicamente para los objetos de la Biblioteca Paralela de Tareas.

Cada aplicación .Net, excepto las aplicaciones de consola, tiene una implementación particular de esta clase basada en el marco subyacente específico, es decir: WPF, WindowsForm, Asp Net, Silverlight, etc.

La importancia de este objeto está ligada a la sincronización entre los resultados que regresan de la ejecución asíncrona de código y la ejecución de código dependiente que espera los resultados de ese trabajo asincrónico.

Y la palabra "contexto" significa contexto de ejecución, que es el contexto de ejecución actual donde se ejecutará ese código de espera, es decir, la sincronización entre el código asíncrono y su código de espera ocurre en un contexto de ejecución específico, por lo que este objeto se llama SynchronizationContext: representa el contexto de ejecución que se ocupará de la sincronización del código asíncrono y la ejecución del código de espera .


1

Este ejemplo es 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());
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.