CancellationToken 永远不会取消我长时间运行的加载数据功能

Posted

技术标签:

【中文标题】CancellationToken 永远不会取消我长时间运行的加载数据功能【英文标题】:CancellationToken never cancel on my long running loading data function 【发布时间】:2021-09-25 09:00:06 【问题描述】:

我有一个 Blazor 组件,它必须显示来自长时间运行的操作的数据。出于这个原因,我显示了一个微调器,但由于它需要很长时间,我希望能够在例如用户导航离开时取消此加载(例如,用户在加载数据时单击登录)。

我在组件中使用 CancellationTokenSource 对象实现了 Dispose 模式,我使用 Token 作为参数使我的函数异步,但似乎在我的加载数据函数中,令牌的“IsCanceled”从未设置为 true,也不会引发 OperationCanceledException .如果我使用一个只等待 Task.Delay 20 秒的虚拟函数进行测试,并且我传递了令牌,则它被正确取消。我做错了什么?

最终结果是,当数据正在加载并且微调器正在显示时,如果用户单击按钮以导航离开,它会等待数据加载完成。

我显示数据的视图; “LoadingBox”在未创建列表时显示微调器。

<Card>
    <CardHeader><h3>Ultime offerte</h3></CardHeader>
    <CardBody>
        <div class="overflow-auto" style="max-height: 550px;">
            <div class="@(offersAreLoading ?"text-danger":"text-info")">LOADING: @offersAreLoading</div>
            <LoadingBox IsLoading="lastOffers == null">
                @if (lastOffers != null)
                
                    @if (lastOffers.Count == 0)
                    
                        <em>Non sono presenti offerte.</em>
                    
                    <div class="list-group list-group-flush">
                        @foreach (var off in lastOffers)
                        
                            <div class="list-group-item list-group-item-action flex-column align-items-start">
                                <div class="d-flex w-100 justify-content-between">
                                    <h5 class="mb-1">
                                        <a href="@(NavigationManager.BaseUri)offerta/@off.Oarti">
                                            @off.CodiceOfferta4Humans-@off.Versione
                                        </a>
                                    </h5>
                                    <small>@((int) ((DateTime.Now - off.Created).TotalDays)) giorni fa</small>
                                </div>
                                <p class="mb-1"><em>@(off.OggettoOfferta.Length > 50 ? off.OggettoOfferta.Substring(0, 50) + "..." : off.OggettoOfferta)</em></p>
                                <small>@off?.Redattore?.Username - @off.Created</small>
                            </div>
                        
                    </div>
                

            </LoadingBox>
        </div>
    </CardBody>
</Card>

组件代码隐藏。在这里,我调用了长时间运行的函数 (GetRecentAsync),当用户离开或执行其他操作时,我想取消该函数:

public partial class Test : IDisposable
    
        private CancellationTokenSource cts = new();
        private IList<CommercialOffer> lastOffers;
        private bool offersAreLoading;
        [Inject] public CommercialOfferService CommercialOfferService  get; set; 
        async Task LoadLastOffers()
        
            offersAreLoading = true;
            await InvokeAsync(StateHasChanged);
            var lo = await CommercialOfferService.GetRecentAsync(cancellationToken: cts.Token);
            lastOffers = lo;
            offersAreLoading = false;
            await InvokeAsync(StateHasChanged);


        

        async Task fakeLoad()
        
            offersAreLoading = true;
            await InvokeAsync(StateHasChanged);
            await Task.Delay(TimeSpan.FromSeconds(20), cts.Token);
            offersAreLoading = false;
            await InvokeAsync(StateHasChanged);
        

        protected override async Task OnAfterRenderAsync(bool firstRender)
        
            if (firstRender)
            
                await LoadLastOffers();
            
            await base.OnAfterRenderAsync(firstRender);
        

        public void Dispose()
        
            cts.Cancel();
            cts.Dispose();
        
    
public async Task<List<CommercialOffer>> GetRecentAsync(CancellationToken cancellationToken)
        
            try
            
                cancellationToken.ThrowIfCancellationRequested();
                var result = await _cache.GetOrCreateAsync<List<CommercialOffer>>("recentOffers", async entry =>
                
                    entry.AbsoluteExpiration = DateTimeOffset.Now.Add(new TimeSpan(0, 0, 0, 30));
                    var list = await _unitOfWork.CommercialOfferRepository.GetAllWithOptionsAsync();
                    foreach (var commercialOffer in list)
                    
                        // sta operazione è pesante, per questo ho dovuto cachare

                        // BOTH ISCANCELLATIONREQUESTED AND THROWIFCANCELLATINREQUESTED DOES NOT WORK, ISCANCELLATIONREQUESTED IS ALWAYS FALSE.

                        cancellationToken.ThrowIfCancellationRequested();
                        if (cancellationToken.IsCancellationRequested) return new List<CommercialOffer>();
                       
                        await _populateOfferUsersAsync(commercialOffer);
                    
                    return list.Take(15).OrderByDescending(o => o.Oarti).ToList();
                );

                return result;
            
            catch (OperationCanceledException)
            
                // HERE I SET A BREAKPOINT IN ORDER TO SEE IF IT RUNS, BUT IT DOESN'T WORK
            
        

