admin管理员组

文章数量:1410689

Sorry to ask what is probably a simple answer but I've spent a couple of hours looking through documentation and can't seem to get my code working how I think it should work, I've not used C# in years either which isn't helping.

What I'm trying to write a simple C# application that monitors a serial port for incoming data and displays it in a text box.

I'm trying to use a thread to run the serial port method and have it start / stop with a button.

The closest I have it working is now, but when you press the stop button it throws an exception

System.ObjectDisposedException: 'The CancellationTokenSource has been disposed.'

I thought it should just stop the thread but it throws an exception, wrapping it in a try/catch the application keeps running but the server fails to stop.

Any pointers for problems or a better / alternative way of doing it or any help is greatly appreciated, maybe I'm going about the approach all wrong. I'd like to learn though. Thanks in advance if you made it this far!

private void btn_start_Click(object sender, EventArgs e)
{
   using var cts = _cts = new();

   try
   {
      Thread thread = new(() => serialserver(_cts.Token));
      thread.Start();
   }
   catch
   {
   }

   btn_start.Enabled = false;
   btn_stop.Enabled = true;
}

public void serialserver(CancellationToken token)
{
   Invoke((MethodInvoker)(() => txtbox_log.AppendText("Serial Server Started")));
   Invoke((MethodInvoker)(() => txtbox_log.AppendText(Environment.NewLine)));

   while (!token.IsCancellationRequested)
   { 
       Invoke((MethodInvoker)(() => txtbox_log.AppendText("TEST! I'm running")));
       Invoke((MethodInvoker)(() => txtbox_log.AppendText(Environment.NewLine)));
       Thread.Sleep(2500);

       // do some more serial stuff here 
   }

   Invoke((MethodInvoker)(() => txtbox_log.AppendText("Serial Server Stopped")));
   Invoke((MethodInvoker)(() => txtbox_log.AppendText(Environment.NewLine)));
}

private void btn_stop_Click(object sender, EventArgs e)
{
   _cts!.Cancel();
   _cts = null;
  
   btn_start.Enabled = true;
   btn_stop.Enabled = false;
}

Sorry to ask what is probably a simple answer but I've spent a couple of hours looking through documentation and can't seem to get my code working how I think it should work, I've not used C# in years either which isn't helping.

What I'm trying to write a simple C# application that monitors a serial port for incoming data and displays it in a text box.

I'm trying to use a thread to run the serial port method and have it start / stop with a button.

The closest I have it working is now, but when you press the stop button it throws an exception

System.ObjectDisposedException: 'The CancellationTokenSource has been disposed.'

I thought it should just stop the thread but it throws an exception, wrapping it in a try/catch the application keeps running but the server fails to stop.

Any pointers for problems or a better / alternative way of doing it or any help is greatly appreciated, maybe I'm going about the approach all wrong. I'd like to learn though. Thanks in advance if you made it this far!

private void btn_start_Click(object sender, EventArgs e)
{
   using var cts = _cts = new();

   try
   {
      Thread thread = new(() => serialserver(_cts.Token));
      thread.Start();
   }
   catch
   {
   }

   btn_start.Enabled = false;
   btn_stop.Enabled = true;
}

public void serialserver(CancellationToken token)
{
   Invoke((MethodInvoker)(() => txtbox_log.AppendText("Serial Server Started")));
   Invoke((MethodInvoker)(() => txtbox_log.AppendText(Environment.NewLine)));

   while (!token.IsCancellationRequested)
   { 
       Invoke((MethodInvoker)(() => txtbox_log.AppendText("TEST! I'm running")));
       Invoke((MethodInvoker)(() => txtbox_log.AppendText(Environment.NewLine)));
       Thread.Sleep(2500);

       // do some more serial stuff here 
   }

   Invoke((MethodInvoker)(() => txtbox_log.AppendText("Serial Server Stopped")));
   Invoke((MethodInvoker)(() => txtbox_log.AppendText(Environment.NewLine)));
}

