如何优雅地终止一个被阻塞的线程?

5
有很多地方都处理了如何优雅终止C#线程。然而,它们依赖于循环或者if条件执行在一个循环内,这意味着当设置stop布尔标志后,线程会迅速退出。但是,如果我的线程并不是那样的呢? 在我的情况下,这是一个被设置为从服务器接收数据的线程,通常会在调用读取输入流中的数据时阻塞,但是没有提供数据,所以它会等待。
以下是要处理的线程的循环:
    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处。

鉴于此,是否有任何方法可以优雅地终止此线程(即不使用Abort),并清理其资源(有一个需要关闭的客户端)?


减少流读取超时时间并增加重试频率,从而为您提供更多退出机会。 - Asad Saeeduddin
如果这是一个 TcpClient,你可以将其 ReceiveTimeout 属性设置为任何你喜欢的值。可能甚至有一个构造函数可以让你在创建时设置它,尽管我不确定。 - Asad Saeeduddin
1
你的代码有一个重大缺陷,你假设从 stream.Read 中获取了完整的消息,但在单次读取期间可能只获取了 1/2 条消息。你真的应该在其中加入某种循环,检查 '\0' 是否存在,如果消息没有空字符,则再次调用读取操作,然后将 data 传递给 ProcessMessage - Scott Chamberlain
@ScottChamberlain 我知道这种可能性;我已经为此进行了规划。由于我知道客户端和服务器之间所有消息的长度,我知道没有任何消息超过4096字节,如果有些消息到达时是损坏的或不完整的,它们足够不同,以至于我有代码可以推断出它是什么消息。 - ArtOfCode
大家都有很好的想法,如果你们能把它们发布为答案就太好了。Asad,你的想法很有效。虚假的网络积分等待着答案。 - ArtOfCode
显示剩余4条评论
1个回答

4
假设您可以使用 async / Tasks,在进行异步和 IO 操作的干净停止时,需要使用连接到 CancelationTokenSource 的 CancelationToken。以下代码片段说明了在将其应用于代码的简化版本时使用的简化示例。
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();
        }
    }
}

.


它是否像线程一样工作 - 我可以同时运行这个和另一个客户端吗? - ArtOfCode
1
任务是使用线程池线程进行调度的。因此,您可以拥有多个任务,就像您可以拥有多个线程一样。好处在于,在IO挂起时,服务于任务的物理线程池线程将被释放以执行其他工作,直到IO请求完成、取消或失败。 - Alex
2
任务可以使用线程池线程进行调度,但不一定必须这样做。它们可能在同一个线程上运行(在ASP.NET或WPF应用程序中的异步/等待常见),线程池、另一个线程池或它们自己的独立线程(长时间运行的任务)。 - Jonathan Allen
@JonathanAllen 在这个例子中是正确的,但是由于使用了异步IO操作并且没有像ConfigureAwait(true)这样的指令或者创建长时间运行的任务,所以很可能会涉及到多个线程池线程。 - Alex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接