Рекомендации по работе с потоками

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

ПримечаниеПримечание

В .NET Framework 4 библиотека параллельных задач и PLINQ предоставляют интерфейсы API, которые уменьшают сложность и сокращают риски многопотокового программирования.Дополнительные сведения см. в разделе Параллельное программирование в .NET Framework.

Взаимоблокировки и состояние гонки

Многопоточность позволяет решить проблемы с пропускной способностью и быстротой ответа системы, однако при этом возникают новые проблемы: взаимоблокировки и состояние гонки.

Взаимоблокировки

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

Для обнаружения взаимоблокировок многие методы классов управляемых потоков предоставляют временную задержку. Например, в следующем примере выполняется попытка запроса блокировки для текущего экземпляра. Если блокировка не получена в течение 300 миллисекунд, метод Monitor.TryEnter возвращает значение ЛОЖЬ.

If Monitor.TryEnter(Me, 300) Then
    Try
        ' Place code protected by the Monitor here.
    Finally
        Monitor.Exit(Me)
    End Try
Else
    ' Code to execute if the attempt times out.
End If
if (Monitor.TryEnter(this, 300)) {
    try {
        // Place code protected by the Monitor here.
    }
    finally {
        Monitor.Exit(this);
    }
}
else {
    // Code to execute if the attempt times out.
}

Состояние гонки

Состояние гонки — это ошибка, возникающая, когда результат программы зависит от того, какой из двух (или более) потоков первым достигнет определенного блока кода. При многократном запуске программы получаются разные непредсказуемые результаты.