private void btn_stop_Click(object sender, EventArgs e)
{
   _cts!.Cancel();
   _cts = null;
  
   btn_start.Enabled = true;
   btn_stop.Enabled = false;
}
Share Improve this question edited Mar 11 at 18:25 marc_s 756k184 gold badges1.4k silver badges1.5k bronze badges asked Mar 11 at 12:46 Wes PriceWes Price 3566 silver badges12 bronze badges 1
  • The following may be of interest: stackoverflow/a/79252620/10024425 – It all makes cents Commented Mar 11 at 15:03
Add a comment  | 

3 Answers 3

Reset to default 1

Your post is a bit tricky because it asks about stopping a thread (we'll call that "X") but strongly implies that reading SerialPort data is the ultimate goal (that we'll call "Y"). You said:

Any pointers for problems or a better / alternative way of doing it or any help is greatly appreciated.

In keeping with this, I'll try and share what's worked for me for both X and for Y.


Stopping the Thread (X)

If the code that you posted is the code you wish to keep (i.e. having a polling loop) then you might want to experiment with making the loop asynchronous so that you don't lose the UI thread context while the background work proceeds on an alternate thread. In this snippet:

  • The Task.Delay makes use of the CancellationToken to exit the delay immediately.
  • We make a point of catching the OperationCancelled exception.
  • At the top of each loop iteration, we have the token throw if it's been cancelled.
public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
        checkBoxToggleServer.CheckedChanged += async (sender, e) =>
        {
            if (checkBoxToggleServer.Checked)
            {
                _cts?.Cancel();
                // Wait for previous run (if any) to cancel
                await _awaiter.WaitAsync();
                _cts = new CancellationTokenSource();
                try
                {
                    txtbox_log.AppendText("Serial Server Started", true, Color.Green);
                    while (true)
                    {
                        _cts.Token.ThrowIfCancellationRequested();
                        var timestamp = $@"[{DateTime.Now:hh\:mm\:ss\ tt}] ";
                        txtbox_log.AppendText($"{timestamp} TEST! I'm running", true, Color.Blue);
                        await Task.Delay(TimeSpan.FromSeconds(1.5), _cts.Token); // Taken from posted code.
                        await Task.Run(() => 
                        {
                            // "do some more serial stuff here"
                            _cts.Token.ThrowIfCancellationRequested();
                        }, _cts.Token);
                    }
                }
                catch (OperationCanceledException)
                {
                    txtbox_log.AppendText("Serial Server Canceled", true, Color.Maroon);
                    checkBoxToggleServer.Checked = false;
                    _awaiter.Wait(0);
                    _awaiter.Release();
                }
            }
            else
            {
                if (_cts is not null && !_cts.IsCancellationRequested) _cts.Cancel();
            }
        };
    }
    SemaphoreSlim 
        _awaiter = new SemaphoreSlim(1, 1),
        _criticalSection = new SemaphoreSlim(1, 1);
    CancellationTokenSource? _cts = null;
}

Where AppendText is an Extension for RichTextBox.
static class Extensions
{
    public static void AppendText(this RichTextBox @this, string text, bool newLine, Color? color = null)
    {
        var colorB4 = @this.SelectionColor;
        if(color is Color altColor) @this.SelectionColor = altColor;
        @this.AppendText($"{text}{(newLine ? Environment.NewLine : string.Empty)}");
        @this.SelectionColor = colorB4;
    }
}

Reading Serial Port Data

What I have found is that retrieving asynchronous data from a SerialPort takes on a different flavor because we're often listening to the DataReceived event and responding on an interrupt basis. This code snippet:

  • Opens or Closes the Serial Port based on the button toggle.
  • Is listening for data to become available by subscribing to the DataReceived event (in this case, using an inline Lambda method).
  • Marshals the data onto the UI thread in order to display it.
