ILGPU should work on any 64-bit platform that .NET supports.
I have even used it on the inexpensive nvidia jetson nano with pretty decent cuda performance.
1. Install the most recent .NET SDK for your chosen platform.
2. Create a new C# project: dotnet new console
3. Add the ILGPU package: dotnet add package ILGPU
01 A GPU is not a CPU
CPU | SIMD: Single Instruction Multiple Data
CPUs have a trick for parallel programs called SIMD.
These are a set of instructions that allow you to have one instruction do operations on multiple pieces of data at once.
GPU | SIMT: Single Instruction Multiple Threads
SIMT is the same idea as SIMD but instead of just doing the math instructions in parallel why not do all the instructions in parallel.
using System;
using System.Threading.Tasks;
static void TestParallelFor()
{
// Load the data
int[] data = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int[] output = new int[10_000];
// Load the action and execute
Parallel.For(0, output.Length,
(int i) =>
{
output[i] = data[i % data.Length];
});
}
-
using ILGPU
using ILGPU.Runtime;
using ILGPU.Runtime.CPU;
static void TestILGPU_CPU()
{
// Initialize ILGPU.
Context context = Context.CreateDefault();
Accelerator accelerator = context.CreateCPUAccelerator(0);
// Load the data.
var deviceData = accelerator.Allocate1D(new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
var deviceOutput = accelerator.Allocate1D<int>(10_000);
// load / compile the kernel
var loadedKernel = accelerator.LoadAutoGroupedStreamKernel(
(Index1D i, ArrayView<int> data, ArrayView<int> output) =>
{
output[i] = data[i % data.Length];
});
// tell the accelerator to start computing the kernel
loadedKernel((int)deviceOutput.Length, deviceData.View, deviceOutput.View);
// wait for the accelerator to be finished with whatever it's doing
// in this case it just waits for the kernel to finish.
accelerator.Synchronize();
accelerator.Dispose();
context.Dispose();
}
async Task DoSomethingAsync()
{
int value = 1;
// Context 저장
// null ? SynchronizationContext : TaskScheduler
// ASP.NET Core는 별도의 요청 컨텍스트가 아닌 threadpool context 사용
await Task.Delay(TimeSpan.FromSeconds(1));
value *= 2;
await Task.Delay(TimeSpan.FromSeconds(1));
Trace.WriteLine(value);
}
항상 코어 '라이브러리' 메서드 안에서 ConfigureAwait를 호출하고, 필요할 때만 다른 외부 '사용자 인터페이스' 메서드에서 컨택스트를 재개하는 것이 좋다.
async Task DoSomethingAsync()
{
int value = 1;
await Task.Delay(TimeSpan.FromSeconds(1))
.ConfigureAwait(false);
// Threadpool thread에서 실행을 재개한다.
value *= 2;
await Task.Delay(TimeSpan.FromSeconds(1))
.ConfigureAwait(false);
Trace.WriteLine(value);
}
ValueTask<T>
메모리 내 캐시에서 결과를 읽을 수 있는 등 메모리 할당을 줄일 수 있는 형식
Task 인스턴스를 만드는 방법
CPU가 실행해야 할 실제 코드를 나타내는 계산 작업은 Task.Run으로 생성
특정 스케줄러에서 실행해야 한다면 TaskFactory.StartNew로 생성
이벤트 기반 작업은 TaskCompletionSource<TResult> (대부분 I/O 작업은 TaskCompletionSource<TResult> 사용)
오류 처리
async Task TrySomethingAsync()
{
// 예외는 Task에서 발생한다.
var task = PossibleExceptionAsync();
try
{
// 여기서 예외 발생
await task;
}
catch (NotSupportedException ex)
{
// 이렇게 잡힌 예외는 자체적으로 적절한 스택 추적(Stack trace)을 보존하고 있으며
// 따로 TargetInvocationException이나 AggregateException으로 쌓여 있지 않다.
LogException(ex);
throw;
}
}
Deadlock
async Task WaitAsync()
{
// 3. 현재 context 저장
await Task.Delay(TimeSpan.FromSeconds(1));
// ...
// 3. 저장된 context 안에서 재개 시도
// Deadlock 메서드의 2. task.Wait()에서 차단된 thread
// context는 한 번에 하나의 thread만 허용하므로 재개할 수 없음
}
void Deadlock()
{
// 1. 지연 시작
var task = WaitAsync();
// 2. 동기적으로 차단하고 async 메서드의 완료 대기
task.Wait();
}
위의 코든느 UI 컨텍스트나 ASP.NET 클래식 컨텍스트에서 호출하면 교착 상태에 빠진다.
ConfigureAwait(false)로 해결
async Task WaitAsync()
{
// 3. 현재 context 저장
await Task.Delay(TimeSpan.FromSeconds(1))
.ConfigureAwait(false);
// ...
// 3. Threadpool thread에서 재개
}
void Deadlock()
{
// 1. 지연 시작
var task = WaitAsync();
// 2. 동기적으로 차단하고 async 메서드의 완료 대기
task.Wait();
}
async Task<string> DownloadStringWithRetries(HttpClient client, string uri)
{
TimeSpan nextDelay = TimeSpan.FromSeconds(1);
for (int i = 0; i < 3; ++i)
{
try
{
return await client.GetStringAsync(uri);
}
catch { }
await Task.Delay(nextDelay);
nextDelay = nextDelay + nextDelay;
}
// 오류를 전파할 수 있게 마지막으로 한 번 더 시도
return await client.GetStringAsync(uri);
}
Soft timeout
async Task<string> DownloadStringWithTimeout(HttpClient client, string uri)
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
Task<string> downloadTask = client.GetStringAsync(uri);
Task timeoutTask = Task.Delay(Timeout.InfiniteTimeSpan, cts.Token);
Task completedTask = await Task.WhenAny(downloadTask, timeoutTask);
if (completedTask == timeoutTask)
{
// WARNING: downloadTask는 여전히 동작한다.
return null;
}
return await downloadTask;
}
타임아웃이 지나면 실행을 중단해야 할 때
async Task IssueTimeoutAsync()
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
CancellationToken token = cts.Token;
await Task.Delay(TimeSpan.FromSeconds(10), token);
}
async Task IssueTimeoutAsync()
{
using var cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
cts.CancelAfter(TimeSpan.FromSeconds(3));
await Task.Delay(TimeSpan.FromSeconds(10), token);
}
비동기 시그니처를 사용해서 동기 메서드 구현
interface IMyAsync
{
Task<int> GetValueAsync(CancellationToken token);
Task DoSomethingAsync();
}
class MySync : IMyAsync
{
// 자주 사용하는 작업 결과라면 미리 만들어 놓고 쓴다.
private static readonly Task<int> ZeroTask = Task.FromResult(0);
public Task<int> GetValueAsync(CancellationToken token)
{
if (token.IsCancellationRequested)
return Task.FromCanceled<int>(token);
return Task.FromResult(10);
}
public Task<T> NotImplementedAsync()
{
return Task.FromException<T>(new NotImplementedException());
}
protected void DoSomethingSynchronously()
{
}
public Task DoSomethingAsync()
{
try
{
DoSomethingSynchronously();
return Task.CompletedTask;
}
catch (Exception ex)
{
return Task.FromException(ex);
}
}
}
async Task<string> DownloadAllAsync(HttpClient client,
IEnumerable<string> urls)
{
var downloads = urls.Select(url => client.GetStringAsync(url));
// 아직 실제로 시작한 작업은 없다.
// 동시에 모든 URL에서 다운로드 시작
Task<string>[] downloadTasks = downloads.ToArray();
// 모든 다운로드 완료를 비동기적으로 대기
string[] htmlPages = await Task.WhenAll(downloadTasks);
return string.Concat(htmlPages);
}
작업 중 하나가 예외를 일으키면 Task.WhenAll은 작업과 함께 해당 예외를 반환하며 실패한다.
여러 작업이 예외를 일으키면 모든 예외를 Task.WhenAll이 반환하는 Task에 넣는다.
async Task<int> DelayAndReturnAsync(int value)
{
await Task.Delay(TimeSpan.FromSeconds(value));
return value;
}
async Task AwaitAndProcessAsync(Task<int> task)
{
int rv = await task;
Trace.WriteLine(rv);
}
async Task ProcessTasksAsync(int flag)
{
Task<int> taskA = DelayAndReturnAsync(2);
Task<int> taskB = DelayAndReturnAsync(3);
Task<int> taskC = DelayAndReturnAsync(1);
var tasks = new Task[] { taskA, taskB, taskC };
Task[] processingTasks;
if (flag == 1)
{
IEnumerable<Task> taskQuery =
from t in tasks select AwaitAndProcessAsync(t);
processingTasks = taskQuery.ToArray();
await Task.WhenAll(processingTasks);
}
else if (flag == 2)
{
processingTasks = tasks.Select(async t =>
{
var rv = await t;
Trace.WriteLine(rv);
}).ToArray();
await Task.WhenAll(processingTasks);
}
else if (flag == 3)
{
foreach (Task<int> task in tasks.OrderByCompletion())
{
int rv = await task;
Trace.WriteLine(rv);
}
}
}
async void 메서드의 예외 처리
sealed class MyAsyncCommand : ICommand
{
async void ICommand.Execute(object parameter)
{
await Execute(parameter);
}
public async Task Execute(object parameter)
{
; // 비동기 작업 구현
}
; // CanExecute 등 구현
}
ValueTask 생성/사용
반환할 수 있는 동기적 결과가 있고 비동기 동작이 드문 상황에서 반환 형식으로 사용
프로파일링을 통해 애플리케이션의 성능 향상을 확인할 수 있을 때만 고려해야 함
ValueTask를 반환하는 DisposeAsync 메서드가 있는 IAsyncDisposable을 구현할 때
private Task<int> SlowMethodAsync();
public ValueTask<int> MethodAsync()
{
if (CanBehaveSynchronously)
return new ValueTask<int>(1);
return new ValueTask<int>(SlowMethodAsync());
}
async Task ConsumingMethodAsync()
{
ValueTask<int> valueTask = MethodAsync();
; // 기타 동시성 작업
int value = await valueTask;
;
}
ValueTask는 딱 한 번만 대기할 수 있다. 더 복잡한 작업을 하려면 AsTask를 호출해서 ValueTask<T>를 Task<T>로 변환해야 한다. ValueTask에서 동기적으로 결과를 얻으려면 ValueTask를 완료한 뒤에 한 번만 할 수 있다.
async Task ConsumingTaskAsync()
{
Task<int> task = MethodAsync().AsTask();
; // 기타 동시성 작업
int value = await task;
// Task<T>는 await로 여러 번 대기해도 완벽하게 안전하다.
int anotherValue = await task;
}
async Task ConsumingTaskAsync()
{
Task<int> task1 = MethodAsync().AsTask();
Task<int> task2 = MethodAsync().AsTask();
int[] results = await Task.WhenAll(task1, task2);
}
Asynchronous Stream
async IAsyncEnumerable<string> GetValuesAsync(HttpClient client)
{
const int limit = 10;
for (int offset = 0; true; offset += limit)
{
string result = await client.GetStringAsync(
$"https://example.com/api/values?offset={offset}&limit={limit}");
string[] valuesOnThisPage = result.Split('\n');
// 현재 페이지의 결과 전달
foreach (string value in valuesOnThisPage)
yield return value;
// 마지막 페이지면 끝
if (valuesOnThisPage.Length != limit)
break;
}
public async Task ProcessValuesAsync(HttpClient client)
{
await foreach (string value in GetValuesAsync(client))
{
Console.WriteLine(value);
}
}
public async Task ProcessValuesAsync(HttpClient client)
{
await foreach (string value in GetValuesAsync(client).ConfigureAwait(false))
{
await Task.Delay(100).ConfigureAwait(false); // 비동기 작업
Console.WriteLine(value);
}
}
비동기 스트림과 LINQ 함께 사용
IEnumerable<T>에는 LINQ to Objects가 있고 IObservable<T>에는 LINQ to Events가 있다.
IAsyncEnumerable<T>도 System.Linq.Async NuGet Package를 통해 LINQ 지원
IAsyncEnumerable<int> values = SlowRange().WhereAwait(
async value =>
{
// 요소의 포함 여부를 결정할 비동기 작업 수행
await Task.Delay(10);
return value % 2 == 0;
})
.Where(value => value % 4 == 0); // 결과는 비동기 스트림
await foreach (int result in values)
Console.WriteLine(result);
// 진행에 따라 속도가 느려지는 시퀀스 생성
async IAsyncEnumerable<int> SlowRange()
{
for (int i = 0; i < 10; ++i)
{
await Task.Delay(i * 100);
yield return i;
}
}
Async 접미사는 값을 추출하거나 계산을 수행한 뒤에 비동기 시퀀스가 아닌 비동기 스칼라 값을 반환하는 연산자에만 붙는다.
int count = await SlowRange().CountAsync(
value => value % 2 == 0);
// 조건자가 비동기적일 땐 AwaitAsync 접미사가 붙는 연산자를 사용
int count = await SlowRange().CountAwaitAsync(
async value =>
{
await Task.Delay(10);
return value % 2 == 0;
});
비동기 스트림 취소
using var cts = new CancellationTokenSource(500);
CancellationToken token = ct.Token;
await foreach (int result in SlowRange(token))
{
Console.WriteLine(result);
}
// 진행에 따라 속도가 느려지는 시퀀스 생성
async IAsyncEnumerable<int> SlowRange(
[EnumeratorCancellation] CancellationToken token = default)
{
for (int i = 0; i < 10; ++i)
{
await Task.Delay(i * 100, token);
yield return i;
}
}
비동기 스트림의 열거에 CancellationToken을 추가할 수 있는 WithCancellation 확장 메서드 지원
async Task ConsumeSequence(IAsyncEnumerable<int> items)
{
using var cts = new CancellationTokenSource(500);
CancellationToken token = cts.Token;
await foreach (int result in items.WithCancellation(token))
{
Console.WriteLine(result);
}
}
await ConsumeSequence(SlowRange());