Home > other >  Exiting from a BackgroundWorker
Exiting from a BackgroundWorker

Time:10-19

Here is a brief description about what am trying to do:

I have 2 buttons : Button_Auto that starts the backgroundWorker_Auto and Button_Manual which stops(should stop) the running backgroundWorker_Auto and starts another one, backgroundWorker_Manual. Basically, the buttons should allow the user to switch between the 2 modes of operation in my application. Auto & Manual

        private Button_Auto_Click(object sender, EventArgs e)
        {
             if (!backgroundWorker_Auto.IsBusy)
                 backgroundWorker_Auto.RunWorkerAsync();
        }      


        private Button_Manual_Click(object sender, EventArgs e)
        {
          //some code to stop backgroundWorker_Auto..

             if (!backgroundWorker_Manual.IsBusy)
                 backgroundWorker_Manual.RunWorkerAsync();
        }      

The backgroundWorker_Auto is simply a TCP client connected to a server, receiving data from API calls made to server from another application.

I have seen lot of solution to cancel background worker with iterators, where it checks the CancellationPending property on each iteration. However, in my below code , the backgroundworker simply waits for data from TCP server.

        public static TcpClient client;

        private void backgroundWorker_Auto_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
        {

            try
            {              
                NetworkStream nwStream = client.GetStream();
                while (client.Connected)
                {
                    
                    byte[] bytesToRead = new byte[client.ReceiveBufferSize];
                    int bytesRead = nwStream.Read(bytesToRead, 0, client.ReceiveBufferSize);  //CODE WAITS HERE!!
                    String responseData = String.Empty;
                    responseData = Encoding.ASCII.GetString(bytesToRead, 0, bytesRead);
                    switch (responseData)
                          {
                            case "1":
                            //Do something;
                            break;
                                        
                            case "2":
                            //Do some other thing;
                            break;
                          }
                }
             }

            catch (Exception ex)
             {
                MessageBox.Show(ex.Message)
             }                                                                                        
        }

The issue is that when the backgroundWorker_Auto is started, it waits at the int bytesRead line to receive data from server. Once received, it executes the below functions and goes back to the same listening state as above. So, even if I trigger CancelAsync from my Button_Manual and change the while loop condition to backgroundWorker_Auto.CancellationPending, that won't be checked unless a data is received by the client.

And since backgroundWorker_Auto is not stopped, I won't be able to start it again ie, switching between Auto and Manual is not possible.

How can I check for CancellationPending condition in this scenario or stop the backgroundWorker_Auto properly ?

CodePudding user response:

Your Auto method should be executed on a separate thread so that a loop can interrupt the separate thread whenever you want.

For simplicity, you should probably use a CancellationToken and use the ReadAsync.

So in each event you can use the RunWorkAsync(object o) with an object being a CancellationToken.

You can probably test this with this sample console program:

    class Program
    {
        static void Main(string[] args)
        {
            var autoctsource = new CancellationTokenSource();
            var autoct = autoctsource.Token;

            var manualctsource = new CancellationTokenSource();
            var manualct = manualctsource.Token;

            Task auto = null;
            Task manual = null;

            auto = new Task(async () =>
            {
                if (manual.Status == TaskStatus.Running)
                {
                    manualctsource.Cancel();
                }

                var tcp = new TcpClient();
                while (tcp.Connected)
                {
                    var stream = tcp.GetStream();
                    byte[] bytesToRead = new byte[tcp.ReceiveBufferSize];
                    int bytesRead = await stream.ReadAsync(bytesToRead.AsMemory(0, tcp.ReceiveBufferSize), autoct);
                    //TCP code
                }

            }, autoct);

            manual = new Task(() =>
            {
                if (auto.Status == TaskStatus.Running)
                {
                    autoctsource.Cancel();
                }
                Console.WriteLine(auto.Status);
                
                while(!manualct.IsCancellationRequested)
                {
                    //Manual code loop
                }
            }, manualct);

            auto.Start();
            Task.Delay(5000);
            manual.Start();

        }
    }

CodePudding user response:

