为啥我不能将异步查询移动到方法中?

Posted

技术标签:

【中文标题】为啥我不能将异步查询移动到方法中?【英文标题】:Why can't I move async query into method?为什么我不能将异步查询移动到方法中? 【发布时间】:2014-03-23 06:22:15 【问题描述】:

以下代码有效

[Route("case-studies/slug")]
public async Task<ActionResult> Details(string slug)

    var item = await Db.Pages.OfType<CaseStudy>()
             .WithSlug(slug)
             .FirstOrDefaultAsync();

    if (item == null)
    
        return HttpNotFound();
    

    var related = await Db.Pages.OfType<CaseStudy>()
           .Where(r => r.Client == item.Client && r.Id != item.Id)
           .Where(r => !r.IsArchived)
           .Include(r => r.Media)
           .Take(3)
           .Project()
           .To<RelatedPageModel>()
           .ToListAsync();

    var archived = await Db.Pages.OfType<CaseStudy>()
            .Where(r => r.Client == item.Client && r.Id != item.Id)
            .Where(r => r.IsArchived)
            .Take(3)
            .Project()
            .To<RelatedPageModel>()
            .ToListAsync();

    ViewData.Model = new DetailPageModel<CaseStudy>()
    
        Item = item,
        RelatedItems = related,
        ArchivedItems = archived
    ;

    return View();

但是,当我尝试如下重构异步方法调用时

[Route("case-studies/slug")]
public async Task<ActionResult> Details(string slug)

    var item = await Db.Pages.OfType<CaseStudy>()
             .WithSlug(slug)
             .FirstOrDefaultAsync();

    if (item == null)
    
        return HttpNotFound();
           

    var related = await GetRelatedCaseStudies(item, false);
    var archived = await GetRelatedCaseStudies(item, true);

    ViewData.Model = new DetailPageModel<CaseStudy>()
    
        Item = item,
        RelatedItems = related,
        ArchivedItems = archived
    ;

    return View();



private Task<List<RelatedPageModel>> GetRelatedCaseStudies(CaseStudy casestudy, bool archived)

    return Db.Pages.OfType<CaseStudy>()
            .Where(r => r.Client == casestudy.Client && r.Id != casestudy.Id)
            .Where(x => x.IsArchived == archived)
            .Include(r => r.Media)
            .Take(3)
            .Project().To<RelatedPageModel>()
            .ToListAsync();

它没有给我以下错误

第二个操作在前一个上下文之前开始 异步操作完成。使用“等待”确保任何 异步操作在调用另一个方法之前已经完成 在这种情况下。不保证任何实例成员都是线程 安全。

这是为什么?我怎样才能做到这一点?

更新:

Db 在基本控制器中声明如下

private WebSiteDb db;

protected WebSiteDb Db

    get
    
        LazyInitializer.EnsureInitialized(ref db, () => new WebSiteDb());

        return db;
    

WebSiteDb 扩展 DbContext 如下

   [DbConfigurationType(typeof(DbConfig))]
    public class WebSiteDb : DbContext
    
        static WebSiteDb() 
            Database.SetInitializer<WebSiteDb>(new WebSiteDbInitializer());
        
        public IDbSet<Page> Pages  get; set; 
        public IDbSet<Media> Media  get; set; 
        ...some missing sets

        public WebSiteDb() : base("MyDatabaseName")  
     

如果我在方法内部等待,则会从方法内部抛出错误

WithSlug()如下

public static IQueryable<T> WithSlug<T>(this IQueryable<T> pages, string slug) where T : Page
        
            return pages.Where(p => p.Slug == slug);
        

【问题讨论】:

知道它在哪一行失败了吗? @MarcGravell 第一个 await 方法调用 - var related = await GetRelatedCaseStudies(item, false); 什么是Db?它是如何声明的? @StephenCleary 已更新问题 Project()方法的定义是什么?这是您添加的扩展程序还是其他内容的一部分? 【参考方案1】:

使用最新的EF 6.1.0 Beta 试用您的代码。现在的EF6 definition of thread safety有点模糊:

线程安全

虽然线程安全会使异步更有用,但它是正交的 特征。目前尚不清楚我们是否可以在 最一般的情况,假设 EF 与组成的图交互 用户代码来维护状态并且没有简单的方法来确保 这段代码也是线程安全的。

目前,EF 将检测开发人员是否尝试执行 一次执行两个异步操作并抛出。

