循环不会因 Thread 和 CancellationToken 而停止

Loop won't stop with Thread and CancellationToken

我正在处理一个使用异步回调的 Windows 套接字应用程序。如果我用Thread启动_StartListening,当我调用StopListening时,循环仍然停在allDone.WaitOne()。但是任务版本就可以了。

有什么区别?

我的代码是 this

的修改版本

带有 ManualResetEvent 的原始版本具有 提到的竞争条件。我将其更改为 SemaphoreSlim 但问题仍然存在。

我在调试模式下试过,即使我没有启动客户端,在我调用 StopListening 之后似乎断点从未在 if (cancelToken.IsCancellationRequested) 命中。

对不起。我发现我不小心启动了两个套接字服务器。就是这个问题。

  class WinSocketServer:IDisposable
  {
        public SemaphoreSlim semaphore = new SemaphoreSlim(0);
        private CancellationTokenSource cancelSource = new CancellationTokenSource();
        public void AcceptCallback(IAsyncResult ar)
        {
            semaphore.Release();
            //Do something
        }

        private void _StartListening(CancellationToken cancelToken)
        {
            try
            {
                while (true)
                {
                    if (cancelToken.IsCancellationRequested)
                        break;
                    Console.WriteLine("Waiting for a connection...");
                    listener.BeginAccept(new AsyncCallback(AcceptCallback),listener);
                    semaphore.Wait();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
            Console.WriteLine("Complete");
        }
        public void StartListening()
        {
            Task.Run(() => _StartListening(cancelSource.Token));//OK

            var t = new Thread(() => _StartListening(cancelSource.Token));
            t.Start();//Can't be stopped by calling StopListening
        }

        public void StopListening()
        {
            listener.Close();
            cancelSource.Cancel();
            semaphore.Release();
        }

        public void Dispose()
        {
            StopListening();
            cancelSource.Dispose();
            semaphore.Dispose();
        }
    }

您的代码存在可能导致死锁(有时)的竞争条件。我们将线程命名为 "listener"(运行 _StartListening 的线程)和 "control"(运行 StopListening 的线程):

  1. 侦听器线程:if (cancelToken.IsCancellationRequested) -> false
  2. 控制线程:cancelSource.Cancel()
  3. 控制线程:allDone.Set()
  4. 侦听器线程:allDone.Reset() -> 不小心重置了停止请求!
  5. 监听线程:listener.BeginAccept(...)
  6. 控制线程:stopListening()退出,而侦听器继续工作!
  7. 侦听器线程:allDone.WaitOne() -> 死锁!没有人会做 allDone.Set().

问题在于你如何使用 allDone 事件,它应该是相反的:_StartListening 应该在它出于任何原因退出之前执行 allDone.Set(),而 StopListening 应该做 allDone.WaitOne():

class WinSocketServer:IDisposable
{
    // I guess this was in your code, necessary to show proper stopping
    private Socket listener = new Socket(......); 

    public ManualResetEvent allDone = new ManualResetEvent(false);
    private CancellationTokenSource cancelSource = new CancellationTokenSource();

    private void _StartListening(CancellationToken cancelToken)
    {
        try
        {
            listener.Listen(...); // I guess 
            allDone.Reset(); // reset once before starting the loop
            while (!cancelToken.IsCancellationRequested)
            {
                Console.WriteLine("Waiting for a connection...");
                listener.BeginAccept(new AsyncCallback(AcceptCallback),listener);
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e.ToString());
        }

        allDone.Set(); // notify that the listener is exiting
        Console.WriteLine("Complete");
    }
    public void StartListening()
    {
        Task.Run(() => _StartListening(cancelSource.Token));
    }
    public void StopListening()
    {
        // notify the listener it should exit
        cancelSource.Cancel(); 
        // cancel possibly pending BeginAccept
        listener.Close();
        // wait until the listener notifies that it's actually exiting
        allDone.WaitOne();
    }
    public void Dispose()
    {
        StopListening();
        cancelSource.Dispose();
        allDone.Dispose();
    }
}

更新

值得注意的是 listener.BeginAccept 不会 return 直到有新的客户端连接。停止监听器时,需要关闭socket(listener.Close())强制BeginAccept退出。

Thread/Task 行为的差异真的很奇怪,它可能源于任务线程是后台线程,而常规线程是前台线程。