Рекомендации по работе с потокамиManaged threading best practices

Многопоточность требует тщательного программирования.Multithreading requires careful programming. Большинство задач можно упростить, поместив запросы на выполнение в очередь по потокам пулов потоков.For most tasks, you can reduce complexity by queuing requests for execution by thread pool threads. В этом разделе рассматриваются более сложные ситуации, такие как координация работы нескольких потоков или обработка потоков, вызывающих блокировку.This topic addresses more difficult situations, such as coordinating the work of multiple threads, or handling threads that block.

Примечание

Начиная с версии .NET Framework 4 библиотека параллельных задач и PLINQ предоставляют интерфейсы API, которые несколько снижают сложность и риски многопоточного программирования.Starting with the .NET Framework 4, the Task Parallel Library and PLINQ provide APIs that reduce some of the complexity and risks of multi-threaded programming. Дополнительные сведения см. в статье Параллельное программирование в .NET.For more information, see Parallel Programming in .NET.

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

Многопоточность позволяет решить проблемы с пропускной способностью и скоростью реагирования, но при этом возникают новые проблемы: взаимоблокировки и конфликты.Multithreading solves problems with throughput and responsiveness, but in doing so it introduces new problems: deadlocks and race conditions.

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

Взаимоблокировка происходит, когда каждый из двух потоков пытается заблокировать ресурс, уже заблокированный другим потоком.A deadlock occurs when each of two threads tries to lock a resource the other has already locked. Ни один из потоков не может продолжить работу.Neither thread can make any further progress.

Многие методы классов управляемых потоков предоставляют значения времени ожидания для обнаружения взаимоблокировок.Many methods of the managed threading classes provide time-outs to help you detect deadlocks. Например, следующий код пытается получить блокировку для объекта с именем lockObject.For example, the following code attempts to acquire a lock on an object named lockObject. Если блокировка не будет получена в течение 300 миллисекунд, Monitor.TryEnter возвратит false.If the lock is not obtained in 300 milliseconds, Monitor.TryEnter returns 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.  
}  

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

Конфликт — это ошибка, которая возникает, когда результат программы зависит от того, какой из двух или более потоков первым достигнет определенного блока кода.A race condition is a bug that occurs when the outcome of a program depends on which of two or more threads reaches a particular block of code first. Выполнение программы часто дает различные результаты, и предсказать результат выполнения конкретного запуска невозможно.Running the program many times produces different results, and the result of any given run cannot be predicted.

