AntikM
AntikM

Reputation: 651

C# - Correct way to set up CancellationTokens for highly concurrent methods

I have a function that refreshes the UI every time an object raises an event. However, any single object can raise multiple events within 1 sec, and there could be 10,000+ objects in the collection doing this. My idea is to capture the very last event and discard any pending ones after 1 sec intervals.

The following RefreshCollection() function is called every time any object raises any event.

SemaphoreSlim _semaphoreUpdatingList = new SemaphoreSlim(1);
SemaphoreSlim _semaphoreRefreshingView = new SemaphoreSlim(1);
CancellationTokenSource _ctsRefreshView = null;

internal void RefreshCollection()
{
    // if we're in the process of changing the collection, return
    if (_semaphoreUpdatingList.CurrentCount == 0)
    {
        return;
    }
    if (_ctsRefreshView != null)
    {
        _ctsRefreshView.Cancel();
    }
    Task.Run(async () =>
    {
        if (_ctsRefreshView == null)
        {
            _ctsRefreshView = new CancellationTokenSource();
        }
        var ct = _ctsRefreshView.Token;
        try
        {
            await _semaphoreRefreshingView.WaitAsync(ct);
            var stopWatch = new Stopwatch();
            stopWatch.Start();
            Application.Current?.Dispatcher?.Invoke(() =>
            {
                CollectionView.Refresh();
            });
            stopWatch.Stop();

            // Only refresh every 1 sec
            if (stopWatch.ElapsedMilliseconds < 1000)
            {
                await Task.Delay(1000 - (int)stopWatch.ElapsedMilliseconds);
            }
            _semaphoreRefreshingView.Release();
        }
        catch (OperationCanceledException)
        {
            return;
        }
        finally
        {
            _ctsRefreshView = null;
        }
    });
}

The problem is, very rarely I am getting a _ctsRefreshView is null error inside the task when I'm calling this var ct = _ctsRefreshView.Token;. I am scratching my head as to why this is happening.

Thanks a lot for any help.

Upvotes: 0

Views: 106

Answers (1)

Enigmativity
Enigmativity

Reputation: 117019

You should use Microsoft's Reactive Framework (aka Rx) - NuGet System.Reactive.Windows.Threading (for WPF bits) and add using System.Reactive.Linq;.

And if you had a collection this class:

public class MyObject
{
    public event EventHandler Ping;
}

Then you can do this:

IObservable<EventPattern<EventArgs>> query =
    collection
        .ToObservable()
        .SelectMany(x =>
            Observable
                .FromEventPattern<EventHandler, EventArgs>(
                    h => x.Ping += h,
                    h => x.Ping -= h))
        .Sample(TimeSpan.FromSeconds(0.1))
        .ObserveOnDispatcher();
        
IDisposable subscription = query.Subscribe(x => CollectionView.Refresh());

That will give you at most one call to CollectionView.Refresh() every 0.1 seconds.

That's much easier than mucking around with cancellation token sources.

And just call subscription.Dispose(); to stop it all.

Upvotes: 1

Related Questions