2011-11-30 8 views
14

Al actualizar el código de mi UI (C# en una aplicación .NET 4.0), me encontré con un bloqueo extraño debido a una llamada a la interfaz de usuario que se estaba ejecutando en el hilo equivocado. Sin embargo, ya estaba invocando esa llamada en el hilo principal, por lo que el bloqueo no tenía sentido: MainThreadDispatcher.Invoke(new Action(View.Method)) se colgó con "El hilo que llama no puede acceder a este objeto porque lo posee un hilo diferente". en la propiedad Ver.Usando el grupo de métodos C# ejecuta el código

Tras una investigación más profunda encontré la causa: estaba invocando a través de un grupo de métodos. Pensé que usar un grupo de métodos o un delegado/lambda son esencialmente lo mismo (ver también this question y this question). En su lugar, la conversión del grupo de métodos a un delegado hace que se ejecute el código, verificando el valor de View. Esto se realiza de inmediato, es decir, en la secuencia original (no UI), que provocó el bloqueo. Si utilizo una lambda en su lugar, verificando la propiedad se hace más tarde, y por lo tanto en el hilo correcto.

Eso parece interesante, por decir lo menos. ¿Hay algún lugar en el estándar C# donde se menciona esto? ¿O eso está implícito debido a la necesidad de encontrar la conversión correcta?

Aquí hay un programa de prueba. Primero, la manera directa. En segundo lugar, en dos pasos, que muestra mejor lo que sucede. Para mayor diversión, modifico Item después de que se haya creado el delegado.

namespace ConsoleApplication1 // Add a reference to WindowsBase to a standard ConsoleApplication 
{ 
    using System.Threading; 
    using System.Windows.Threading; 
    using System; 

    static class Program 
    { 
     static Dispatcher mainDispatcher; 
     static void Main() 
     { 
      mainDispatcher = Dispatcher.CurrentDispatcher; 
      mainDispatcher.Thread.Name = "Main thread"; 
      var childThread = new Thread(() => 
       { 
        Console.WriteLine("--- Method group ---"); 
        mainDispatcher.Invoke(new Action(Item.DoSomething)); 

        Console.WriteLine("\n--- Lambda ---"); 
        mainDispatcher.Invoke(new Action(() => Item.DoSomething())); 

        Console.WriteLine("\n--- Method group (two steps) ---"); 
        var action = new Action(Item.DoSomething); 
        Console.WriteLine("Invoking"); 
        mainDispatcher.Invoke(action); 

        Console.WriteLine("\n--- Lambda (two steps) ---"); 
        action = new Action(() => Item.DoSomething()); 
        Console.WriteLine("Invoking"); 
        mainDispatcher.Invoke(action); 

        Console.WriteLine("\n--- Method group (modifying Item) ---"); 
        action = new Action(Item.DoSomething); 
        item = null; 
        mainDispatcher.Invoke(action); 
        item = new UIItem(); 

        Console.WriteLine("\n--- Lambda (modifying Item) ---"); 
        action = new Action(() => Item.DoSomething()); 
        item = null; 
        Console.WriteLine("Invoking"); 
        mainDispatcher.Invoke(action); 

        mainDispatcher.InvokeShutdown(); 
       }); 
      childThread.Name = "Child thread"; 
      childThread.Start(); 

      Dispatcher.Run(); 
     } 

     static UIItem item = new UIItem(); 
     static UIItem Item 
     { 
      get 
      { 
       // mainDispatcher.VerifyAccess(); // Uncomment for crash. 
       Console.WriteLine("UIItem: In thread: {0}", Dispatcher.CurrentDispatcher.Thread.Name); 
       return item; 
      } 
     } 

     private class UIItem 
     { 
      public void DoSomething() 
      { 
       Console.WriteLine("DoSomething: In thread: {0}", Dispatcher.CurrentDispatcher.Thread.Name); 
      } 
     } 
    } 
} 

versión corta:

namespace ConsoleApplication1 // Add a reference to WindowsBase to a standard ConsoleApplication 
{ 
    using System.Threading; 
    using System.Windows.Threading; 
    using System; 

    static class Program 
    { 
     static Dispatcher mainDispatcher; 
     static void Main() 
     { 
      mainDispatcher = Dispatcher.CurrentDispatcher; 
      mainDispatcher.Thread.Name = "Main thread"; 
      var childThread = new Thread(() => 
       { 
        Console.WriteLine("--- Method group ---"); 
        mainDispatcher.Invoke(new Action(Item.DoSomething)); 

        Console.WriteLine("\n--- Lambda ---"); 
        mainDispatcher.Invoke(new Action(() => Item.DoSomething()));  

        mainDispatcher.InvokeShutdown(); 
       }); 
      childThread.Name = "Child thread"; 
      childThread.Start(); 

      Dispatcher.Run(); 
     } 

     static UIItem item = new UIItem(); 
     static UIItem Item 
     { 
      get 
      { 
       mainDispatcher.VerifyAccess(); 
       Console.WriteLine("UIItem: In thread: {0}", Dispatcher.CurrentDispatcher.Thread.Name); 
       return item; 
      } 
     } 

     private class UIItem 
     { 
      public void DoSomething() 
      { 
       Console.WriteLine("DoSomething: In thread: {0}", Dispatcher.CurrentDispatcher.Thread.Name); 
      } 
     } 
    } 
} 
+2

¿Qué llamada falla? ¿Cuál es la pila de llamadas? – SLaks

+0

Elimine la línea comentada con VerifyAccess(), y verá que todas las llamadas que usan grupos de métodos fallan, ya que se accede a la propiedad Item en el subproceso secundario. –

+1

Un programa corto pero completo * solo * que muestre la llamada problemática sería realmente útil. –

Respuesta

4

El hecho de que se puede acceder con impaciencia la propiedad no es especial para los miembros del grupo del método de cualquier manera; es una característica de las expresiones de miembros en general.

En realidad, la lambda está creando el caso especial: su cuerpo (y por lo tanto el acceso a la propiedad) se aplazará hasta que el delegado se ejecute realmente.

De la especificación:

7.6.4 Acceso Miembro

[...] Un miembro de acceso es o bien de la forma EI o de la forma de la IE, donde E es una Expresión primaria.

[...] si E es una propiedad o un indexador de acceso, entonces el valor de la propiedad o indexador de acceso se obtiene (§7.1.1) y E se reclasificado como un valor.

6

Estás creando un closed delegate, que almacena el objeto this dentro del delegado. (pasar como el primer parámetro oculto al método.)

Por lo tanto, cuando crea un delegado de un grupo de métodos, se accede al objeto inmediatamente para almacenarlo en el delegado.

Por el contrario, cuando crea una expresión lambda, solo se tiene acceso al objeto que posee el delegado cuando se llama al delegado.
Sus expresiones lambda crean un delegado abierto que accede a la propiedad static directamente dentro del delegado.

Si hubiera accedido a una propiedad no estática o una variable local, habría creado un delegado cerrado from a closure, y aún así funcionaría.

Cuestiones relacionadas