【问题标题】:Stopping threads in C#在 C# 中停止线程
【发布时间】:2013-12-07 08:30:04
【问题描述】:

我需要让 TcpClient 事件驱动而不是一直轮询消息,所以我想:我将创建一个线程来等待消息到来并在它发生后触发事件。这是一个总体思路:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Sockets;
using System.Net;

namespace ThreadsTesting
{
    class Program
    {
        static void Main(string[] args)
        {
            Program p = new Program();

            //imitate a remote client connecting
            TcpClient remoteClient = new TcpClient();
            remoteClient.Connect(IPAddress.Parse("127.0.0.1"), 80);

            //start listening to messages
            p.startMessageListener();

            //send some fake messages from the remote client to our server
            for (int i = 0; i < 5; i++)
            {
                remoteClient.GetStream().Write(new byte[] { 0x80 }, 0, 1);
                Thread.Sleep(200);
            }

            //sleep for a while to make sure the cpu is not used
            Console.WriteLine("Sleeping for 2sec");
            Thread.Sleep(2000);

            //attempt to stop the server
            p.stopMessageListener();

            Console.ReadKey();
        }

        private CancellationTokenSource cSource;
        private Task listener;
        private TcpListener server;
        private TcpClient client;

        public Program()
        {
            server = new TcpListener(IPAddress.Parse("127.0.0.1"), 80);
            server.Start();
        }

        private void startMessageListener()
        {
            client = server.AcceptTcpClient();

            //start listening to the messages
            cSource = new CancellationTokenSource();
            listener = Task.Factory.StartNew(() => listenToMessages(cSource.Token), cSource.Token);
        }

        private void stopMessageListener()
        {
            Console.Out.WriteLine("Close requested");
            //send cancelation signal and wait for the thread to finish
            cSource.Cancel();
            listener.Wait();
            Console.WriteLine("Closed");
        }

        private void listenToMessages(CancellationToken token)
        {
            NetworkStream stream = client.GetStream();

            //check if cancelation requested
            while (!token.IsCancellationRequested)
            {
                //wait for the data to arrive
                while (!stream.DataAvailable)
                { }

                //read the data (always 1 byte - the message will always be 1 byte)
                byte[] bytes = new byte[1];
                stream.Read(bytes, 0, 1);

                Console.WriteLine("Got Data");

                //fire the event
            }
        }
    }
}

这显然无法正常工作:

  • while (!stream.DataAvailable) 阻塞线程并始终使用 25% 的 CPU(在 4 核 CPU 上),即使那里没有数据。
  • listener.Wait(); 将一直等待,因为 while 循环没有发现取消已被调用。

我的替代解决方案是在 listenToMessages 方法中使用异步调用:

private async Task listenToMessages(CancellationToken token)
{
    NetworkStream stream = client.GetStream();

    //check if cancelation requested
    while (!token.IsCancellationRequested)
    {
         //read the data
         byte[] bytes = new byte[1];
         await stream.ReadAsync(bytes, 0, 1, token);

         Console.WriteLine("Got Data");

          //fire the event
    }
}

这完全符合我的预期:

  • 如果队列中没有消息,CPU 不会阻塞,但我们仍在等待它们
  • 取消请求被正确提取,线程按预期完成

我想更进一步。由于 listenToMessages 现在返回一个 Task 本身,我认为没有必要启动一个将执行该方法的任务。这是我所做的:

private void startMessageListener()
{
    client = server.AcceptTcpClient();

    //start listening to the messages
    cSource = new CancellationTokenSource();
    listener = listenToMessages(cSource.Token);
}

这不起作用,因为当调用 Cancel() 时,ReadAsync() 方法似乎没有从令牌中获取取消消息,并且线程没有停止,相反,它卡在 ReadAsync() 行上。

知道为什么会这样吗?我认为 ReadAsync 仍会像以前一样拾取令牌...
感谢您的所有时间和帮助。

-- 编辑--
好的,经过更深入的评估,我的解决方案 2 并没有按预期工作:
线程本身结束于调用者,因此调用者可以继续。但是,线程并没有“死”,所以如果我们发送一些数据,它会再次执行!
这是一个例子:

//send some fake messages from the remote client to our server
for (int i = 0; i < 5; i++)
{
    remoteClient.GetStream().Write(new byte[] { 0x80 }, 0, 1);
    Thread.Sleep(200);
}

Console.WriteLine("Sleeping for 2sec");
Thread.Sleep(2000);

//attempt to stop the server
p.stopListeners();

//check what will happen if we try to write now
remoteClient.GetStream().Write(new byte[] { 0x80 }, 0, 1);
Thread.Sleep(200);

Console.ReadKey();

即使理论上我们停止了,这也会输出消息“Got Data”!我将进一步调查并报告我的发现。

【问题讨论】:

  • 好吧,ReadAsync 确实返回了 Task&lt;int&gt;Result 是读取的字节数。也许如果您检查返回值并看到它是 0,您可以假设操作已被取消。

标签: c# multithreading asynchronous tcp async-await


【解决方案1】:

使用现代库,只要您输入 new Thread,您就已经获得了遗留代码。

您的情况的核心解决方案是异步套接字方法。不过,有几种方法可以实现您的 API 设计:想到 Rx、TPL 数据流和普通 TAP。如果你真的想要事件,那么EAP 是一个选择。

我有一个 EAP 套接字库here。它确实需要同步上下文,因此如果您需要从控制台应用程序中使用它,则必须使用 ActionDispatcher 之类的东西(包含在同一个库中)(如果您从控制台应用程序中使用它,则不需要它) WinForms/WPF)。

【讨论】:

  • @Noseratio:好的,好的,但那是最后一个剩余的用例! :)
  • 关于 TPL 数据流的有趣建议。不幸的是,我做出如此大的改变为时已晚。我想我将不得不坚持我的第二个解决方案,因为它的行为符合预期。我的主要问题确实是 ReadAsync 是如何在第二个解决方案中被取消的,而不是在第三个解决方案中。不管怎样,谢谢你的帖子。我现在正在阅读 EAP :)
【解决方案2】:

ReadAsync 似乎不支持取消 NetworkStream - 请查看此线程中的答案:

NetworkStream.ReadAsync with a cancellation token never cancels

【讨论】:

  • 感谢您的链接。在那种情况下,我的第二个解决方案如何正确取消线程?它肯定会卡在同一条线上,对吧?
  • 将您的答案标记为答案,因为它完全回答了我的问题。谢谢。
猜你喜欢
  • 1970-01-01
  • 2010-12-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多