Простой пример состояния гонки — увеличение поля.A simple example of a race condition is incrementing a field. Предположим, что класс содержит закрытое поле static (Shared в Visual Basic), которое увеличивается всякий раз при создании класса с помощью кода, например objCt++; (в C#) или objCt += 1 (в Visual Basic).Suppose a class has a private static field (Shared in Visual Basic) that is incremented every time an instance of the class is created, using code such as objCt++; (C#) or objCt += 1 (Visual Basic). Для этой операции необходимо загрузить значение из objCt в регистр, увеличить или уменьшить это значение и сохранить его в objCt.This operation requires loading the value from objCt into a register, incrementing the value, and storing it in objCt.

В многопоточных приложениях поток, загружающий и увеличивающий значение, может быть вытеснен другим потоком, который выполняет все три эти действия; если первый поток возобновляет выполнение и сохраняет его значение, он переопределяет objCt, не принимая во внимание тот факт, что в промежутке значение изменилось.In a multithreaded application, a thread that has loaded and incremented the value might be preempted by another thread which performs all three steps; when the first thread resumes execution and stores its value, it overwrites objCt without taking into account the fact that the value has changed in the interim.

Конкретно этого состояния гонки можно легко избежать, применяя методы класса Interlocked, например Interlocked.Increment.This particular race condition is easily avoided by using methods of the Interlocked class, such as Interlocked.Increment. Сведения о других технологиях синхронизации данных между несколькими потоками см. в разделе Синхронизация данных для многопоточности.To read about other techniques for synchronizing data among multiple threads, see Synchronizing Data for Multithreading.

Конфликты могут также возникать при синхронизации действий различных потоков.Race conditions can also occur when you synchronize the activities of multiple threads. При написании каждой строки кода необходимо учитывать, что может произойти, если поток будет вытеснен другим потоком до ее выполнения (или до одной из индивидуальных машинных команд, составляющих эту строку).Whenever you write a line of code, you must consider what might happen if a thread were preempted before executing the line (or before any of the individual machine instructions that make up the line), and another thread overtook it.

Статические члены и статические конструкторыStatic members and static constructors

Класс не инициализируется, пока не завершится выполнение его конструктора (конструктор static в C# Shared Sub New в Visual Basic).A class is not initialized until its class constructor (static constructor in C#, Shared Sub New in Visual Basic) has finished running. Чтобы предотвратить выполнение кода в еще не инициализированном типе, CLR блокирует все вызовы из других потоков для членов класса static (члены Shared в Visual Basic) до тех пор, пока выполнение конструктора класса не будет завершено.To prevent the execution of code on a type that is not initialized, the common language runtime blocks all calls from other threads to static members of the class (Shared members in Visual Basic) until the class constructor has finished running.

Например, если конструктор класса запускает новый поток, а процедура потока вызывает член static класса, новый поток блокируется до завершения конструктора класса.For example, if a class constructor starts a new thread, and the thread procedure calls a static member of the class, the new thread blocks until the class constructor completes.

Это относится к любому типу, который может иметь конструктор static.This applies to any type that can have a static constructor.

Число процессоровNumber of processors

Наличие нескольких процессоров или только одного процессора в системе может повлиять на многопоточную архитектуру.Whether there are multiple processors or only one processor available on a system can influence multithreaded architecture. Дополнительные сведения см. в разделе Количество процессоров.For more information, see Number of Processors.

Используйте свойство Environment.ProcessorCount, чтобы определить количество процессоров, доступных во время выполнения.Use the Environment.ProcessorCount property to determine the number of processors available at runtime.

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

При использовании нескольких потоков соблюдайте следующие рекомендации:Consider the following guidelines when using multiple threads:

  • Не используйте Thread.Abort для завершения других потоков.Don't use Thread.Abort to terminate other threads. Вызов метода Abort для другого потока аналогичен вызову исключения в этом потоке, когда неизвестно, на каком этапе находится обработка этого потока.Calling Abort on another thread is akin to throwing an exception on that thread, without knowing what point that thread has reached in its processing.

  • Не используйте Thread.Suspend и Thread.Resume для синхронизации действий между потоками.Don't use Thread.Suspend and Thread.Resume to synchronize the activities of multiple threads. Используйте вместо этого Mutex, ManualResetEvent, AutoResetEvent и Monitor.Do use Mutex, ManualResetEvent, AutoResetEvent, and Monitor.

  • Не контролируйте выполнение рабочих потоков из основной программы (например, с помощью событий).Don't control the execution of worker threads from your main program (using events, for example). Вместо этого составьте программу так, чтобы рабочие потоки ожидали доступности задания, выполняли его и оповещали другие части программы о его завершении.Instead, design your program so that worker threads are responsible for waiting until work is available, executing it, and notifying other parts of your program when finished. Если рабочие потоки не блокируются, можно использовать потоки из пула потоков.If your worker threads do not block, consider using thread pool threads. Monitor.PulseAll можно использовать в ситуациях, когда рабочие потоки блокируются.Monitor.PulseAll is useful in situations where worker threads block.

  • Не используйте типы как объекты блокировки.Don't use types as lock objects. Это означает, что следует избегать кода lock(typeof(X)) в C# или SyncLock(GetType(X)) в Visual Basic, а также использования Monitor.Enter с объектами Type.That is, avoid code such as lock(typeof(X)) in C# or SyncLock(GetType(X)) in Visual Basic, or the use of Monitor.Enter with Type objects. Для каждого конкретного типа существует только один экземпляр System.Type в каждом домене приложения.For a given type, there is only one instance of System.Type per application domain. Если блокируемый тип является открытым, его может заблокировать чужой код, вызвав тем самым взаимоблокировку.If the type you take a lock on is public, code other than your own can take locks on it, leading to deadlocks. Дополнительные вопросы см. Рекомендации по обеспечению надежности.For additional issues, see Reliability Best Practices.

  • Будьте внимательны при блокировке экземпляров, например lock(this) в C# или SyncLock(Me) в Visual Basic.Use caution when locking on instances, for example lock(this) in C# or SyncLock(Me) in Visual Basic. Если другой код в приложении, который является внешним для типа, заблокирует объект, может возникнуть взаимоблокировка.If other code in your application, external to the type, takes a lock on the object, deadlocks could occur.

  • Следите за тем, чтобы каждый поток, который входит в монитор, обязательно вышел из этого монитора, даже если за время, пока поток находится в мониторе, возникает исключение.Do ensure that a thread that has entered a monitor always leaves that monitor, even if an exception occurs while the thread is in the monitor. Оператор C# lock и оператор Visual Basic SyncLock делают это автоматически, обеспечивая вызов метода Monitor.Exit с помощью блока finally.The C# lock statement and the Visual Basic SyncLock statement provide this behavior automatically, employing a finally block to ensure that Monitor.Exit is called. Если вы не можете проконтролировать вызов метода Exit, включите в свое приложение мьютекс.If you cannot ensure that Exit will be called, consider changing your design to use Mutex. Мьютекс автоматически освобождается, как только прекращается выполнение владеющего им потока.A mutex is automatically released when the thread that currently owns it terminates.

  • Для задач, которые требуют различных ресурсов, используйте несколько потоков и старайтесь не назначать несколько потоков одному ресурсу.Do use multiple threads for tasks that require different resources, and avoid assigning multiple threads to a single resource. Например, любая задача с использованием ввода-вывода выигрывает от наличия собственного потока, поскольку во время операций ввода-вывода этот поток блокируется и, таким образом, разрешает выполнение других потоков.For example, any task involving I/O benefits from having its own thread, because that thread will block during I/O operations and thus allow other threads to execute. Входные данные пользователя — еще один ресурс, которому пойдет на пользу выделенный поток.User input is another resource that benefits from a dedicated thread. На однопроцессорном компьютере задача, требующая активных вычислений, сосуществует с входными данными пользователя и задачами, которые предусматривают операции ввода-вывода, однако несколько ресурсоемких задач могут конкурировать друг с другом.On a single-processor computer, a task that involves intensive computation coexists with user input and with tasks that involve I/O, but multiple computation-intensive tasks contend with each other.

  • Вместо оператора lock (SyncLock в Visual Basic) для простого изменения состояния лучше использовать методы класса Interlocked.Consider using methods of the Interlocked class for simple state changes, instead of using the lock statement (SyncLock in Visual Basic). Оператор lock — хороший универсальный инструмент, но класс Interlocked обеспечивает высокую производительность для обновлений, которые должны быть атомарными.The lock statement is a good general-purpose tool, but the Interlocked class provides better performance for updates that must be atomic. Если конкуренции нет, он выполняет внутри единственный префикс lock.Internally, it executes a single lock prefix if there is no contention. При проверке кода ищите код, похожий на показанный в следующих примерах.In code reviews, watch for code like that shown in the following examples. В первом примере увеличивается переменная состояния:In the first example, a state variable is incremented:

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

    Вы можете повысить производительность, применяя метод Increment вместо оператора lock, как показано ниже.You can improve performance by using the Increment method instead of the lock statement, as follows:

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

    Примечание

    В .NET Framework 2.0 и более поздних версий используйте метод Add для атомарных приращений более 1.In the .NET Framework 2.0 and later, use the Add method for atomic increments larger than 1.

    Во втором примере переменная ссылочного типа обновляется только в том случае, если она является пустой ссылкой (Nothing в Visual Basic).In the second example, a reference type variable is updated only if it is a null reference (Nothing in 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, как показано ниже.Performance can be improved by using the CompareExchange method instead, as follows:

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

    Примечание

    Начиная с .NET Framework 2.0 перегрузка метода CompareExchange<T>(T, T, T) предоставляет типобезопасную альтернативу для ссылочных типов.Beginning with .NET Framework 2.0, the CompareExchange<T>(T, T, T) method overload provides a type-safe alternative for reference types.

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

При разработке библиотек классов для многопоточности необходимо учитывать следующие рекомендации.Consider the following guidelines when designing class libraries for multithreading:

  • Старайтесь не создавать потребность в синхронизации.Avoid the need for synchronization, if possible. Особенно это относится к коду, который используется наиболее часто.This is especially true for heavily used code. Например, алгоритм можно скорректировать таким образом, чтобы он допускал конфликты, а не устранял их.For example, an algorithm might be adjusted to tolerate a race condition rather than eliminate it. Ненужная синхронизация снижает производительность и может привести к взаимоблокировке и конфликтам.Unnecessary synchronization decreases performance and creates the possibility of deadlocks and race conditions.

  • Сделайте статические данные (Shared в Visual Basic) по умолчанию потокобезопасными.Make static data (Shared in Visual Basic) thread safe by default.

  • Данные экземпляров не должны быть потокобезопасными по умолчанию.Do not make instance data thread safe by default. Добавление блокировок для создания потокобезопасного кода снижает производительность, увеличивает конфликт блокировки и создает условия для возникновения взаимоблокировок.Adding locks to create thread-safe code decreases performance, increases lock contention, and creates the possibility for deadlocks to occur. В обычных моделях приложений пользовательский код одновременно выполняется только одним потоком, что уменьшает необходимость потокобезопасности.In common application models, only one thread at a time executes user code, which minimizes the need for thread safety. По этой причине библиотеки классов .NET Framework не являются потокобезопасными по умолчанию.For this reason, the .NET Framework class libraries are not thread safe by default.

  • Не предоставляйте статические методы, изменяющие статическое состояние.Avoid providing static methods that alter static state. В обычных сценариях сервера статическое состояние используется запросами совместно, а значит, код одновременно могут выполнять сразу несколько потоков.In common server scenarios, static state is shared across requests, which means multiple threads can execute that code at the same time. Это открывает возможность для появления потоковых ошибок.This opens up the possibility of threading bugs. Попробуйте применить конструктивный шаблон, инкапсулирующий данные в экземпляры, которые не являются общими для запросов.Consider using a design pattern that encapsulates data into instances that are not shared across requests. Кроме того, если статические данные синхронизируются, вызовы между статическими методами, изменяющие состояние, могут приводить к взаимоблокировкам или избыточной синхронизации, что, в свою очередь, снижает производительность.Furthermore, if static data are synchronized, calls between static methods that alter state can result in deadlocks or redundant synchronization, adversely affecting performance.

См. такжеSee also