0

The question is specifically about when it is safe to invoke ref with a member that is expected to only be read or written while under a monitor lock.

In the example below, the class fields are ONLY expected to be checked and set while under the lock. The private implementations DO ONLY access the values under the lock, BUT, they in fact take in the member as ref, and then lock the lock and do the work on that given ref. The public Get and TrySet methods in fact invoke the private methods by passing the requested member field by ref, and they do NOT lock the lock there at the call site --- and the question was is that actually safe.

It SHOULD be safe, because: though the public methods reference the member field by ref without the lock; at this call site, that ref IS going to ONLY be the pointer; and the actual member value will NOT be dereferenced until in the private method under the required lock.

It would not be safe if:

  1. If the public method passing the ref actually reads the value. If so then the private method would receive the value in the argument, and act on that value instead of the current field value; which could have been changed by another thread (and then the logic is broken: the field then is NOT being read under the lock where it is compared, and that has a now stale value).
  2. Or if the pointer moved between the public call site and the private method; but I am pretty sure that the CLR ensures no such thing is possible.

Please note that I understand that STILL the returned object ITSELF is not safe: my question is only IN the actual dereferencing of the REF memory.

I have read the spec under 5.1.5; and also looked at the generated IL code; and I believe it is safe.

Here is the example: are the public methods thread safe?

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;


namespace Test
{
    public class TestRef
    {
        private readonly object syncLock = new object();
        private ulong eventCounter;
        private object objectValue;
        private long longValue = 1L;


        private T getValue<T>(ref T member)
        {
            lock (syncLock) {
                return member;
            }
        }

        private bool trySetValue<T>(ref T member, T value)
        {
            lock (syncLock) {
                if (object.Equals(member, value))
                    return false;
                member = value;
                ++eventCounter;
            }
            SomeValueChanged?.Invoke(this, EventArgs.Empty);
            return true;
        }


        public object GetObjectValue()
            => getValue(ref objectValue);

        public long GetLongValue()
            => getValue(ref longValue);

        public bool TrySetObjectValue(object value)
            => trySetValue(ref objectValue, value);

        public bool TrySetLongValue(long value)
            => trySetValue(ref longValue, value);


        public ulong EventCounter
        {
            get {
                lock (syncLock) {
                    return eventCounter;
                }
            }
        }

        public event EventHandler SomeValueChanged;
    }


    public static class Program
    {
        public static async Task<bool> ExampleTest(int taskCount)
        {
            TestRef testRef = new TestRef(); // longValue is 1L
            List<Task> tasks = new List<Task>(taskCount);
            for (int i = 0; i < taskCount; ++i) {
                tasks.Add(Task.Run(Callback)); // All Tasks will try set 2L
            }
            await Task.WhenAll(tasks);
            bool success = testRef.EventCounter == 1UL;
            Console.WriteLine(
                    $@"Ran {taskCount} Tasks: Raised event count: {testRef.EventCounter} (success: {success}).");
            return success;
            async Task Callback()
            {
                await Task.Delay(taskCount); // Cheaply try to pile Tasks on top of each other
                testRef.TrySetLongValue(2L);
            }

            // If not safe, then it is possible for MORE THAN ONE
            // Task to raise the event: i.e. two may
            // begin and the public method could read the
            // current value outside the lock, and both
            // would read 1L; and then BOTH would compare
            // the argument in the private method AS 1L
            // and both would then set the value and raise the event.
            // If safe, then only the first Task in would change
            // the value
        }

        public static void Main(string[] args)
        {
            int defaultTaskCount = Environment.ProcessorCount * 500;
            Console.WriteLine($@"Hello World.");
            Console.WriteLine(
                    $@"Specify how many parallel Tasks to run against {Environment.ProcessorCount} instances (each):");
            Console.WriteLine(
                    $@"--- The default will be {
                                defaultTaskCount
                            } Tasks against each instance [just type enter for the default]:");
            if (!int.TryParse(Console.ReadLine(), NumberStyles.Any, CultureInfo.CurrentCulture, out int taskCount))
                taskCount = defaultTaskCount;
            Console.WriteLine($@"Will Run {taskCount} Tasks against {Environment.ProcessorCount} instances (each) ...");
            List<Task<bool>> tasks = new List<Task<bool>>(Environment.ProcessorCount);
            for (int i = 0; i < Environment.ProcessorCount; ++i) {
                tasks.Add(Program.ExampleTest(taskCount));
            }
            Task.WhenAll(tasks)
                    .Wait();
            bool success = tasks.All(task => task.Result);
            Console.WriteLine($@"Success = {success}.");
            Console.WriteLine($@"Type a key to exit ...");
            Console.ReadKey();
        }
    }
}

The public methods do not lock the lock, and pass the member by reference; and the private methods lock the lock before reading and writing.

I am under the assumption that it IS safe: the passed reference is ONLY the pointer at the call site; and the private method bodies actually dereference the pointer; under the lock there.

In the generated IL code, IT APPEARS to be safe: only the pointer is passed, and that is not dereferenced until under the lock in the private method.

The spec DOES say that "Within a function member or anonymous function, a reference parameter is considered initially assigned." --- But it says "CONSIDERED initially assigned" ... which might add more question, but leads me to think that the pointer is not deferenced until it is used, and so the above IS always safe.

