简体   繁体   中英

Contractual async and sync code

There are a lot of questions asking whether to mix async and sync code.

Most answers say it is a bad idea to expose sync wrappers for async methods, and to expose async wrappers for sync methods.

However, none of the answers address the specific scenario where you HAVE to mix async and sync code, and how to avoid common pitfalls that arise because of it.

See the following example:

class Program
{
    static void Main(string[] args)
    {
        IContract signatory = new SyncSignatory();
        signatory.FullfillContractAsync().Wait();
        signatory = new AsyncSignatory();
        signatory.FullfillContractAsync().Wait();
    }
}

using System.Threading.Tasks;

interface IContract
{
    Task FullfillContractAsync();
}

using System.Threading.Tasks;

class AsyncSignatory : IContract
{
    public async Task FullfillContractAsync()
    {
        await Task.Delay(5000);
    }
}

using System.Threading;
using System.Threading.Tasks;

class SyncSignatory : IContract
{
    public Task FullfillContractAsync()
    {
        Thread.Sleep(5000);
        return Task.FromResult<object>(null);
    }
}

Or:

using System.Threading;
using System.Threading.Tasks;

class SyncSignatory : IContract
{
    public Task FullfillContractAsync()
    {
        return Task.Run(() => Thread.Sleep(5000));
    }
}

In this example, SyncSignatory and AsyncSignatory represent two interchangeable classes because they perform similar functions, however they perform these functions in different manners - synchronously and asynchronously.

How do you mix contractual sync and async code while avoiding common pitfall scenarios?

How about a user that expects syncSig to run async, but instead it runs sync?

What about a user who could have optimized syncSig by running it async, but not longer does because they assume it already runs async?

Is this the proper way to mix async and sync?

Mostly yes. If you have a common interface, where some implementations will be able to provide a synchronous implementation, but others only an asynchronous one, returning Task<T> makes sense. A simple return Task.FromResult can be appropriate. But there are exceptions, see below.

How about a user that expects syncSig to run async, but instead it runs sync?

There are some aspects of interfaces that cannot be expressed in code.

If it is a requirement of your IContract that it not block the calling thread for more than X milliseconds before returning a Task (not a reasonable hard requirement in this exact form, but you get the basic idea), and it blocks the thread anyway, that's a contract violation and the user should report it as a bug to whoever implemented syncSig.

If it is a requirement of your IContract that it not throw synchronous exceptions, that any errors be reported using Task.FromException or equivalent, and your syncSig throws a synchronous exception anyway, that too is a contract violation.

Other than that, if syncSig simply immediately returns the correct result, there's no reason why that should bother any user.

What about a user who could have optimized syncSig by running it async, but not longer does because they assume it already runs async?

If syncSig running synchronously doesn't cause any problems, it doesn't matter, it doesn't need to be optimised yet.

If syncSig running synchronously does cause problems, basic debugging tools should soon enough tell the developer that syncSig is causing problems and should be investigated.

However, none of the answers address the specific scenario where you HAVE to mix async and sync code, and how to avoid common pitfalls that arise because of it.

That's because it's a bad idea. Any mixing code should be fully temporary in nature, existing only during a transition to asynchronous APIs.

That said, I have written an entire article on the subject .

How do you mix contractual sync and async code while avoiding common pitfall scenarios?

As I describe in my blog post on async interfaces , async is an implementation detail. A task-returning method may complete synchronously. However, I'd recommend avoiding blocking implementations; if they're not avoidable for some reason, then I'd carefully document that they exist.

Alternatively, you could use Task.Run as the implementation; I don't recommend this (for reasons detailed on my blog ), but it is an option if you have to have a blocking implementation.

How about a user that expects syncSig to run async, but instead it runs sync?

The calling thread is blocked synchronously. This is usually only a problem for UI threads - see below.

What about a user who could have optimized syncSig by running it async, but not longer does because they assume it already runs async?

If the calling code is ASP.NET, then it should call it directly and await . Synchronous code will be executed synchronously, which is desirable on that platform.

If the calling code is a UI app, then it needs to be aware that there are synchronous implementations, and can call it wrapped in a Task.Run . This avoids blocking the UI thread regardless of whether the implementation is synchronous or asynchronous.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM