Resumo : neste tutorial, você aprenderá como usar o C# SemaphoreSlim
para limitar o número de threads que podem acessar um recurso compartilhado simultaneamente.
Introdução à classe C# SemaphoreSlim
Um semáforo é um mecanismo para limitar o número de threads que podem acessar um recurso compartilhado simultaneamente.
O conceito de semáforo é baseado em um contador que especifica a quantidade de recursos disponíveis.
Para acessar o recurso compartilhado, um thread precisa solicitar uma permissão do semáforo.
Se a licença estiver disponível, o semáforo diminuirá o contador. Entretanto, se o contador for zero, o semáforo será bloqueado até que uma licença seja disponibilizada.
Depois que o thread concluir o processamento do recurso compartilhado, ele precisará liberar a permissão de volta ao semáforo para que outros threads possam obter a permissão. Quando a thread libera a permissão, o semáforo incrementa o contador
C# possui duas classes que implementam o conceito de semáforo: Semaphore
e SemaphoreSlim
.
A Semaphore
classe está disponível desde a versão inicial do .NET Framework. A SemaphoreSlim
é a classe mais recente introduzida no .NET Framework 4.0
e no .NET core.
A
classe é uma implementação leve da SemaphoreSlim
Semaphore
classe. O
é mais rápido e mais eficiente em termos de memória do que a SemaphoreSlim
Semaphore
classe.
Como usar a classe SemaphoreSlim
Para usar a SemaphoreSlim
classe, siga estas etapas:
Primeiro, crie um SemaphoreSlim
objeto e passe o número inicial de permissões para seu construtor:
SemaphoreSlim semaphore = new(3);
Linguagem de código: C# ( cs )
Neste exemplo, o objeto semáforo possui um contador inicial de 3. Isso significa que até três threads podem acessar recursos compartilhados simultaneamente.
Segundo, chame o WaitAsync()
método do objeto semáforo para solicitar uma permissão:
await semaphore.WaitAsync();
Linguagem de código: C# ( cs )
O WaitAsync()
método retorna um Task
objeto que aguarda a concessão da licença.
Terceiro, chame o Release()
método do objeto semáforo para liberar a permissão assim que você concluir o acesso ao recurso compartilhado:
semaphore.Release();
Linguagem de código: C# ( cs )
É uma boa prática usar o try...finally
bloco para garantir que o Release()
método seja sempre chamado mesmo se uma exceção for levantada ao acessar o recurso compartilhado:
await semaphore.WaitAsync();
try
{
// Access the shared resource
}
finally
{
semaphore.Release();
}
Linguagem de código: C# ( cs )
O exemplo a seguir demonstra como usar a SemaphoreSlim
classe para permitir que um número limitado de tarefas possa acessar o recurso compartilhado simultaneamente:
using static System.Console;
SemaphoreSlim semaphore = new(3);
int amount = 0;
async Task AccessAsync(int id)
{
WriteLine($"Task {id} is waiting to access the amount.");
await semaphore.WaitAsync();
try
{
WriteLine($"Task {id} is now accessing the amount.");
// simulate some work
await Task.Delay(TimeSpan.FromSeconds(1));
// increase the counter
Interlocked.Increment(ref amount);
// completed the work
WriteLine($"Task {id} has completed accessing the amount {amount}");
}
finally
{
semaphore.Release();
}
}
// start 10 tasks to access the amount concurrently
var tasks = new List<Task>();
for (int i = 1; i <= 10; i++)
{
tasks.Add(AccessAsync(i));
}
// wait for all task to complete
await Task.WhenAll(tasks);
WriteLine("All tasks completed.");
ReadLine();
Linguagem de código: C# ( cs )
Como funciona.
Primeiro, crie um SemaphoreSlim
objeto que permita que até três tarefas acessem o recurso compartilhado ao mesmo tempo:
SemaphoreSlim semaphore = new(3);
Linguagem de código: C# ( cs )
A seguir, declare a amount
variável que atua como um recurso compartilhado:
int amount = 0;
Linguagem de código: C# ( cs )
Em seguida, defina um método assíncrono AccessAsync
que receba um parâmetro inteiro id
:
async Task AccessAsync(int id)
{
WriteLine($"Task {id} is waiting to access the amount.");
await semaphore.WaitAsync();
try
{
WriteLine($"Task {id} is now accessing the amount.");
// simulate some work
await Task.Delay(TimeSpan.FromSeconds(1));
// increase the counter
Interlocked.Increment(ref amount);
// completed the work
WriteLine($"Task {id} has completed accessing the amount {amount}");
}
finally
{
semaphore.Release();
}
}
Linguagem de código: C# ( cs )
O AccessAsync()
método simula o acesso à variável compartilhada amount
usando o objeto semáforo.
A AccessAsync()
chamada do método WaitAsync()
do objeto semáforo para aguardar uma licença disponível, incrementa a amount
variável usando o Increment()
método da Interlocked
classe e retorna a licença de volta ao semáforo usando o Release()
método do objeto semáforo.
O AccessAsync()
método também grava algumas mensagens no console para indicar o início e o fim da tarefa.
Depois disso, crie dez tarefas que executam o AccessAsync
método e aguarde a conclusão delas usando o
método:Task.WhenAll
()
// start 10 tasks to access the amount concurrently
var tasks = new List<Task>();
for (int i = 1; i <= 10; i++)
{
tasks.Add(AccessAsync(i));
}
// wait for all task to complete
await Task.WhenAll(tasks);
Linguagem de código: C# ( cs )
Por fim, escreva uma mensagem no console para notificar a conclusão de todas as tarefas e aguarde o usuário pressionar uma tecla antes de encerrar o programa:
WriteLine("All tasks completed.");
ReadLine();
Linguagem de código: C# ( cs )
O seguinte mostra a saída do programa:
Task 1 is waiting to access the amount.
Task 1 is now accessing the amount.
Task 2 is waiting to access the amount.
Task 2 is now accessing the amount.
Task 3 is waiting to access the amount.
Task 3 is now accessing the amount.
Task 4 is waiting to access the amount.
Task 5 is waiting to access the amount.
Task 6 is waiting to access the amount.
Task 7 is waiting to access the amount.
Task 8 is waiting to access the amount.
Task 9 is waiting to access the amount.
Task 10 is waiting to access the amount.
Task 1 has completed accessing the amount 1
Task 3 has completed accessing the amount 2
Task 5 is now accessing the amount.
Task 4 is now accessing the amount.
Task 2 has completed accessing the amount 3
Task 6 is now accessing the amount.
Task 5 has completed accessing the amount 6
Task 6 has completed accessing the amount 6
Task 8 is now accessing the amount.
Task 7 is now accessing the amount.
Task 4 has completed accessing the amount 6
Task 9 is now accessing the amount.
Task 9 has completed accessing the amount 7
Task 8 has completed accessing the amount 8
Task 7 has completed accessing the amount 9
Task 10 is now accessing the amount.
Task 10 has completed accessing the amount 10
All tasks completed.
Linguagem de código: C# ( cs )
A saída mostra que:
- Apenas três tarefas podem acessar o valor da variável compartilhada simultaneamente.
- Assim que uma licença estiver disponível, a próxima tarefa poderá acessar a variável compartilhada.
Um exemplo prático da classe SemaphoreSlim
O programa a seguir baixa arquivos de forma assíncrona usando classes SemaphoreSlim
e HttpClient
:
using static System.Console;
var client = new HttpClient();
var semaphore = new SemaphoreSlim(3);
async Task DownloadFileAsync(string url)
{
await semaphore.WaitAsync();
try
{
WriteLine($"Downloading file {url}...");
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync())
using (var fileStream = File.Create(Path.GetFileName(url)))
{
await stream.CopyToAsync(fileStream);
}
WriteLine($"Completed Downloading file {url}.");
}
finally
{
semaphore.Release();
}
}
var urls = new List<string>
{
"https://www.ietf.org/rfc/rfc791.txt",
"https://www.ietf.org/rfc/rfc792.txt",
"https://www.ietf.org/rfc/rfc793.txt",
"https://www.ietf.org/rfc/rfc794.txt",
"https://www.ietf.org/rfc/rfc795.txt",
"https://www.ietf.org/rfc/rfc796.txt",
"https://www.ietf.org/rfc/rfc797.txt",
"https://www.ietf.org/rfc/rfc798.txt",
"https://www.ietf.org/rfc/rfc799.txt",
"https://www.ietf.org/rfc/rfc800.txt",
};
var tasks = new List<Task>();
foreach (var url in urls)
{
tasks.Add(DownloadFileAsync(url));
}
await Task.WhenAll(tasks);
WriteLine("Completed downloading all files.");
Linguagem de código: C# ( cs )
Como funciona.
Primeiro, crie uma nova instância da HttpClient
classe que faz solicitações HTTP:
var client = new HttpClient();
Linguagem de código: C# ( cs )
Segundo, crie um novo SemaphoreSlim
objeto que limite o número de downloads simultâneos a três:
var semaphore = new SemaphoreSlim(3);
Linguagem de código: C# ( cs )
Terceiro, defina o DownloadFileAsync()
método que usa uma URL como parâmetro. O método espera no semáforo usando o WaitAsync()
método, usa o HttpClient
objeto para baixar o arquivo especificado pela URL e salva o arquivo no sistema de arquivos. Além disso, o método libera o semáforo usando o Release()
método:
async Task DownloadFileAsync(string url)
{
await semaphore.WaitAsync();
try
{
WriteLine($"Downloading file {url}...");
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync())
using (var fileStream = File.Create(Path.GetFileName(url)))
{
await stream.CopyToAsync(fileStream);
}
WriteLine($"Completed Downloading file {url}.");
}
finally
{
semaphore.Release();
}
}
Linguagem de código: JavaScript ( javascript )
Quarto, defina uma lista de URLs dos arquivos para download:
var urls = new List<string>
{
"https://www.ietf.org/rfc/rfc791.txt",
"https://www.ietf.org/rfc/rfc792.txt",
"https://www.ietf.org/rfc/rfc793.txt",
"https://www.ietf.org/rfc/rfc794.txt",
"https://www.ietf.org/rfc/rfc795.txt",
"https://www.ietf.org/rfc/rfc796.txt",
"https://www.ietf.org/rfc/rfc797.txt",
"https://www.ietf.org/rfc/rfc798.txt",
"https://www.ietf.org/rfc/rfc799.txt",
"https://www.ietf.org/rfc/rfc800.txt",
};
Linguagem de código: C# ( cs )
Quinto, crie uma lista de tarefas que executam DownloadFileAsync()
simultaneamente e aguarde a conclusão de todas as tarefas usando o Task.WhenAll()
método:
var tasks = new List<Task>();
foreach (var url in urls)
{
tasks.Add(DownloadFileAsync(url));
}
await Task.WhenAll(tasks);
Linguagem de código: C# ( cs )
Por fim, envie uma mensagem para o console para notificar que o processo de download foi concluído:
WriteLine("Completed downloading all files.");
Linguagem de código: C# ( cs )
A saída mostra que existem threads que baixam os arquivos simultaneamente:
Downloading file https://www.ietf.org/rfc/rfc791.txt...
Downloading file https://www.ietf.org/rfc/rfc792.txt...
Downloading file https://www.ietf.org/rfc/rfc793.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc793.txt.
Downloading file https://www.ietf.org/rfc/rfc794.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc794.txt.
Downloading file https://www.ietf.org/rfc/rfc795.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc792.txt.
Downloading file https://www.ietf.org/rfc/rfc796.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc791.txt.
Downloading file https://www.ietf.org/rfc/rfc797.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc795.txt.
Downloading file https://www.ietf.org/rfc/rfc798.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc796.txt.
Downloading file https://www.ietf.org/rfc/rfc799.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc797.txt.
Downloading file https://www.ietf.org/rfc/rfc800.txt...
Completed Downloading file https://www.ietf.org/rfc/rfc799.txt.
Completed Downloading file https://www.ietf.org/rfc/rfc800.txt.
Completed Downloading file https://www.ietf.org/rfc/rfc798.txt.
Completed downloading all files.
Linguagem de código: texto simples ( texto simples )
Resumo
- O semáforo limita o número de threads que podem acessar recursos compartilhados simultaneamente.
- Use a
SemaphoreSlim
classe C# para implementar o padrão de semáforo.