C# 优雅地取消线程 – 为什么这么长

本文关键字:为什么 线程 取消 | 更新日期: 2023-09-27 18:32:10

最近我正在研究Joe Albahari的多线程星系的奇妙指南。我发现了一件我无法解释的事情:设置取消变量后,执行最后一个循环需要花费大量时间(就其他部分的执行时间而言)。我尝试了易失性方法,还有储物柜对象和 MemoryBarier 调用,结果总是相同的

class Program
{
    static ReaderWriterLockSlim _rw = new ReaderWriterLockSlim();
    static List<int> _items = new List<int>();
    static Random _rand = new Random();
    static /*volatile*/ bool _cancel = false;
    static object _cancelLock = new object();
    static void Main()
    {
        Thread t1 = new Thread(Read); t1.Start();
        Thread t2 = new Thread(Read); t2.Start();
        Thread t3 = new Thread(Read); t3.Start();
        Thread t4 = new Thread(Write); t4.Start("A");
        Thread t5 = new Thread(Write); t5.Start("B");
        System.Console.ReadKey();
        Console.WriteLine("Cancelling... " + DateTime.Now.ToString("hh:mm:ss.ffff"));
        _cancel = true;
        Console.WriteLine("Cancelled " + DateTime.Now.ToString("hh:mm:ss.ffff"));

    }
    static void Read()
    {
        while (true)
        {
            _rw.EnterReadLock();
            foreach (int i in _items) Thread.Sleep(10);
            _rw.ExitReadLock();
        }
    }
    static void Write(object threadID)
    {
        while (true)
        {
            if (_cancel)
              break;
            int newNumber = GetRandNum(100);
            _rw.EnterWriteLock();
            _items.Add(newNumber);
            _rw.ExitWriteLock();
            Console.WriteLine("Thread " + threadID + " added " + newNumber+" at "+DateTime.Now.ToString("hh:mm:ss.ffff"));
            //Thread.Sleep(100);
        }
    }
    static int GetRandNum(int max) { lock (_rand) return _rand.Next(max); }
}

请看输出:

Thread B added 37 at 01:52:20.2916  
Thread B added 64 at 01:52:20.2916  
Thread B added 89 at 01:52:20.2926  
Thread B added 92 at 01:52:20.2926  
Thread B added 55 at 01:52:20.2926  
Thread B added 60 at 01:52:20.2926  
Thread B added 0 at 01:52:20.2926  
Thread A added 74 at 01:52:20.2926  
Thread A added 90 at 01:52:20.2926  
Thread A added 86 at 01:52:20.2926  
Thread A added 91 at 01:52:20.2926  
Thread A added 19 at 01:52:20.2926  
Thread A added 67 at 01:52:20.2926  
Thread A added 52 at 01:52:20.2926  
Thread A added 73 at 01:52:20.2926  
Thread A added 39 at 01:52:20.2926  
Thread A added 24 at 01:52:20.2926  
Thread B added 0 at 01:52:20.2926  
cCancelling... 01:52:23.0229  
Cancelled 01:52:23.0229  
Thread A added 93 at 01:52:26.0542  
Thread B added 83 at 01:52:26.0542  

我所期望的是,要么取消行之后没有更多内容,要么执行速度更快,例如在 1:52:23.02*40*

(上面的结果是当程序直接从命令提示符运行时,当从Visual Studio中运行时,"间隙"较小,但仍然几乎为0,5秒)。

C# 优雅地取消线程 – 为什么这么长

这被称为线程竞赛,是线程代码中的经典错误。 您的程序还遇到了一个称为争用的问题。

争用 首先,您可以轻松地从程序产生输出的速率中看出 Write() 方法很难获得写锁。 这是因为您有 3 个线程在读取,并且它们每个线程在迭代循环时只在短的时间内释放读锁,最多是纳秒。 这还不足以让 Write() 方法在获取写锁方面有一个不错的机会,它需要 Windows 线程调度程序的帮助才能获得它。 这为阻塞时间过长(秒)的线程提供了优先级提升,以给他们一个机会。

这种争用几乎确保了写入线程在 EnterWriteLock() 调用中都停滞不前。 这是在_cancel检查之后。 他们最终将获取锁并再执行一次写入。

尝试使用 CancellationToken

您可以实现它来代替_cancel变量。

此外,请考虑使用 .网络的线程安全集合类。在这种情况下,不要使用 List<T>而是使用 ConcurrentBag<T> 类。