在非 UI 线程上运行 async/await 方法[关闭]

Posted

技术标签:

【中文标题】在非 UI 线程上运行 async/await 方法[关闭]【英文标题】:Run async/await method on other than UI thread [closed] 【发布时间】:2021-01-04 22:04:42 【问题描述】:

我有一个通过 EF 连接到数据库的 WPF 应用程序。但是当我调用 await DbSet().SingleAsync() 我的 UI 线程被卡住并且没有响应。我发现当我运行 await 时,它仍会在主 UI 线程上运行,因此我可以访问 UI。这将解释为什么当我只使用Task.Delay() 时它可以工作,因为SingleAsync 正在使用线程,但Task.Delay() 没有。 ConfigureAwait(false) 应该解决这个问题并在不同的线程上运行它。但它没有,我的 UI 仍然冻结。

我做错了吗?

代码:

private async void Button_Login_Click(object sender, RoutedEventArgs e)

   await userService.ValidateCredentials(textBoxLogin.Text, textBoxLogin.Text).ConfigureAwait(false);


public async Task<bool> ValidateCredentials(string username, string password)

    User user = await userDao.SingleOrNull(true, o => o.Username == username && o.Password == password);

    if (user == null)
        return (false);
    return (true);


public async Task<ResourceDbType> SingleOrNull(bool noTracking, Expression<Func<ResourceDbType, bool>> where)

    ResourceDbType ret;
    try
    
        if (noTracking)
        
            ret = await GetDbSet().AsNoTracking().SingleAsync(where);
        
        else
            ret = await GetDbSet().SingleAsync(where);
    
    catch (Exception ex)
    
        return null;
    

    return ret;

编辑: BaseDao 应该只公开 DbContext 中的函数,并在 MainDbContext 注册选定的 DbSet 模型。我们在 RestApi 服务中使用了这段代码,所以我重用了它,因为我已经习惯了。

[RegisterClass(Lifetime.Scoped)] 是标记类的属性,以便在应用程序启动时通过反射将其注册到 DependencyInjection。

道码:

public class BaseDao<ResourceDbType> : IDisposable where ResourceDbType : class, new()

 public DbContext DbContext  get; protected set; 

 public BaseDao(MainDbContext mainDbContext)
 
  DbContext = mainDbContext;
 
 public DbSet<ResourceDbType> GetDbSet()
 
  return this.DbContext.Set<ResourceDbType>();
 
        
 public List<ResourceDbType> ToList()
 
  return this.GetDbSet().ToList();
 

 public ResourceDbType[] ToArray()
 
 return this.GetDbSet().ToArray();
 
 public async Task<ResourceDbType> SingleOrNull(bool noTracking, Expression<Func<ResourceDbType, bool>> where)
 
 ResourceDbType ret;
 try
  
   if (noTracking)
   
    ret = await GetDbSet().AsNoTracking().SingleAsync(where);
   
   else
    ret = await GetDbSet().SingleAsync(where);
   
  catch (Exception ex)
   
    return null;
   
 return ret;


public void Dispose()
 
  this.DbContext?.Dispose();
 

UserDao 代码:

[RegisterClass(Lifetime.Scoped)]
public class UserDao : BaseDao<User>

 public UserDao(MainDbContext mainDbContext) : base(mainDbContext)
 

 

【问题讨论】:

不应该是private async Task Button_Login_Click(object sender, RoutedEventArgs e)而不是private async void Button_Login_Click(object sender, RoutedEventArgs e)吗? @Delphi.Boy 不,它应该是异步 void 对于 OP,这里也有很多假设,其中很多是错误的或不正确的 这里没有典型的死锁问题(在显示的代码中)。故事可能还有更多内容,但根据当前代码很难知道在哪里 @Andy 我在所有地方都尝试过 ConfigureAwait,但结果相同。 【参考方案1】:

异步不是灵丹妙药,从我从示例中看到的情况来看,它很可能是不必要的。异步并不适合“随时随地使用”,而是可能减少昂贵的操作。

通过将 EF 隐藏在服务和存储库之后,您的 EF 操作的效率会大大降低。

例如:没有存储库级别。 (信任EF被服务使用,并注入到服务中)

// UI
private void Button_Login_Click(object sender, RoutedEventArgs e)

   var result = userService.ValidateCredentials(textBoxLogin.Text, textBoxLogin.Text);


// Service
public bool ValidateCredentials(string username, string password)

    var result = Context.Users.Any(x => x.UserName == userName && x.Password == password);
    return result;

EF 可以生成非常高效的EXISTS 查询,而不是仅仅为了检查行是否存在而加载实体。更快的代码,无需担心移交。

如果您希望能够对服务进行单元测试,那么您可以引入一个存储库。我建议利用IQueryable

// Service
public bool ValidateCredentials(string username, string password)

    using (var unitOfWork = UnitOfWorkFactory.Create())
    
        var result = UserRepository.FindUserByName(username, password).Any();
        return result;
    


// Repository
public IQueryable<User> FindUserByName(string userName, string password)

    var query = Context.Users.Where(x => x.UserName == userName && x.Password == password);
    return query;

存储库可以保护对实体的访问,以确保遵循所需的规则等,但比 DbContext 更容易模拟出来。这需要考虑在工作单元中确定 DbContext 的范围,以促进服务控制 DbContext 的边界并与存储库返回的结果实体进行交互。我在 EF 中使用的实现是 Medhime 的 DbContextScope。 (有可用于 EF Core 的分叉)这使服务可以完全控制实体如何与存储库一起使用,从而执行规则并使模拟更简单。

