[.NET Core] 코드 예제를 통해 멀티스레딩 마스터하기

C# .NET Core의 멀티스레딩과 관련하여 최적의 성능을 달성하고 일반적인 함정을 피하기 위해 명심해야 할 몇 가지 모범 사례가 있습니다.

몇 가지 코드 예제를 통해 각각에 대해 자세히 살펴보겠습니다.

과도한 잠금 방지

다중 스레드 코드로 작업할 때 흔히 저지르는 실수 중 하나는 너무 많은 잠금을 사용하는 것입니다. 여러 스레드가 동시에 액세스하지 못하도록 공유 리소스를 보호하려면 잠금이 필요하지만 과도한 잠금은 스레드 경합 및 성능 저하를 초래할 수 있습니다. 대신 필요한 경우에만 잠금을 사용하고 적절한 경우 Interlocked 작업 또는 Concurrent 컬렉션 클래스와 같은 다른 동기화 메커니즘을 사용하는 것을 고려하십시오.

1
2
3
4
5
6
7
8
9
10
private readonly object _lock = new object();
private int _count;

public void IncrementCount()
{
lock (_lock)
{
_count++;
}
}

Thread-Safe 데이터 구조 사용

공유 데이터 구조로 작업할 때 데이터 손상이나 경합 조건을 방지하기 위해 thread-safe 컬렉션을 사용하는 것이 중요합니다. .NET Core 라이브러리는 스레드 간에 데이터를 안전하게 공유하는 데 사용할 수 있는 ConcurrentDictionaryConcurrentQueue 와 같은 여러 스레드로부터 안전한 컬렉션을 제공합니다.

1
2
3
4
5
6
private readonly ConcurrentDictionary<string, int> _dict = new ConcurrentDictionary<string, int>();

public void AddOrUpdateDict(string key, int value)
{
_dict.AddOrUpdate(key, value, (k, v) => v + value);
}

ThreadPool 사용

스레드를 만들고 관리하는 작업은 비용이 많이 들 수 있으므로 가능하면 .NET Core ThreadPool을 사용하는 것이 가장 좋습니다. ThreadPool은 재사용할 수 있는 스레드 풀을 관리하므로 스레드 생성 및 삭제에 따른 오버헤드를 줄여 성능을 향상시킬 수 있습니다.

1
2
3
ThreadPool.QueueUserWorkItem((state) => {
// Do some work on a background thread
});

교착상태 주의

교착 상태는 두 개 이상의 스레드가 서로 리소스를 해제할 때까지 기다리면서 차단되어 더 이상 진행이 불가능한 상황이 발생하는 경우에 발생합니다. 교착 상태를 방지하려면 잠금을 올바른 순서로 획득 및 해제하고 장기간 잠금을 유지하지 않는 것이 중요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private readonly object _lock1 = new object();
private readonly object _lock2 = new object();

public void DoWork()
{
lock (_lock1)
{
// Do some work
lock (_lock2)
{
// Do some more work
}
}
}

비동기 프로그래밍 사용

비동기 프로그래밍은 다중 스레드 코드로 작업할 때 호출 스레드를 차단하지 않고 여러 작업을 동시에 수행할 수 있는 강력한 도구가 될 수 있습니다. asyncawait 키워드를 사용하면 동기적인 것처럼 보이지만 실제로는 별도의 스레드에서 비동기적으로 실행되는 코드를 작성할 수 있습니다.

