MAUI上传Azure Blob存储时取消操作的UI响应与异常问题
MAUI Azure Blob上传:解决取消时UI无响应、异常刷屏及并发控制问题
问题背景
开发MAUI应用实现文件上传至Azure Blob Storage,通过切换开关控制上传启停,但在取消操作时遇到UI响应性与异常处理问题,同时对并发控制的实现存在疑问。
当前实现
- 切换开关绑定上传启停逻辑
- 通过
CancellationTokenSource实现上传取消 - 用
SemaphoreSlim控制并发上传,支持取消进行中及待处理的上传任务
遇到的问题
- 取消上传时UI卡顿无响应
- 控制台被
TaskCanceledException大量刷屏 - 需要区分处理已启动的上传(触发
OperationCanceledException)与待启动的上传(需通过CancelPendingUploadsAsync处理) - 疑似过度使用
Task.Run:StartSyncAsync已在独立线程执行,但仍在UploadFilesAsync中用Task.Run启动每个上传任务 - 对Azure API的
TransferOptions.MaximumConcurrency理解有误,设置该属性后100个上传任务仍同时启动,不得不依赖SemaphoreSlim控制并发
解决方案
1. 修复UI无响应问题
取消操作导致UI卡顿,通常是因为取消逻辑中存在阻塞UI线程的同步代码:
- 确保所有取消相关操作(如
CancellationTokenSource.Cancel()、清理待处理任务)都在异步方法中执行,避免直接在UI事件回调中处理耗时操作 - 若上传启停通过
ICommand绑定,使用AsyncCommand而非普通ICommand,确保命令执行逻辑是异步的,不会阻塞UI线程
2. 解决TaskCanceledException刷屏问题
主动取消操作触发的异常属于预期情况,无需让它刷屏控制台:
- 在每个上传任务的异步逻辑中,针对性捕获
OperationCanceledException(TaskCanceledException是其子类),仅记录必要日志,不向上抛出 - 通过
CancellationToken.IsCancellationRequested判断是否为主动取消,区分处理取消与其他异常:
try { using var fileStream = file.OpenRead(); await blobClient.UploadAsync(fileStream, cancellationToken); } catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) { // 主动取消,仅记录日志即可 Debug.WriteLine($"[{file.Name}] 上传任务已取消"); } catch (Exception ex) { // 处理真实上传异常 Debug.WriteLine($"[{file.Name}] 上传失败: {ex.Message}"); }
3. 区分已启动与待启动上传的取消逻辑
- 已启动的上传:通过传递给
UploadAsync的CancellationToken发送取消信号,Azure SDK会响应令牌并触发OperationCanceledException,按上述异常处理逻辑即可 - 待启动的上传:维护一个线程安全的待处理任务队列(如
ConcurrentQueue<FileInfo>),调用CancelPendingUploadsAsync时,清空队列并标记取消状态;在启动每个任务前先检查CancellationToken是否已取消,若已取消则跳过启动
4. 优化Task.Run的过度使用
如果StartSyncAsync已经在后台线程执行,UploadFilesAsync中无需用Task.Run包装上传任务:
UploadAsync本身是异步非阻塞方法,直接调用即可,避免不必要的线程切换- 确保所有异步方法都正确使用
await,避免出现未等待的任务导致的上下文混乱
5. 正确控制多文件上传并发
Azure Blob SDK的TransferOptions.MaximumConcurrency控制的是单个大文件分块上传的并发数,而非多文件上传的并发数。要控制多文件上传的并发数,推荐以下两种方式:
方式一:使用Parallel.ForEachAsync(.NET 6+)
直接通过MaxDegreeOfParallelism参数控制同时上传的文件数,无需额外使用SemaphoreSlim:
await Parallel.ForEachAsync(filesToUpload, new ParallelOptions { MaxDegreeOfParallelism = 5, // 设置并发上传的文件数量 CancellationToken = cancellationToken }, async (file, ct) => { var blobClient = containerClient.GetBlobClient(file.Name); using var stream = file.OpenRead(); await blobClient.UploadAsync(stream, cancellationToken: ct); });
方式二:正确使用SemaphoreSlim
如果坚持使用SemaphoreSlim,确保初始化计数与期望的并发数一致,且每个上传任务正确等待信号量:
var semaphore = new SemaphoreSlim(5); // 允许5个并发任务 var uploadTasks = new List<Task>(); foreach (var file in filesToUpload) { if (cancellationToken.IsCancellationRequested) break; await semaphore.WaitAsync(cancellationToken); uploadTasks.Add(Task.Run(async () => { try { var blobClient = containerClient.GetBlobClient(file.Name); using var stream = file.OpenRead(); await blobClient.UploadAsync(stream, cancellationToken); } finally { semaphore.Release(); } }, cancellationToken)); } await Task.WhenAll(uploadTasks);
内容的提问来源于stack exchange,提问作者federico
相关产品推荐
相关产品推荐

