Home > database >  How to cancel a task - trying to use a CancellationTokenSource, but I think I don't get the con
How to cancel a task - trying to use a CancellationTokenSource, but I think I don't get the con

Time:12-20

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 = new Task(RunMessagePump); // 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}");
            }
        }

    ...
}

CodePudding user response:

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.

CodePudding user response:

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.

  • Related