【问题标题】:C# multi-threaded unsigned incrementC# 多线程无符号增量
【发布时间】:2010-10-30 09:29:25
【问题描述】:

我想从多个线程中增加一个无符号整数。

我知道 Interlocked.Increment,但它不处理无符号整数。我可以使用 lock(),但出于性能原因,我宁愿不使用。

仅以正常方式递增它是线程安全的吗?偶尔的增量是否丢失也没关系,因为它仅用于统计。我不想要的是被破坏的价值。

【问题讨论】:

  • 这可以用 ILEmit 来创建 Interlocked 类的任何方法,使用任何 32 位或 64 位的 blittable 类型工作,有一个答案描述了 SO...

标签: c# multithreading unsigned interlocked-increment


【解决方案1】:

您说出于性能原因不想使用lock - 但您测试过吗?无争议的锁(听上去很可能是这样)非常便宜。

在线程方面(一般来说,尤其是线程方面),我通常会选择“明显正确”而不是“聪明且性能可能更好”。

对您的应用进行是否有锁定的基准测试,看看您是否能注意到差异。如果锁定产生了显着的不同,那么当然,使用狡猾的东西。否则,我只会坚持使用锁。

可能想要做的一件事是将Interlocked.Incrementint 一起使用,并在必要时将其转换为uint,如下所示:

using System;
using System.Reflection;
using System.Threading;

public class Test
{
    private static int count = int.MaxValue-1;

    public static uint IncrementCount()
    {
        int newValue = Interlocked.Increment(ref count);
        return unchecked((uint) newValue);
    }

    public static void Main()
    {
        Console.WriteLine(IncrementCount());
        Console.WriteLine(IncrementCount());
        Console.WriteLine(IncrementCount());
    }

}

输出:

2147483647
2147483648
2147483649

(换句话说,它可以毫无问题地包装。)

【讨论】:

  • 我同意为什么要把事情复杂化,除非性能确实成为问题。我无法想象用于增加单个值的锁会保留很长时间或使用大量资源。
  • 演员不是(危险地)假设“未经检查”的上下文吗?我们希望将 2 的补码有符号 int 的位重新解释为 uint,而不进行范围检查。为此,我们不应该明确地做 return unchecked((uint)newValue); 吗? stackoverflow.com/a/1131851 顺便说一句,我在 C# 规范中看不到任何明确提及 2 的补码,但我认为我们可以依赖它。
  • @MaxBarraclough:是的,最好明确地取消选中。会做出这样的改变。是的,您可以依赖 2 的补码,尽管它现在不在规范中。我们希望将它包含在以后的修订版中:)
【解决方案2】:

如果您确实需要完整范围的无符号整数 (2^32 - 1) 而不是有符号整数 (2^31 -1),您可以转换为 int64(有一个 Interlocked.Increment 重载需要int64),然后转换回无符号整数。

【讨论】:

  • 请注意,Interlocked.Increment(ref long) 实际上只是在 64 位操作系统下是原子的(参见文档)
  • 出于一般安全考虑,我个人会使用常规的 int ...和 4G 测量。
  • @Jerryjvl - 让我想起了在 8 位系统中进行 16 位计算的日子。
  • 事实上,正如我的回答中所述,没有范围缩小。
  • @jerryjvl 在documentation 中没有提到Interlocked.Increment(Int64) 在 32 位操作系统上不是原子的。
【解决方案3】:

pre-kidney's answer 的基础上,您可以创建自己的助手类。由于增量在二进制级别上的工作方式相同,因此您可以在使用 Unsafe 类进行增量之前将类型从无符号更改为有符号:

using System.Runtime.CompilerServices;
using System.Threading;

public static class InterlockedEx
{
    /// <summary>
    /// unsigned equivalent of <see cref="Interlocked.Increment(ref Int32)"/>
    /// </summary>
    public static uint Increment(ref uint location)
    {
        int incrementedSigned = Interlocked.Increment(ref Unsafe.As<uint, int>(ref location));
        return Unsafe.As<int, uint>(ref incrementedSigned);
    }

