Можно ли внутри Semaphore использовать lock?

Рейтинг: 1Ответов: 1Опубликовано: 18.01.2023

Я написал следующий код:

public class WorkerQueue<T>
{
    private readonly List<T> _resource = new();

    private readonly Semaphore _semaphore = new(5, 10);

    public void AddItem(T item)
    {
        Console.WriteLine($"Поток {Thread.CurrentThread.Name} готов добавить элемент");

        _semaphore.WaitOne();

        try
        {
            lock (_resource)
            {
                _resource.Add(item);
            }
        }
        finally
        {
            Console.WriteLine($"Поток {Thread.CurrentThread.Name} добавил элемент");
            _semaphore.Release();
        }
    }

    public int GetCountItems()
    {
        return _resource.Count();
    }

    public IEnumerable<T> GetItems()
    {
        return _resource.OrderBy(x=>x);
    }
}

public class Program
{
    private static List<Thread> _threads = new();

    private static WorkerQueue<int> _worker = new();

    static void Main(string[] args)
    {
        for (int i = 1; i <= 6; i++)
        {
            Thread thread = new(ThreadProc);

            thread.Name = i.ToString();

            thread.Start();
        }

        Console.ReadLine();

        int count = _worker.GetCountItems();

        var items = _worker.GetItems().GroupBy(x=>x)
            .Select(x=> new
            {
                Key = x.Key,
                Count =  x.Count()
            }).Where(x=> x.Count != 6);
    }

    private static void ThreadProc()
    {
        for (int i = 0; i < 100000; i++)
        {
            _worker.AddItem(i);
        }
    }
}

Я здесь использую lock так как внутри Semaphore к моему ресурсу будет одновременно обращаться 5 потоков, что приведёт к тому, что у меня в списке будут неполные данные. Но есть подозрения, что тогда тут Semaphore и не нужен вообще)

Ответы

▲ 2Принят

Semaphore ограничивает число одновременных заходов в блок кода, поэтому он может являться заменой lock только в том случае, если семафор настроен ровно на 1 заход. А если он настроен, скажем, на 5 одновременных заходов, то такой код не будет потокобезопасным, нужно применять lock или использовать потокобезопасные коллекции.

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

И ещё - всегда выделяйте для lock отдельный простой объект object, обычно его так и называют lockObject, никогда не делайте lock на сложные объекты (например, коллекции), и те объекты, которые вы используете в своём коде каким-то другим образом. Это может привести к нехорошим последствиям. Объяснения можно прочитать в документации, но проще запомнить - для lock используйте отдельный приватный object, специально выделенный для этой цели.

Вообще Semaphore используется не для создания потокобезопасного кода, а чтобы создать "бутылочное горлышко", не позволяющее блоку кода одновременно обрабатывать слишком много запросов. Это может быть нужно по разным причинам. Например, если у вас есть код, который очень сильно потребляет ресурсы: CPU, память, сеть, БД, и есть вероятность, что в этот фрагмент кода попадёт одновременно много запросов, то у вас просто не хватит ресурсов, чтобы это всё разгрести одновременно (например, кончится память на сервере и программа просто упадёт), либо код будет просто не оптимально работать (своппинг, "драка" за CPU, блокировки БД) - вот тогда можно использовать Semaphore.

И ещё сейчас чаще используют SemaphoreSlim, он более легковесный.

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

P.S. Да, конкретно в вашем случае семафор вообще бесполезен, конечно, всё-равно всё упрётся в lock. Но если бы в вашем коде было что-то ещё, а lock ограничивал бы только небольшие части вашего кода, тогда в нём, возможно, был бы смысл.