1

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

public class WorkerQueue<T>
{
    private readonly List<T> _resource = new();
private readonly Semaphore _semaphore = new(5, 10);

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

    _semaphore.WaitOne();

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

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

public IEnumerable&lt;T&gt; GetItems()
{
    return _resource.OrderBy(x=&gt;x);
}

}

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

private static WorkerQueue&lt;int&gt; _worker = new();

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

        thread.Name = i.ToString();

        thread.Start();
    }

    Console.ReadLine();

    int count = _worker.GetCountItems();

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

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

}

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

  • 2
    Такие же подозрения, смысл в семафоре, если в итоге все равно lock? – tym32167 Jan 18 '23 at 19:58
  • 1
    GetCountItems() должно быть под локом, GetItems() - под лок и снапшотом возвращать. В противном случае, с семафором, локом или без, этот код нельзя назвать потокобезопасным. return _resource.Count(); можно оптимизировать - return _resource.Count; – aepot Jan 18 '23 at 20:12
  • 1
    вообще, тут больше вопросов, чем ответов. Например, зачем вам lock вообще, почему бы не использовать ConcurrentBag и е париться о конкурентном доступе. Ну и почему класс называется Queue, но там только добавление, а извлечения из очерези нет? Но я так понял, это учебный пример, для учебного это ок конечно, чисто поиграть с многопоточностью. – tym32167 Jan 18 '23 at 21:15
  • https://ru.stackoverflow.com/q/1303748/373567 вот немного примеров использования семафора – aepot Jan 19 '23 at 10:41

1 Answers1

2

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

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

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

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

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

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

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

CrazyElf
  • 71,194
  • 1
    lockObject - syncRoot? – aepot Jan 19 '23 at 10:38
  • @aepot Да без разницы в общем как называть по большому счёту то. – CrazyElf Jan 19 '23 at 11:37
  • 1
    @aepot тоже всегда называл lockObject :) Имхо, но тут дело вкуса) – OwDafuq Jan 19 '23 at 12:57
  • 1
    @LiptonDev это из разряда List<User> usersList, читать полезную книжку Роберт Мартин "Чистый код". – aepot Jan 19 '23 at 13:29