Простой пример состояние гонки — приращение поля. Предположим, что класс содержит закрытое поле static (Shared в Visual Basic), которое получает приращение всякий раз при создании класса с помощью кода, например objCt++; (в C#) или objCt += 1 (в Visual Basic). При выполнении этой операции значение из objCt заносится в регистр; значение увеличивается и сохраняется в objCt.

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

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

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

Количество процессоров

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

Однопроцессорные компьютеры

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

  • С любым экземпляром запускается только один поток.

  • Фоновый поток выполняется, только когда бездействует основной поток пользователя. Непрерывно выполняющийся основной поток может приводить к зависанию фоновых потоков.

  • При вызове метода Thread.Start для потока данный поток не будет выполняться до тех пор, пока не закончится выполнение текущего потока или этот поток не будет прерван операционной системой.

  • Как правило, состояние гонки возникает из-за того, что программист не учитывает возможность прерывания потока в любой момент времени в пользу другого потока, который достигнет блока кода первым.

Многопроцессорные компьютеры

Многопоточность повышает пропускную способность. Десять процессоров могут в десять раз ускорить процесс выполнения, но только в том случае, если работа построена таким образом, что все процессоры будут работать одновременно. Потоки позволяют распределить работу так, что она будет выполняться с максимальной производительностью. Использование функции многопоточности на компьютере с несколькими процессорами обеспечивает следующие возможности:

  • Количество одновременно выполняющихся потоков ограничено количеством используемых процессоров.

  • Фоновый поток выполняется, только когда количество основных потоков меньше количества используемых процессоров.

  • При вызове метода Thread.Start для потока момент начала его выполнения зависит от количества процессоров и количества потоков, ожидающих выполнения.

  • Состояние гонки может возникнуть не только вследствие непредвиденного прерывания потоков, но и в результате обращения к одному блоку кода двух потоков, выполняющихся на разных процессорах.

Статические члены и статические конструкторы

Класс не будет инициализирован до тех пор, пока его конструктор (конструктор static в C#, Shared Sub New в Visual Basic) не завершит работу. Чтобы предотвратить выполнение кода для типа, который не был инициализирован, общеязыковая среда выполнения блокирует все вызовы из других потоков для членов класса типа static (члены Shared в Visual Basic) до тех пор, пока конструктор класса не завершит работу.

Например, если конструктор класса начинает новый поток, и процедура потока вызывает член класса типа static, новый поток блокируется до тех пор, пока конструктор класса не завершит работу.

Такое поведение характерно для всех типов, у которых есть конструктор типа static.

Основные рекомендации

При работе с несколькими потоками придерживайтесь следующих рекомендаций:

  • Не следует использовать Thread.Abort для завершения других потоков. Вызов метода Abort для другого потока равнозначен созданию исключения для данного потока; при этом текущая точка остановки потока неизвестна.

  • Не следует использовать методы Thread.Suspend и Thread.Resume для синхронизации работы нескольких потоков. Рекомендуется использовать методы Mutex, ManualResetEvent, AutoResetEvent и Monitor.

  • Не следует контролировать выполнение рабочих потоков из основной программы (например, с помощью событий). Вместо этого программу необходимо настроить таким образом, чтобы рабочие потоки могли ждать, пока задача будет доступна, выполнять ее, а затем сообщать другим частям программы о ее завершении. Если рабочие потоки не блокируются, следует рассмотреть вариант, предполагающий использование потоков из пула потоков. Метод Monitor.PulseAll полезен в ситуациях, когда рабочие потоки блокируются.

  • Не следует использовать типы в качестве объектов блокировки. Другими словами, не стоит использовать такой код как lock(typeof(X)) в C# или SyncLock(GetType(X)) в Visual Basic, или же использовать Monitor.Enter в сочетании с объектами Type. Для заданного типа в каждом домене приложения существует только один экземпляр класса System.Type. Если блокируемый тип является открытым, то кроме кода пользователя его может заблокировать любой другой код, что приведет к взаимоблокировке. Дополнительные вопросы см. в разделе Рекомендации по обеспечению надежности.

  • Следует соблюдать осторожность при блокировке экземпляров, например lock(this) в C# или SyncLock(Me) в Visual Basic. Если любой другой код приложения, который является внешним по отношению к типу, заблокирует объект, может произойти взаимоблокировка.

  • Убедитесь, что поток, вошедший в программу контроля, вышел из нее, даже если в программе контроля возникло исключение. Оператор lock в C# и оператор SyncLock в Visual Basic позволяют реализовать такое поведение автоматически, обеспечивая с помощью блока finally вызов метода Monitor.Exit. Если невозможно обеспечить вызов метода Exit, попробуйте воспользоваться объектом Mutex. Этот объект освобождается автоматически после того, как владеющий им поток завершает работу.

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

  • Попробуйте использовать методы класса Interlocked для изменения простых состояний вместо оператора lock (SyncLock в Visual Basic). Оператор lock является надежным средством общего назначения, однако класс Interlocked обеспечивает более высокую производительность обновлений, которые должны быть атомарными. Внутри он выполняет единственный префикс lock в отсутствие конкуренции. Во время анализа кода ищите код, который напоминает следующие примеры. В первом примере выполняется приращение переменной состояния.

    SyncLock lockObject
        myField += 1
    End SyncLock
    
    lock(lockObject) 
    {
        myField++;
    }
    

    Метод Increment можно использовать вместо оператора lock с целью повышения производительности:

    System.Threading.Interlocked.Increment(myField)
    
    System.Threading.Interlocked.Increment(myField);
    
    ПримечаниеПримечание

    В среде .NET Framework версии 2.0 метод Add обеспечивает атомарные обновления с приращением значения более чем на единицу.

    Во втором примере обновление переменной ссылочного типа выполняется только при условии, что ссылка является пустой (Nothing в Visual Basic).

    If x Is Nothing Then
        SyncLock lockObject
            If x Is Nothing Then
                x = y
            End If
        End SyncLock
    End If
    
    if (x == null)
    {
        lock (lockObject)
        {
            if (x == null)
            {
                x = y;
            }
        }
    }
    

    Метод CompareExchange позволяет повысить производительность следующим образом:

    System.Threading.Interlocked.CompareExchange(x, y, Nothing)
    
    System.Threading.Interlocked.CompareExchange(ref x, y, null);
    
    ПримечаниеПримечание

    В среде .NET Framework 2.0 у метода CompareExchange есть универсальная перегрузка, которая обеспечивает типобезопасную замену для любого ссылочного типа.

Рекомендации по работе с библиотеками классов

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

  • По возможности устраняйте потребность в синхронизации. Это особенно актуально для часто используемого кода. В некоторых случаях лучше настроить алгоритм таким образом, чтобы он допускал состояние гонки, вместо того, чтобы стремиться полностью исключить такой вариант. Необязательная синхронизация снижает производительность и создает условия для возникновения взаимоблокировки и состояния гонки.

  • Необходимо сделать статические данные (Shared в Visual Basic) потокобезопасными по умолчанию.

  • Данные экземпляра потокобезопасными по умолчанию делать не нужно. Добавление блокировок с целью создания потокобезопасного кода уменьшает производительность, увеличивает конкуренцию между блокировками и создает условия для возникновения взаимоблокировок. В обычных моделях приложений пользовательский код выполняется только в одном потоке в любой момент времени, что минимизирует необходимость в потокобезопасности. По этой причине библиотеки классов .NET Framework по умолчанию не являются потокобезопасными.

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

См. также

Основные понятия

Потоки и работа с потоками

Другие ресурсы

Управляемая поточность