Here is a breif 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 receievd by the client.
And since backgroundWorker_Auto is not stopped, I won't be able to start it agian 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.