방프리

24.05.24 C# 동시성 프로그래밍 (함수형 친화적 OOP) 본문

C#/동시성 처리

24.05.24 C# 동시성 프로그래밍 (함수형 친화적 OOP)

방프리 2024. 5. 24. 22:43

1. 비동기 인터페이스와 상속

async 키워드는 구현을 지니는 메서드에서만 적용할 수 있다. 즉, 기본 구현이 없는 추상 메서드나 인터페이스 메서드에는 적용할 수 없다. 하지만 async 키워드가 없어도 async 메서드와 시그니처가 똑같은 메서드를 정의할 순 있지만 대기하는 대상은 메서드가 아닌 형식을 대기하는 것이다.

interface IMyAsyncInterface
{
    Task<int> CountBytesAsync(HttpClient client, string url);
}

class MyAsyncClass : IMyAsyncInterface
{
    public async Task<int> CountBytesAsync(HttpClient client, string url)
    {
        var bytes = await client.GetByteArrayAsync(url);
        return bytes.Length;
    }
}

await Task UseMyInterfaceAsync(HttpClient client, IMyAsyncInterface service)
{
    var result = await service.CountBytesAsync(client, "https://www.example.com");
    Trace.WriteLine(result);
}

// 동기적으로 구현할 때
class MyAsyncClassStub : IMyAsyncInterface
{
    public Task<int> CountBytesAsync(HttpClient client, string url)
    {
        return Task.FromResult(13);
    }
}

 

2. 비동기 생성: 팩토리

생성자에서는 async/await 키워드를 사용할 수 없다. 그렇기에 형식을 팩토리로 만들어 사용하는 것이 좋다.
아예 정적 팩토리 메서드를 통해서만 생성자를 호출하도록 막는 것이다.
만약 DI 라이브러리 형태라던가 제어 역전 라이브러리는 적용할 수 없다.

class MyAsyncClass
{
    private MyAsyncClass()
    {
    }
    
    private async Task<MyAsyncClass> InitializeAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        return this;
    }
    
    public static Task<MyAsyncClass> CreateAsync()
    {
        var result = new MyAsyncClass();
        return result.InitializeAsync();
    }
}

 

3. 비동기 생성: 비동기 초기화 패턴

비동기 초기화가 필요한 형식이 있으면 마커 인터페이스에 정의한다.

public interface IAsyncInitialization
{
    Task Initialization { get; }
}

public static class AsyncInitialization
{
    public static Task WhenAllInitializedAsync(params object[] instances)
    {
        return Task.WhenAll(instances
            .OfType<IAsyncInitialization>()
            .Select(x => x.Initialization));
    }
}

private async Task InitializationAsync()
{
    await AsyncInitialization.WhenAllInitializedAsync(_fundamental,
        _anotherType, _yetAnother);
}

 

4. 비동기 속성

기존 코드를 async로 변환하는 과정에서 많이 발생한다. getter 프로퍼티에서 자주 나오는데 읽을 때마다 비동기적으로 평가해야하는 값인지 한 번 비동기적으로 평가한 뒤에 나중에 사용할 수 있게 캐싱하는 값인지에 따라 구현이 다르다.

// 읽을 때마다 비동기적으로 평가 (비추천)
public async Task<int> GetDataAsync()
{
    await Task.Delay(TimeSpan.FromSeconds(1));
    return 13;
}

public Task<int> Data
{
    get { return GetDataAsync(); }
}

// 한 번 비동기적으로 평가한 뒹 나중에 사용할 수 있게 캐싱하는 값
public AsyncLazy<int> Data
{
    get { return _data; }
}

private readonly AsyncLazy<int> _data = 
    new AsyncLazy<int>(async () =>
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        return 13;
    });

 

5. 비동기 이벤트

async void의 반환 시점은 확인할 수 없다. 따라서 비동기 핸들러의 완료 시점을 확인할 수 없다. 단 Nito.AsyncEx 라이브러리에서는 디퍼럴이라는 개념을 통해서 이벤트 핸들러도 비동기적으로 운용할 수 있게 한다.

public class MyEventArgs : EventArgs, IDeferralSource
{
    private readonly DeferralManager _deferrals = new DeferralManager();
    
    ...// 원하는 생성자와 속성을 추가한다.
   
    public IDisposable GetDeferral()
    {
        return _deferrals.DeferralSource.GetDeferral();
    }
    
    internal Task WaitForDeferralsAsync()
    {
        return _deferrals.WaitForDeferralsAsync();
    }
}

// 이벤트 인수 형식을 스레드로부터 안전하게 만드는 방법 중 가장 쉬운 것은
// 모든 속성을 읽기 전용으로 만드는 것이다.

public event EventHandler<MyEventArgs> MyEvent;

private async Task RaiseMyEventAsync()
{
    EventHandler<MyEventArgs> handler = MyEvent;
    if (handler == null)
    {
        return;
    }
    
    var args = new MyEventArgs(...);
    handler(this, args);
    await args.WaitForDeferralsAsync();
}

async void AsyncHandler(object sender, MyEventArgs args)
{
    using IDisposable deferrals = args.GetDeferrals();
    await Task.Delay(TimeSpan.FromSeconds(2));
}

 

6. 비동기 삭제

인스턴스를 삭제할 때 기존 작업에 적용할 취소 요청으로 취급할 수도 있고, 비동기 삭제를 구현할 수도 있다.

// 모든 기존 작업에 적용할 취소 요청
class MyClass : IDisposable
{
    private readonly CancellationTokenSource _disposeCts = 
        new CancellationTokenSource();
    
    public async Task<int> CalculateValueAsync()
    {
    	using CancellationTokenSource combinedCts = CancellationTokenSource
            .CreateLinkedTokenSource(cancellationToken, _disposeCts.Token);
        await Task.Delay(TimeSpan.FromSeconds(2), combinedCts.Token);
        return 13;
    }
    
    public void Dispose()
    {
        _disposeCts.Cancel();
    }
}

async Task UseMyClassAsync()
{
    Task<int> task;
    using (var resource = new MyClass())
    {
        task = resource.CalculateValueAsync(default);
    }
    
    var result = awaiat task;
}

// 비동기 삭제
class MyClass : IAsyncDisposable
{
    public async ValueTask DisposeAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(2));
    }
}

await using (var myClass = new MyClass())
{
    ///
} // DisposeAsync 호출 후 대기

var myClass = new MyClass();
await using (myClass.ConfigureAwait(false))
{
    ...
}
Comments