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

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

Примечание.

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

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

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

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

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

Многие методы классов управляемых потоков предоставляют значения времени ожидания для обнаружения взаимоблокировок. Например, следующий код пытается получить блокировку для объекта с именем lockObject. Если блокировка не будет получена в течение 300 миллисекунд, Monitor.TryEnter возвратит false.

If Monitor.TryEnter(lockObject, 300) Then  
    Try  
        ' Place code protected by the Monitor here.  
    Finally  
        Monitor.Exit(lockObject)  
    End Try  
Else  
    ' Code to execute if the attempt times out.  
End If  
if (Monitor.TryEnter(lockObject, 300)) {  
    try {  
        // Place code protected by the Monitor here.  
    }  
    finally {  
        Monitor.Exit(lockObject);  
    }  
}  
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. Сведения о других технологиях синхронизации данных между несколькими потоками см. в разделе Синхронизация данных для многопоточности.

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

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

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

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

Это относится к любому типу, который может иметь конструктор static.

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

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

Используйте свойство Environment.ProcessorCount, чтобы определить количество процессоров, доступных во время выполнения.

Общие рекомендации

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

  • Не используйте 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. Если другой код в приложении, который является внешним для типа, заблокирует объект, может возникнуть взаимоблокировка.

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

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

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

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

    Вы можете повысить производительность, применяя метод Increment вместо оператора lock, как показано ниже.

    System.Threading.Interlocked.Increment(myField)  
    
    System.Threading.Interlocked.Increment(myField);  
    

    Примечание.

    Используйте метод Add для атомарных приращений более 1.

    Во втором примере переменная ссылочного типа обновляется только в том случае, если она является пустой ссылкой (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)  
        {  
            x ??= y;
        }  
    }  
    

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

    System.Threading.Interlocked.CompareExchange(x, y, Nothing)  
    
    System.Threading.Interlocked.CompareExchange(ref x, y, null);  
    

    Примечание.

    Перегрузка метода CompareExchange<T>(T, T, T) предоставляет типобезопасную альтернативу для ссылочных типов.

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

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

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

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

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

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

См. также