20

I want to increment an unsigned integer from multiple threads.

I know about Interlocked.Increment, but it does not handle unsigned integers. I could use lock(), but I would rather not if possible for performance reasons.

Is it thread safe just to increment it in the normal way? It would not matter if the occasional increment got lost, as it's only used for statistics. What I don't want is the value to get corrupted.

1
  • This can be done with ILEmit to create any method of the Interlocked class work with any blittable type of 32 or 64 bits, there is an answer describing that somewhere on SO... Commented Feb 19, 2019 at 12:01

6 Answers 6

45

You say you don't want to use lock for performance reasons - but have you tested it? An uncontested lock (which this is likely to be, by the sounds of it) is pretty cheap.

I generally go for "obviously correct" rather than "clever and possibly better performing" when it comes to threading (and in general, but especially for threading).

Benchmark your app with and without locking, and see whether you can even notice the difference. If locking makes a significant difference then sure, use cunning stuff. Otherwise, I'd just stick with a lock.

One thing you might want to do is use Interlocked.Increment with an int and just cast it when necessary to get a uint, like this:

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());
    }

}

Output:

2147483647
2147483648
2147483649

(In other words it wraps with no problems.)

Sign up to request clarification or add additional context in comments.

3 Comments

I agree why complicate things unless performance actually becomes an issue. I can't imagine a lock for incrementing a single value is going to remain for very long or use much resources.
Doesn't the cast (dangerously) assume an 'unchecked' context? We want to reinterpret the bits of the 2's complement signed int, as a uint, without range checks. For this shouldn't we explicitly do return unchecked((uint)newValue); ? stackoverflow.com/a/1131851 Incidentally, I can't see any explicit mention of 2's complement in the C# spec, but I presume we can rely on it.
@MaxBarraclough: Yes, it would be better to be explicitly unchecked. Will make that change. And yes, you can rely on 2's complement although it's not in the spec right now. We want to include it in a later revision :)
11

If you really need the full range of an unsigned int (2^32 - 1) rather than a signed int (2^31 -1), you could cast to an int64 (there is an Interlocked.Increment overload that takes int64) and then cast back to an unsigned int.

5 Comments

Note though that Interlocked.Increment(ref long) is only actually atomic under a 64-bit OS (see documentation)
Personally I'd make do with the regular int for general safety... if 31 bits is not enough, then I doubt 32 will actually work much better, unless the problem is specifically restricted exactly between 2G and 4G measurements.
@Jerryjvl - Reminds me of the days of 16bit calculations in 8bit systems.
In fact there is no range reduction, as described in my answer.
@jerryjvl there is no mention in the documentation about Interlocked.Increment(Int64) not being atomic on a 32-bit OS.
4

Building on pre-kidney's answer, you can create your own helper class. Since the increment will work in the same way on a binary level, you can just change the type from unsigned to signed before incrementing with the Unsafe class:

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);
    }
}

1 Comment

Shouldnt it be "public static uint Increment(ref uint location)"?
3

Since .NET 5.0 there are Interlocked.Increment overloads for both unsigned and signed int32 and int64.

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

Comments

-1

On systems using a twos complement representation of signed integers ("virtually all", according to wikipedia) incrementing an unsigned integer has the same effect as incrementing a signed integer represented using the same set of bits. Thus, one can use InterlockedIncrement on unsigned integers without sacrificing anything.

For example, with 3 bits we have the following table:

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

Incrementing by one (and taking overflow into account) is equivalent to moving down one entry in the table, in both cases. Note that this doesn't work for ones complement arithmetic, since the negatives are arranged in the opposite order.

6 Comments

"Thus, one can use InterlockedIncrement on unsigned integers without sacrificing anything." - no you can't because Interlocked.Increment() takes either Int32 or Int64- both are signed. See Jon Skeet's answer from 9 years ago....Just because something should be possible does not make it so. We know how 2's complement works. Your answer does not address the question.
The point of this answer is that one can perform the necessary casts without loss of information.
you mean like was demonstrated by jon skeet 9 years ago? I fail to see what your answer adds.
His answer is a recommendation against solving the problem asked by the questioner, and he mentions that wraparound is fine as a side remark at the end. My answer explains what the conditions are to make this true, and why it is so.
jon skeets answer states : "(In other words it wraps with no problems.)"
|
-15

you can declare the uint as volatile.

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

6 Comments

That only makes sure that any changes to the variable are written back to memory immediately, instead of waiting until either the thread yields or the value is displaced in a register. It does not protect access to the field, i.e. two (or more) threads may read the same value, increment once, and write back, effectively increasing the value by 1.
Please read the requirements above and also the example at msdn.microsoft.com/en-us/library/7a2f3ay4.aspx. For the given case it is sufficient.
How performant is volatile compared to Interlocked.Increment? Same? Better?
@jrista: Perhaps not the most definitive benchmark but on my machine starting 8 tasks on 8 CPU cores incrementing the counter in a tight loop with a Thread.Sleep(0) in the loop resulted in the same performance for volatile versus Interlocked.Increment. Using lock was several times slower.
This answer looks wrong to me. volatile would work when assigning. But not for incrementing. Incrementing requires two operations. volatile won't prevent other threads from interposing between the two operations. For this to be safe, you either need an atomic increment (fetch-and-add), like with Interlocked.Increment(), or you need to synchronize access with locking.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.