try thread ejemplos ejemplo detener catch c# multithreading thread-abort

thread - try catch c# ejemplo



¿Cómo funciona Thread.Abort()? (5)

¿Seguro que lees la página a la que apuntabas? Al final se reduce a:

La llamada a Thread.Abort se reduce a .NET estableciendo un indicador en un hilo para que sea abortado y luego verificando ese indicador durante ciertos puntos de la vida del hilo, lanzando la excepción si el indicador está establecido.

Por lo general, emitimos una excepción cuando se pasa una entrada no válida a un método o cuando un objeto está a punto de entrar en un estado no válido. Consideremos el siguiente ejemplo

private void SomeMethod(string value) { if(value == null) throw new ArgumentNullException("value"); //Method logic goes here }

En el ejemplo anterior, inserté una instrucción throw que arroja ArgumentNullException . Mi pregunta es cómo el tiempo de ejecución logra lanzar la ThreadAbortException . Obviamente, no es posible utilizar una sentencia throw en todos los métodos, incluso el tiempo de ejecución se las arregla para lanzar la ThreadAbortException en nuestros métodos personalizados también.

Me preguntaba ¿cómo lo hacen? Tenía curiosidad por saber lo que está sucediendo detrás de las escenas, abrí un reflector para abrir Thread.Abort y terminar con esto

[MethodImplAttribute(MethodImplOptions.InternalCall)] private extern void AbortInternal();//Implemented in CLR

Entonces busqué en Google y encontré esto ¿Cómo funciona realmente la excepción ThreadAbortException ? Este enlace dice que el tiempo de ejecución publica APC a través de la función QueueUserAPC y así es como hacen el truco. No tenía conocimiento del método QueueUserAPC . Acabo de intentar ver si es posible con algún código. El siguiente código muestra mi intento.

[DllImport("kernel32.dll")] static extern uint QueueUserAPC(ApcDelegate pfnAPC, IntPtr hThread, UIntPtr dwData); delegate void ApcDelegate(UIntPtr dwParam); Thread t = new Thread(Threadproc); t.Start(); //wait for thread to start uint result = QueueUserAPC(APC, new IntPtr(nativeId), (UIntPtr)0);//returns zero(fails) int error = Marshal.GetLastWin32Error();// error also zero private static void APC(UIntPtr data) { Console.WriteLine("Callback invoked"); } private static void Threadproc() { //some infinite loop with a sleep }

Si estoy haciendo algo mal, perdóname, no tengo idea de cómo hacerlo. De nuevo a la pregunta, ¿puede alguien con conocimiento sobre esto o parte del equipo de CLR explicar cómo funciona internamente? Si APC es el truco, ¿qué es lo que estoy haciendo mal aquí?


Descargué el código SSCLI y comencé a hurgar. Es difícil para mí seguir el código (principalmente porque no soy un experto en C ++ o ASM), pero sí veo muchos ganchos en los que los inyectores se inyectan de forma semi-sincrónica.

  • Procesamiento de control de flujo de prueba / captura / finalmente / falla
  • Activaciones GC (asignación de memoria)
  • proxy a través de interrupciones suaves (como con Thread.Interrupt) cuando se encuentra en estado de alerta
  • interceptación de llamadas virtuales
  • Preparaciones de llamadas de cola JIT
  • no gestionado para transiciones gestionadas

Eso es sólo para nombrar unos pocos. Lo que quería saber era cómo se inyectaban los abortos asíncronos. La idea general de secuestrar el puntero de instrucción es parte de cómo sucede. Sin embargo, es mucho más complejo de lo que he descrito anteriormente. No parece que siempre se use una expresión de suspensión-modificación-reanudar. Desde el código SSCLI puedo ver que suspende y reanuda el hilo en ciertos escenarios para prepararse para el secuestro, pero este no es siempre el caso. Me parece que el secuestro puede ocurrir mientras el hilo se ejecuta también.

El artículo al que se vincula menciona que se ha establecido un indicador de cancelación en el subproceso de destino. Esto es técnicamente correcto. El indicador se llama TS_AbortRequested y hay mucha lógica que controla cómo se establece este indicador. Hay comprobaciones para determinar si existe una región de ejecución restringida y si el subproceso está actualmente en un bloque try-catch-finally-fault. Parte de este trabajo implica un rastreo de pila, lo que significa que el hilo debe suspenderse y reanudarse. Sin embargo, cómo se detecta el cambio de la bandera es donde ocurre la magia real. El artículo no lo explica muy bien.

Ya mencioné varios puntos de inyección semi-síncronos en la lista anterior. Esos deben ser bastante triviales de entender. Pero, ¿cómo se produce exactamente la inyección asíncrona? Bueno, me parece que el JIT es el asistente detrás de la cortina aquí. Existe algún tipo de mecanismo de sondeo integrado en el JIT / GC que determina periódicamente si se debe producir una recopilación. Esto también brinda la oportunidad de verificar si alguno de los subprocesos administrados ha cambiado de estado (como tener el conjunto de indicadores de cancelación). Si se configura TS_AbortRequested , el secuestro ocurre en ese momento.

Si está viendo el código SSCLI, aquí hay algunas funciones útiles que debe tener en cuenta.

  • HandleThreadAbort
  • CommonTripThread
  • JIT_PollGC
  • JIT_TailCallHelper
  • COMPlusCheckForAbort
  • ThrowForFlowControl
  • JIT_RareDisableHelper

Hay muchas otras pistas. Tenga en cuenta que este es el SSCLI, por lo que es posible que los nombres de los métodos no coincidan exactamente con las pilas de llamadas observadas en producción (como lo que descubrió Josh Poley ), pero habrá similitudes. Además, gran parte del secuestro de hilos se realiza con el código de ensamblaje, por lo que a veces es difícil de seguir. JIT_PollGC porque creo que aquí es donde ocurren las cosas interesantes. Este es el gancho que creo que el JIT colocará dinámica y estratégicamente en el subproceso de ejecución. Este es básicamente el mecanismo por el cual esos bucles ajustados aún pueden recibir las inyecciones de cancelación. El subproceso de destino realmente es esencialmente un sondeo para la solicitud de cancelación, pero como parte de una estrategia más amplia para invocar el GC 1

Así que claramente el JIT, GC y los abortos de hilos están íntimamente relacionados. Es obvio cuando miras el código SSCLI. Como ejemplo, el método utilizado para determinar los puntos seguros para abortos de subprocesos es el mismo que el utilizado para determinar si se permite que el GC funcione.

1 CLI Essentials de fuente compartida, David Stutz, 2003 , pág. 249-250


Es fácil, el sistema operativo subyacente lo hace. Si el subproceso se encuentra en cualquier estado, excepto "ejecutándose en otro núcleo", no hay ningún problema: su estado está configurado para "no ejecutarse nunca más". Si el hilo se está ejecutando en otro núcleo, el sistema operativo interrumpe el hardware a través de otro. es un controlador interprocessor y así extermina el hilo.

Cualquier mención de ''intervalo de tiempo'', ''cuántica'', etc. es solo .....


Para que el QueueUserAPC funcione, debes hacer dos cosas.

  1. Adquirir el mango del hilo de destino. Tenga en cuenta que esto no es lo mismo que el ID de subproceso nativo.
  2. Permitir que el subproceso de destino pase a un estado de alerta.

Aquí hay un programa completo que demuestra esto.

class Program { [DllImport("kernel32.dll", EntryPoint = "DuplicateHandle", CallingConvention = CallingConvention.StdCall, SetLastError = true)] public static extern bool DuplicateHandle([In] System.IntPtr hSourceProcessHandle, [In] System.IntPtr hSourceHandle, [In] System.IntPtr hTargetProcessHandle, out System.IntPtr lpTargetHandle, uint dwDesiredAccess, [MarshalAsAttribute(UnmanagedType.Bool)] bool bInheritHandle, uint dwOptions); [DllImport("kernel32.dll", EntryPoint = "GetCurrentProcess", CallingConvention = CallingConvention.StdCall, SetLastError = true)] public static extern IntPtr GetCurrentProcess(); [DllImport("kernel32.dll")] private static extern IntPtr GetCurrentThread(); [DllImport("kernel32.dll")] private static extern uint QueueUserAPC(ApcMethod pfnAPC, IntPtr hThread, UIntPtr dwData); private delegate void ApcMethod(UIntPtr dwParam); static void Main(string[] args) { Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId); IntPtr threadHandle = IntPtr.Zero; var threadHandleSet = new ManualResetEvent(false); var apcSet = new ManualResetEvent(false); var thread = new Thread( () => { Console.WriteLine("thread started"); threadHandle = GetCurrentThread(); DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), out threadHandle, 0, false, 2); threadHandleSet.Set(); apcSet.WaitOne(); for (int i = 0; i < 10; i++) { Console.WriteLine("thread waiting"); Thread.Sleep(1000); Console.WriteLine("thread running"); } Console.WriteLine("thread finished"); }); thread.Start(); threadHandleSet.WaitOne(); uint result = QueueUserAPC(DoApcCallback, threadHandle, UIntPtr.Zero); apcSet.Set(); Console.ReadLine(); } private static void DoApcCallback(UIntPtr dwParam) { Console.WriteLine("DoApcCallback: " + Thread.CurrentThread.ManagedThreadId); } }

