简体   繁体   中英

How to cancel a task - trying to use a CancellationTokenSource, but I think I don't get the concept

I am using VS2019 and create a C# Windows Forms App (.NET Framework) together with a C# Class Library (.NET Framework), both using .NET Framework 4.7.2. The main goal of my application is to interface with SimConnect on MSFS2020.

(c) Original code is from Dragonlaird on the MSFS forum

When I connect with SimConnect, I need a WndProc "messagePump", which I make by deriving from the NativeWindow class. My Connect method creates a Task which is creating the messagePump and connects with SimConnect passing the handle of the messagePump (which is the NativeWindow derivate). After that I use an AutoResetEvent to signal back to the main thread that the messagePump is running, before I start the endless Application.Run().

When I disconnect, some cleanup is done by stopping the messagePump and getting rid of the AutoResetEvent object.

So far so good. All seems working fine.

But I was trying to stop the Task by using a CancellationTokenSource, which I pass to the messagePump Task. I hoped that by calling Cancel() method, the Task would be killed. But that seems not to work, because if I connect/disconnect several times, then I see that each time an additional Task is created (using Debug/Window/Tasks). So the cancel has no effect at all.

I think I know why, because all the information on the web talks about "cooperative cancellation", which I think means that the task itself needs to regularly check if cancel has been triggered and exit when this is the case (be "cooperative"). But because the Application.Run() is completely blocking my Task, I have no means of "controlling" the cancellation any longer.

Below the relevant code (only relevant pieces). How can I dispose of my Task when I Disconnect, avoiding memory leaks and at the end even performance issues.

namespace SimConnectDLL
{
    internal class MessageHandler : NativeWindow
    {
        public event EventHandler<Message> MessageReceived;
        public const int WM_USER_SIMCONNECT = 0x0402;

        internal void CreateHandle()
        {
            CreateHandle(new CreateParams());
        }

        protected override void WndProc(ref Message msg)
        {
            // filter messages here for SimConnect
            if (msg.Msg == WM_USER_SIMCONNECT && MessageReceived != null)
                try
                {
                    MessageReceived.DynamicInvoke(this, msg);
                }
                catch { } // If calling assembly generates an exception, we shouldn't allow it to break this process
            else
                base.WndProc(ref msg);
        }

        internal void Stop()
        {
            base.ReleaseHandle();
            base.DestroyHandle();
        }
    }

    public class SimConnectDLL
    {
        private static MessageHandler handler = null;
        private static CancellationTokenSource source = null;
        private static CancellationToken token = CancellationToken.None;
        private static Task messagePump;
        private static AutoResetEvent messagePumpRunning = new AutoResetEvent(false);
        private static SimConnect simConnect = null;

        public static bool IsConnected { get; private set; } = false;

        public static void Connect()
        {
            Debug.WriteLine("SimConnectDLL.Connect");
            if (source != null)
                Disconnect();
            source = new CancellationTokenSource(); // Is needed to be able to cancel the messagePump Task
            token = source.Token;
            token.ThrowIfCancellationRequested();
            messagePump = new Task(RunMessagePump, token); // Create Task to run the messagePump
            messagePump.Start(); // Start task to run the messagePump
            messagePumpRunning = new AutoResetEvent(false); // Create Synchronization primitive allowing the messagePump Task to signal back that it is running
            messagePumpRunning.WaitOne(); // Wait until the synchronization primitive signals that the messagePump Task is running
        }

        public static void Disconnect()
        {
            Debug.WriteLine("SimConnectDLL.Disconnect");
            StopMessagePump();
            // Raise event to notify client we've disconnected
            SimConnect_OnRecvQuit(simConnect, null);
            simConnect?.Dispose(); // May have already been disposed or not even been created, e.g. Disconnect called before Connect
            simConnect = null;
        }

        private static void RunMessagePump()
        {
            Debug.WriteLine("SimConnectDLL.RunMessagePump");
            // Create control to handle windows messages
            if (!IsConnected)
            {
                handler = new MessageHandler();
                handler.CreateHandle();
                ConnectFS(handler);
            }
            messagePumpRunning.Set(); // Signals that messagePump is running
            Application.Run(); // Begins running a standard application message loop on the current thread.
            Debug.WriteLine("Application is running");
        }

        private static void StopMessagePump()
        {
            Debug.WriteLine("SimConnectDLL.StopMessagePump");
            if (source != null && token.CanBeCanceled)
            {
                source.Cancel();
                source = null;
            }
            if (messagePump != null)
            {
                handler.Stop();
                handler = null;

                messagePumpRunning.Close();
                messagePumpRunning.Dispose();
            }
            messagePump = null;
        }

        private static void ConnectFS(MessageHandler messageHandler)
        {
            Debug.WriteLine("SimConnectDLL.ConnectFS");
            // SimConnect must be linked in the same thread as the Application.Run()
            try
            {
                simConnect = new SimConnect("RemoteClient", messageHandler.Handle, MessageHandler.WM_USER_SIMCONNECT, null, 0);

                messageHandler.MessageReceived += MessageReceived;
            }
            catch (Exception ex)
            {
                // Is MSFS is not running, a COM Exception is raised. We ignore it!
                Debug.WriteLine($"Connect Error: {ex.Message}");
            }
        }

    ...
}

But because the Application.Run() is completely blocking my Task, I have no means of "controlling" the cancellation any longer.

You are right - if you want to use cancellation token in this way, you would have to cyclically use token.ThrowIfCancellationRequested() or give a additional argument to your loop inside Application.Run() - CancellationToken.IsCancellationRequested (I presume that it is some sort of a loop in there).
Otherwise you are checking the state of the token only once, at the first usage of token.ThrowIfCancellationRequested() .

Also, in your current app state, if you do Connect twice or more, there will be no cancellation possible, because you are overwriting your CancellationTokenSource when connecting.

First check if CTS is null, if not, cancel, then create new cancellation token source.

The problem is that I can't control the Application.Run() myself, can't I?

Does anybody have an idea how I should write this code? I'm a bit lost in this Cancellation stuff.

I'm not sure why my reply has been deleted by @The Fabio. Maybe he thinks that I was another person asking an additional question? But that is not the case - I simply want more clarification and help on my own question that I posted . I'm sure that this will benefit for all users reading this post.

So I try again, and hopefully I don't get kicked out of this forum (I don't want to offend you @The Fabio - I simply think there is a misunderstanding?).

So my question remains, how can I correct my code and make it correctly terminate my Task on a cancellation? I can't get into Application.Run(), because that is not written by myself. Any ideas?

A CancellationTokenSource object requires handling a TaskCanceledException within the asynchronous task, as in the sample code below (provided for refence only); now, I seem to understand you are not in control of what happens inside the asynchronous task; however, it looks like in your code you are overwriting the value of the messagePump variable so, in the end, you do not pass the CancellationToken to the asynchronous task

messagePump = new Task(RunMessagePump, token);
messagePump = new Task(RunMessagePump);

The following sample code (once again: provided for reference only) runs an asynchronous task (the Test method) that writes to the console every second, and then it stops it after five seconds

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Test
{
    class Program
    {
        static void Main()
        {
            CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
            CancellationToken cancellationToken = cancellationTokenSource.Token;
            Task.Run(() => Test(cancellationToken));
            Task.Delay(5000).Wait();
            cancellationTokenSource.Cancel();
        }

        async static Task Test(CancellationToken cancellationToken)
        {
            while (true)
            {
                try
                {
                    Console.WriteLine("The task is running");
                    await Task.Delay(1000, cancellationToken);
                }
                catch (TaskCanceledException)
                {
                    break;
                }
            }
        }

    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM