SemaphoreSlim을 사용해 동시 병렬 처리 개수 제한

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

하지만 위 코드는 가독성이 떨어지는 편이라 생각한다. 일단 데이터를 리스트로 가공해서 집어넣어 줘야 하고, 그걸 다시 대리자로 받아서 처리하는 구조이기 때문이다…

이럴 때 SemaphoreSlim1을 사용할 수 있다.

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");

SemaphoreSliminitialCount 매개 변수(첫 번째 인자)가 한 번에 병렬 처리하는 작업의 최대 개수를 정한다.

Enumerable.Range .... 부분을 forforeach 문으로 바꿨다고 생각해 보자. 어디에서도 대리자를 사용해 전달되는 부분이 없으므로 가독성이 더 뛰어난 편이다. 또한 동시성을 제어할 수 없는 전자와 다르게 여기서는 원하는 대로 제어할 수 있다. 물론 동기화에 주의해야 한다.


  1. Semaphore는 OS가 제공하는 동기화 오브젝트를 재사용하지만 SemaphoreSlim은 닷넷 런타임에서 제공한다. 따라서 프로세스간 동기화 객체를 공유해야 하는 상황이 아니라면 SemaphoreSlim을 사용하면 된다. 성태의 닷넷 이야기 ↩︎

댓글

이 블로그의 인기 게시물

C# 남아도는 메모리에도 불구하고 OutOfMemoryException이 발생한다면?

USB를 뒤는 괜찮은데 앞에 꽂으면 인식이 힘들다?

MySQL 데이터 타입과 Java 데이터 타입 비교/매칭