How to handle code that is synchronous on one plat

2019-06-03 07:30发布

In writing my Xamarin-based cross-platform C# app, a situation I encounter fairly often is code that is synchronous on one platform but async on another. For example, file access is synchronous on iOS, Android, and Mac, but async on Windows.

This leads to some tricky issues. First of all, on a pure coding level, the compiler wants a method to be either async or not.

But it's worse than that. For example, if the iOS UI thread discovers that it needs to load a small file, and does so, synchronously, on the UI thread, this probably won't cause any noticeable hiccup for the user. Yes, I realize that as a general rule one wants to pre-load such resources. But if that doesn't happen, and the file is small, it's not going to cause a problem.

BUT if one calls a C# async method from the iOS UI code and awaits the result, the app will hang.

The upshot is that we have two methods, Foo() and FooAsync(), and which one we call depends on the platform. But we can't just make the whole thing async.

In light of that, how does one organize one's code to stick to DRY as much as possible, while remaining synchronous on one platform and async on the other?

**

EDIT -- In response to the request for an example: here is some dummy code that illustrates the problem. If one calls

 await StringLoaders.ReadAllText(somePath) 

from the iOS UI thread, the result is a hang. Yes, there are work arounds for this particular example. But the general problem is a recurring one. It is particularly bad if the app has a lot of layers, as this one does -- the sync/async distinction may need to be carried throughout the code, resulting in two versions of an enormous number of methods.

public class AsyncStringLoader
{
  public async Task<string> ReadAllTextAsync(string path) {
    return await Task.Run(() => "Hello");
  }
}
public class StringLoader {
  public string ReadAllText(string path) {
    return "Hello";
  }
}
public static class StringLoaders {
  public static object Current { get; set;} // already trouble here -- we can't really have a proper interface for ReadAllText.
  public static async Task<string> ReadAllText(string path) {
    object loader = StringLoaders.Current;
    if (loader is StringLoader) {
      return ((StringLoader)loader).ReadAllText(path);
    } else if (loader is AsyncStringLoader) {
      return await ((AsyncStringLoader)loader).ReadAllTextAsync(path);  // DRY violation, in addition to the hang
    } else {
      throw new Exception("Unknown loader!");
    }
  }
}

If you want a real example, as opposed to a dummied-up one, download the PCLStorage library from NuGet, then try using it for anything that is called from the iOS UI thread . . .

1条回答
狗以群分
2楼-- · 2019-06-03 08:17

You can define an interface (or set of interfaces) on the top level that only contain async methods for consistency. This top level interface is to be consumed by your higher level application / UI code so higher level code would take that interface object as dependency. Underneath that interface implement C# adapters per OS type that implement the async interface methods. For Windows it is straightforward you use the natively async Windows api s. For other operating systems that do not provide async api s, use their sync methods but return an awaitable task object that wraps the already computed result like this:

return Task.FromResult(/*the result of the sync api call from iOS*/);

Your higher level code that consumes the interface will just await on the async methods of the interface without being concerned about the implementation underneath. If underneath implementation is iOS that call will run synchronously and exactly as if you called the sync method directly. If underneath implementation is Windows the async method will run.

The reason I suggest the interface to be all in async methods (vs all sync) is that it is safer to make sync calls look like async (by Task.FromResult) than forcing async calls work synchronously which can cause deadlocks. The other reason is that obviously you would want to leverage async api s as much as possible at least for the operating systems that do provide async methods.

You can decide which actual concrete implementation of the interface (ie. Windows, IOS, etc..) to pass in to the higher level code on run time based on a configuration parameter etc. You can also use one of the existing Dependency Injection Containers for registering concrete classes to interfaces.

Your question was a bit open ended so provide more details if you are looking for something else.

查看更多
登录 后发表回答