Exiting from a BackgroundWorker



下面是我想做的事情的简要说明:

我有2个按钮:Button_Auto启动backgroundWorker_AutoButton_Manual停止(应该停止)运行的backgroundWorker_Auto并启动另一个backgroundWorker_Manual。基本上,按钮应该允许用户在我的应用程序中的2种操作模式之间切换。汽车,手册

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();
}      

backgroundWorker_Auto只是一个连接到服务器的TCP客户端,从另一个应用程序对服务器的API调用中接收数据。

我已经看到了很多解决方案,取消后台工作与迭代器,其中检查CancellationPending属性在每次迭代。然而,在下面的代码中,backgroundworker只是等待来自TCP服务器的数据。

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)
}                                                                                        
}

问题是,当backgroundWorker_Auto启动时,它在int bytesRead行等待从服务器接收数据。一旦接收到,它将执行以下函数并返回到与上面相同的侦听状态。因此,即使我从Button_Manual触发CancelAsync并将while循环条件更改为backgroundWorker_Auto.CancellationPending,也不会检查,除非客户端接收到数据。

由于backgroundWorker_Auto没有停止,我将无法再次启动它,即在自动和手动之间切换是不可能的。

在这种情况下如何检查CancellationPending的条件或正确停止backgroundWorker_Auto?

一开始就不要使用BackgroundWorker。该级别已经过时,几乎在10年前就被async/await,Task.RunIProgress<T>完全取代。在async/await中有很多琐碎的事情,而在BGW中却非常困难。这包括取消和组合多个异步操作。

在这种情况下,看起来BGW可以被DoWork所做的单个异步方法所取代:

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)
}     
}

可以改进和简化:

  1. 在worker方法本身中创建TcpClient,以便可以安全地处置
  2. 使用StreamReader而不是手动解码字节
  3. 只读取预期的字符,或者有处理多个消息的方法。如果服务器发送了2个或3个连续的数字,例如1,3,5,当前代码将它们读取为135

最近有人说:Almost all Sockets problems are framing problems.

在本例中,我假设每个字符是一个单独的消息。代码可以简化为:

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实例由CancellationTokenSource类提供。该类只能用于取消一次,这意味着您每次都需要创建一个新类:

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);
}   

将监听与处理分开

另一个改进是分离轮询和处理代码,特别是如果这两种情况的处理是相同的。ListenAutoListenManual将只检查消息并将它们发送给异步处理它们的worker,而不是同时侦听和处理消息。有几种方法可以实现这样一个worker。
  • 在。net Core和。net Framework中使用ActionBlock
  • 中使用通道
  • 在。net Core 3及以后版本中使用IAsyncEnumerable

假设worker是一个ActionBlock:

ActionBlock _block=new ActionBlock(msg=>ProcessMsg(msg));
async Task ProcessMsg(char msg)
{
switch(msg)
{
case '1':
...
}
}

ActionBlock使用一个或多个任务(默认为1)按顺序处理发送到其输入缓冲区的所有消息。默认情况下,可以缓冲的项数没有限制。

在这种情况下,ListenAuto方法将更改为:

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)
}     
}

一旦创建了ActionBlock,它将继续处理消息。当我们想要停止它时,我们调用Complete()并等待所有挂起的消息通过Completion任务得到处理:

public async void StopProcessing_Click()
{
_manualCancel?.Cancel();
_autoCancel?.Cancel();
_block.Complete();
await _block.Completion;
}

你的Auto方法应该在一个单独的线程中执行,这样循环就可以在你想要的时候中断这个单独的线程。

为简单起见,您可能应该使用CancellationToken并使用ReadAsync.

所以在每个事件中,你可以使用RunWorkAsync(object o)和一个对象作为CancellationToken。

您可以使用下面的示例控制台程序进行测试:

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();
}
}

您可以用一些值设置NetworkStream.ReadTimeout属性(例如,5000 ms/5 sec)。如果服务器在5秒内没有响应,处理超时异常并进行新的循环迭代。然后一次又一次。每5秒循环将检查是否BackgroundWorker被取消,如果是循环将被打破。您可以配置超时值,但请记住,NetworkStream.Read将在新迭代之前等待该时间,这将检查BGW取消。

像这样:

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();
}

编辑。我不建议在这里使用NetworkStream.ReadAsync,因为一旦你开始await,它-RunWorkerCompleted会随着BackgroundWorker完成而触发。如果将TcpClientTask.RunCancellationToken一起运行,ReadAsync可能可用。

在读取NetworkStream之前检查DataAvailable,这样当没有东西可读时就不会阻塞线程。

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