1
2
3
4
5
6
7
8
public async Task<string> DownloadAsync(string url)
{
using (var client = new HttpClient())
{
var response = await client.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
}

모범 사례

이러한 모범 사례를 따르면 C# .NET Core에서 확장 가능한 고성능 다중 스레드 코드를 작성할 수 있습니다.

다음은 .NET Core의 다중 스레딩에 대한 모범 사례를 사용하는 몇 가지 실제 예입니다.

비동기식 HTTP 요청

웹 애플리케이션에서는 다른 서비스에 HTTP 요청을 보내는 것이 일반적입니다. 기본 스레드를 차단하지 않으려면 이러한 요청은 HttpClient를 사용하여 비동기적으로 이루어져야 합니다.

1
2
3
4
5
6
public async Task<string> GetApiDataAsync(string apiUrl)
{
using var client = new HttpClient();
var response = await client.GetAsync(apiUrl);
return await response.Content.ReadAsStringAsync();
}

CPU 바인딩된 작업 병렬화

CPU 바인딩된(CPU-bound) 작업을 수행할 때 여러 스레드에 걸쳐 작업을 병렬화하여 성능을 향상시키는 것이 유용한 경우가 많습니다. .NET Core의 Parallel 클래스는 루프를 병렬화하는 쉬운 방법을 제공합니다.

1
2
3
Parallel.For(0, 100000, (i) => {
// Perform CPU-bound work here
});

경쟁 조건을 피하기 위해 잠금 사용

여러 스레드가 공유 리소스에 액세스하면 한 스레드가 리소스를 읽거나 수정하는 동안 다른 스레드도 리소스에 액세스하는 경쟁 조건이 발생할 위험이 있습니다. 이를 방지하기 위해 잠금을 사용하여 한 번에 하나의 스레드만 공유 리소스에 액세스할 수 있도록 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
private readonly object _lock = new object();
private int _sharedResource = 0;

public void IncrementSharedResource()
{
lock (_lock)
{
_sharedResource++;
}
}

메인 스레드에서 blocking 방지

UI 애플리케이션에서는 기본 스레드를 blocking하고 애플리케이션이 응답하지 않게 만드는 것을 방지하기 위해 파일 I/O 또는 데이터베이스 쿼리와 같은 blocking 작업을 백그라운드 스레드에서 수행해야 합니다.

1
2
3
4
5
6
7
8
9
10
public async Task LoadDataAsync()
{
var data = await GetDataFromDatabaseAsync();
// Update UI with data on the main thread
await Task.Run(() =>
{
// Perform file I/O or other blocking operation on a background thread
});
// Continue updating UI with more data on the main thread
}

예제

제품 정보, 가격 및 가용성에 대한 대량의 동시 요청을 처리하는 대규모 전자 상거래 웹 사이트용 애플리케이션을 구축한다고 가정해 보겠습니다. 웹사이트는 각 제품 카테고리에 대해 별도의 서비스를 제공하는 마이크로서비스 아키텍처를 사용하며, 각 서비스는 여러 클라이언트의 요청을 처리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class ProductService
{
private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
private readonly ConcurrentDictionary<int, Product> _products = new ConcurrentDictionary<int, Product>();

public async Task<Product> GetProductAsync(int productId)
{
// Use a read lock to allow multiple threads to read from the dictionary simultaneously
_lock.EnterReadLock();
try
{
if (_products.TryGetValue(productId, out Product product))
{
return product;
}
else
{
// If the product is not found in the dictionary, use a write lock to add it
_lock.ExitReadLock();
_lock.EnterWriteLock();
try
{
product = await GetProductFromServiceAsync(productId);
_products.TryAdd(productId, product);
return product;
}
finally
{
_lock.ExitWriteLock();
_lock.EnterReadLock();
}
}
}
finally
{
_lock.ExitReadLock();
}
}

private async Task<Product> GetProductFromServiceAsync(int productId)
{
// Use async/await to make an asynchronous API call
using (HttpClient client = new HttpClient())
{
string url = $"https://api.example.com/products/{productId}";
HttpResponseMessage response = await client.GetAsync(url);
string json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<Product>(json);
}
}
}

이 예에서는 여러 스레드가 _products에서 동시에 읽을 수 있도록 ReaderWriterLockSlim을 사용하고 있습니다. Dictionary에 없는 제품이 요청되면 write lock을 사용하여 추가합니다. 또한 제품 데이터를 검색하기 위해 비동기 API 호출을 만들기 위해 async/await를 사용하고 있습니다.

Share