Как работает Thread.Abort()?

Обычно мы генерируем исключение, когда неверный ввод передается методу или когда объект собирается войти в недопустимое состояние. Давайте рассмотрим следующий пример

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

В приведенном выше примере я вставил оператор throw, который ArgumentNullException, Мой вопрос, как время выполнения удается бросить ThreadAbortException, Очевидно, что невозможно использовать throw оператор во всех методах, даже во время выполнения удается бросить ThreadAbortException в наших пользовательских методах тоже.

Мне было интересно, как они это делают? Мне было интересно узнать, что происходит за кулисами, я открыл отражатель, чтобы открыть Thread.Abort и в конечном итоге с этим

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

Затем я погуглил и обнаружил, что ThreadAbortException действительно работает. По этой ссылке написано, что во время выполнения посты через APC QueueUserAPC функционировать, и вот как они делают свое дело. Я не знал о QueueUserAPC Метод, который я только что дал, чтобы посмотреть, возможно ли это с некоторым кодом. Следующий код показывает мою попытку.

[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
}

Если я делаю что-то не так, прости меня, я понятия не имею, как это сделать. Снова вернемся к вопросу, может ли кто-нибудь, обладающий знаниями об этом или части команды CLR, объяснить, как это работает внутри компании? Если APC за ходом трюка следует то, что я делаю не так здесь?

5 ответов

Вы уверены, что прочитали страницу, на которую указывали? В конце концов, это сводится к:

Вызов Thread.Abort сводится к тому, что.NET устанавливает флаг в потоке, который должен быть прерван, а затем проверяет этот флаг в течение определенных точек в течение времени жизни потока, выдает исключение, если этот флаг установлен.

Чтобы ваш обратный вызов APC заработал, вам нужен дескриптор потока (который не совпадает с идентификатором потока). Я также обновил атрибуты в PInvokes.

Также имейте в виду, что поток должен находиться в состоянии ожидания с возможностью оповещения для вызова APC (что нам даст Thread.Sleep). Так что, если поток занят чем-то, он может не вызываться.

[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();
}


Редактировать:
Как CLR вводит исключение
Учитывая этот цикл для функции потока:

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

После, я .Abortотредактировал поток и посмотрел на собственную трассировку стека

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

Глядя на обратный адрес RedirectForThrowControl_FixRsp call, он указывает на середину моего цикла, для которого нет переходов или вызовов:

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

Таким образом, очевидно, что CLR фактически модифицирует указатель инструкций рассматриваемого потока, чтобы физически вырвать управление из нормального потока. Очевидно, им нужно было предоставить несколько оболочек для исправления и восстановить все регистры стека, чтобы он работал правильно (таким образом, метко названный _FixRsp а также _RspAligned API-интерфейсы.


В отдельном тесте я просто Console.Write() вызовы внутри моего потока потока, и там выглядело, как будто CLR ввел тест незадолго до физического вызова 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)

Я скачал код SSCLI и начал ковыряться. Мне трудно следовать коду (в основном потому, что я не эксперт по C++ или ASM), но я вижу много хуков, когда прерывания вводятся полусинхронно.

  • try / catch / finally / неисправность блока управления потоком обработки
  • GC активации (выделение памяти)
  • прокси через мягкие прерывания (как с Thread.Interrupt), когда в состоянии тревоги
  • перехватывает виртуальный вызов
  • JIT хвостовой подготовки
  • неуправляемые к управляемым переходам

Это просто назвать несколько. То, что я хотел знать, было то, как асинхронные прерывания были введены. Общая идея перехвата указателя инструкций является частью того, как это происходит. Тем не менее, это гораздо сложнее, чем я описал выше. Не похоже, что идиома Suspend-Modify-Resume всегда используется. Из кода SSCLI я вижу, что он приостанавливает и возобновляет поток в определенных сценариях, чтобы подготовиться к угону, но это не всегда так. Мне кажется, что угон может произойти, когда резьба также работает полнопроходной.

В статье, на которую вы ссылаетесь, упоминается, что в целевом потоке установлен флаг прерывания. Это технически правильно. Флаг называется TS_AbortRequested и есть много логики, которая контролирует, как этот флаг установлен. Существуют проверки для определения, существует ли ограниченная область выполнения и находится ли поток в данный момент в блоке try-catch-finally-fault. Часть этой работы включает сканирование стека, что означает, что поток должен быть приостановлен и возобновлен. Однако, как обнаруживается смена флага, происходит настоящее волшебство. Статья не очень хорошо объясняет это.

Я уже упоминал несколько полусинхронных точек ввода в списке выше. Это должно быть довольно просто понять. Но как именно происходит асинхронный впрыск? Ну, мне кажется, что JIT - это волшебник, стоящий здесь за занавесом. В JIT/GC встроен какой-то механизм опроса, который периодически определяет, должна ли происходить коллекция. Это также дает возможность проверить, изменилось ли состояние какого-либо из управляемых потоков (например, установлен флаг отмены). Если TS_AbortRequested устанавливается тогда угон происходит тут же.

Если вы смотрите на код SSCLI, вот несколько хороших функций для просмотра.

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

Есть много других подсказок. Имейте в виду, что это SSCLI, поэтому имена методов могут не совпадать точно со стеками вызовов, наблюдаемыми в производстве (например, как обнаружил Джош Поули), но будут сходства. Кроме того, большая часть перехвата потоков выполняется с помощью ассемблерного кода, поэтому иногда трудно следовать за ним. Я выделил JIT_PollGC потому что я верю, что именно здесь происходят интересные вещи. Это тот крючок, который, я считаю, JIT динамически и стратегически поместит в поток выполнения. Это в основном механизм того, как эти узкие петли все еще могут получать инъекции прерывания. Целевой поток действительно опрашивает запрос на прерывание, но как часть более широкой стратегии для вызова GC 1

Очевидно, что JIT, GC и прерывания потоков тесно связаны. Это очевидно, когда вы смотрите на код SSCLI. Например, метод, используемый для определения безопасных точек для прерывания потока, такой же, как метод, используемый для определения, разрешен ли запуск GC.


1 Shared Source CLI Essentials, David Stutz, 2003, pg. 249-250

Чтобы заставить QueueUserAPC работать, вам нужно сделать две вещи.

  1. Получить целевой дескриптор потока. Обратите внимание, что это не то же самое, что нативный идентификатор потока.
  2. Разрешите целевому потоку перейти в состояние оповещения.

Вот полная программа, которая демонстрирует это.

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);
    }

}

По сути, это позволяет разработчику внедрить выполнение метода в любой произвольный поток. Целевой поток не должен иметь сообщения, как это было бы необходимо для традиционного подхода. Одна из проблем этого подхода заключается в том, что целевой поток должен находиться в состоянии оповещения. Поэтому в основном поток должен вызывать один из стандартных вызовов.NET, например, Thread.Sleep, WaitHandle.WaitOneи т. д. для выполнения очереди APC.

Это просто, основная ОС делает это. Если поток находится в каком-либо состоянии, кроме "работает на другом ядре", проблем нет - для него установлено состояние "никогда не запускаться снова". Если поток выполняется на другом ядре, ОС выполняет аппаратное прерывание другого ядра через. это межпроцессорный драйвер и поэтому уничтожает поток.

Любое упоминание о временном интервале, кванте и т. Д. Просто.....

Другие вопросы по тегам