Steven Coco
  • 542
  • 6
  • 16
  • All the "considered assigned" means is that the rules of c# require that the refd variable be assigned a value before the call, so the callee can know that it has a value. Its just a compile time bug checker. – Eric Lippert Feb 25 '19 at 03:29
  • If you have a collection of Test object, enumeration also is not considered threadsafe. while enumerating, someone might change the collection. Its good to use ICollection.SyncRoot or Array.SyncRoot. – Gauravsa Feb 25 '19 at 05:14
  • @EricLippert I understand: and I THINK that MEANS that it is always safe to get the `ref` pointer at any time, and you must only be sure you dereference it under your lock ... So I do think that all is safe. I cannot make the test program fail ... – Steven Coco Feb 26 '19 at 01:33
  • I think the answer to your question is a ```ref``` gets dereferenced every time it is used, not just once. – Jerry Feb 26 '19 at 02:04
  • 1
    `private T getValue(ref T member) { lock (syncLock) { return member; } }` Why would I need a `lock` there? – mjwills Feb 26 '19 at 02:43
  • I am utterly baffled by the thinking behind this question. The `lock` keyword doesn't lock variables, referenced or otherwise. All it does is prevent two threads from executing the same block of code at the same time, and puts up a [memory fence](https://stackoverflow.com/questions/2844452/memory-barrier-by-lock-statement) at the beginning and ending of the `lock` block. Any code outside of a `lock` can update the referenced variable at any time. You need to make the variables private and only allow access via properties that agree on the locking mechanism. – John Wu Feb 26 '19 at 02:55
  • @Jerry OK, yes: I understand what you just said there; and that should in fact answer the question. The synchronization only needs to be around the reading and writing of the member; and passing `ref` will only pass the pointer until it is actually used. So it's OK. --- You can pass a `ref` anywhere, any time, and only need to be concerned with the value when you read or write it. – Steven Coco Feb 26 '19 at 22:48

2 Answers2

1

Since you are Getting and Setting the value from outside your class this will not ensure thread safety at all. And with an object the reference is getting changed not the value, which means that each thread that sets the value back to your Test class will be changing the reference. The thread that retrieved the reference prior to this will be working on the original reference, not the modified one.

With the code below you can see not only are the numbers out of order, but Seven and Ten are missing because the threads are all in a race condition and your lock isn't blocking any of the logic.

var test = new Test();
test.SetValue("");

var newStrings = new string[] { "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten" };

newStrings.AsParallel()
    .ForAll(newString =>
    {
        var value = test.GetValue() as string;
        value += $" {newString}";
        test.SetValue(value);
    });

// Eight, Six, Three, Four, One, Two, Five, Nine
var result = test.GetValue() as string;

If you want thread safety you need to be locking the value while your threads are changing it.

With the improved code below, you will notice that now you have all the words added, but they are still out of order.

When dealing with thread safety you really need to step back and look at the whole picture as not one place with a couple locks is going to magically make an entire class or application thread safe.

public class Test<T> where T : class
{
    private readonly object syncLock = new object();
    private T _value;

    public delegate void MyAction(ref T value);

    public void EditObject(MyAction action)
    {
        lock (syncLock)
        {
            action(ref _value);
        }
    }

    public T GetValue()
    {
        lock (syncLock)
        {
            return _value;
        }
    }
}

var test = new Test<string>();

var newStrings = new string[] { "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten" };

newStrings.AsParallel()
    .ForAll(newString =>
    {
        test.EditObject((ref string o) => o += $" {newString}");
    });

// Three Four Five Six One Two Ten Nine Eight Seven
var result = test.GetValue() as string;

Jerry
  • 1,477
  • 7
  • 14
  • 1
    `The threads that retrieved the object prior to this will be working on the original object, not the modified one.` This is correct, but potentially misleading. I would continue to speak in terms of references, not objects. This would be much more accurate if it read `The thread that retrieved the reference prior to this will be working on the original reference, not the modified one.` – Patrick Tucci Feb 26 '19 at 01:30
  • @Jerry: I "plussed" your answer because it IS correct; but my question is not fully answered. I edited my question for clarity ... – Steven Coco Feb 26 '19 at 01:31
  • 1
    @PatrickTucci I agree and have updated my answer. I could rewrite my answers over and over and would never be satisfied with the wording... – Jerry Feb 26 '19 at 01:37
0

I wrote this little sample program to demonstrate what I mean about ref being dereferenced every time it is used. I hope this helps.

As far as thread safety is concerned, when ref gets dereferenced has little effect. Your code needs to ensure that no two threads are modifying the value at the same time, and that no threads are reading the value until the writing threads have completed.

Your new code is much improved. But remember, it is the locks that are making your code thread safe, not when ref is dereferenced.

class Program
{
    static void Main(string[] args)
    {
        var a = new Test { Name = "First" };

        ref Test b = ref a;
        ref Test c = ref a;

        Console.WriteLine(b.Name); // dereferences b, prints "Hello World!"
        Console.WriteLine(c.Name); // dereferences c, prints "Hello World!"

        b = new Test { Name = "Goodbye :(" }; // change the target of ref b to a new object

        // dereference c again, points to the new object
        // prints "Goodbye :("
        Console.Write(c.Name); 
    }
}

public class Test
{
    public string Name { get; set; }
}
Jerry
  • 1,477
  • 7
  • 14