public partial class MainForm : Form
{
    SerialPort _serialPort = new();
    public MainForm()
    {
        InitializeComponent();

        _serialPort.DataReceived += async (sender, e) =>
        {
            await _criticalSection.WaitAsync();
            if (!IsDisposed) BeginInvoke((MethodInvoker)delegate
            {
                try
                {
                    if (sender is SerialPort port)
                    {
                        while (port.BytesToRead > 0)
                        {
                            byte[] buffer = new byte[Math.Min(port.BytesToRead, 16)];
                            int success = port.Read(buffer, 0, buffer.Length);
                            BeginInvoke(() => 
                            { 
                                txtbox_log.AppendText($@"[{DateTime.Now:hh\:mm\:ss.ff tt}] ", false, Color.CornflowerBlue);
                                txtbox_log.AppendText( BitConverter.ToString(buffer, 0, success).Replace("-", " "),  true);
                            });
                        }
                    }
                }
                finally
                {
                    _criticalSection.Release();
                }
            });
        };

        checkBoxToggleServer.CheckedChanged += (sender, e) =>
        {
            if (checkBoxToggleServer.Checked)
            {
                _serialPort.Open();
                txtbox_log.AppendText($"Serial Server Started", true, Color.Green);
            }
            else
            {
                _serialPort.Close();
                txtbox_log.AppendText("Serial Server Canceled", true, Color.Maroon);
            }
        };
    }
    SemaphoreSlim 
        _awaiter = new SemaphoreSlim(1, 1),
        _criticalSection = new SemaphoreSlim(1, 1);
    CancellationTokenSource? _cts = null;
}

The using will make sure that the CancellationTokenSource will be disposed at the end of your method btn_start_Click. But you will need it later in your method btn_stop_Click. Hence you must not dispose it in the start method. Remove the using there and dispose in your stop method.

private void btn_start_Click(object sender, EventArgs e)
       {
           var cts = _cts = new(); // no using, otherwise Dispose will be called at the very end of this method
           try
           {
              
              Thread thread = new(() => serialserver(_cts.Token));
              thread.Start();

           }
           catch
           {

           }
           btn_start.Enabled = false;
           btn_stop.Enabled = true;
       }

private void btn_stop_Click(object sender, EventArgs e)
       {
          
               _cts!.Cancel();
               _cts.Dispose(); // it is important to call dispose here
               _cts = null;

          

           btn_start.Enabled = true;
           btn_stop.Enabled = false;
       }

The problem is that you dispose the CancellationTokenSource but the stop button handler is still using it.

This should be resolved using this pattern:

using var cts = this._cts = new();
try
{
   // here your should wait for the task/thread to finish otherwise this._cts is disposed.
}
finally
{
    this._cts = null;
}

The stop button handler should use:

this._cts?.Cancel();

Maybe an implementation could be:

public async Task SerialServerAsync(CancellationToken token = default)
{
           Invoke((MethodInvoker)(() => txtbox_log.AppendText("Serial Server Started")));
           Invoke((MethodInvoker)(() => txtbox_log.AppendText(Environment.NewLine)));
           while (true)
           { 

               Invoke((MethodInvoker)(() => txtbox_log.AppendText("TEST! I'm running")));
               Invoke((MethodInvoker)(() => txtbox_log.AppendText(Environment.NewLine)));
               await Task.Delay(2500, token);

        // do some more serial stuff here 
           }
           Invoke((MethodInvoker)(() => txtbox_log.AppendText("Serial Server Stopped")));
           Invoke((MethodInvoker)(() => txtbox_log.AppendText(Environment.NewLine)));
       }


private async void btn_start_Click(object? sender, EventArgs e)
       {
           btn_start.Enabled = false;
           btn_stop.Enabled = true;

           try
           {
               using var cts = _cts = new(); 
               try
               {              
                   await Task.Run(() => this.SerialServerAsync(cts.Token));
               }
               catch (OperationCancelledException)
               {
               }
               catch
               {
  
               }
           }
           finally 
           {
               btn_start.Enabled = true;
               btn_stop.Enabled = false;
           }
       }

/// Stop-Handle: only this._cts?.Cancel();

本文标签: Can39t get a thread to stop via button in C and WinformsStack Overflow