2023. 6. 17. 22:44ㆍLanguage/C#
개요
C# 프로그래밍에 익숙해지다 보면 비동기 프로그래밍에 관심을 갖게 됩니다. 비동기 프로그래밍은 사용자 경험을 향상시키고, 자원을 최적화하며, 성능을 높여주기 때문에 무척 중요한 개념입니다.
이 글에서는 C#에서 사용되는 주요 비동기 작업 유형인 Task와 ValueTask에 대해 알아보겠습니다. 이 두 가지 유형의 차이점을 이해하면 비동기 작업을 더 효율적으로 설계하고 구현할 수 있습니다.
기본 개념
Task는 비동작 작업을 나타내는 참조 형식
입니다. 비동기 작업을 표현하기 위해 Task를 사용하면 코드의 가독성과 유지 관리가 향상 됩니다.
ValueTask는 비동기 작업을 나타내는 값 형식
입니다. Task와 달리, 메모리 할당 및 GC에 더 효율적입니다.
여기서 중요한 것은 참조 형식이냐? 값 형식이냐 입니다.
ValueTask의 경우 값 형식이기 때문에 스택에 메모리가 할당 됩니다. 그래서 메모리 사용이 Task에 비해 덜 합니다.
하지만 메모리 사용에 좋다고 해서 꼭 속도가 빠른 것은 아닙니다. 속도는 Task쪽이 미세하게 빠를 수 있습니다. ValueTask의 경우 값 형식이기 때문에 스택에 값을 복사해야 하는데, ValueTask 자체의 필드가 3개 있으므로 Task가 IntPtr.Size 만큼의 복사만 발생하는 것과 비교하면 부하가 더 있을 수도 있습니다.
물론, 추후 GC로 인해 속도는 ValueTask가 전체적으로 빠를 수도 있습니다.
사용방법
사실 Task나 ValueTask나 사용법에 대해서는 크게 차이가 없습니다.
async Task<int> GetTask()
{
await Task.Delay(1000);
return 1;
}
async ValueTask<int> GetValueTask()
{
await Task.Delay(1000);
return 1;
}
var taskResult = await GetTask();
var taskValueResult = await GetValueTask();
System.Console.WriteLine(taskResult);
System.Console.WriteLine(taskValueResult);
똑같이 async로 선언하고 await로 호출하면 값을 받을 수 있습니다.
그럼 언제 ValueTask를 써야 할까?
한번 또는 1% 정도만 비동기를 수행해야 하고 99%의 작업을 동기로 수행해야 할 때 ValueTask를 사용하면 좋습니다.
예를 들면, 디스크의 파일을 비동기로 값을 읽고 그 후로는 읽은 값을 계속 반환하는 메서드의 경우와 같이 캐싱을 구현하여 사용 할 때 사용 하면 좋습니다.
public class ReadFile
{
private string filePath = $@"D:\Workspace\Project\BlogPosts\CSharp\ValueTask\Some.txt";
private string readText = string.Empty;
public async Task<string> GetTask()
{
if(!string.IsNullOrEmpty(readText))
return readText;
var result = await File.ReadAllTextAsync(filePath);
return result;
}
public async ValueTask<string> GetValueTask()
{
if(!string.IsNullOrEmpty(readText))
return readText;
var result = await File.ReadAllTextAsync(filePath);
return result;
}
}
위 예제를 보면 GetTask()의 경우 ReadAllTextAsync
를 100% 수행합니다.
하지만 GetValueTask()의 경우는 1번 읽은 파일을 계속 동기로 반환하고 있습니다.
이럴 때 ValueTask를 사용하면 성능이 많이 향상이 됩니다.
이유는??
위 메서드에서 보면 아래와 같은 코드가 있습니다.
if(!string.IsNullOrEmpty(readText))
return readText;
하지만 실제로는 return Task.FromResult(readText)
이며, return new ValueTask(result)
입니다.
Task는 참조 형식이기 때문에 힙에 계속 저장이 되고 ValueTask는 스택에 저장되기 때문에 GC가 메모리를 해제 할 때 스택이 훨씬 빠르고 효율적일 수 밖에 없기 때문입니다.
주의점
ValueTask
는 절대 한번에 여러번 호출하면 안됩니다. 첫 번째 호출 개체가 삭제 될 수 있기 때문에 Exception이 발생합니다.
ValueTask<List<Employee>> employeesTask = GetEmployees();
Var a = await employeesTask;
Var b = await employeesTask;
또는
ValueTask<List<Employee>> employeesTask = GetEmployees();
Task.Run(async a => await employeesTask);
Task.Run(async b => await employeesTask);
그리고 GetAwaiter()는 사용하면 안됩니다. 메서드가 언제 완료 될지 모르기 때문에 IsCompleted
속성을 사용하여 작업이 완료되었는지를 확인해야 합니다.
ValueTask<List<Employee>> employeesTask = GetEmployees();
var a = emplyeeTask.GetAwaiter().GetResult();
'Language > C#' 카테고리의 다른 글
MemoryCache (0) | 2023.06.19 |
---|---|
dotnet cli nuget 저장소 지정 (0) | 2023.06.16 |
.NET AOP DynamicProxy (0) | 2023.06.05 |
StringBuilder vs String Join (0) | 2023.06.04 |
상속에서 Dispose 패턴 (0) | 2023.05.31 |