谢谢!

编辑 20/07/2021

感谢@Henk Holterman。 GetRecentAsync 获取所有最近的商业报价,由一个简单的表格编译,并有一些数据作为通常的用例。 这些商业报价中的每一个都涉及 4 个用户(他们管理报价、上级、批准者等),我为每个这些用户填充了一个 foreach 循环,用于我想要显示的每个商业报价。

我知道我应该从一开始就从 SQL 查询中创建整个实体(商业报价),但我需要这样做以解决顺序问题和关注点分离问题。

因此,_populateOfferUsersAsync(commercialOffer) 查询某个商品的 4 个用户,创建这 4 个实体并将它们分配给该商品:

private async Task _populateOfferUsersAsync(CommercialOffer commercialOffer)
        
            commercialOffer.Responsabile = await _unitOfWork.UserRepository.GetByIdAsync(commercialOffer.IdResponsabile);
            commercialOffer.Redattore = await _unitOfWork.UserRepository.GetByIdAsync(commercialOffer.IdRedattore);
            commercialOffer.Approvatore = await _unitOfWork.UserRepository.GetByIdAsync(commercialOffer.IdApprovatore);
            commercialOffer.Revisore = await _unitOfWork.UserRepository.GetByIdAsync(commercialOffer.IdRevisore);
        

在后台我使用 Dapper 进行数据库查询:

public async Task<User> GetByIdAsync(long id)
        
            var queryBuilder = _dbTransaction.Connection.QueryBuilder($@"SELECT * FROM GEUTENTI /**where**/");
            queryBuilder.Where($"CUSER = id");
            queryBuilder.Where($"BSTOR = 'A'");
            queryBuilder.Where($"BDELE = 'S'");

            var users = await queryBuilder.QueryAsync<User>(_dbTransaction);
            return users.FirstOrDefault();
        

据我所见,没有简单有效的方法来传递 CancellationToken 来停止 Dapper 查询,可能是我或 Dapper 不适合这些东西

【问题讨论】:

我对 Blazor 不是很熟悉,但我注意到在 this article 中,@implements IDisposable 和 Dispose 方法直接添加到组件代码中,而不是模型类中。有什么需要调查的吗? 好的,我错过了 Dapper 角度。这是你最重要的标签。 Ans 看到这个***.com/q/25540793/60761 让我们考虑一下 Dapper 可以使用令牌(实际上不是很好)但是我无法停止 GetRecentAsync 内的循环这一事实并没有使感觉。在那种情况下,我正在从 db 读取数据,因此没有必要取消 sql 事务(如果我正在写入 db,那将是一个不同的故事,在这种情况下,我想我将不得不回滚事务)。我现在取得的最好成绩是使用await Task.Run(() =&gt; CommercialOfferService.GetRecentAsync(cancellationToken: cts.Token), cts.Token); 现在它抛出 【参考方案1】:

我做错了什么?

将您的取消令牌转发到所有异步 I/O 方法很重要:

// var list = await _unitOfWork.CommercialOfferRepository.GetAllWithOptionsAsync();
var list = await _unitOfWork.CommercialOfferRepository.GetAllWithOptionsAsync(cancellationToken);

然后当然要相应地修改GetAllWithOptionsAsync()。实体框架中的所有异步方法都有一个接受CancellationToken 的重载。

...导航离开它等待数据加载完成。

当 GetAllWithOptionsAsync() 占用大部分时间时,这就是数字。下一个 foreach 循环应该在取消时中断,但这可能并不明显。 不过,_populateOfferUsersAsync(commercialOffer) 也应该将 CancellationToken 作为参数。

从您自己的 FakeLoad() 中可以看出,Blazor 和 CancellationTokenSource 没有损坏。

【讨论】:

不幸的是,即使我将令牌传递给后续函数,例如 _populateOfferUsersAsync(commercialOffer) 我也无法获得 OperationCanceledException,isCancellationRequested == true 也无法在任何地方获得,似乎这些函数中的令牌(从 GetAllWithOptionsAsync 开始)从未设置为取消,就像它是另一个对象或者我不知道是什么 您能发布 GetAllWithOptionsAsync 和 _populateOfferUsersAsync 的大纲吗?它们有多异步? 我用更多信息编辑了我的问题

以上是关于CancellationToken 永远不会取消我长时间运行的加载数据功能的主要内容,如果未能解决你的问题,请参考以下文章

如何取消 CancellationToken

C#:使用 CancellationToken 取消 MySqlCommand,给出 NULLReferenceException

使用 CancellationToken 取消任务而不在任务中明确检查?

取消 HttpClient 请求 - 为啥 TaskCanceledException.CancellationToken.IsCancellationRequested 为假?

C#源码生成器的cancellationtoken啥时候取消?

CancellationToken 取消机制