You can set NetworkStream.ReadTimeout property with some value (for example, 5000 ms / 5 sec). If there wouldn't be response from server for 5 sec - handle timeout exception and go to a new loop iteration. Then again and again. Each 5 seconds loop will check, was BackgroundWorker cancelled or not and if was - loop would be breaked. You may configure timeout value, but remember, that NetworkStream.Read will wait for that time before new iteration, which would check BGW cancellation.

Something like that:

private TcpClient client;

private void ButtonRunWorker_Click(object sender, EventArgs e)
{
    if (!backgroundWorker_Auto.IsBusy)
        backgroundWorker_Auto.RunWorkerAsync();
}

private void BackgroundWorker_Auto_DoWork(object sender, DoWorkEventArgs e)
{
    try
    {
        client = new TcpClient(yourServerIP, yourServerPort);
    }
    catch (Exception ex) // If failed to connect or smth...
    {
        MessageBox.Show(ex.Message);
        // If client failed to initialize - no sense to continue, so close it and return.
        client?.Close();
        client?.Dispose();
        return;
    }

    using (NetworkStream ns = client.GetStream())
    {
        // Set time interval to wait for server response
        ns.ReadTimeout = 5000;

        while (client.Connected)
        {
            // If BackgroundWorker was cancelled - break loop
            if (backgroundWorker_Auto.CancellationPending)
            {
                e.Cancel = true;
                break;
            }

            byte[] bytesToRead = new byte[client.ReceiveBufferSize];
            int bytesRead = 0;

            // Wrap read attempt into try
            do
            {
                try
                {
                    // Code still waits here, but now only for 5 sec
                    bytesRead = ns.Read(bytesToRead, 0, client.ReceiveBufferSize);
                }
                catch (Exception ex)
                {
                    // Handle timeout exception (but not with MessageBox). Maybe with some logger.
                }
            } while (ns.DataAvailable); // Read while data from server available

            // Process response
            string responseData = Encoding.ASCII.GetString(bytesToRead, 0, bytesRead);
            switch (responseData)
            {
                case "1":
                    //Do something;
                    break;
                case "2":
                    //Do some other thing;
                    break;
            }
        }
    }
 
    // Close TCP Client properly
    client?.Close();
    client?.Dispose();
}

private void ButtonStopWorker_Click(object sender, EventArgs e)
{
    // Cancel BackgroundWorker
    backgroundWorker_Auto.CancelAsync();
}

EDIT. I do not recommend to use NetworkStream.ReadAsync here, because once you begin await it - RunWorkerCompleted fires with BackgroundWorker completion. ReadAsync probably usable, if run TcpClient with Task.Run and CancellationTokens.

CodePudding user response:

Check for DataAvailable prior to reading the NetworkStream, so it won't block the thread when there is nothing to read.

