【问题标题】:IDbConnection.BeginTransaction on GUI thread, using async/await, causing dead-lock with multiple callsGUI线程上的IDbConnection.BeginTransaction,使用异步/等待,导致多次调用死锁
【发布时间】:2019-01-30 17:33:47
【问题描述】:

我遇到了一个问题,我完全理解,但正在努力解决。

  1. 我有一个使用 async/await 的事件循环。
  2. 我在所有数据库操作中使用async/await,所有的延续都登陆所有运行事件循环的同一个线程。
  3. 我还需要使用事务,它在其生命周期内会有一些其他的延续。

话虽如此,请考虑这段代码。

public async Task UpdateItem(int mediaItemId)
{
    using (var connection = _dataService.OpenDbConnection())
    {
        using (var transaction = connection.BeginTransaction())
        {
            var item = await connection.SingleByIdAsync<MediaItem>(mediaItemId);

            item.Index++;
            await connection.UpdateAsync(item);

            transaction.Commit();
        }
    }
}

此方法的调用者来自主事件循环。

我首先创建一个事务。使用 SqlLite,这实际上是一个数据库级互斥体。这意味着当一个事务正在进行时,对BeginTransaction 的其他调用将被阻塞。

现在,考虑多次快速连续调用此方法。在awaiting SingleByIdAsync 之后,第二个调用将尝试调用BeginTransaction,但它会在那里等到第一个事务完成。这是意料之中的,除了它会阻塞主事件循环,阻止任何进一步的继续发生,让第一个事务保持打开状态。

繁荣,僵局。

如果有IDbConnection.BeginTransactionAsync,这将得到解决,但没有。那我可以跳过事件循环,一旦事务成功打开就继续。

因此,请考虑以下修复:

public async Task UpdateItem(int mediaItemId)
{
    using (var connection = _dataService.OpenDbConnection())
    {
        // Note that we are awaiting the opening of the transaction.
        using (var transaction = await Task.Run(() => connection.BeginTransaction()))
        {
            var item = await connection.SingleByIdAsync<MediaItem>(mediaItemId);

            item.Index++;
            await connection.UpdateAsync(item);

            transaction.Commit();
        }
    }
}

话虽如此,您认为打开未打开数据库连接的线程的事务有什么害处吗?为什么没有IDbConnection.BeginTransactionAsyncawait Task.Run(() =&gt; connection.BeginTransaction()) 是可接受的解决方案吗?

【问题讨论】:

  • 你能显示“事件循环”吗? .NET 通常没有这样的东西,至少不是这里建议的方式。
  • 它是基于 glib 的。很难描述,但我使用的是 QmlNet
  • 请想办法表达情况的本质。是否有一个线程具有同步上下文?
  • 是的,一个线程,带有一个事件循环,带有一个自定义同步上下文,可以将延续分派到在单个线程上运行的事件循环上。

标签: c# .net multithreading transactions async-await


【解决方案1】:

话虽如此,您认为打开未打开数据库连接的线程的事务有什么害处吗?

这里有几个考虑因素。第一个是在与打开数据库连接的线程不同的线程上打开事务。另一个是您在与打开它的线程不同的线程上提交和处理事务。

await Task.Run(() => connection.BeginTransaction()) 是可接受的解决方案吗?

“这是一个问题吗?”的问题。只能由您的(客户)数据库提供商回答。

如果你想确保它不会成为问题,你可以包含你自己的互斥锁:

private static readonly SemaphoreSlim _mutex = new SemaphoreSlim(1);
public async Task UpdateItem(int mediaItemId)
{
  using (var connection = _dataService.OpenDbConnection())
  {
    await _mutex.WaitAsync();
    try
    {
      using (var transaction = await connection.BeginTransaction())
      {
        var item = await connection.SingleByIdAsync<MediaItem>(mediaItemId);
        item.Index++;
        await connection.UpdateAsync(item);

        transaction.Commit();
      }
    }
    finally
    {
      _mutex.Release();
    }
  }
}

或者,using AsyncEx for a nicer syntax

private static readonly SemaphoreSlim _mutex = new SemaphoreSlim(1);
public async Task UpdateItem(int mediaItemId)
{
  using (var connection = _dataService.OpenDbConnection())
  using (_mutex.LockAsync())
  using (var transaction = await connection.BeginTransaction())
  {
    var item = await connection.SingleByIdAsync<MediaItem>(mediaItemId);
    item.Index++;
    await connection.UpdateAsync(item);

    transaction.Commit();
  }
}

【讨论】:

  • 谢谢!我刚刚查看了 .NET sqlite 实现,尤其是 sqlite3_exec,它是线程安全的。我将在另一个线程上等待。
猜你喜欢
  • 2021-09-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-24
  • 1970-01-01
相关资源
最近更新 更多