StephenCleary / AsyncEx

A helper library for async/await.
MIT License
3.52k stars 356 forks source link

Sometimes AsyncContext would not stop #242

Open szmcdull opened 3 years ago

szmcdull commented 3 years ago

I understand that AsyncContext would not stop until all tasks are finished. I want to find out which tasks are out running. But I didn't find out a good way. Both the Tasks view and Parallel Stacks view didn't help. Then I found something strange, the _outstandingOperations is not matching with _queue.Count image I read the source but didn't find any clue. Can you help me?

StephenCleary commented 3 years ago

I want to find out which tasks are out running.

Unfortunately, AsyncEx can't help you with that, since it doesn't have that information.

the _outstandingOperations is not matching with _queue.Count

The _outstandingOperations tracks the number of "operations" that have been registered with the SynchronizationContext. Depending on how you use AsyncContext, it may use 0 or 1 internally, and the rest represent async void methods. The _queue is the continuations that have been queued to the AsyncContext. These track different kinds of asynchronous operations and aren't supposed to be the same.

If the screenshot you posted is at a time when you expected the AsyncContext to be complete, then you can conclude that since _outstandingOperations is 3, then there are at least two async void methods that have not completed.

szmcdull commented 3 years ago

By async void do you mean async void Func(...)? I see _outstandingOperations is increased from AsyncContext.TaskScheduler.Enqueue(Task). These Tasks are mainly completion callbacks from other threads than the main thread, for example, timer or socket callbacks.

        private void Enqueue(Task task, bool propagateExceptions)
        {
            OperationStarted();
            task.ContinueWith(_ => OperationCompleted(), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, _taskScheduler);
            _queue.TryAdd(task, propagateExceptions);
tSynchronizationContext(WinFormsSynchronizationContext).
        }

        private void OperationCompleted()
        {
            var newCount = Interlocked.Decrement(ref _outstandingOperations);
            if (newCount == 0)
                _queue.CompleteAdding();
        }

        public void Execute()
        {
            SynchronizationContextSwitcher.ApplyContext(_synchronizationContext, () =>
            {
                var tasks = _queue.GetConsumingEnumerable();
                foreach (var task in tasks)
                {
                    _taskScheduler.DoTryExecuteTask(task.Item1);

                    // Propagate exception if necessary.
                    if (task.Item2)
                        task.Item1.WaitAndUnwrapException();
                }
            });
        }

This is where _outstandingOperations is mainly increased and decreased (the other 2 is in AsyncContext.Run). Seems _outstandingOperations is highly bound with _queue. Each time _outstandingOperations is increased a task will be added to the queue. The task will be then popped from the queue and executed. When it is complete, the counter decreases.

szmcdull commented 3 years ago

OK I found out that AsyncVoidMethodBuilder does call SynchronizationContext.OperationStarted.

Anyway, I decide to forcefully decrease _outstandingOperations when I believe my program should exit, using reflection haha

StephenCleary commented 3 years ago

Ah, I forgot that it's also used in Enqueue.

I decide to forcefully decrease _outstandingOperations when I believe my program should exit, using reflection

I strongly recommend against this. I recommend finding what code isn't completing and ensure it completes, instead.