Skip to main content
 首页 » 编程设计

c#之GUI 线程上的 IDbConnection.BeginTransaction,使用异步/等待,导致多次调用死锁

2024年02月25日32youxin

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

  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.BeginTransactionAsync? await Task.Run(() => connection.BeginTransaction()) 是一个可接受的解决方案吗?

请您参考如下方法:

With that said, do you see any harm in opening a transaction of a thread that didn't open the database connection?

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

Is await Task.Run(() => connection.BeginTransaction()) an acceptable solution?

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

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

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(); 
  } 
}