如何正确实现专为异步使用而设计的接口?

How to properly implement an interface that was designed for async usage?

从以下(简化的)界面开始,考虑到 async/await,我想使用 LiteDB 数据库来实现它。

public interface IDataService
{
    Task<User> GetUserAsync(int key);
}

很遗憾,LiteDB 目前不支持异步方法。所有方法都是同步的。

我尝试了以下方法,但后来 运行 遇到了一些问题(进程并没有真正等待任何原因)。之后,我读到您应该只将 Task.Run() 用于 CPU 绑定算法,因此不能用于数据库访问。

public Task<User> GetUserAsync(int key)
{
    var task = Task.Run(() =>
    {
        return _users
            .Find(x => x.Key == key)
            .SingleOrDefault();
    });
    return task;
}

尽管我阅读了几篇博客文章和 SO 问题,但我仍然不清楚如何为基于同步 IO 的代码编写正确编写的可等待方法。

那么我怎样才能编写一个适当等待的方法呢?

注意:我无法更改界面。这是给出的。

一种选择是使用 Task.FromResult 并同步实现您的界面。

return Task.FromResult(_users.Find(x => x.Key == key).SingleOrDefault());

async/await 的回报是使用 I/O 完成端口,它暂停线程并允许它为其他传入请求提供服务,直到等待的 I/O 操作完成。鉴于您的实施无法利用这种将您的工作包装在 Task.Run 或类似形式中没有任何好处。

不得使用Task.Run。这不是这样做在道德上是否正确的问题。正如您所注意到的,将工作线程分配给不受 CPU 约束的任务在道德上是错误的,但这不是这里的问题。这里的问题是 LiteDB 是单线程的,并且在试图将调用转移到工作线程时并不是为了健壮而构建的。

更新:显然 LiteDB 在版本 4 中是线程安全的,据一位可能比我在这个问题上更了解情况的评论者说。因此,请查阅 LiteDB 文档以了解线程安全的种类。并非所有线程安全对象都可以在任何线程上不受限制地使用。

您的选择是:

  • 放弃使用此接口。
  • 正如另一个答案所暗示的那样,撒谎。假设您的实现是异步的,而实际上它是同步的。这可能会导致您的用户界面挂起,但是嘿,当您进行同步 long-运行 调用时,无论如何您都会挂起您的用户界面。明确调用 FromResult 是正确的方法。
  • 获得更好的支持异步的数据库。或者等待 LiteDB 成为一个更好的数据库,支持异步。
  • 所有 调用移至 LiteDB,包括创建和销毁与此数据库关联的 每个 对象,到专用线程上。 (可能在进程中,但是,嘿,如果你愿意,你可以把它放在自己的进程中。)为 LiteDB 实现你自己的异步前端,适当地编组进出这个专用线程的调用。是的,您烧毁了大部分时间都在休眠的整个线程,但这是您使用同步数据库所付出的代价。

满足您规定的所有约束的解决方案是最后一个。这也是最适合您的工作。正如木工喜欢说的那样:您在廉价材料上节省的钱将花在劳动力上。尝试将正确的异步接口改装到非异步但 I/O 绑定的单线程库是一个棘手的问题。祝你好运!