0

I would like to learn more about the exact behavior of a certain problem so That I can decide whether to use lock (with its Performance implications).

Given the following pseudo code:

class Thread1
{
    public decimal TotalValue {get; private set;}
    private decimal StockAmount;
    private decimal OldPrice;
    public async Task GetStockPrices(string fictionalAsset)
    {
        while (true)
        {
            decimal totalCopy = TotalValue;
            totalCopy -= (StockAmount * OldPrice);
            OldPrice = StockPrices.GetValue(fictionalAsset);
            totalCopy += (StockAmount * OldPrice);
            TotalValue = totalCopy;
        }
    }
}

Let's assume, that Thread1 is the only Thread ever modifying TotalValue. All other Threads (no matter their count) will only ever be reading from it.

For sure, it could happen that a reading Thread accesses TotalValue while TotalValue = totalCopy;.

What are the implications of it? Will the reading Thread "just" receive an old version of TotalValue (OK) or could there be another unwanted result (such as 0 or any other number - FATAL). Are there other implications such as performance. Or time for update on other threads? I would expect the above code to be more performant than

lock (TotalLock)
{
    TotalValue = totalCopy;
}

especially since reading threads could be many and very frequently, effectively locking up the value.

In case Locking is required how are the locks served? (I would imagine fifo or random) - can there be a priority assigned for the writing thread? (something which checks if the variable is locked and if so, wait)

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
julian bechtold
  • 1,875
  • 2
  • 19
  • 49
  • 1
    Don't know if exchanging a decimal is an atomar operation. If it is, others may know, you might not need to lock anything. If not you might rather look at a readerwriter lock like https://learn.microsoft.com/en-us/dotnet/api/system.threading.readerwriterlockslim?view=net-6.0 – Ralf Jun 30 '22 at 15:31
  • After some testing, it appears that using no lock is around 10 Times faster than with lock, around 4 times faster than ReaderWriterLockSlim (5 threads). So far I could only do simplified testing and do not know any side effects of exchanging a decimal. – julian bechtold Jun 30 '22 at 21:43
  • 2
    Related: [Reproduce torn reads of decimal in C#](https://stackoverflow.com/questions/23262513/reproduce-torn-reads-of-decimal-in-c-sharp) – Theodor Zoulias Jul 01 '22 at 00:20
  • 2
    A 128-bit value absolutely can be torn, the CLR and x86 processors only guarantee 64-bit atomicity. You could use `Interlocked.Exchange` instead of locking. You also would need that on the reader side. Even if you had guaranteed atomicity, the reader could get a *very* old value, such as the first value assigned: `0`. There is no necessity for the reader thread to ever update its cache unless there is some kind of locking/interlocking going on – Charlieface Jul 01 '22 at 00:39
  • 1
    @Charlieface I don't think that the `Interlocked.Exchange` has any overload that accepts a `decimal` as an argument. – Theodor Zoulias Jul 01 '22 at 03:02
  • Interlocked exchange indeed does not exist for decimal. Under the hood, Decimal appears to be a struct. One could wrap this decimal in a class and use interlocked exchange on this. volatile could be used to make sure that the value is beeing pushed to ram. – julian bechtold Jul 01 '22 at 06:06

1 Answers1

1

You could implement the TotalValue property in a lock-free fashion, by boxing the decimals in Tuple<T> wrappers like this:

private volatile Tuple<decimal> _totalValue = new(default);

public decimal TotalValue
{
    get => _totalValue.Item1;
    private set => _totalValue = new(value);
}

The volatile keyword ensures that the compiler will not optimize the code in a way that would not affect a single-thread program, but could cause some threads to lose visibility of the field in a multithreaded program.

My expectation is that this implementation should be slightly faster than using a lock, especially if the gets greatly outnumber the sets. Otherwise, if the property is updated more frequently than it is read, the cost of garbage-collecting a large number of short-lived Tuple<decimal> instances will probably negate all the benefits of avoiding the synchronization cost.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Garbage collection can be reduced by using a reference container which can be re-used. This (might) as well mitigate a thorn read as replacing a reference is atomic – julian bechtold Jul 01 '22 at 08:25
  • 1
    @julianbechtold I would like to see how you could make the container reusable, while maintaining thread-safety, using exclusively lock-free techniques. – Theodor Zoulias Jul 01 '22 at 08:51
  • @julian btw you don't have to make your own reusable container. There is the built-in [`StrongBox`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.strongbox-1) with a writable `Value` field. 1st problem: you have no way to know when all threads have completed reading the `Value` so that you can reassign it. 2nd problem: a thread that has read the `Value` and has cached it, might reuse the cached value instead of reading it afresh. To solve this you'd need to make the `Value` `volatile`, which you can't because it's a `decimal`, and we're back in square one. – Theodor Zoulias Jul 01 '22 at 09:10
  • I would introduce a ring type array of `box` and make sure it's size is large enough that other threads have enough time to read the old decimal from it before it's rewritten. Then replace the `TotalValue` reference by the currently written box from the ringarray. Lets say a data update comes every 100ms but reading often, might only require `StrongBox[3]`. A class might be more efficient than `StrongBox` due to boxing/unboxing. Tuple appears to be Value type, not reference type. – julian bechtold Jul 01 '22 at 09:20
  • 1
    @julian so you are talking about a single-writer system only, since otherwise you would need to synchronize the access to the ring-array. Fair enough, that's the system that you describe in your question anyway. Next question: how much time is *"enough time"*, that would guarantee the correctness of such an implementation? Is there any documentation about it? Or you'll just pick a number based on your gut feelings, and hope to be proved lucky? Btw the [`Tuple`](https://learn.microsoft.com/en-us/dotnet/api/system.tuple-1) is a class. – Theodor Zoulias Jul 01 '22 at 09:33
  • Yes I am Talking about a one write - many read system. I have not found Documentation of how long a read of a decimal actually takes, I'd take the approach of "good enough" (300ms should be way plenty. If it takes >300ms something is fundamentally wrong. Thank you for the hint with the Tuple reference type. Another stack question stated otherwise but Microsoft documentation now made it clear. Does StrongBox have baxing/unboxing implications? (checking documentation now) – julian bechtold Jul 01 '22 at 09:50
  • @julian *"300ms should be way plenty"* -- In other words 300ms should be way more than the maximum duration that any operation system (currently existing or future) that runs on any hardware (currently existing or future) can put a thread in sleep mode. That's a bold claim to make! Personally I would not be thrilled if I knew that a software that I use is based on such assumptions. – Theodor Zoulias Jul 01 '22 at 09:59
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/246093/discussion-between-julian-bechtold-and-theodor-zoulias). – julian bechtold Jul 01 '22 at 10:05