【问题标题】:How to create a Task/TPL based work queue/event loop?如何创建基于任务/TPL 的工作队列/事件循环?
【发布时间】:2016-05-15 22:58:45
【问题描述】:

我有一个系统,其中所有数据库操作 (SQLite) 必须通过使用工作处理器来运行,因此我可以随意安排它们。

现在处理器有一个Task<TResult> PostAsync<TResult>(Func<SQLiteConnection, TResult> dbFunc),但我能想出的唯一理智的实现是一个基于堆栈的 UI 线程工作循环,它利用Task 能够将结果推迟到处理实际操作的那一刻。

它工作正常,但它用 SQLite 调用阻塞了 UI 线程。我宁愿不发生这种情况,但我能想出的唯一想法是产生一个工作线程来刷新队列或为每个操作提供它自己的工作线程。我认为这两种方法都不是最明智的方法,因为它们最终会在每次需要访问数据库时创建线程。

如何实现堆栈中的dbFunc 参数在后台线程中执行但所述线程只创建一次或尽可能轻量级的实现?

这必须在 Xamarin 环境中工作。在 PCL 或 Android+iOS 实现中。

【问题讨论】:

  • 就多线程而言,我认为 .NET PCL 中没有任何东西支持启动另一个线程。 System.ComponentModel 不包含 BackgroundWorker 类。 System.Threading 命名空间不包含 Thread 类。使用其中任何一个,您都可以构建一个队列(我已经做过很多次了。但它依赖于Thread 类)。异步任务可能是您在 PCL 中可以获得的最好的任务。这个 SO 线程似乎证实了:stackoverflow.com/questions/9251917/…
  • 我没有提到我可以摆脱 Android+iOS 平台特定的工作接口实现。
  • 看来您的PostAsync 参数应该是Func<SQLiteConnection, TResult>,而不是相反。
  • @KirillShlenskiy 不错。固定。

标签: c# multithreading xamarin


【解决方案1】:

您不需要泵或队列类型的解决方案来完成此操作。只需通过SemaphoreSlim 序列化对SQLiteConnection 的访问即可解决问题(当然,您需要单例SQLiteConnectionSemaphoreSlim 实例):

private readonly SQLiteConnection _sharedConnection;
private readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1);

public async Task<TResult> PostAsync<TResult>(Func<SQLiteConnection, TResult> dbFunc, CancellationToken ct)
{
    TResult result;
    bool needToRelease = false;

    try
    {
        await Semaphore.WaitAsync(ct).ConfigureAwait(false);

        // If we got this far, Release *must* be called.
        needToRelease = true;

        ct.ThrowIfCancellationRequested();

        // Push the work off to the thread pool in the event that
        // WaitAsync completed synchronously and we're still on the UI thread.
        result = await Task
            .Run(() => dbFunc(_sharedConnection), ct)
            .ConfigureAwait(false);
    }
    finally
    {
        if (needToRelease) {
            Semaphore.Release();
        }
    }

    return result;
}

以上内容适用于 Xamarin.iOS 和 Xamarin.Android - 但是我知道 SemaphoreSlim 在某些 PCL 配置文件中不可用。

您可能认为每次需要访问数据库时都启动多个 Task 实例有点丰富,但实际上它并没有太多工作,即使对于移动设备也是如此。

上面的一个更大的问题是它仍然是“假”的异步。我们只是在滥用Task.Run 将阻塞操作卸载到线程池。

因此,值得一提的是更基本的替代方案(I myself use)。它是通过包装类公开您的SQLiteConnection,其中每个公共方法locks 在共享对象上执行SQLiteConnection 实例上的相关方法之前。在这种情况下,调用者是想直接执行潜在的阻塞调用,还是通过Task.Run 将它们推到线程池中,这取决于调用者。在SQLite.Net 开始提供true异步之前,我相信这是使用SQLiteConnection 管理并发的最佳方法。

【讨论】:

  • 我真的很想避免Task.Run 使用一个线程,该线程在堆栈得到一个推送到它的工作时创建并在没有更多可泵送时死亡。这种事情能实现吗?信号量与Stack&lt;FuncWithTaskCompletionSourceClass&gt; 实现相比有什么好处?我现在正在运行它,它可以工作,但我想知道我是否错过了什么。
  • Task.Run 不会创建和终止线程 - 它会从线程池中获取线程,并且如果您确保在任何给定时间只允许通过 Task.Run 创建的一个任务(即SemaphoreSlim 做了什么——它是一个真正的异步互斥锁来保护任务),然后(理论上)你永远不需要多个线程池线程来服务你的数据库调用。换句话说,这会产生您描述的确切语义(减去堆栈部分 - 这是非常非常规的)。您只是让线程池管理“泵”的启动和重新启动,它非常擅长。
  • 当然,您可以滚动托管“在可用时继续执行工作,然后终止直到推送更多工作”状态机。问题是,这样做并没有真正的好处,因为您仍然需要它在工作可用时运行 sonewhere。所以它将从线程池中借一个线程(当工作可用时)来这样做。结果与我上面的示例相同,但是在安排新工作以及工作完成时,您还必须担心(可能不重要的)线程同步。这是一项有趣的学术活动,但痛苦多于其价值。
  • 很棒的描述,我将把我的实现切换到这个。非常感谢。
  • @Machinarius,感谢您的反馈。很高兴听到该解决方案非常适合您的目的。
猜你喜欢
  • 2021-05-17
  • 2018-11-18
  • 1970-01-01
  • 1970-01-01
  • 2011-12-13
  • 1970-01-01
  • 2021-04-03
  • 1970-01-01
  • 2018-09-18
相关资源
最近更新 更多