SemaphoreSlim을 사용해 동시 병렬 처리 개수 제한
병렬 처리
병렬 처리는 잘 쓰면 굉장한 성능 이점을 가져올 수 있지만, 잘못 쓰면 오히려 굉장한 성능 하락을 가져온다. 병목 현상이고 뭐고 멀리 갈 것도 없이 다음과 같은 경우에는 확실하게 성능이 저하된다.
A 로직이 실행되는 데에 시스템 리소스의 10%가 필요함.
한 번에 20개의 A 로직 병렬 실행
이런 경우에는 한 번에 처리할 수 있는 병렬 처리 작업의 최대치를 정해야 한다.
Parallel.ForEachAsync
Parallel.ForEach
는 완료를 대기하지 않으므로 .NET 6
이상이라면 Parallel.ForEachAsync
를 사용해 다음과 같이 구성할 수 있다.
var rnd = new Random();
var voList = Enumerable.Range(0, 99).ToList();
await Parallel.ForEachAsync(voList,
new ParallelOptions { MaxDegreeOfParallelism = 10},
async (i, token) =>
{
await Task.Delay(rnd.Next(200, 1200)).ConfigureAwait(false);
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}: Finished work: {i:#,##0}");
})
.ConfigureAwait(false);
MaxDegreeOfParallelism
이 한 번에 병렬 처리하는 최대 작업의 최대 개수이다.
SemaphoreSlim
하지만 위 코드는 가독성이 떨어지는 편이라 생각한다. 일단 데이터를 리스트로 가공해서 집어넣어 줘야 하고, 그걸 다시 대리자로 받아서 처리하는 구조이기 때문이다…
이럴 때 SemaphoreSlim
1을 사용할 수 있다.
var rnd = new Random();
async Task<int> DoWork(int i)
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}: Started work: {i:#,##0}");
await Task.Delay(rnd.Next(200, 1200)).ConfigureAwait(false);
Console.WriteLine(
$"{DateTime.Now:HH:mm:ss.fff}: Finished work: {i:#,##0}");
return i;
}
var semaphore = new SemaphoreSlim(10);
var results = new List<int>();
var tasks = Enumerable.Range(1, 100)
.Select(async i =>
{
await semaphore.WaitAsync().ConfigureAwait(false);
try
{
results.Add(await DoWork(i).ConfigureAwait(false));
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(tasks).ConfigureAwait(false);
Console.WriteLine($"Results: {string.Join(", ", results)}");
Console.WriteLine("Finished");
SemaphoreSlim
의 initialCount
매개 변수(첫 번째 인자)가 한 번에 병렬 처리하는 작업의 최대 개수를 정한다.
Enumerable.Range ....
부분을 for
나 foreach
문으로 바꿨다고 생각해 보자. 어디에서도 대리자를 사용해 전달되는 부분이 없으므로 가독성이 더 뛰어난 편이다. 또한 동시성을 제어할 수 없는 전자와 다르게 여기서는 원하는 대로 제어할 수 있다. 물론 동기화에 주의해야 한다.
Semaphore
는 OS가 제공하는 동기화 오브젝트를 재사용하지만SemaphoreSlim
은 닷넷 런타임에서 제공한다. 따라서 프로세스간 동기화 객체를 공유해야 하는 상황이 아니라면SemaphoreSlim
을 사용하면 된다. 성태의 닷넷 이야기 ↩︎
댓글
댓글 쓰기