【问题标题】:How can I gracefully terminate a BLOCKED thread?如何优雅地终止 BLOCKED 线程?
【发布时间】:2015-06-07 23:10:22
【问题描述】:

有很多地方可以优雅地终止 C# 线程。但是,它们依赖于循环或在循环内执行的 if 条件,假设该语句将被频繁执行;因此,当设置stop bool 标志时,线程会快速退出。

如果我有一个不正确的线程怎么办?在我的例子中,这是一个设置为从服务器接收的线程,它经常阻塞从输入流中读取数据的调用,其中尚未提供任何数据,因此它会等待。

这是有问题的循环:

    while (true)
        {
            if (EndThread || Commands.EndRcvThread)
            {
                Console.WriteLine("Ending thread.");
                return;
            }

            data = "";
            received = new byte[4096];

            int bytesRead = 0;
            try
            {
                bytesRead = stream.Read(received, 0, 4096);
            }
            catch (Exception e)
            {
                Output.Message(ConsoleColor.DarkRed, "Could not get a response from the server.");
                if (e.GetType() == Type.GetType("System.IO.IOException"))
                {
                    Output.Message(ConsoleColor.DarkRed, "It is likely that the server has shut down.");
                }
            }

            if (bytesRead == 0)
            {
                break;
            }

            int endIndex = received.Length - 1;
            while (endIndex >= 0 && received[endIndex] == 0)
            {
                endIndex--;
            }

            byte[] finalMessage = new byte[endIndex + 1];
            Array.Copy(received, 0, finalMessage, 0, endIndex + 1);

            data = Encoding.ASCII.GetString(finalMessage);

            try
            {
                ProcessMessage(data);
            }
            catch (Exception e)
            {
                Output.Message(ConsoleColor.DarkRed, "Could not process the server's response (" + data + "): " + e.Message);
            }
        }

块顶部的if 语句执行通常的停止线程优雅设置所做的工作:检查标志,如果设置了则终止线程。然而,这个线程通常会在下面几行之后找到,位于stream.Read

鉴于此,有什么方法可以优雅地终止这个线程(即没有Aborting),并清理它的资源(有一个需要关闭的客户端)?

【问题讨论】:

  • 减少您的流读取超时并更频繁地重试,让您有更多机会退出。
  • @Asad - 不知道这是可能的。我是用我拥有的TcpClient 还是底层的Socket TcpClient.Client 来做这个?
  • 如果这是TcpClient,您可以将其ReceiveTimeout 属性设置为您喜欢的任何值。甚至可能还有一个构造函数可以让您在制作时设置它,尽管我不确定。
  • 您的代码有一个重大缺陷,您假设您从stream.Read 收到一条完整的消息,您在单次阅读期间可能只收到 1/2 条消息。你真的应该有某种循环在那里检查'\0',如果你的消息没有空字符,它会在将data传递给ProcessMessage之前再次调用read。

标签: c# multithreading


【解决方案1】:

假设您可以使用 async / Tasks,完全停止异步和 IO 操作的方法是使用连接到 CancelationTokenSourceCancelationToken。下面的代码 sn-p 说明了将其应用于您的代码的简化版本时的简化示例。

class MyNetworkThingy 
{
    public async Task ReceiveAndProcessStuffUntilCancelled(Stream stream, CancellationToken token)
    {
        var received = new byte[4096];
        while (!token.IsCancellationRequested)
        {
            try
            {
                var bytesRead = await stream.ReadAsync(received, 0, 4096, token);
                if (bytesRead == 0 || !DoMessageProcessing(received, bytesRead))
                    break; // done.
            }
            catch (OperationCanceledException)
            {
                break; // operation was canceled.
            }
            catch (Exception e)
            {
                // report error & decide if you want to give up or retry.
            }
        }
    }

    private bool DoMessageProcessing(byte[] buffer, int nBytes)
    {
        try
        {
            // Your processing code.
            // You could also make this async in case it does any I/O.
            return true;
        }
        catch (Exception e)
        {
            // report error, and decide what to do.
            // return false if the task should not
            // continue.
            return false;
        }
    }
}

class Program
{
    public static void Main(params string[] args)
    {
        using (var cancelSource = new CancellationTokenSource())
        using (var myStream = /* create the stream */)
        {
            var receive = new MyNetworkThingy().ReceiveAndProcessStuffUntilCancelled(myStream, cancelSource.Token);
            Console.WriteLine("Press <ENTER> to stop");
            Console.ReadLine();
            cancelSource.Cancel();
            receive.Wait();
        }
    }
}

.

【讨论】:

  • 它像线程一样工作吗?我可以同时运行这个客户端和另一个客户端吗?
  • 使用线程池线程调度任务。所以是的,你可以有多个任务,就像你可以有多个线程一样。好消息是,当 IO 挂起时,为任务服务的物理线程池线程将被释放以执行其他工作,直到 IO 请求完成、被取消或失败。
  • 任务可以使用线程池线程进行调度,但并非必须如此。它们可能在同一个线程(ASP.NET 或 WPF 应用程序中的 async/await 常见)、线程池、另一个线程池或它们自己独特的线程(长时间运行的任务)上运行。
  • @JonathanAllen 是的,但在本例中,如果使用异步 IO 操作并且没有像 ConfigureAwait(true) 这样的指令或将任务创建为长时间运行的任务,它可能是多个线程池线程。