Можно ли внутри Semaphore использовать lock?
Я написал следующий код:
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 и не нужен вообще)
Ответы (1 шт):
Semaphore ограничивает число одновременных заходов в блок кода, поэтому он может являться заменой lock только в том случае, если семафор настроен ровно на 1 заход. А если он настроен, скажем, на 5 одновременных заходов, то такой код не будет потокобезопасным, нужно применять lock или использовать потокобезопасные коллекции.
И да, под lock возможно нужно помещать не только добавление в список, но и какие-то обращения к нему, тут я точно не скажу, лучше посмотреть какую-нибудь готовую реализацию потокобезопасного списка на эту тему или прочитать в документации, какие обращения к этой коллекции являются потокобезопасными, а какие нет.
И ещё - всегда выделяйте для lock отдельный простой объект object, обычно его так и называют lockObject, никогда не делайте lock на сложные объекты (например, коллекции), и те объекты, которые вы используете в своём коде каким-то другим образом. Это может привести к нехорошим последствиям. Объяснения можно прочитать в документации, но проще запомнить - для lock используйте отдельный приватный object, специально выделенный для этой цели.
Вообще Semaphore используется не для создания потокобезопасного кода, а чтобы создать "бутылочное горлышко", не позволяющее блоку кода одновременно обрабатывать слишком много запросов. Это может быть нужно по разным причинам. Например, если у вас есть код, который очень сильно потребляет ресурсы: CPU, память, сеть, БД, и есть вероятность, что в этот фрагмент кода попадёт одновременно много запросов, то у вас просто не хватит ресурсов, чтобы это всё разгрести одновременно (например, кончится память на сервере и программа просто упадёт), либо код будет просто не оптимально работать (своппинг, "драка" за CPU, блокировки БД) - вот тогда можно использовать Semaphore.
И ещё сейчас чаще используют SemaphoreSlim, он более легковесный.
В целом семафор полезен, например, для кода, разгребающего некую очередь - чтобы чётко ограничить в ресурсах обработчик очереди.
P.S. Да, конкретно в вашем случае семафор вообще бесполезен, конечно, всё-равно всё упрётся в lock. Но если бы в вашем коде было что-то ещё, а lock ограничивал бы только небольшие части вашего кода, тогда в нём, возможно, был бы смысл.