목록C#/More Effective C# (48)
방프리
Item 40 : 동기화에는 lock()을 최우선으로 사용하라 동기화 요소는 특정 스레드가 임계영역 내에서 연산을 수행하는 동안 다른 스레드로부터 이를 보호하는 역할을 수행한다. .NET 기본 클래스 라이브러리에서는 다양한 기능을 통해 공유 데이터에 대한 접근을 안전하게 동기화하는데 특히 Monitor.Enter()와 Monitor.Exit()는 특별하게 다루어진다. 하지만 lock()이 가장 기본이 되는 기능이므로 최우선적으로 고려 시에는 lock()을 먼저 사용하는 것이 좋다. lock()은 값 타입을 지원하지 않는다. 그러기에 Monitor.Enter()을 사용하게 되는데 이 때 주의할 점이 있다. 하단의 코드는 정상적으로 컴파일은 되지만 문제가 발생할 수 있다. public void Increme..
Item 39 : XAML 환경에서 스레드 간 호출을 이해하라 이벤트 핸들러를 좀 더 쉽게 작성하고자 할 때 제네릭을 활용하여 다음과 같이 작성해볼 수 있다. //Background 스레드 사용 시 public static class XAMLControlExtensions { public static void InvokeIfNeeded( this System.Windows.threading.DispatcherObject ctl, Action doit, system.Windows.Threading.DispatcherPriority priority) { if (System.Threading.Thread.CurrentThread != ctl.Dispatcher.Thread) { ctl.Dispatcher.In..
Item 38 : 스레드 간 커뮤니케이션에는 BackgroundWorker를 사용하라 BackgroundWorker는 ThreadPool을 이용하도록 만들어졌고 스레드 간 커뮤니케이션을 위한 여러 기능을 제공한다. 기존 QueueUserWorkItem의 단점 1. WaitCallback에서 발생하는 예외처리를 처리해야하는데 이 메서드가 예외를 던지면 전체 어플리케이션이 종료된다. 2. 백그라운드 스레드와 포그라운드 스레드 간 커뮤니케이션을 위한 기능을 전혀 제공하지 않는다. 만약 필요하다면 QueueUserWorkItem()의 기능을 활용해 만든 BackgroundWorker 컴포넌트를 사용 BackgroundWorker 사용방법 BackgroundWorker backgroundWorkerExample ..
Item 37 : 스레드를 생성하지 말고 스레드 풀을 사용하라 스레드 개수를 개발자가 관리하기엔 무리가 있다. CLR에서 가비지 컬렉터 사용을 위해 스레드를 얼마나 할당해야되는지 알 수 없고 이외에도 Task를 관리하기 위해 얼마나 생성해야하는지 모르기 때문이다. 그러기에 우리는 스레드풀 세팅을 통해 스레드들이 스레드 풀에서 관리하도록 해야한다. 또한 스레드 풀이 태스크의 생명주기도 관리하기 때문에 스레드 풀을 적극적으로 사용해야 한다. 스레드 풀이 태스크의 작업 진행 상황을 리소스에 기초하여 할당하고 부하를 적당히 분산하기에 부하 분산 로직을 개발자가 만들 필요가 없다. 적절한 스레드의 개수를 찾아내는 건 코어 수에 비례해 찾는건 너무 고전적인 해법이라 필자는 이야기 한다. 그럼 어떤 방식으로 찾아야할..
Item 36 : 예외를 염두에 두고 병렬 알고리즘을 만들라 PLINQ는 자식 스레드가 잘못 동작하는 가능성에는 고려하지 않았는데 실제로는 언제든지 문제가 발생할 수 있다. 단 이 처리의 몫은 항상 개발자가 져야 한다. 예외는 스레드의 경계를 넘어 다른 스레드에 전달될 수 없다. 예외가 발생할 경우 예외를 백그라운드 작업을 수행하는 태스크를 떠나지 않게 해야된다. 예로 TaskCompletionSource 클래스를 사용할 경우에는 TrySetException()을 호출해서는 안되고 태스크의 완료 상황을 나타내기 위해 TrySetResult()를 어떻게든 호출해야 한다. private static Task startDownload(string url) { var tcs = new TaskCompletion..
Item 35 : PLINQ가 병렬 알고리즘을 구현하는 방법을 이해하라 PLINQ를 사용하면 병렬로 처리하기 때문에 속도가 향상되지만 무조건적으로는 아니다. 어느 정도 계산을 분배한 뒤 동작하는 방식인데 제대로 사용하지 않으면 분할에 오히려 작업 속도가 더 많이 소요되어 PLINQ를 사용하지 않은 것보다 더 오래 걸릴 수 있다. PLINQ를 분할하는 방식은 아래 중 하나를 채용한다. 범위 분할 덩어리 분할 줄 단위 분할 해시 분할 1. 범위분할 가장 단순한 알고리즘으로 동작한다. 입력값의 범위를 태스크 수로 나누고, 나눈 집합을 각 태스크에 할당한다. 대신 분할하는 작업 때문에 배열형태인 IList 형태만 지원한다. 2. 덩어리 분할 대스크가 추가 작업을 요구할 때마다 청크를 할당하는 방식. 작은 청크 ..
Item 34 : 비동기 메서드의 반환값을 캐시하는 경우 ValueTask를 사용하라 Task는 사용될 때마다 Task 인스턴스를 생성한다. 이조차도 비용이 될 수도 있고 만약 자주 호출되는 API일 경우 호출 될 때마다 계산하여 가져와야 하는 데이터라면 연산량을 줄이기 위해 결과값을 미리 캐싱하는 경우가 있다. 이 결과값을 비동기 메서드로 가져올 때 ValueTask가 빛을 발휘한다. public ValueTask RetrieveHistoricalData() { if (DateTime.Now - lastReading > TimeSpan.FromMinutes(5)) { return new ValueTask(recentObservations); } else { async Task loadCache() { ..
Item 33 : 태스크 취소 프로토콜 구현을 고려하라 태스크 실행 도중에 문제가 생긴다면 메서드 호출 도중 취소 처리를 하는 방식도 고려해야 한다. 주로 CancellationToken을 활용하여 태스크 취소 처리를 진행한다. public class ProgressReporter : IProgress { public void Report((int percent, string message) value) { WriteLine($"{value.Percent} completed: {value.message}"); } } await generator.RunPayroll(DateTime.Now, new ProgressReporter()); public Task RunPayroll(DateTime payrollP..