下面是关于我正在尝试做的事情的简要描述:
我有两个按钮:启动backgroundWorker_Auto的Button_Auto和停止(应该停止)正在运行的backgroundWorker_Auto并启动另一个backgroundWorker_Manual的Button_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();
} 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?
发布于 2021-10-19 10:32:12
不要一开始就使用BackgroundWorker。这个类已经过时了,几乎在10年前就被async/await、Task.Run和IProgress<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)
}
}这是可以改进和简化的:
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)
}
}CancellationTokenSource类提供CancellationToken实例。这个类只能取消一次,这意味着每次都需要创建一个新的:
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);
} 将监听与处理分开
另一个改进是将轮询和处理代码分开,特别是在两种情况下处理相同的情况下。ListenAuto和ListenManual将只检查消息并将其发送到异步处理它们的工作进程,而不是同时进行侦听和处理。有几种方法可以实现这样的worker。
在核心和.NET中使用ActionBlock的
IAsyncEnumerable .NET核心3和更高版本的中的通道
假设工人是一个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;
}发布于 2021-10-19 08:02:41
您的Auto方法应该在单独的线程上执行,以便循环可以随时中断单独的线程。
为简单起见,您可能应该使用CancellationToken和ReadAsync。
因此,在每个事件中,您都可以对作为CancellationToken的对象使用RunWorkAsync(object o)。
您可能可以使用以下示例控制台程序对此进行测试:
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();
}
}发布于 2021-10-19 08:12:55
您可以将NetworkStream.ReadTimeout属性设置为某个值(例如,5000 ms /5秒)。如果在5秒内没有来自服务器的响应,则处理超时异常并转到新的循环迭代。然后一遍又一遍。每5秒循环检查一次,是否取消了BackgroundWorker,以及是否会破坏was循环。您可以配置超时值,但请记住,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 it - RunWorkerCompleted就会触发BackgroundWorker补全。如果使用Task.Run和CancellationTokens运行TcpClient,则可能会使用ReadAsync。
https://stackoverflow.com/questions/69626591
复制相似问题