    /// <summary>
    /// unsigned equivalent of <see cref="Interlocked.Increment(ref Int64)"/>
    /// </summary>
    public static ulong Increment(ref ulong location)
    {
        long incrementedSigned = Interlocked.Increment(ref Unsafe.As<ulong, long>(ref location));
        return Unsafe.As<long, ulong>(ref incrementedSigned);
    }
}

【讨论】:

  • 应该是“public static uint Increment(ref uint location)”吗?
【解决方案4】:

从 .NET 5.0 开始,Interlocked.Increment 重载无符号和有符号 int32int64

https://docs.microsoft.com/en-us/dotnet/api/system.threading.interlocked.increment?view=net-5.0

【讨论】:

    【解决方案5】:

    在使用有符号整数的二进制补码表示的系统上("virtually all",根据维基百科)递增无符号整数与递增使用相同位集表示的有符号整数具有相同的效果。因此,可以在无符号整数上使用 InterlockedIncrement 而不牺牲任何东西。

    例如,对于 3 位,我们有下表:

    raw bits | unsigned integer | twos complement signed integer
    ------------------------------------------------------------
    000      |                0 |                             0 
    001      |                1 |                             1 
    010      |                2 |                             2 
    011      |                3 |                             3 
    100      |                4 |                            -4 
    101      |                5 |                            -3
    110      |                6 |                            -2
    111      |                7 |                            -1
    

    在这两种情况下,递增一(并考虑溢出)相当于在表中向下移动一个条目。请注意,这不适用于补码算术,因为负数的排列顺序相反。

    【讨论】:

    • “因此,可以在无符号整数上使用 InterlockedIncrement 而不牺牲任何东西。” - 不,你不能因为 Interlocked.Increment() 采用 Int32 或 Int64 - 两者都已签名。请参阅 9 年前 Jon Skeet 的回答......只是因为某些事情应该是可能的,但事实并非如此。我们知道 2 的补码是如何工作的。您的回答没有解决问题。
    • 这个答案的重点是可以在不丢失信息的情况下执行必要的转换。
    • 你的意思是像 9 年前 jon skeet 演示的那样?我看不到你的回答增加了什么。
    • 他的回答是建议不要解决提问者提出的问题,并且他提到环绕作为最后的旁注很好。我的回答解释了实现这一点的条件,以及为什么会这样。
    • jon skeets 回答状态:“(换句话说,它包装没有问题。)”
    【解决方案6】:

    您可以将 uint 声明为 volatile。

    http://msdn.microsoft.com/en-us/library/x13ttww7(VS.71).aspx

    【讨论】:

    • 这只确保对变量的任何更改都立即写回内存,而不是等到线程屈服或寄存器中的值被替换。它不保护对字段的访问,即两个(或更多)线程可以读取相同的值,递增一次,然后写回,有效地将值增加 1。
    • 请阅读上述要求以及msdn.microsoft.com/en-us/library/7a2f3ay4.aspx 的示例。对于给定的情况,这就足够了。
    • 与 Interlocked.Increment 相比,volatile 的性能如何?相同的?更好?
    • @jrista:也许不是最明确的基准测试,但在我的机器上,在 8 个 CPU 内核上启动 8 个任务,在一个紧密的循环中递增计数器,循环中的 Thread.Sleep(0) 导致 @ 的性能相同987654324@ 与 Interlocked.Increment。使用 lock 会慢几倍。
    • 这个答案在我看来是错误的。 volatile 在分配时会起作用。但不是为了增加。递增需要两个操作。 volatile 不会阻止其他线程插入两个操作之间。为了安全起见,您要么需要一个原子增量(获取和添加),如 Interlocked.Increment(),要么需要同步访问与锁定。
    猜你喜欢
    • 1970-01-01
    • 2010-10-15
    • 2011-11-02
    • 1970-01-01
    • 2013-03-31
    • 1970-01-01
    • 2018-06-18
    • 2022-08-17
    • 2018-10-28
    相关资源
    最近更新 更多