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 CancellationToken
s.
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:
- Create the TcpClient in the worker method itself, so it can be safely disposed
- Use a StreamReader instead of manually decoding bytes.
- 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;
}