旁注:一些开发人员不喜欢服务需要了解 EF 问题(例如 EF 可以理解的合法 Lambda),但这是为了更精简、更灵活的存储库而做出的权衡。 (处理额外的标准、投影、排序、分页等很容易,但特定的消费者需要。)我已经看到许多尝试抽象出 EF 以接受标准、排序、投影和分页到使用 Func 等的存储库方法中。但现实情况是,这些都相当复杂,而且仍然必须遵守 EF 的规则。最终,当您选择使用 EF 时,您需要信任它作为解决方案的一部分,以便充分利用它。

异步更适合特别昂贵的操作。 WPF 的同步上下文本质上支持与异步代码交互,但由于这将确保代码在 UI 线程上恢复,因此使用它可以说没有什么好处,除了促进它能够等待异步方法。这更有助于事件处理程序在调用和等待几个异步操作时正常工作。

private async void Button_Login_Click(object sender, RoutedEventArgs e)

   var firstTask = userService.DoSomethingThatMightTake5SecondsAsync(); // Don't actually name your tasks this! :)
   var secondTask = userService.DoSomethingThatMightTake5SecondsAsync();
   var thirdTask = userService.DoSomethingThatMightTake5SecondsAsync();

   // Do stuff that doesn't rely on those 3 tasks....

   var firstResult = await firstTask;
   // Do something with first task results
   var secondResult = await secondTask;
   // Do something with second task results
   var thirdResult = await thirdTask;
   // Do something with third task results

同步地,这 3 个操作需要 15 秒,而其他任何不依赖它们的操作都必须等到它们完成。异步它们可以更快地完成,并且可以在处理时执行独立代码。

尽管在上面使用 EF 的示例中需要小心,因为 DbContext 之类的东西不是线程安全的,因此启动 3 个导致使用单个 DbContext 的异步方法会导致多个线程调用 DbContext。依次使用 UI 线程同步上下文等待它们实际上与同步调用相同,只是稍微慢了一点。 (产生线程池线程和等待同步上下文的开销)

使用async 应该视情况而定。

编辑:异步与同步的示例:

使用带有 2 个按钮和一个文本框 (uxEntry) 的简单 WPF 表单。一个按钮用于同步事件,一个按钮用于异步事件。调用其中一个按钮后,文本框将获得焦点,您可以尝试在其中输入以查看 UI 线程是否响应:

private async void AsyncButton_Click(object sender, RoutedEventArgs e)

    uxEntry.Focus();
    var result = await DoSomethingAsync();
    MessageBox.Show(result);


private void SyncButton_Click(object sender, RoutedEventArgs e)

    uxEntry.Focus();
    var result = DoSomethingSync();
    MessageBox.Show(result);


private async Task<string> DoSomethingAsync()

    await Task.Run(() =>
    
        Thread.Sleep(5000);
    );
    return "Done";


private string DoSomethingSync()

    Thread.Sleep(5000);
    return "Done";

如果您单击“同步”按钮,文本框将在 5 秒后才会获得焦点或接受输入。在异步示例中,它将在异步任务运行时立即响应。异步事件允许 UI 线程继续响应可以让您的应用感觉更灵敏的事件,但是将此与非线程安全的 EF DbContexts 结合使用可能会导致问题。

使用 Async 并行化操作对于使用单个注入的 DbContext 引用的代码会很危险,例如:

private async void AsyncMultiButton_Click(object sender, RoutedEventArgs e)

    uxEntry.Focus();

    var task1 = DoSomethingAsync();
    var task2 = DoSomethingAsync();
    var task3 = DoSomethingAsync();

    var result1 = await task1;
    var result2 = await task2;
    var result3 = await task3;

    var message = string.Format("Task1: 0 Task2: 1  Task3: 2", result1, result2, result3);

    MessageBox.Show(message);

如果DoSomethingAsync 与存储库 DbContext 通信,那么这 3 个任务将同时启动,而 DbContext不会那样。根据代码的不同,它可能会导致令人头疼的问题,因为它有时或在调试环境中似乎工作,只是在其他人或生产中因错误而崩溃。解决方案是依次等待每个人:

uxEntry.Focus();
var result1 = await DoSomethingAsync();
var result2 = await DoSomethingAsync();
var result3 = await DoSomethingAsync();

这对于 DbContext 来说是安全的,并且 UI 会响应,但是现在需要 15 秒才能完成。并行启动它们可能很诱人,只是在使用异步时要小心。它有助于使您的应用程序看起来更具响应性,但在认为它可以使您的应用程序“更快”时要谨慎。

查看您的原始代码示例,我看不出等待的事件处理程序仍会锁定 UI 的明显原因,尽管您当前的代码和您提供的示例之间可能存在一些差异。

【讨论】:

我建议在方法DoSomethingThatMightTake5Seconds后面加上后缀Async,以符合guidelines。 呵呵,明明是伪方法,但是异步方法应该加Async后缀。 @StevePy 谢谢你的回答。我知道登录是不必要的,但这只是为了测试。但是将来会有更复杂的查询。例如,获取用户可以看到的构造列表以及每个构造的负载工人等。因此,使用连接查询将更加复杂。而且登录的时候很明显是UI卡住了,加载建设就更惨了。我想用“DoSomethingThatMightTake5SecondsAsync”做你在例子中所做的事情,但我怎么能用 EF 做呢? @Alox,我已经扩展了我的答案,其中提供了一个异步方法示例以及在异步和同步以及并行异步行为之间考虑的不同场景。

以上是关于在非 UI 线程上运行 async/await 方法[关闭]的主要内容,如果未能解决你的问题,请参考以下文章

使用 async/await 并行执行任务继续

Kotlin Coroutine,Android Async Task 和 Async await 的区别

Oracle 托管驱动程序可以正确使用 async/await 吗?

异步编程async/await

TAP 和 async/await 是不是意味着用于专用线程?

TPL 和 async/await 之间的区别(线程处理)