首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >退出BackgroundWorker

退出BackgroundWorker
EN

Stack Overflow用户
提问于 2021-10-19 07:22:19
回答 4查看 102关注 0票数 0

下面是关于我正在尝试做的事情的简要描述:

我有两个按钮:启动backgroundWorker_AutoButton_Auto和停止(应该停止)正在运行的backgroundWorker_Auto并启动另一个backgroundWorker_ManualButton_Manual。基本上,按钮应该允许用户在我的应用程序中的两种操作模式之间切换。自动和手动

代码语言:javascript
运行
复制
        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服务器的数据。

代码语言:javascript
运行
复制
        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

EN

回答 4

Stack Overflow用户

发布于 2021-10-19 10:32:12

不要一开始就使用BackgroundWorker。这个类已经过时了,几乎在10年前就被async/awaitTask.RunIProgress<T>完全取代了。对于async/await来说,有很多事情是微不足道的,而在BGW中却非常困难。这包括取消和组合多个异步操作。

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

代码语言:javascript
运行
复制
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,因此可以使用StreamReader安全地处理它,而不是手动解码字节。
  2. 只读取预期的字符,或者有办法处理多个消息。如果服务器发送2个或3个连续的数字,例如1、3、5,则当前代码会将它们读取为135.

就像最近有人说的:Almost all Sockets problems are framing problems

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

代码语言:javascript
运行
复制
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实例。这个类只能取消一次,这意味着每次都需要创建一个新的:

代码语言:javascript
运行
复制
    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。

在核心和.NET中使用ActionBlock的

  • 和更高版本的.NET
  • 中使用IAsyncEnumerable .NET核心3和更高版本的

中的通道

假设工人是一个ActionBlock:

代码语言:javascript
运行
复制
ActionBlock _block=new ActionBlock(msg=>ProcessMsg(msg));

async Task ProcessMsg(char msg)
{
    switch(msg)
    {
        case '1':
        ...
    }
}

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

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

代码语言:javascript
运行
复制
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任务得到处理:

代码语言:javascript
运行
复制
public async void StopProcessing_Click()
{
    _manualCancel?.Cancel();
    _autoCancel?.Cancel();

    _block.Complete();
    await _block.Completion;
}
票数 2
EN

Stack Overflow用户

发布于 2021-10-19 08:02:41

您的Auto方法应该在单独的线程上执行,以便循环可以随时中断单独的线程。

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

因此,在每个事件中,您都可以对作为CancellationToken的对象使用RunWorkAsync(object o)

您可能可以使用以下示例控制台程序对此进行测试:

代码语言:javascript
运行
复制
    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();

        }
    }
票数 0
EN

Stack Overflow用户

发布于 2021-10-19 08:12:55

您可以将NetworkStream.ReadTimeout属性设置为某个值(例如,5000 ms /5秒)。如果在5秒内没有来自服务器的响应,则处理超时异常并转到新的循环迭代。然后一遍又一遍。每5秒循环检查一次,是否取消了BackgroundWorker,以及是否会破坏was循环。您可以配置超时值,但请记住,NetworkStream.Read将在新迭代之前等待该时间,这将检查BGW取消。

大概是这样的:

代码语言:javascript
运行
复制
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.RunCancellationTokens运行TcpClient,则可能会使用ReadAsync

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/69626591

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档