one.world – A Follow-up Exchange on C#


Here is a follow-up exchange between Robert Grimm from UW and Scott Wiltamuth from Microsoft. First, the questions by Robert Grimm:

About the out/ref parameters: I understand that method invocations on the passed in reference for refs are not thread-safe. But, is the reference thread-safe? In other words, is the content of the reference copied when entering the method and copied back when the method exits? Or, can another thread asign a different object to the reference while the method is executing and thus change the object seen by the method? Similarly, when does the out reference become visible outside the method? I'm asking b/c in my experience with Modula-3 in the SPIN extensible operating system, both caller and callee had access to the same variable and one thread could thus change the object during method invocation. This made it harder to write code that is guaranteed to be thread-safe. The alternative to call-by-reference described in the literatur is call-by-value/result which copies the address on method entry and exit and avoids any concurrency issues.

On inner classes: I don't think they are strictly necessary. As you said, you can easily simulate them, which is just fine. However, interfaces in C# do not allow any nested class definitions which we think is an actual limitation. Consider an interface that needs a container class. Defining this container class as a nested class just makes the code more compact. Even more importantly, being able to define a nested enumeration can define constants used by the interface.

Here is Scott's response:

Regarding out and ref, what you are really asking if there is true aliasing, or whether the semantics are "copy" semantics -- copy in/out for "ref", and copy-out for "out". The answer to this is that true aliasing is supported. E.g., this program:

  using System;

  class Test
  {
   static int x = 1;
   static int y = 2;
   
   static void Swap(ref int a, ref int b) {
    Console.WriteLine("a={0}, b={1}, x={2}, y={3}", a, b, x, y);
    int t = a;
    a = b;
    b = t;
    Console.WriteLine("a={0}, b={1}, x={2}, y={3}", a, b, x, y);
   }

   static void Main ()
   {
    Swap(ref x, ref y);
   }
  }

outputs

    Pre: a=1, b=2, x=1, y=2
    Post: a=2, b=1, x=2, y=1

Because true aliasing is used, the effects of the assignments in Swap are immediate. If copy in/out had been used, then the results would have been:

    Pre: a=1, b=2, x=1, y=2
    Post: a=2, b=1, x=1, y=2

It seems to me that using copy semantics would not help for the multi-threading situation you describe. As long as you have two threads writing to the same memory location, you have a problem. Whether procedure calls use copy in/out semantics or reference semantics just changes what kind of confusing results are possible, not whether confusing results are possible.

For example, assume that two different threads are executing a procedure call Incr(ref A.i) where i is a static int variable on class A with initial value 0, and Incr is defined as:

        void Incr(ref int var) {
            var = var + 1;
        }

in order for the program to do what you would expect in all cases, there has to be synchronization. Since two Incr operations are performed, the correctly synchronized result is that A.i has the value 2.

If there is no synchronization then you can get confusing results with either copy in/out or aliasing semantics. E.g., consider the copy in/out case, in which the Incr call and execution is equivalent to:

    (a) int var = A.i;   // copy-in
    (b) var = var + 1;   // increment 
    (c) A.i = var;       // copy out          

There are a number of possible orderings of these, depending on what thread executes first. Any ordering of (1a), (1b), (1c), (1d), (2a), (2b), (2c), (2d), will do so long as (1a) precedes (1b) which precedes 1(c) and the same for the thread the thread2 operations -- (2a), (2b), (2c) operations.

Assume that copy semantics are used, and consider the ordering (1a), (2a), (2b), (2c), (1b), (1c):

    (1a) int var = A.i;   // copy-in: var = 0 in thread 1
    (2a) int var = A.i;   // copy-in: var = 0 in thread 2
    (2b) var = var + 1;   // increment: var = 1 in thread 2 
    (2c) A.i = var;       // copy out: A.i = 1          
    (1b) var = var + 1;   // increment: var = 1 in thread 1 
    (1c) A.i = var;       // copy out: A.i = 1

In this case, the result is 1 rather than the expected 2. Thread2's Incr didn't get to have any effect because it was overwritten by Thread1. It's unlikely that this is what the developer intended. I don't mean this as a slam on copy in/out. There are similar orderings that produce unexpected results for the aliasing case too. The key is that if two threads are going to use the same memory location, then synchronization is required.

Your suggestion of allowing nested types in interfaces is interesting suggestion. We'll consider it.