您的代码似乎不会同时执行两个以上的异步操作,但在 ASP.NET 中,线程切换可能而且确实发生在 await 延续之间。理论上,EF6 应该仍然支持这种场景。但是,要消除 ASP.NET 中缺少线程亲和性导致的 EF6 错误的可能性,您可以试试我的 ThreadWithAffinityContext from the related question,如下所示:

public async Task<ActionResult> Details(string slug)

    Func<Task<ActionResult>> doAsync = async () =>
    
        var item = await Db.Pages.OfType<CaseStudy>()
                 .WithSlug(slug)
                 .FirstOrDefaultAsync();

        if (item == null)
        
            return HttpNotFound();
        

        var related = await Db.Pages.OfType<CaseStudy>()
               .Where(r => r.Client == item.Client && r.Id != item.Id)
               .Where(r => !r.IsArchived)
               .Include(r => r.Media)
               .Take(3)
               .Project()
               .To<RelatedPageModel>()
               .ToListAsync();

        var archived = await Db.Pages.OfType<CaseStudy>()
                .Where(r => r.Client == item.Client && r.Id != item.Id)
                .Where(r => r.IsArchived)
                .Take(3)
                .Project()
                .To<RelatedPageModel>()
                .ToListAsync();

        ViewData.Model = new DetailPageModel<CaseStudy>()
        
            Item = item,
            RelatedItems = related,
            ArchivedItems = archived
        ;

        return View();
    ;

    using (var staThread = new Noseratio.ThreadAffinity.ThreadWithAffinityContext(
        staThread: false, pumpMessages: false))
    
        return await staThread.Run(() => doAsync(), CancellationToken.None);
    

注意,这不是生产解决方案,但它可能有助于发现 EF6 中的错误。 如果有错误,您可以考虑使用另一个帮助程序类 ThreadAffinityTaskScheduler,直到该错误在未来的 EF 版本中得到修复。 ThreadAffinityTaskScheduler 运行 ThreadWithAffinityContext 线程池,因此应该比上面的代码更好地扩展。 linked question 包含一个使用示例。

【讨论】:

那么 EF6 中是否存在错误? @gibbocool,我无法重现和确认这一点,但我目前没有参与任何实质性的 EF6 项目。【参考方案2】:

下面的测试代码可以正常工作。展示.WithSlug( slug )的实现

class Program

    static void Main( string[] args )
    
        using( var db = new TestEntities() )
        
            db.EntityBs.Add( new EntityB()
                
                    EntityBId = 78
                 );

            db.SaveChanges();

            Task.WaitAll( Test( db ) );
        

        var input = Console.ReadLine();
    

    static Task<List<EntityB>> GetEntityBsAsync( TestEntities db )
    
        return db.EntityBs.ToListAsync();
    

    static async Task Test( TestEntities db )
    
        var a0 = await GetEntityBsAsync( db );
        var a1 = await GetEntityBsAsync( db );
    

【讨论】:

奇怪。注释掉除.ToListAsync 之外的所有IQueryable 方法调用,看看是否可行。如果是这样,开始添加另一个方法调用,直到它中断。 您的 a0 和 a1 将填充一个 Task> 类型对象,而不是您期望的 List ,因为它两次都执行完全相同的查询,第二次命中时,它会识别出它已经在上下文中执行了这个请求,并将返回之前的结果。【参考方案3】:

您在函数中缺少异步等待,应该是:

private async Task<List<RelatedPageModel>> GetRelatedCaseStudies(CaseStudy casestudy, bool archived)

return await Db.Pages.OfType<CaseStudy>()
        .Where(r => r.Client == casestudy.Client && r.Id != casestudy.Id)
        .Where(x => x.IsArchived == archived)
        .Include(r => r.Media)
        .Take(3)
        .Project().To<RelatedPageModel>()
        .ToListAsync();

如果没有这些,它将同时在不同的线程中运行两个调用,从而同时对上下文造成两次命中。

【讨论】:

这完全等同于OP的代码。它没有什么不同,除了你不必要地启动了一个你不使用任何功能的状态机。

以上是关于为啥我不能将异步查询移动到方法中?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我不能将异步代码作为同步运行 [重复]

为啥 C++11 不能将不可复制的仿函数移动到 std::function?

Android:为啥我不能使用“Intent”移动到另一个Activity?

Pybind11:为啥来自 Python 的异步调用不能在 C++ 中正确执行回调?

为啥我不能从我自己的 iOS 应用程序将图片上传到我粘贴的事件?

为啥我的查询不能从一个表中选择然后插入到另一个表中?