if(nwStream.CanRead && nwStream.DataAvailable)
{    
    int bytesRead = nwStream.Read(bytesToRead, 0, client.ReceiveBufferSize);
    ...

CodePudding user response:

Don't use a BackgroundWorker to begin with. That class is obsolete and was fully replaced by async/await, Task.Run and IProgress<T> almost 10 years ago. There are a lot of things that are trivial to do with async/await that are very difficult with BGW. That includes cancellation and combining multiple asynchronous operations.

In this case it looks like the BGW can be replaced by a single async method that does what DoWork does :

async Task ListenAuto(TcpClient client,CancellationToken token=default)
{
    try
        {              
            using var nwStream = client.GetStream();
            var bytesToRead = new byte[client.ReceiveBufferSize];
            while (client.Connected && !token.IsCancellationRequested)
            {
                int bytesRead = await nwStream.ReadAsync(bytesToRead, 0, 
                                         client.ReceiveBufferSize,token);  //Not blocking
                var responseData = Encoding.ASCII.GetString(bytesToRead, 0, bytesRead);
                switch (responseData)
                      {
                        case "1":
                        await Task.Run(()=>DoSomething1());
                        
                        break;
                                    
                        case "2":
                        await Task.Run(()=>DoSomething2());
                        break;
                      }
            }
         }
         catch(OperationCanceledException)
         {
             //Cancelled, no need to show anything
         }
         catch (Exception ex)
         {
            MessageBox.Show(ex.Message)
         }     
}

This can be improved and simplified:

  1. Create the TcpClient in the worker method itself, so it can be safely disposed
  2. Use a StreamReader instead of manually decoding bytes.
  3. Only read the expected characters, or have a way to handle multiple messages. If the server sent 2 or three consecutive numbers, eg 1, 3, 5, the current code would read them as 135.

As someone said recently: Almost all Sockets problems are framing problems.

In this case, I'll assume each character is a separate message. The code could be reduced to:

async Task ListenAuto(IPAddress address,int port,CancellationToken token=default)
{
    try
        {        
            using var client=new TcpClient(endpoint);
            await client.ConnedtAsync(address,port,token);

            using var nwStream = client.GetStream();
            using var reader=new StreamReader(nwStream,Encoding.ASCII);

            var chars = new char[client.ReceiveBufferSize];
            while (client.Connected && !token.IsCancellationRequested)
            {
                int charsRead = await reader.ReadAsync(chars, 0,chars.Length,token);  //Not blocking
                for(int i=0;i<charsRead;i  )
                {
                    switch (chars[i])
                    {
                     ...
                    }
                 } 
            }
         }
         catch(OperationCanceledException)
         {
             //Cancelled, no need to show anything
         }
         catch (Exception ex)
         {
            MessageBox.Show(ex.Message)
         }     
}

CancellationToken instances are provided by the CancellationTokenSource class. This class can only be used to cancel once, which means you need to create a new one each time :

    CancellationTokenSource _autoCancel;
    CancellationTokenSource _manualCancel;

    private async void Button_Auto_Click(object sender, EventArgs e)
    {
        //Just in case it's null
        _manualCancel?.Cancel();

        _autoCancel=new CancellationTokenSource();
        await ListenAuto(server,port,_autoCancel.Token);
    }   

    private async void Button_Manual_Click(object sender, EventArgs e)
    {
        //Just in case it's null
        _autoCancel?.Cancel();

        _manualCancel=new CancellationTokenSource();
        await ListenManual(server,port,_manualCancel.Token);
    }   

Separate Listening from Processing

Another improvement is to separate the polling and processing code, especially if processing is the same for both cases. Instead of both listening and processing, ListenAuto and ListenManual will only check for messages and post them to a worker that processes them asynchronously. There are several ways to implement such a worker.

  • Using an ActionBlock in both .NET Core and .NET Framework
  • Using a Channel in both
  • Using IAsyncEnumerable in .NET Core 3 and later

Let's say the worker is an ActionBlock:

ActionBlock _block=new ActionBlock(msg=>ProcessMsg(msg));

async Task ProcessMsg(char msg)
{
    switch(msg)
    {
        case '1':
        ...
    }
}

An ActionBlock uses one or more tasks (1 by default) to process all messages posted to its input buffer in sequence. By default there's no limit to how many items can be buffered.

In this case the ListenAuto method would change to :

async Task ListenAuto(IPAddress address,int port,CancellationToken token=default)
{
    try
        {        
            using var client=new TcpClient(endpoint);
            await client.ConnedtAsync(address,port,token);

            using var nwStream = client.GetStream();
            using var reader=new StreamReader(nwStream,Encoding.ASCII);

            var chars = new char[client.ReceiveBufferSize];
            while (client.Connected && !token.IsCancellationRequested)
            {
                int charsRead = await reader.ReadAsync(chars, 0,chars.Length,token);  
                
                for(int i=0;i<charsRead;i  )
                {
                    _block.PostAsync(chars[i]);
                } 
            }
         }
         catch(OperationCanceledException)
         {
             //Cancelled, no need to show anything
         }
         catch (Exception ex)
         {
            MessageBox.Show(ex.Message)
         }     
}

Once an ActionBlock is created it will keep processing messages. When we want to stop it, we call Complete() and await for all pending messages to get processed through the Completion task:

public async void StopProcessing_Click()
{
    _manualCancel?.Cancel();
    _autoCancel?.Cancel();

    _block.Complete();
    await _block.Completion;
}
  • Related