[Перевод] Решение проблем с race condition и критическими секциями в C#

Race condition в C# возникает, когда два или более потока одновременно обращаются к общим данным, и результат программы зависит от непредсказуемого порядка выполнения этих потоков. Это может привести к несогласованным или некорректным результатам, делая проблемы race condition критическими в многопоточных приложениях.

В этой статье мы изучим все на практике и постараемся не только понять, но и решить проблемы race condition и критических секций в .NET.

Как возникают race condition? Race condition, как правило, возникают при выполнении следующих условий:

  1. Общий ресурс: два или более потока пытаются читать, записывать или изменять один и тот же общий ресурс (например, переменную, объект или файл).

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

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

Если вам не нравится читать статьи, вот моё видео на YouTube, где я объясняю всё по шагам.

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

Перейдём прямо к нашему примеру ниже и изучим обе стороны вопроса.

public class Transaction
{
    public bool IsDone { get; set; }
    public void Transfer(decimal amount)
    {
        if (!IsDone)//critical section
        {
            Console.WriteLine($"Transaction operation started in thread number = {Environment.CurrentManagedThreadId}");
            TransferInternally(amount);
            Console.WriteLine($"Transaction operation ended in thread number = {Environment.CurrentManagedThreadId}");
            IsDone = true;
        }
    }
    private void TransferInternally(decimal amount)
    {
        Console.WriteLine($"TransferInternally in thread...{Environment.CurrentManagedThreadId}, the amount = {amount}");
        Console.WriteLine($"TransferInternally is done in thread...{Environment.CurrentManagedThreadId}, the amount = {amount}");
    }
}

В предоставленном коде критическая секция и проблема race condition связаны с тем, как несколько потоков могут взаимодействовать с классом Transaction, в частности, со свойством IsDone и методом Transfer.

Критическая секция

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

if (!IsDone)//critical section
        {
            Console.WriteLine($"Transaction operation started in thread number = {Environment.CurrentManagedThreadId}");
            TransferInternally(amount);
            Console.WriteLine($"Transaction operation ended in thread number = {Environment.CurrentManagedThreadId}");
            IsDone = true;
        }

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

Проблема race condition

Race condition возникает, когда результат выполнения программы зависит от времени или порядка выполнения потоков. В этом коде race condition может произойти, если несколько потоков одновременно вызовут метод Transfer.

Сценарий, ведущий к race condition

  1. Поток 1 проверяет свойство IsDone и обнаруживает, что оно имеет значение false.

  2. Поток 1 продолжает выполнение транзакции, выводит сообщение о начале и входит в метод TransferInternally.

  3. Затем Поток 2 проверяет свойство IsDone до того, как Поток 1 завершит выполнение, и также видит, что оно имеет значение false.

  4. Поток 2 также продолжает выполнение транзакции, несмотря на то, что её уже обрабатывает Поток 1.

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

Вот как выглядит наш основной метод:

static void Main(string[] args)
{
    Transaction2 transaction = new Transaction2();
    for (int i = 0; i < 10; i++)
    {
        Task.Run(() =>
        {
            transaction.Transfer(3000);
        });
    }
    Console.ReadLine();
}

Предоставленный код демонстрирует, как может возникнуть проблема race condition, когда несколько потоков одновременно пытаются выполнить метод Transfer класса Transaction.

Объяснение кода Экземпляр Transaction\

Transaction transaction = new Transaction();

Здесь создаётся единственный экземпляр класса Transaction. Этот экземпляр используется всеми потоками, которые будут созданы в последующем цикле.

Создание и выполнение задач:

for (int i = 0; i < 10; i++)
{
    Task.Run(() =>
    {
        transaction.Transfer(3000);
    });
}

Этот цикл выполняется 10 раз, и каждая итерация запускает новую задачу с использованием Task.Run. Каждая задача вызывает метод Transfer объекта transaction, пытаясь перевести сумму 3000.

Задача Каждый Task.Run порождает новый поток (или использует один из пула потоков) для выполнения кода в лямбда-выражении. Таким образом, до 10 потоков могут одновременно выполнять метод Transfer.

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

Race condition возникает, потому что проверка IsDone и последующие операции не являются атомарными. Это означает, что даже если один поток установит IsDone в true, другие потоки могли уже пройти проверку и также выполняют перевод, что приводит к многократному выполнению того, что должно быть одноразовой транзакцией.

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

The result of race condition in C#

The result of race condition in C#

Чтобы предотвратить проблемы race condition и захватить критическую секцию, мы будем использовать ключевое слово lock. Конечно, существует множество способов избежать этих проблем, но самый простой из них — использование ключевого слова lock.

public class Transaction2
{
    public bool IsDone { get; set; }
    private static readonly object _object = new object();
    public void Transfer(decimal amount)
    {
        lock(_object)
        {
            if (!IsDone)//should act as a single atomic operation
            {
                Console.WriteLine($"Transaction operation started in thread number = {Environment.CurrentManagedThreadId}");
                TransferInternally(amount);
                Console.WriteLine($"Transaction operation ended in thread number = {Environment.CurrentManagedThreadId}");
                IsDone = true;
            }

        }

    }

    private void TransferInternally(decimal amount)
    {
        Console.WriteLine($"TransferInternally in thread...{Environment.CurrentManagedThreadId}, the amount = {amount}");
        Console.WriteLine($"TransferInternally is done in thread...{Environment.CurrentManagedThreadId}, the amount = {amount}");
    }
}

resolving race condition

resolving race condition

© Habrahabr.ru