Básicamente, esto permite que un desarrollador inyecte la ejecución de un método en cualquier subproceso arbitrario. El subproceso de destino no tiene que tener un mensaje de bombeo como sería necesario para el enfoque tradicional. Sin embargo, un problema con este enfoque es que el subproceso de destino debe estar en un estado de alerta. Básicamente, el subproceso debe llamar a una de las llamadas de bloqueo .NET enlatadas como Thread.Sleep , WaitHandle.WaitOne , etc. para que se ejecute la cola APC.


Para que su devolución de llamada APC funcione, necesita un controlador de hilo (que no es el mismo que el ID de hilo). También he actualizado los atributos en los PInvokes.

También tenga en cuenta que el subproceso debe estar en un estado de espera "alerta" para que se llame al APC (que Thread.Sleep nos dará). Así que si el hilo está ocupado haciendo cosas, puede que no sea llamado.

[DllImport("kernel32.dll", EntryPoint = "GetCurrentThread", CallingConvention = CallingConvention.StdCall)] public static extern IntPtr GetCurrentThread(); [DllImport("kernel32.dll", EntryPoint = "QueueUserAPC", CallingConvention = CallingConvention.StdCall, SetLastError = true)] public static extern uint QueueUserAPC(ApcDelegate pfnAPC, IntPtr hThread, UIntPtr dwData); [UnmanagedFunctionPointerAttribute(CallingConvention.StdCall)] public delegate void ApcDelegate(UIntPtr dwParam); [DllImport("kernel32.dll", EntryPoint = "DuplicateHandle", CallingConvention = CallingConvention.StdCall, SetLastError = true)] public static extern bool DuplicateHandle([In] System.IntPtr hSourceProcessHandle, [In] System.IntPtr hSourceHandle, [In] System.IntPtr hTargetProcessHandle, out System.IntPtr lpTargetHandle, uint dwDesiredAccess, [MarshalAsAttribute(UnmanagedType.Bool)] bool bInheritHandle, uint dwOptions); [DllImport("kernel32.dll", EntryPoint = "GetCurrentProcess", CallingConvention = CallingConvention.StdCall, SetLastError = true)] public static extern IntPtr GetCurrentProcess(); static IntPtr hThread; public static void SomeMethod(object value) { DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), out hThread, 0, false, 2); while (true) { Console.WriteLine("."); Thread.Sleep(1000); } } private static void APC(UIntPtr data) { Console.WriteLine("Callback invoked"); } static void Main(string[] args) { Console.WriteLine("in Main/n"); Thread t = new Thread(Program.SomeMethod); t.Start(); Thread.Sleep(1000); // wait until the thread fills out the hThread member -- don''t do this at home, this isn''t a good way to synchronize threads... uint result = QueueUserAPC(APC, hThread, (UIntPtr)0); Console.ReadLine(); }


Editar:
Cómo el CLR inyecta la excepción
Dado este bucle para la función de hilo:

while (true) { i = ((i + 7) * 3 ^ 0x73234) & 0xFFFF; }

Luego. .Abort el subproceso y .Abort la traza nativa de la pila.

... ntdll!KiUserExceptionDispatcher KERNELBASE!RaiseException clr!RaiseComPlusException clr!RedirectForThrowControl2 clr!RedirectForThrowControl_RspAligned clr!RedirectForThrowControl_FixRsp csTest.Program.SomeMethod(System.Object) ...

Mirando la dirección de retorno de la llamada RedirectForThrowControl_FixRsp , está apuntando al medio de mi bucle, para el cual no hay saltos ni llamadas:

nop mov eax,dword ptr [rbp+8] add eax,7 // code flow would return to execute this line lea eax,[rax+rax*2] xor eax,73234h and eax,0FFFFh mov dword ptr [rbp+8],eax nop mov byte ptr [rbp+18h],1 jmp 000007fe`95ba02da // loop back to the top

Entonces, aparentemente, el CLR está modificando el puntero de instrucción del hilo en cuestión para extraer físicamente el control del flujo normal. Obviamente, necesitaban suministrar varios envoltorios para reparar y restaurar todos los registros de la pila para que funcionen correctamente (por lo tanto, las APIs _RspAligned nombre _FixRsp y _RspAligned .


En una prueba separada, solo recibí llamadas de Console.Write() dentro de mi bucle de subprocesos, y allí parecía que el CLR había inyectado una prueba justo antes de la llamada física a WriteFile :

KERNELBASE!RaiseException clr!RaiseTheExceptionInternalOnly clr! ?? ::FNODOBFM::`string'' clr!HelperMethodFrame::PushSlowHelper clr!JIT_RareDisableHelper mscorlib_ni!DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr) mscorlib_ni!System.IO.__ConsoleStream.WriteFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean)