对数字进行原子等待操作

时间:2013-05-21 19:54:50

标签: c# multithreading .net-4.0 synchronization atomic

我有以下课程:

public class AtomicLong
{
    private long initial;
    private long value;

    public AtomicLong(long value = 0)
    {
        this.initial = value;
        this.value = value;
    }

    public class Handle : IDisposable
    {
        private AtomicLong source;
        private long amount;

        public Handle(AtomicLong source, long amount)
        {
            this.source = source;
            this.amount = amount;
        }

        public void Dispose()
        {
            if (source == null)
                return;
            Interlocked.Add(ref source.value, amount);
            source = null;
        }
    }

    public Handle Claim(long amount)
    {
        if (amount > initial)
            throw new ArgumentOutOfRangeException("amount", amount, "Must be no more than the initial amount.");
        if (amount < 0)
            throw new ArgumentOutOfRangeException("amount", amount, "Must be nonnegative.");
        while (true)
        {
            var oldValue = Interlocked.Read(ref value);
            var newValue = oldValue - amount;
            if (newValue >= 0 &&
                oldValue == Interlocked.CompareExchange(ref value, newValue, oldValue))
            {
                return new Handle(this, amount);
            }
        }
    }
}

这方面的一个示例用法是我可以有一个AtomicLong unusedMemory,表示一组工作人员可用的当前内存字节数。 (这并不意味着接近完全 - 这只是一个粗略的措施。)然后我在一堆不同的工作线程上做这个:

while (true)
{
    var unitOfWork = WaitForUnitOfWork();
    long requiredMemory = unitOfWork.RequiredMemory;
    using (var handle = unusedMemory.Claim(requiredMemory))
    {
        //wait until requireMemory can be claimed from unusedMemory
        //do work with reserved memory, represented by handle
        //when handle disposes, memory is released back to the unusedMemory
    }
}

我的AtomicLong课程的问题是对Claim的调用会忙 - 等到他们返回。我想通过使用某种操作系统级别的句柄抽象来解决这个问题。

你能建议我怎么做吗?


动机

考虑以下情况:

  • unusedMemory的初始值为10GB(10 << 30
  • 100个工作线程
  • 10个单位的工作,每个工作需要10GB和1分钟才能完成
  • 第一个工人打电话给Claim(10 << 30),它几​​乎立即返回
    • 它开始做1分钟后完成的工作
  • 其他9名工作人员对Claim(10 << 30)打了一个相同的电话,然后做了“坏”忙等待1分钟
    • 9个线程在while(true){/*do nothing*/}方法中执行类似Claim循环的操作!
    • 大量不必要的CPU使用率
  • 其余的工作人员(90)在WaitForUnitOfWork()方法中执行“良好”操作系统级别等待

重要的一点: Claim只有“请求的amount内存实际可用时才”便宜“。如果不是,则忙碌等待直到可用。

为了完全清楚,在Claim方法中,我指出了确切的表达方式(newValue >= 0):

while (true)
{
    var oldValue = Interlocked.Read(ref value);
    var newValue = oldValue - amount;
    if (newValue >= 0 && // <--------------------------- THIS IS THE PROBLEM
        oldValue == Interlocked.CompareExchange(ref value, newValue, oldValue))
    {
        return new Handle(this, amount);
    }
}

问题不在于Interlocked.CompareExchange是否会变得昂贵 - 我知道它很便宜。问题是如何处理当amount来电者希望Claim当前大于amount AtomicLong时出现的忙碌等待情况


如果你有一个完全不同的方法来解决这类问题,或者看到我已经拥有的一些缺陷,我也想听到这个!

2 个答案:

答案 0 :(得分:0)

您有几种选择。

例如,您可以通过将活动线程置于休眠状态一段时间来创建更智能的忙等待,因此它并不总是检查您的状况,而是定期检查它。

另一个解决方案是创建一个自定义事件并在活动线程中等待该事件,并且您可以定义一个自定义事件来完成我认为的任务。

您可以阅读有关事件here的更多信息。您可以阅读有关自定义事件创建here的信息。

答案 1 :(得分:0)

以下是我提出的解决方案:

参数

  • objMonitor
  • 使用的同步对象
  • pollIterval:在成功完成交易之前调用的速率
  • value:交易修改的值
  • precondition:在交易开始时必须为true的可选条件
  • transform:更改值的操作
  • postcondition:在交易结束时必须为true的可选条件

代码

public static class AtomicHelper
{
    public static void LongTransaction(
        object obj,
        TimeSpan pollInterval,
        ref long value,
        Func<long, bool> precondition,
        Func<long, long> transform,
        Func<long, bool> postcondition)
    {
        while (true)
        {
            var oldValue = Interlocked.Read(ref value);
            if (precondition != null && !precondition(oldValue))
            {
                Monitor.Wait(obj, pollInterval);
                continue;
            }
            var newValue = transform(oldValue);
            if (postcondition != null && !postcondition(newValue))
            {
                Monitor.Wait(obj, pollInterval);
                continue;
            }
            if (Interlocked.CompareExchange(ref value, newValue, oldValue) == oldValue)
            {
                Monitor.PulseAll(obj);
                return;
            }
        }
    }
}

使用示例

long n = 10;
object obj = new object();
//On many different threads, run this concurrently:
AtomicHelper.LongTransaction(
    obj,
    TimeSpan.FromSeconds(1),
    ref n,
    null,
    x => x - 1,
    x => x >= 0);
Thread.Sleep(TimeSpan.FromSeconds(3));
AtomicHelper.LongTransaction(
    obj,
    TimeSpan.Zero,
    ref n,
    null,
    x => x + 1,
    null);