C# .NET Core의 멀티스레딩과 관련하여 최적의 성능을 달성하고 일반적인 함정을 피하기 위해 명심해야 할 몇 가지 모범 사례가 있습니다.
몇 가지 코드 예제를 통해 각각에 대해 자세히 살펴보겠습니다.
과도한 잠금 방지
다중 스레드 코드로 작업할 때 흔히 저지르는 실수 중 하나는 너무 많은 잠금을 사용하는 것입니다. 여러 스레드가 동시에 액세스하지 못하도록 공유 리소스를 보호하려면 잠금이 필요하지만 과도한 잠금은 스레드 경합 및 성능 저하를 초래할 수 있습니다. 대신 필요한 경우에만 잠금을 사용하고 적절한 경우 Interlocked
작업 또는 Concurrent
컬렉션 클래스와 같은 다른 동기화 메커니즘을 사용하는 것을 고려하십시오.
1 | private readonly object _lock = new object(); |
Thread-Safe 데이터 구조 사용
공유 데이터 구조로 작업할 때 데이터 손상이나 경합 조건을 방지하기 위해 thread-safe 컬렉션을 사용하는 것이 중요합니다. .NET Core 라이브러리는 스레드 간에 데이터를 안전하게 공유하는 데 사용할 수 있는 ConcurrentDictionary
및 ConcurrentQueue
와 같은 여러 스레드로부터 안전한 컬렉션을 제공합니다.
1 | private readonly ConcurrentDictionary<string, int> _dict = new ConcurrentDictionary<string, int>(); |
ThreadPool 사용
스레드를 만들고 관리하는 작업은 비용이 많이 들 수 있으므로 가능하면 .NET Core ThreadPool을 사용하는 것이 가장 좋습니다. ThreadPool은 재사용할 수 있는 스레드 풀을 관리하므로 스레드 생성 및 삭제에 따른 오버헤드를 줄여 성능을 향상시킬 수 있습니다.
1 | ThreadPool.QueueUserWorkItem((state) => { |
교착상태 주의
교착 상태는 두 개 이상의 스레드가 서로 리소스를 해제할 때까지 기다리면서 차단되어 더 이상 진행이 불가능한 상황이 발생하는 경우에 발생합니다. 교착 상태를 방지하려면 잠금을 올바른 순서로 획득 및 해제하고 장기간 잠금을 유지하지 않는 것이 중요합니다.
1 | private readonly object _lock1 = new object(); |
비동기 프로그래밍 사용
비동기 프로그래밍은 다중 스레드 코드로 작업할 때 호출 스레드를 차단하지 않고 여러 작업을 동시에 수행할 수 있는 강력한 도구가 될 수 있습니다. async
및 await
키워드를 사용하면 동기적인 것처럼 보이지만 실제로는 별도의 스레드에서 비동기적으로 실행되는 코드를 작성할 수 있습니다.
1 | public async Task<string> DownloadAsync(string url) |
모범 사례
이러한 모범 사례를 따르면 C# .NET Core에서 확장 가능한 고성능 다중 스레드 코드를 작성할 수 있습니다.
다음은 .NET Core의 다중 스레딩에 대한 모범 사례를 사용하는 몇 가지 실제 예입니다.
비동기식 HTTP 요청
웹 애플리케이션에서는 다른 서비스에 HTTP 요청을 보내는 것이 일반적입니다. 기본 스레드를 차단하지 않으려면 이러한 요청은 HttpClient
를 사용하여 비동기적으로 이루어져야 합니다.
1 | public async Task<string> GetApiDataAsync(string apiUrl) |
CPU 바인딩된 작업 병렬화
CPU 바인딩된(CPU-bound) 작업을 수행할 때 여러 스레드에 걸쳐 작업을 병렬화하여 성능을 향상시키는 것이 유용한 경우가 많습니다. .NET Core의 Parallel 클래스는 루프를 병렬화하는 쉬운 방법을 제공합니다.
1 | Parallel.For(0, 100000, (i) => { |
경쟁 조건을 피하기 위해 잠금 사용
여러 스레드가 공유 리소스에 액세스하면 한 스레드가 리소스를 읽거나 수정하는 동안 다른 스레드도 리소스에 액세스하는 경쟁 조건이 발생할 위험이 있습니다. 이를 방지하기 위해 잠금을 사용하여 한 번에 하나의 스레드만 공유 리소스에 액세스할 수 있도록 할 수 있습니다.
1 | private readonly object _lock = new object(); |
메인 스레드에서 blocking 방지
UI 애플리케이션에서는 기본 스레드를 blocking하고 애플리케이션이 응답하지 않게 만드는 것을 방지하기 위해 파일 I/O 또는 데이터베이스 쿼리와 같은 blocking 작업을 백그라운드 스레드에서 수행해야 합니다.
1 | public async Task LoadDataAsync() |
예제
제품 정보, 가격 및 가용성에 대한 대량의 동시 요청을 처리하는 대규모 전자 상거래 웹 사이트용 애플리케이션을 구축한다고 가정해 보겠습니다. 웹사이트는 각 제품 카테고리에 대해 별도의 서비스를 제공하는 마이크로서비스 아키텍처를 사용하며, 각 서비스는 여러 클라이언트의 요청을 처리합니다.
1 | public class ProductService |
이 예에서는 여러 스레드가 _products
에서 동시에 읽을 수 있도록 ReaderWriterLockSlim
을 사용하고 있습니다. Dictionary에 없는 제품이 요청되면 write lock을 사용하여 추가합니다. 또한 제품 데이터를 검색하기 위해 비동기 API 호출을 만들기 위해 async/await를 사용하고 있습니다.