>
>
>
V720. The 'SuspendThread' function is u…


V720. The 'SuspendThread' function is usually used when developing a debugger. See documentation for details.

Анализатор обнаружил, что в программе используется функция SuspendThread() или Wow64SuspendThread(). Сам по себе вызов этих функций не является ошибкой. Однако, разработчики часто используют их не по назначению. Из-за этого, программа может вести себя не так, как ожидает программист.

Функция SuspendThread() должна помогать разрабатывать отладчики и подобные им приложения. Если Вы используете эту функцию в прикладном программном обеспечении для задач синхронизации, то высока вероятность, что в вашей программе есть ошибка.

Суть проблемы неправильного использования функции SuspendThread() изложена в следующих статьях:

Прочитайте их. Если выяснится, что функция SuspendThread() используется неправильно, то необходимо переписать код. Если всё хорошо, то просто отключите диагностику V720 в настройках анализатора.

Статьи в интернете иногда пропадают или меняют своё расположение. Поэтому, мы на всякий случай приводим в документации текст обеих статей.

Примечание. Для российских пользователей мы перевели текст статей. Ознакомиться с оригиналом вы можете, перейдя по ссылкам, приведённым ниже или переключить язык нашего сайта.

Почему никогда не стоит приостанавливать работу потока

Это практически так же плохо, как уничтожение потока.

Вместо того, чтобы просто ответить на вопрос, я задам вопросы вам и посмотрю, сможете ли вы найти на них ответы.

Рассмотрим такую программу на (вздох) C#:

using System.Threading;
using SC = System.Console;

class Program {
  public static void Main() {
    Thread t = new Thread(new ThreadStart(Program.worker));
    t.Start();
    SC.WriteLine("Press Enter to suspend");
    SC.ReadLine();
    t.Suspend();
    SC.WriteLine("Press Enter to resume");
    SC.ReadLine();
    t.Resume();
  }
  static void worker() {
    for (;;) SC.Write("{0}\r", System.DateTime.Now);
  }
}

Когда вы запускаете эту программу и жмёте Enter, программа зависает. Но если вы измените функцию worker просто на "for(;;) {}", программа будет работать совершенно нормально. Давайте разберёмся, почему так происходит.

Рабочий поток тратит практически всё время своей работы на вызовы System.Console.WriteLine, поэтому, когда вы зовёте Thread.Suspend(), поток с наибольшей вероятностью и находится в System.Console.WriteLine.

Вопрос: Является ли метод System.Console.WriteLine потокобезопасным?

Хорошо, я сам отвечу на этот: Да. Мне даже не нужно было залезать в документацию, чтобы понять это. Наша программа зовёт его из двух разных потоков без какой-либо синхронизации, так что лучше бы ему быть потокобезопасным, иначе у нас начались бы большие неприятности ещё до того, как мы дошли до приостановки потока.

Вопрос: Каким образом обычно объекты делают потокобезопасными?

Вопрос: Каков результат приостановки потока в момент выполнения им потокобезопасной операции?

Вопрос: Что случится, если вы, в дальнейшем, попытаетесь обратиться к тому же объекту (в данном случае – к консоли) из другого потока?

Такой результат не специфичен именно для C#. Подобная логика применима как к потоковой модели Win32, так и к любой другой. В Win32, структура кучи процесса является потокобезопасным объектом, и, поскольку сложно что-либо сделать в Win32 без обращений к куче, приостановка потока в Win32 имеет большую вероятность привести к блокировке вашего процесса.

Но тогда зачем вообще появилась функция SuspendThread?

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

Функция SuspendThread приостанавливает поток, но делает это асинхронно

Итак, коллега решил игнорировать мой совет потому что он прогонял несколько экспериментов с потоковой безопасностью и взаимоблокирующими операциями, и приостановка потока была удобным способом обнаружения "окон", в которых могут возникать гонки доступа к объектам.

В процессе этих экспериментов, он обнаружил некоторое странное поведение.

LONG lValue;

DWORD CALLBACK IncrementerThread(void *)
{
 while (1) {
  InterlockedIncrement(&lValue);
 }
 return 0;
}

// This is just a test app, so we will abort() if anything
// happens we don't like.

int __cdecl main(int, char **)
{
 DWORD id;
 HANDLE thread = CreateThread(NULL, 0, IncrementerThread, 
                              NULL, 0, &id);
 
 if (thread == NULL) abort();

 while (1) {
  if (SuspendThread(thread) == (DWORD)-1) abort();

  if (InterlockedOr(&lValue, 0) != InterlockedOr(&lValue, 0)) 
  {

    printf("Huh? The variable lValue was modified by a suspended
           thread?\n");
  }

  ResumeThread(thread);
 }
 return 0;
}

Странным здесь является то, что сообщение "Huh?" было распечатано. Разве может приостановленный поток модифицировать переменную? Возможно ли, что InterlockedIncrement начнёт увеличивать значение переменной, затем окажется "замороженным", но каким-то образом всё-же закончит начатое позже?

Но ответ гораздо проще. Функция SuspendThread говорит планировщику заморозить поток, но не ждёт подтверждения от него о том, что "заморозка" уже произошла. На это намекается в документации на SuspendThread, где говорится

Эта функция в основном разработана для использования отладчиками. Она не предназначена для синхронизации потоков.

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

Если вы хотите убедиться в том, что поток на самом деле приостановлен, вам нужно выполнить синхронную операцию, которая зависит от факта приостановки потока. Это принуждает выполнение обработки запроса на приостановку, т.к. этот запрос является необходимым условием для вашей операции, и поскольку ваша операция синхронна, вы знаете, что к моменту, когда она завершится, приостановка потока гарантированно произойдёт.

Традиционный способ для этого – позвать GetThreadContext, т.к. это требует от ядра прочитать из контекста приостановленного потока, что, в свою очередь, требует сохранения контекста, что и потребует приостановки потока.

Выявляемые диагностикой ошибки классифицируются согласно ГОСТ Р 71207–2024 как критические и относятся к типу: Ошибки при работе с многопоточными примитивами (интерфейсами запуска потоков на выполнение, синхронизации и обмена данными между потоками и пр.).

Данная диагностика классифицируется как: