2009-11-15 11 views
17

Estoy familiarizado con el multi-threading porque lo leí pero nunca lo he usado en la práctica.C# Multithreading - Invoque sin control

Tengo un proyecto que utiliza una biblioteca de terceros que comparte el estado de un dispositivo de entrada mediante la generación de eventos. El problema es que, por la forma en que se escribe la biblioteca, estos eventos surgen de un hilo diferente.

Mi aplicación no necesita ser de subprocesos múltiples y he tenido muchos problemas clásicos de subprocesamiento (los controles de la UI se quejan de que se interactúa con un hilo diferente, las colecciones que se modifican cuando una parte del código se itera sobre ella, etc.).

Solo quiero que el evento de la biblioteca de terceros se devuelva a mi hilo de interfaz de usuario. Específicamente, lo que creo que debería suceder es:

Mi clase recibe el evento y el controlador se está ejecutando en un hilo diferente al de la IU. Quiero detectar esta condición (como con InvokeRequired), y luego realizar el equivalente de BeginInvoke para devolver el control al subproceso de UI. A continuación, las notificaciones adecuadas se pueden enviar en la jerarquía de clases y todos mis datos solo se ven afectados por el único hilo.

El problema es que la clase que recibe estos eventos de entrada no se deriva de Control y, por lo tanto, no tiene InvokeRequired o BeginInvoke. La razón de esto es que traté de separar limpiamente la IU y la lógica subyacente. La clase todavía se está ejecutando en el hilo de la interfaz de usuario, simplemente no tiene ninguna interfaz de usuario dentro de la clase.

Ahora mismo solucioné el problema arruinando esa separación. Paso en una referencia al control que mostrará datos de mi clase y usando es Invocar métodos. Parece que eso frustra el propósito de separarlos porque ahora la clase subyacente depende directamente de mi clase específica de UI.

Quizás haya una manera de guardar una referencia al hilo que ejecutó el constructor y luego hay algo en el espacio de nombres de Threading que ejecutará los comandos Invoke?

¿Hay alguna forma de evitar esto? ¿Mi enfoque es completamente incorrecto?

Respuesta

18

Mire en la clase AsyncOperation. Crea una instancia de AsyncOperation en el subproceso que desea llamar al controlador utilizando el método AsyncOperationManager.CreateOperation. El argumento que uso para Create es generalmente nulo, pero puede establecerlo en cualquier cosa. Para llamar a un método en ese hilo, use el método AsyncOperation.Post.

+0

Me llevó un tiempo entender cómo se usan los delegados, pero funciona. – colithium

1

No necesita el control específico, cualquier control (incluido el Formulario) lo hará. Entonces podrías abstraerlo un poco de la IU.

2

El método de manejo podría simplemente almacenar los datos en una variable miembro de la clase. El único problema con el cross-threading ocurre cuando desea actualizar los hilos a controles no creados en ese contexto de hilos. Entonces, su clase genérica podría escuchar el evento y luego invocar el control real que desea actualizar con una función de delegado.

Nuevamente, solo se deben invocar los controles de la interfaz de usuario que desea actualizar para que sean segmentados. Hace un tiempo escribí una entrada de blog en un "Simple Solution to Illegal Cross-thread Calls in C#"

La publicación entra en más detalles, pero el quid de un enfoque muy simple (pero limitado) es mediante el uso de una función de delegado anónimo en el control de la interfaz de usuario que desea actualización:

if (label1.InvokeRequired) { 
    label1.Invoke(
    new ThreadStart(delegate { 
     label1.Text = "some text changed from some thread"; 
    })); 
} else { 
    label1.Text = "some text changed from the form's thread"; 
} 

Espero que esto ayude. InvokeRequired es técnicamente opcional, pero la invocación de controles es bastante costosa, por lo que asegúrese de que no actualice label1.Text a través de la invocación si no es necesario.

+0

