Write a well designed async / non-async API

前端 未结 3 867
深忆病人
深忆病人 2020-11-27 03:39

I\'m facing the problem of designing methods that with performs network I/O (for a reusable library). I\'ve read this question

c# 5 await/async pattern in API design

相关标签:
3条回答
  • 2020-11-27 04:12

    If you want the most maintainable option, only provide an async API, which is implemented without making any blocking calls or using any thread pool threads.

    If you really want to have both async and synchronous APIs, then you'll encounter a maintainability problem. You really need to implement it twice: once async and once synchronous. Both of those methods will look nearly identical so the initial implementation is easy, but you will end up with two separate nearly-identical methods so maintenance is problematic.

    In particular, there's a no good and simple way to just make an async or synchronous "wrapper". Stephen Toub has the best info on the subject:

    1. Should I expose asynchronous wrappers for synchronous methods?
    2. Should I expose synchronous wrappers for asynchronous methods?

    (the short answer to both questions is "no")

    0 讨论(0)
  • 2020-11-27 04:22

    I ran into the same problem but managed to find a compromise between efficiency and maintainability using two simple facts about async methods:

    • asynchronous method which does not execute any await is synchronous;
    • asynchronous method which awaits only synchronous methods is synchronous.

    This is better to be shown on example:

    //Simple synchronous methods that starts third party component, waits for a second and gets result.
    public ThirdPartyResult Execute(ThirdPartyOptions options)
    {
        ThirdPartyComponent.Start(options);
        System.Threading.Thread.Sleep(1000);
        return ThirdPartyComponent.GetResult();
    }
    

    To provide maintainable sync/async version of this method it has been split to three layers:

    //Lower level - parts that work differently for sync/async version.
    //When isAsync is false there are no await operators and method is running synchronously.
    private static async Task Wait(bool isAsync, int milliseconds)
    {
        if (isAsync)
        {
            await Task.Delay(milliseconds);
        }
        else
        {
            System.Threading.Thread.Sleep(milliseconds);
        }
    }
    
    //Middle level - the main algorithm.
    //When isAsync is false the only awaited method is running synchronously,
    //so the whole algorithm is running synchronously.
    private async Task<ThirdPartyResult> Execute(bool isAsync, ThirdPartyOptions options)
    {
        ThirdPartyComponent.Start(options);
        await Wait(isAsync, 1000);
        return ThirdPartyComponent.GetResult();
    }
    
    //Upper level - public synchronous API.
    //Internal method runs synchronously and will be already finished when Result property is accessed.
    public ThirdPartyResult ExecuteSync(ThirdPartyOptions options)
    {
        return Execute(false, options).Result;
    }
    
    //Upper level - public asynchronous API.
    public async Task<ThirdPartyResult> ExecuteAsync(ThirdPartyOptions options)
    {
        return await Execute(true, options);
    }
    

    The main advantage here is that middle level algorithm which is most likely to change is implemented only once so developer don't have to maintain two almost identical pieces of code.

    0 讨论(0)
  • I agree with both Marc and Stephen (Cleary).

    (BTW, I started to write this as a comment to Stephen's answer, but it turned out to be too long; let me know if it is OK to write this as an answer or not, and feel free to take bits from it and add it to Stephen's answer, in the spirit of "providing the one best answer").

    It really "depends": like Marc said, it is important to know how DoSomethingAsync is asynchronous. We all agree that there is no point in having a the "sync" method call the "async" method and "wait": this can be done in user code. The only advantage of having a separate method is to have actual performance gains, to have an implementation which is, under the hood, different and tailored to the synchronous scenario. This is especially true if the "async" method is creating a thread (or taking it from a threadpool): you end up with something that underneath uses two "control flows", while "promising" with its synchronous looks to be executed in the callers' context. This may even have concurrency issues, depending on the implementation.

    Also in other cases, like the intensive I/O that the OP is mentioning, it may be worth having two different implementation. Most operating systems (Windows for sure) have for I/O different mechanisms tailored to the two scenarios: for example, async execution of and I/O operation takes great advantages from OS level mechanisms like I/O completion ports, which add a little overhead (not significant, but not null) in the kernel (after all, they have to do bookkeeping, dispatch, etc.), and more direct implementation for synchronous operations. Code complexity also varies a lot, especially in functions where multiple operations are done/coordinated.

    What I would do is:

    • have some examples/test for typical usage and scenarios
    • see which API variant is used, where, and measure. Measure also difference in performance between a "pure sync" variant and "sync". (not for the whole API, but for selected few typical cases)
    • based on measurement, decide if the added cost is worth it.

    This mainly because two goals are somehow in contrast with one another. If you want maintainable code, the obvious choice is implementing sync in terms of async/wait (or the other way around) (or, even better, provide only the async variant and let the user do "wait"); if you want performance you should implement the two functions differently, to exploit different underlying mechanisms (from the framework or from the OS). I think that it should not make difference from a unit-testing point of view how you actually implement your API.

    0 讨论(0)
提交回复
热议问题