내용 보기

작성자

관리자 (IP : 106.247.248.10)

날짜

2023-01-05 08:59

제목

[C#] [스크랩] async await tips-and-tricks


async / await 외 다른 닷넷 tips / tricks도 아래 웹사이트에서 내용을 참고해 보면 좋습니다.
.NET tips-and-tricks




async await

The following chapter will deep dive into tips, tricks and pitfalls when it comes down to async and await in C#.

Elide await keyword - Exceptions

Eliding the await keyword can lead to a less traceable stacktrace due to the fact that every Task which doesn’t get awaited, will not be part of the stack trace.

❌ Bad

using System;
using System.Threading.Tasks;

try
{
    await DoWorkWithoutAwaitAsync();
}
catch (Exception e)
{
    Console.WriteLine(e);
}

static Task DoWorkWithoutAwaitAsync()
{
    return ThrowExceptionAsync();
}

static async Task ThrowExceptionAsync()
{
    await Task.Yield();
    throw new Exception("Hey");
}

Will result in

System.Exception: Hey  
 at Program.<<Main>$>g__ThrowExceptionAsync|0_1()  
 at Program.<Main>$(String[] args)  

✅ Good If speed and allocation is not very crucial, add the await keyword.

using System;
using System.Threading.Tasks;

try
{
    await DoWorkWithoutAwaitAsync();
}
catch (Exception e)
{
    Console.WriteLine(e);
}

static async Task DoWorkWithoutAwaitAsync()
{
    await ThrowExceptionAsync();
}

static async Task ThrowExceptionAsync()
{
    await Task.Yield();
    throw new Exception("Hey");
}

Will result in:

System.Exception: Hey
   at Program.<<Main>$>g__ThrowExceptionAsync|0_1()
   at Program.<<Main>$>g__DoWorkWithoutAwaitAsync|0_0()
   at Program.<Main>$(String[] args)

💡 Info: Eliding the async keyword will also elide the whole state machine. In very hot paths that might be worth a consideration. In normal cases one should not elide the keyword. The allocations one is saving is depending on the circumstances but a normally very very small especially if only smaller objects are passed around. Also performance-wise there is no big gain when eliding the keyword (we are talking nano seconds). Please measure first and act afterwards.

Elide await keyword - using block

Eliding inside an using block can lead to a disposed object before the Task is finished.

❌ Bad Here the download will be aborted / the HttpClient gets disposed:

public Task<string> GetContentFromUrlAsync(string url)
{
    using var client = new HttpClient();
        return client.GetStringAsync(url);
}

✅ Good

public async Task<string> GetContentFromUrlAsync(string url)
{
    using var client = new HttpClient();
        return await client.GetStringAsync(url);
}

💡 Info: Eliding the async keyword will also elide the whole state machine. In very hot paths that might be worth a consideration. In normal cases one should not elide the keyword. The allocations one is saving is depending on the circumstances but a normally very very small especially if only smaller objects are passed around. Also performance-wise there is no big gain when eliding the keyword (we are talking nano seconds). Please measure first and act afterwards.

Return null Task or Task<T>

When returning directly null from a synchronous call (no async or await) will lead to NullReferenceException:

❌ Bad Will throw NullReferenceException

await GetAsync();

static Task<string> GetAsync()
{
    return null;
}

✅ Good Use Task.FromResult:

await GetAsync();

static Task<string> GetAsync()
{
    return Task.FromResult(null);
}

async void

The problem with async void is first they are not awaitable and second they suffer the same problem with exceptions and stack trace as discussed a bit earlier. It is basically fire and forget.

❌ Bad Not awaited

public async void DoAsync()
{
    await SomeAsyncOp();
}

✅ Good return Task instead of void

public async Task DoAsync()
{
    await SomeAsyncOp();
}

💡 Info: There are valid cases for async void like top level event handlers.

List<T>.ForEach with async

List<T>.ForEach and in general a lot of LINQ methods don’t go well with async await:

❌ Bad Is the same as async void

var ids = new List<int>();
// ...
ids.ForEach(id => _myRepo.UpdateAsync(id));

One could thing adding async into the lamdba would do the trick:

❌ Bad Still the same as async void because List<T>.ForEach takes an Action and not a Func<Task>.

var ids = new List<int>();
// ...
ids.ForEach(async id => await _myRepo.UpdateAsync(id));

✅ Good Enumerate through the list via foreach

foreach (var id in ids)
{
    await _myRepo.UpdateAsync(id);
}

Favor await over synchronous calls

Using blocking calls instead of await can lead to potential deadlocks and other side effects like a poor stack trace in case of an exception and less scalability in web frameworks like ASP.NET core.

❌ Bad This call blocks the thread.

public async Task SomeOperationAsync()
{
    await ...
}

public void Do()
{
    SomeOperationAsync().Wait();
}

✅ Good Use async & await in the whole chain

public async Task SomeOperationAsync()
{
    await ...
}

public async Task Do()
{
    await SomeOperationAsync();
}

Favor GetAwaiter().GetResult() over Wait and Result

Task.GetAwaiter().GetResult() is preferred over Task.Wait and Task.Result because it propagates exceptions rather than wrapping them in an AggregateException.

❌ Bad

string content = DownloadAsync().Result;

✅ Good

string content = DownloadAsync().GetAwaiter().GetResult();

Don’t use Task.Delay for small precise waiting times

Task.Delay‘s internal timer is dependent on the underlying OS. On most windows machines this resolution is about 15ms.

So: Task.Delay(1) will not wait one millisecond but something between one and 15 milliseconds.

var stopwatch = Stopwatch.StartNew();
await Task.Delay(1);
stopwatch.Stop(); // Don't account the Console.WriteLine into the timer
Console.WriteLine($"Delay was {stopwatch.ElapsedMilliseconds} ms");

Will print for example:

Delay was 6 ms

Properly awaiting concurrent tasks

Often times tasks are independent of each other and can be awaited independently.

❌ Bad The following code will run roughly 1 second.

await DoOperationAsync();
await DoOperationAsync();

async Task DoOperationAsync()
{
    await Task.Delay(500);
} 

✅ Good When tasks or their data is independent they can be awaited independently for maximum benefits. The following code will run roughly 0.5 seconds.

var t1 = DoOperationAsync();
var t2 = DoOperationAsync();
await t1;
await t2;

async Task DoOperationAsync()
{
    await Task.Delay(500);
} 

An alternative to this would be Task.WhenAll:

var t1 = DoOperationAsync();
var t2 = DoOperationAsync();
await Task.WhenAll(t1, t2); // Can also be inlined

async Task DoOperationAsync()
{
    await Task.Delay(500);
} 

ConfigureAwait with await using statement

Since C# 8 you can provide an IAsyncDisposable which allows to have asynchrnous code in the Dispose method. Also this allows to call the following construct:

await using var dbContext = await dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);

In this example CreateDbContextAsync uses the ConfigureAwait(false) but not the IAsyncDisposable. To make that work we have to break apart the statment like this:

var dbContext = await dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
    // ...
}

The last part has the “ugly” snippet that you have to introduce a new “block” for the using statement. For that there is a easy workaround:

var blogDbContext = await dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
await using var _ = blogDbContext.ConfigureAwait(false);
// You don't need the {} block here

출처1

https://linkdotnet.github.io/tips-and-tricks/async_await/

출처2