以下是有關我嘗試執行的操作的簡要說明:
我有 2 個按鈕:Button_Auto啟動backgroundWorker_Auto和Button_Manual停止(應該停止)運行backgroundWorker_Auto并啟動另一個按鈕,backgroundWorker_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)
}
}
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 ?
uj5u.com熱心網友回復:
不要使用 BackgroundWorker 開始。那類是過時的和完全置換async/await,Task.Run以及IProgress<T>近10年前。有很多事情是微不足道的,但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)
}
}
這可以改進和簡化:
- 在作業方法本身中創建 TcpClient,因此可以安全地處置它
- 使用 StreamReader 而不是手動解碼位元組。
- 只讀取預期的字符,或者有辦法處理多條訊息。如果服務器發送了 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);
}
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
IAsyncEnumerablein .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)
}
}
一旦ActionBlock創建,它將繼續處理訊息。當我們想要停止它時,我們呼叫Complete()并等待所有掛起的訊息通過Completion任務得到處理:
public async void StopProcessing_Click()
{
_manualCancel?.Cancel();
_autoCancel?.Cancel();
_block.Complete();
await _block.Completion;
}
uj5u.com熱心網友回復:
您的 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();
}
}
uj5u.com熱心網友回復:
您可以NetworkStream.ReadTimeout使用某個值設定屬性(例如,5000 毫秒/5 秒)。如果 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 完成時觸發。ReadAsync可能可用,如果TcpClient使用Task.Run和CancellationTokens運行。
uj5u.com熱心網友回復:
在讀取 NetworkStream 之前檢查 DataAvailable,因此它不會在沒有任何讀取時阻塞執行緒。
if(nwStream.CanRead && nwStream.DataAvailable)
{
int bytesRead = nwStream.Read(bytesToRead, 0, client.ReceiveBufferSize);
...
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/327006.html
標籤:c# .net multithreading winforms backgroundworker
上一篇:ActiveMQJavaNIO傳輸連接器與PoolConnectionFactory
下一篇:這是升級標準庫鎖的有效方法嗎?
