방프리

23.11.05 C# 동시성 프로그래밍 (기초 기능) 본문

C#/동시성 처리

23.11.05 C# 동시성 프로그래밍 (기초 기능)

방프리 2023. 11. 5. 17:10

1. 일정 시간동안 정지

 

- 간단한 딜레이 타임을 주는 정도의 기능

- 시나리오 코드 및 간단한 테스트 용도로 사용한다. (이외 실제 기능 혹은 배포에서는 들어가면 좋지 않은 코드)

async Task<T> DelayResult<T>(T result, TimeSpan delay)
{
    await Task.Delay(delay);
    return result;
}

// delay를 통한 soft timeout 기능 구현
async Task<string> DownloadStringWithTimeout(HttpClient client, string url)
{
	using var tokenSource=  new CancellationTokenSource(TimeSpan.FromSecond(3));
    Task<string> downloadTask = client.GetStringAsync(uri);
    Task timeoutTask = Task.Delay(Timeout.InfiniteTimeSpan, tokenSource.Token);
    
    var completedTask = await Task.WhenAny(downloadTask, timeoutTask);
    if (completedTask == timeoutTask)
    {
    	return null;
    }
    return await downloadTask;
}

 

2. 완료한 작업 반환

 

- 비동기 시그니처를 사용해서 동기 메서드를 구현

- 단위 테스트 진행 시 간단한 스텁(stub: 빈 메서드) 혹은 목 (mock: 가짜객체)가 필요할 때

interface IMyAsyncInterface
{
    Task<int> GetValueAsync();
}

class MySynchronousImplementation : IMyAsyncInterface
{
    public Task<int> GetValueAsync()
    {
    	return Task.FromResult(13);
    }
}

//간단한 완료형 Task를 사용하고 싶다면
class MySynchronousImplementation : IMyAsyncInterface
{
    //...
    public Task DoSomething()
    {
    	return Task.CompletedTask;
    }
}

//Exception을 반환하고 싶다면
Task<T> NotImplementedAsync<T>()
{
    return Task.FromException<T>(new NotImplementedException());
}

//Task 중간에 작업을 취소해야한다면 적극적으로 CancellationToken을 활용하자
Task<int> GetValueAsync(CancellationToken cancellationToken)
{
    if (cancellationToken.IsCancellationRequested)
    {
    	reuturn Task.FromCancled<int>(cancellationToken);
    }
    return Task.FromResult(13);
}

 

3. 진행 상황 보고

- UI 쓰레드와 별개로 다른 로딩 Task의 진행상황 정보를 알려고 할 때

async Task MyMethodAsync(IProgress<double> progress = null)
{
    bool done = false;
    double percentComplete = 0;
    while (done == false)
    {
        //...
        progress?.Report(percentComplete);
    }
}

// call like this
async Task CallMyMethodAsync()
{
    var progress = new Progress<double>();
    progress.ProgressChanged += (sender, args) =>
    {
    	//...
    };
    await MyMethodAsync(progress);
}

 

4. 모든 작업 완료를 대기

 

- Task.WhenAll로 해결 하지만 수많은 작업 (Task)이 몰렸을 때에는 좋은 방법은 아님 

- 이외에 ForEachAsync 등의 기능을 활용하는 방안도 있음

async Task<string> DownloadAllAsync(HttpClient client, 
    IENumerable<string> urls)
{
    var downloads = urls.Select(url => client.GetStringAsync(url));
    Task<string>[] downloadTasks = downloads.ToArray();
    string[] htmlPages = await Task.WhenAll(downloadTasks);
    
    return string.Concat(htmlPages);
}


5. 여러 작업 중 하나의 완료를 대기

 

- 하나의 작업만 완료해도 바로 대기를 중지한다.

- 단, 나머지 작업들(Task)는 버려진다.

async Task<int> FirstRespondingUrlAsync(HttpClient client,
	string urlA, string urlB)
{
	Task<byte[]> downloadTaskA = client.GetByteArrayAsync(urlA);
    Task<byte[]> downloadTaskB = client.GetByteArrayAsync(urlB);
    
    var completedTask = 
    	await Task.WhenAny(downloadTaskA, downloadTaskB);
        
    var data = await completedTask;
    return data.Length;
}

 

6. 작업이 완료될 때마다 처리

 

- 여러 개의 작업을 동시에 실행하되, 완료되는 순차적으로 처리

- 순차처리가 목적이라면 잠금(lock)을 고려해볼 것

async Task<int> DelayAndReturnAsync(int value)
{
    await Task.Delay(TimeSpan.FromSecond(value));
    return value;
}

async Task AwaitAndProcessAsync(Task<int> task)
{
    var result = await task;
    Trace.WriteLine(result);
}

async Task ProcessTasksAsync()
{
    Task<int> taskA = DelayAndReturnAsync(2);
    Task<int> taskB = DelayAndReturnAsync(3);
    Task<int> taskC = DelayAndReturnAsync(1);
    Task<int>[] tasks = new[] { taskA, taskB, taskC };
    
    IEnumerable<Task> taskQuery = 
    	from t in tasks select AwaitAndProcessAsync(t);
    Task[] processingTasks = taskQuery.ToArray();
    
    await Task.WhenAll(processingTasks);
}

 

7. 연속 작업용 컨텍스트 회피

 

- async, await는 항상 같은 컨텍스트에서 실행된다. 이로 인해 UI 컨텍스트에서 수많은 async 메서드가 실행될 경우

성능 이슈가 있을 수 있다.

async Task ResumeOnContextAsync()
{
    await Task.Delay(TimeSpan.FromSecond(1));
}

async Task ResumeWithoutContextAsync()
{
    await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
}

 

8. async Task 메서드의 예외 처리

 

async Task ThrowExceptionAsync()
{
    await Task.Delay(TimeSpan.FromSecond(1));
    throw new InvalidOperationException("Test");
}

async Task TestAsync()
{
    try
    {
        await ThrowExceptionAsync();
    }
    catch (InvalidOperationException e)
    {
        //...
    }
}

 

9. async void 메서드의 예외 처리

 

- async void 사용을 최대한 하지 말 것 

 

10. ValueTask 생성

 

- 메서드 내의 동작이 비동기 반환형식보다 동기 반환 케이스가 더 많을 때 사용

- ValueTask 또한 내부는 Task와 동일한 구조이지만 Task에 비해 좀 더 오버헤드가 발생

- 결국 실제 성능향상이 이루어지는지 보려면 각 케이스 별로 프로파일링을 진행한 후 선택

 

 

Comments