Interlocked.Exchange有多安全?

3
作为一个线程初始者,我正在尝试找到一种不需要锁定对象的方法来将任务加入线程池,以使其最大并行度为1。
这段代码能实现我的想法吗?
private int status;
private const int Idle = 0;
private const int Busy = 1;

private void Schedule()
{
    // only schedule if we idle
    // we become busy and get the old value to compare with
    // in an atomic way (?)
    if (Interlocked.Exchange(ref status, Busy) == Idle)
    {
        ThreadPool.QueueUserWorkItem(Run);
    }
}

也就是说,以线程安全的方式在状态为 Idle 时将 Run 方法加入队列。

根据我的测试结果,这个方法似乎可以正常工作,但由于这不是我的专业领域,我不能确定。


1
假设Run()以线程安全的方式知道要选择哪个作业,那么这是可以的。你需要一个真正的锁来实现这一点。将其设置回空闲状态是更关键的操作,因为即使线程被中止,你也需要绝对确定它发生了,并且你可能希望立即启动另一个等待的作业。 - Hans Passant
谢谢,那我就走对了,run方法从ConcurrentQueue<T>中获取任务。 - Roger Johansson
1个回答

1

是的,这将完成你想要的功能。它永远不会让您在实际状态为繁忙时得到空闲的返回值,并且它将在同一操作中将状态设置为繁忙,没有冲突的机会。到目前为止还不错。

然而,如果您后来使用了ConcurrentQueue<T>,为什么要这样做呢?为什么要使用线程池重复排队运行,而不是只有一个线程不断使用TryDequeue从并发队列中取数据?

事实上,有一个专门为此设计的生产者-消费者集合,即BlockingCollection<T>。您的消费线程将只调用Take(如有必要,使用取消令牌 - 可能是个好主意),如果有值,则返回该值,就像ConcurrentQueue<T>一样;或者如果没有可用的值,则阻止线程直到有东西可供取。当其他某个线程向集合添加项目时,它将通知所有具有数据的消费者(在您的情况下,不需要任何复杂的东西,因为您只有一个消费者)。

这意味着您只需要处理一个线程的启动和停止,该线程将运行一个“无限”循环,调用 col.Take,而生产者则调用 col.Add
当然,这假定您已经有了 .NET 4.0+,但是再次说一遍,您可能已经有了,因为您正在使用 ConcurrentQueue<T>

我将任务加入线程池的原因是,这个线程池位于我的Actor库的邮箱类中,因此可能会有成千上万个邮箱同时处理消息,而且由于很多邮箱大部分时间都处于空闲状态,所以我使用线程池来调度那些实际上正在活动的邮箱。 - Roger Johansson
这就是我不使用BlockingCollection的原因,因为它是阻塞的。在我的情况下,我不能浪费线程资源。 - Roger Johansson
1
@RogerAlsing:我没有亲自尝试过,但如果使用未绑定容量的构造函数,它不应该阻止添加操作。在这种情况下,它应该作为ConcurrentQueue<T>的相对简单的包装器工作,后者本质上支持通知。总的来说,它不应该使用锁定(而是并发队列和阻塞集合都依赖于使用Interlocked的原子操作,就像您所做的那样),并且它不应该阻塞写入,只阻塞读取。但是,最终所有问题都归结为性能分析 :) - Luaan
@Luaan:你是正确的:一个无限制的BlockingCollection不会阻塞。 - ckuri

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