c# - cancelling multiple TASKs from one cancellation token source - Stack Overflow

admin2025-04-17  3

I'm developing a MAUI application that uploads files to Azure Blob Storage. The app has a toggle switch to start/stop uploads, but I'm encountering issues with UI responsiveness during cancellation and exception handling.

  • Current Implementation:
  • A toggle switch controls upload start/stop
  • Uses CancellationTokenSource for cancellation
  • Implements concurrent uploads with SemaphoreSlim -can cancel both in-progress and pending upload Tasks
  • Issues:
  • UI becomes unresponsive when cancelling uploads
  • Console is flooded with TaskCanceledExceptions
  • Need to handle cancellation separately for in-progress uploads vs pending uploads
// Toggle switch handler
partial void OnIsSyncOnChanged(bool oldValue, bool newValue)
{
    if (newValue)
    {
        _gtecSyncCts?.Cancel();
        _gtecSyncCts?.Dispose();
        _gtecSyncCts = new CancellationTokenSource();

        Task.Run(async () => StartSyncAsync(_gtecSyncCts.Token));
    }
    else
    {
        Task.Run(async () =>
        {
            await StopSync();
            ProgressMessage = "Sync stopped by user";
        });
    }
}

// Main sync method
private async Task StartSyncAsync(CancellationToken token)
{
    try
    {
        ActiveTransfers.Clear();    
        var filesToUpload = await _fileMetadataManager.GetFilesToUploadAsync(token);
        await PrepareUploadsAsync(filesToUpload, new Progress<string>(message => UpdateProgressUI(message, 0)));
        await UploadFilesAsync(token, new Progress<string>(message => UpdateProgressUI(message, 0)));
    }
    catch (OperationCanceledException)
    {
        Debug.WriteLine("Sync operation was canceled.");
        UpdateProgressUI("Sync operation was canceled", 0);
        await CancelPendingUploadsAsync();
    }
}

// Upload preparation
private async Task PrepareUploadsAsync(List<FileMetadata> filesToUpload, IProgress<string> progress)
{
    ActiveTransfers.Clear();
    int totalFiles = filesToUpload.Count;

    for (int i = 0; i < totalFiles; i++)
    {
        var file = filesToUpload[i];
        var blobClient = _uploadManager.GetBlobClient(
            file.destination_folder,
            file.filename,
            file.FileExtension
        );

        var uploadViewModel = new FileUploadViewModel(file, blobClient);
        uploadViewModel.UploadStateChanged += OnUploadStateChanged;
        ActiveTransfers.Add(uploadViewModel);
        progress.Report($"Prepared {i + 1}/{totalFiles} files for upload");
    }
}

// Concurrent upload handling
private async Task UploadFilesAsync(CancellationToken token, IProgress<string> progress)
{
    using var semaphore = new SemaphoreSlim(4);
    var tasks = new List<Task>();
    var completedUploads = 0;
    var totalUploads = ActiveTransfers.Count;

    foreach (var vm in ActiveTransfers)
    {
        await semaphore.WaitAsync(token);
        tasks.Add(Task.Run(async () =>
        {
            try
            {
                await vm.StartUploadAsync(token);
                var completed = Interlocked.Increment(ref completedUploads);
            }
            finally
            {
                semaphore.Release();
            }
        }));
    }

    await Task.WhenAll(tasks);
}

// Individual file upload
public async Task StartUploadAsync(CancellationToken externalToken)
{
    _cts = CancellationTokenSource.CreateLinkedTokenSource(externalToken);

    try
    {
        await UpdateState(UploadState.Checking);
        
        var blobExists = await BlobClient.ExistsAsync(_cts.Token);
        if (blobExists)
        {
            FileMetadata.BlobStoragePath = BlobClient.Uri.ToString();
            await UpdateState(UploadState.AlreadyUploaded);
            return;
        }

        await UpdateState(UploadState.Uploading);

        var options = new BlobUploadOptions
        {
            TransferOptions = new StorageTransferOptions
            {
                MaximumConcurrency = 8,
                MaximumTransferSize = 4 * 1024 * 1024,
                InitialTransferSize = 4 * 1024 * 1024
            },
            ProgressHandler = new Progress<long>(ReportProgress)
        };

        await BlobClient.UploadAsync(FileMetadata.FilePath, options, _cts.Token);
        FileMetadata.BlobStoragePath = BlobClient.Uri.ToString();
        await UpdateState(UploadState.Completed);
    }
    catch (OperationCanceledException)
    {
        await UpdateState(UploadState.Cancelled, "Upload was cancelled by the user");
    }
    catch (Exception ex)
    {
        await UpdateState(UploadState.Failed, ex.Message);
    }
}

somehow this design manage two work but the UI become really unresponsive when i Turn the switch off throwing the cancellation and the console gets clogged with exceptions: Exception thrown: 'System.Threading.Tasks.TaskCanceledException' in System.Private.CoreLib.dll

secondly this way I have to handle separatley the uploads already started that will end in a OperationCanceledException and the ones waiting to start that are handled by the method CancelPendingUploadsAsync().

I am also pretty sure that I am making abuse of the Task.Run call. my StartSyncAsync is already fired on a separate thread bur Lord AI suggested to use it also in UploadFilesAsync() to lunch each upload Task on a single thread.

Finally I am using the Semaphore Slim because I was not able to make good use of

TransferOptions = new StorageTransferOptions
             {
                 MaximumConcurrency = 8,

in the Azure API. even with this call, if I start 100 uploads they all start concurrently maybe I misunderstood the purpose of MaximumConcurrency proprierty

转载请注明原文地址:http://anycun.com/QandA/1744848918a88473.html