Program Tip

비동기 / 대기 아키텍처

programtip 2020. 12. 12. 12:18
반응형

비동기 / 대기 아키텍처


아키텍처의 하위 수준에서 async / await를 사용하는 경우 async / await 호출을 끝까지 "버블 업"해야합니까? 기본적으로 각 계층에 대해 새 스레드를 생성하기 때문에 (비동기 적으로 호출) 비효율적입니까? 각 레이어에 대한 비동기 함수입니까, 아니면 실제로 중요하지 않고 선호도에 따라 달라지는가?

EF에서 비동기 메서드를 가질 수 있도록 EF 6.0-alpha3을 사용하고 있습니다.

내 저장소는 다음과 같습니다.

public class EntityRepository<E> : IRepository<E> where E : class
{
    public async virtual Task Save()
    {
        await context.SaveChangesAsync();
    }
}

이제 내 비즈니스 계층은 다음과 같습니다.

public abstract class ApplicationBCBase<E> : IEntityBC<E>
{
    public async virtual Task Save()
    {
        await repository.Save();
    }
}

그리고 물론 내 UI의 메서드는 호출 할 때 동일한 패턴을 따릅니다.

이것은 :

  1. 필요한
  2. 성능에 부정적인
  3. 단지 선호의 문제

이것이 별도의 레이어 / 프로젝트에서 사용되지 않더라도 동일한 클래스에서 중첩 된 메서드를 호출하는 경우 동일한 질문이 적용됩니다.

    private async Task<string> Dosomething1()
    {
        //other stuff 
        ...
        return await Dosomething2();
    }
    private async Task<string> Dosomething2()
    {
        //other stuff 
        ...
        return await Dosomething3();
    }
    private async Task<string> Dosomething3()
    {
        //other stuff 
        ...
        return await Task.Run(() => "");
    }

아키텍처의 하위 수준에서 async / await를 사용하는 경우 async / await 호출을 끝까지 "버블 업"해야합니까? 기본적으로 각 계층에 대해 새 스레드를 생성하기 때문에 (비동기 적으로 호출) 비효율적입니까? 각 레이어에 대한 비동기 함수입니까, 아니면 실제로 중요하지 않고 선호도에 따라 달라지는가?

이 질문은 몇 가지 오해의 영역을 암시합니다.

첫째, 비동기 함수를 호출 할 때마다 새 스레드를 만들지 않습니다 .

둘째, 비동기 함수를 호출하기 때문에 비동기 메서드를 선언 할 필요가 없습니다. 이미 반환 된 작업에 만족한다면 async 수정자가 없는 메서드에서 반환하면됩니다 .

public class EntityRepository<E> : IRepository<E> where E : class
{
    public virtual Task Save()
    {
        return context.SaveChangesAsync();
    }
}

public abstract class ApplicationBCBase<E> : IEntityBC<E>
{
    public virtual Task Save()
    {
        return repository.Save();
    }
}

This will be slightly more efficient, as it doesn't involve a state machine being created for very little reason - but more importantly, it's simpler.

Any async method where you have a single await expression awaiting a Task or Task<T>, right at the end of the method with no further processing, would be better off being written without using async/await. So this:

public async Task<string> Foo()
{
    var bar = new Bar();
    bar.Baz();
    return await bar.Quux();
}

is better written as:

public Task<string> Foo()
{
    var bar = new Bar();
    bar.Baz();
    return bar.Quux();
}

(In theory there's a very slight difference in the tasks being created and therefore what callers could add continuations to, but in the vast majority of cases, you won't notice any difference.)


is it inefficient since you are basically creating a new thread for each layer (asynchronously calling an asynchronous function for each layer, or does it not really matter and is just dependent on your preference?

No. Asynchronous methods do not necessarily use new threads. In this case, since the underlying asynchronous method call is an IO bound method, there really should be no new threads created.

Is this:

1. necessary

It is necessary to "bubble" up the async calls if you want to keep the operation asynchronous. This really is preferred, however, as it allows you to fully take advantage of the asynchronous methods, including composing them together across the entire stack.

2. negative on performance

No. As I mentioned, this does not create new threads. There is some overhead, but much of this can be minimized (see below).

3. just a matter of preference

Not if you want to keep this asynchronous. You need to do this to keep things asynchronous across the stack.

Now, there are some things you can do to improve perf. here. If you're just wrapping an asynchronous method, you don't need to use the language features - just return the Task:

public virtual Task Save()
{
    return repository.Save();
}

The repository.Save() method already returns a Task - you don't need to await it just to wrap it back in a Task. This will keep the method somewhat more efficient.

You can also have your "low level" asynchronous methods use ConfigureAwait to prevent them from needing the calling synchronization context:

private async Task<string> Dosomething2()
{
    //other stuff 
    ...
    return await Dosomething3().ConfigureAwait(false);
}

This dramatically reduces the overhead involved in each await if you don't need to worry about the calling context. This is typically the best option when working on "library" code, since the "outer" await will capture the UI's context. The "inner" workings of the library don't typically care about synchronization context, so it's best to not capture that.

Finally, I'd caution against one of your examples:

private async Task<string> Dosomething3()
{
    //other stuff 
    ...
    // Potentially a bad idea!
    return await Task.Run(() => "");
}

If you're making an async method which, internally, is using Task.Run to "create asynchrony" around something that's not itself asynchronous, you're effectively wrapping up synchronous code into an async method. This will use a ThreadPool thread, but can "hide" the fact that it's doing so, effectively making the API misleading. It's often better to leave the call to Task.Run for your highest level calls, and let the underlying methods stay synchronous unless they are truly able to take advantage of asynchronous IO or some means of unloading other than Task.Run. (This isn't always true, but "async" code wrapped over synchronous code via Task.Run, then returned via async/await is often a sign of a flawed design.)

참고URL : https://stackoverflow.com/questions/15503782/architecture-for-async-await

반응형