Mi problema es más a lo largo de las líneas de: subproceso de trabajo de la biblioteca notifica mi aplicación y quiero tomar una acción inmediata. El problema es que esa "acción inmediata" se ejecutará en el hilo de trabajo, ocultando completamente otras cosas que suceden en el hilo de la interfaz de usuario (cosas que no están relacionadas con la interfaz de usuario, enumerando listas de objetos, por ejemplo). – colithium

0

¿Hay alguna forma de evitar esto?

Sí, la solución alternativa sería crear una cola segura para subprocesos.

  • Su controlador de eventos es invocado por el hilo 3 ª parte
  • Su controlador de eventos encola algo (datos de eventos) en una colección (por ejemplo, una lista), que es el propietario
  • Su controlador de eventos hace algo para señalar su propio thead, que hay datos en la colección para que dequeue y procese:
    • Su hilo podría estar esperando algo (un mutex o lo que sea); cuando su mutex es señalado por el controlador de eventos, se activa y comprueba la cola.
    • Como alternativa, en lugar de ser señalado, podría activarse periódicamente (por ejemplo, una vez por segundo o lo que sea) y sondear la cola.

En cualquiera de los casos, debido a que su cola está siendo escrito por dos hilos diferentes (el hilo 3 ª parte se encolamos, y el hilo se desencolado), es necesario que haya una, cola protegida seguro para subprocesos .

+0

Vi muchos ejemplos en esta línea.Podría estar equivocado, pero ¿no funcionaría esta técnica cuando el dequeueing es el hilo principal de la interfaz de usuario que no puede pasar al modo de suspensión y no puede sentarse allí sondeando la cola en un bucle? No mencioné esto, pero el otro hilo plantea eventos muchas veces por segundo. – colithium

+0

Tiene razón: esto solo sería apropiado si "su" hilo que desea invocar fuera * no * el hilo de la interfaz de usuario. – ChrisW

13

Use SynchronizationContext.Current, que señalará algo con lo que puede sincronizar.

Esto hará que haga lo correcto ™ según el tipo de aplicación. Para una aplicación WinForms, ejecutará esto en el hilo principal de UI.

En concreto, utilice el método SynchronizationContext.Send, así:

SynchronizationContext context = 
    SynchronizationContext.Current ?? new SynchronizationContext(); 

context.Send(s => 
    { 
     // your code here 
    }, null); 
+7

+1. Tenga en cuenta que 'SynchronizationContext.Current' es nulo si el programa es una aplicación de consola. Un buen patrón es usar 'AsyncOperation' que captura el' SynchronizationContext' actual en la creación y * hace lo correcto ++ * ™. http://msdn.microsoft.com/en-us/library/system.componentmodel.asyncoperation.aspx – dtb

+0

No estaba al tanto de esa clase, gracias por informarme. –

+0

Es WinForms así que funcionaría, pero mi intención era ser independiente de la IU. La aplicación de la consola lo está presionando en términos de UI que consideraría, pero es bueno saber que no funcionará. Todavía +1 – colithium

1

Si está utilizando WPF:

Se necesita una referencia al objeto Dispatcher que gestiona el hilo de interfaz de usuario. Luego puede usar el método Invoke o BeginInvoke en el objeto despachador para programar una operación que tiene lugar en el hilo de la interfaz de usuario.

La forma más sencilla de obtener el despachador es utilizando Application.Current.Dispatcher. Este es el despachador responsable del principal (y probablemente el único) subproceso de interfaz de usuario.

ponerlo todo junto:

class MyClass 
{ 
    // Can be called on any thread 
    public ReceiveLibraryEvent(RoutedEventArgs e) 
    { 
     if (Application.Current.CheckAccess()) 
     { 
      this.ReceiveLibraryEventInternal(e); 
     } 
     else 
     { 
      Application.Current.Dispatcher.Invoke(
       new Action<RoutedEventArgs>(this.ReceiveLibraryEventInternal)); 
     } 
    } 

    // Must be called on the UI thread 
    private ReceiveLibraryEventInternal(RoutedEventArgs e) 
    { 
     // Handle event 
    } 
} 
+0

Esto es sobre WPF, no veo dónde indica la pregunta que se usa. –