Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Thread-safe generic field

I have a generic field and a property that encapsulates it:

T item;

public T Item
{
    get { return item; }
    set { item = value; }
}

The problem is that this property can be written to from one thread and read from multiple threads at the same time. And if T is a struct, or long, readers might get results that are part old value and part new value. How can I prevent that?

I tried using volatile, but that's not possible:

A volatile field cannot be of the type 'T'.

Since this is a simpler case of code I've already written, which uses ConcurrentQueue<T>, I thought about using it here too:

ConcurrentQueue<T> item;

public T Item
{
    get
    {
        T result;
        item.TryPeek(out result);
        return item;
    }

    set
    {
        item.TryEnqueue(value);
        T ignored;
        item.TryDequeue(out ignored);
    }
}

This would work, but it seems to me that it's overcomplicated solution to something that should be simple.

Performance is important, so, if possible, locking should be avoided.

If a set happens at the same time as get, I don't care whether get returns the old value or the new value.

like image 782
svick Avatar asked Oct 07 '22 04:10

svick


1 Answers

It completely depends on the type, T.

If you are able to place a class constraint on T then you don't need to do anything in this particular case. Reference assignments are atomic. This means that you can't have a partial or corrupted write to the underlying variable.

Same thing goes for reads. You won't be able to read a reference that is partially written.

If T is a struct, then only the following structures can be read/assigned atomically (according to section 12.5 of the C# specification, emphasis mine, also justifies the above statement):

Reads and writes of the following data types shall be atomic: bool, char, byte, sbyte, short, ushort, uint, int, float, and reference types. In addition, reads and writes of enum types with an underlying type in the previous list shall also be atomic. Reads and writes of other types, including long, ulong, double, and decimal, as well as user-defined types, need not be atomic. Aside from the library functions designed for that purpose, there is no guarantee of atomic read-modify-write, such as in the case of increment or decrement.

So if all you're doing is trying to read/write, and you meet one of the conditions above, then you don't have to do anything (but it means that you also have to place a constraint on the type T).

If you can't guarantee the constraint on T, then you'll have to resort to something like the lock statement to synchronize access (for reads and writes as mentioned before).

If you find that using the lock statement (really, the Monitor class) degrades performance, then you can use the SpinLock structure, as it's meant to help in places where Monitor is too heavy:

T item;

SpinLock sl = new SpinLock();

public T Item
{
    get 
    { 
        bool lockTaken = false;

        try
        {
            sl.Enter(ref lockTaken);
            return item; 
        }
        finally
        {
            if (lockTaken) sl.Exit();
        }
    }
    set 
    {
        bool lockTaken = false;

        try
        {
            sl.Enter(ref lockTaken);
            item = value;
        }
        finally
        {
            if (lockTaken) sl.Exit();
        }
    }
}

However, be careful, as the performance of SpinLock can degrade and will be the same as the Monitor class if the wait is too long; of course, given that you are using simple assignments/reads, it shouldn't take too long (unless you are using a structure which is just massive in size, due to copy semantics).

Of course, you should test this yourself for the situations that you predict that this class will be used and see which approach is best for you (lock or the SpinLock structure).

like image 52
casperOne Avatar answered Oct 28 '22 18:10

casperOne