В ноябре 2024 года с выходом .NET 9 и обновлением C# до версии 13 мы получили интересные нововведения, которые касаются типов данных и семантики блокировок. Каждый крупный релиз .NET сопровождается новыми инструментами, которые улучшают производительность, безопасность и удобство разработки. В C# 13 такой новинкой стал System.Threading.Lock. Это попытка сделать многопоточность чуть менее токсичной и чуть более предсказуемой.

Что не так со старым lock?

Традиционно в C# для обеспечения взаимного исключения потоков используется ключевое слово lock в сочетании с любым объектом (object). Однако такой подход может приводить к узким местам в производительности и потенциальным рискам взаимной блокировки (deadlock).

С выходом C# 13 и .NET 9 был представлен новый тип блокировки — System.Threading.Lock. Это специализированный механизм синхронизации, который улучшает управление многопоточностью, предоставляя более безопасный и производительный способ блокировки потоков.

Почему это важно?

Многопоточность — это всегда компромисс между производительностью и сложностью реализации. System.Threading.Lock предлагает два основных преимущества:

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

  2. Гибкость и контроль: с System.Threading.Lock вы можете настроить тайм-ауты и отмену операций, избегая зависания потоков.

Кроме того, новая структура эффективнее работает в сценариях с высокой нагрузкой.

Основные сценарии применения

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

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

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

Пример:

Ниже приведен System.Threading.Lockпример кода, демонстрирующий безопасное обновление общего ресурса в многопоточной среде:

using System;
using System.Threading;
using System.Threading.Tasks;

public class Account
{
    private decimal _balance;
    private Lock _balanceLock = new Lock();

    public Account(decimal initialBalance)
    {
        _balance = initialBalance;
    }

    public void Debit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive", nameof(amount));

        using (_balanceLock.EnterScope())
        {
            if (_balance < amount)
                throw new InvalidOperationException("Insufficient funds");

            _balance -= amount;
        }
    }

    public void Credit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive", nameof(amount));

        using (_balanceLock.EnterScope())
        {
            _balance += amount;
        }
    }

    public decimal GetBalance()
    {
        using (_balanceLock.EnterScope())
        {
            return _balance;
        }
    }
}

public class Program
{
    public static async Task Main()
    {
        var account = new Account(1000m);

        var tasks = new Task[10];
        for (int i = 0; i < tasks.Length; i++)
        {
            tasks[i] = Task.Run(() =>
            {
                for (int j = 0; j < 100; j++)
                {
                    account.Credit(10);
                    account.Debit(10);
                }
            });
        }

        await Task.WhenAll(tasks);

        Console.WriteLine($"Final balance: {account.GetBalance()}");
    }
}

В приведенном коде:

  • Класс Account: представляет банковский счёт, включает методы для списания, зачисления и получения баланса.

  • Поле _balanceLock: использует новый тип Lock из System.Threading, чтобы обеспечить потокобезопасный доступ к полю _balance и избежать ошибок, связанных с выбором объекта для традиционного lock.

  • Метод EnterScope(): применяется для входа в область блокировки, обеспечивая, что доступ к общему ресурсу будет безопасным и взаимно-исключающим, благодаря специализированному типу блокировки.

  • Оператор using: автоматизирует освобождение блокировки в конце области действия, предотвращая возникновение взаимоблокировок и упрощая управление блокировками.

Благодаря использованию нового типа System.Threading.Lock код обеспечивает более эффективную и гибкую синхронизацию потоков, снижая риски, связанные с производительностью и потенциальными проблемами, возникающими при использовании универсальных объектов для lock.

Реализация System.Threading.Lock основана на следующих ключевых концепциях:

  • Специализированный объект блокировки: System.Threading.Lock — это тип, специально разработанный для синхронизации потоков, что позволяет избежать недостатков традиционного подхода с использованием произвольных объектов для блокировок.

  • Управление областью действия: метод EnterScope() обеспечивает вход в область блокировки, гарантируя потокобезопасный доступ к общим ресурсам.

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

Комментарии (7)


  1. cstrike
    24.01.2025 12:35

    Где примеры использования? Где side-by-side сравнение кода? Простите, но статья пустая. Что-то там восхваляется, но эти фанфары ничем не подтверждены.


  1. TerekhinSergey
    24.01.2025 12:35

    А ещё бы сравнение с другими примитивами - семафором, readwrite lock, которые уже есть... Бенчмарки для всех вариантов бы


  1. withkittens
    24.01.2025 12:35

    Мне кажется, вы неправильно поняли фичу? lock никуда не девается, плюшки появляются из-за использования System.Threading.Lock вместо object, а не потому что мы lock вручную переписываем на using, в доке есть пример (подозрительно похожий на ваш ;)


    1. sibvic
      24.01.2025 12:35

      Логично, потому что часто встречался код вида

      object dataLock = new object();
      List<string> data;
      int someOtherData;
      
      void SomeFunc()
      {
        lock (dataLock) {...}
      }

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


  1. ryanl
    24.01.2025 12:35

    "в средах с высокой конкуренцией, где требуется частая блокировка и разблокировка, System.Threading.Lock снижает издержки на переключение контекста." - Ла ла ла, дай-ка я на хабр напишу ерунды в пятницу...


  1. Ydav359
    24.01.2025 12:35

    Статья от нейросети?


  1. NightBlade74
    24.01.2025 12:35

    var account = new Account(1000m);
    
    Parallel.For(0, 10, (_) =>
    {
        for (int j = 0; j++ < 100;)
        {
            account.Credit(10);
            account.Debit(10);
        }
    });
    
    Console.WriteLine($"Final balance: {account.GetBalance()}");
    
    
    internal class Account(decimal initialBalance)
    {
        private decimal _balance = initialBalance;
        private readonly Lock _balanceLock = new();
    
    
        public void Debit(decimal amount)
        {
            ArgumentOutOfRangeException.ThrowIfNegative(amount, nameof(amount));
    
            lock (_balanceLock)
            {
                if (_balance < amount)
                    throw new InvalidOperationException("Insufficient funds");
    
                _balance -= amount;
            }
        }
    
    
        public void Credit(decimal amount)
        {
            ArgumentOutOfRangeException.ThrowIfNegative(amount, nameof(amount));
    
            lock (_balanceLock)
                _balance += amount;
        }
    
    
        public decimal GetBalance()
        {
            lock (_balanceLock)
                return _balance;
        }
    }