Are variables thread safe in sclang?

Hi everyone!
Simple question regarding sclang variables: is their access between Threads and Routines guaranteed to be thread safe?

For example, is it always guaranteed here that the variable a will be accessed without any data races by the different Routines that perform the check and increment on it?

(
a = 0;

//One "main" thread
fork {
	//10 parallel threads within it
	10.do {
		fork {
            //Concurrent access and increment to 'a' between threads
			while({a < 100}, {
				a = a + 1;
			});
		}
	}
};
)

There is only one real thread in supercollider. So a supercollider thread is active until there is a wait call somewhere. In your example, the first thread will fully unroll the loop, the second thread will not start unless the first thread is done executing (because there is no wait call). When the second thread start, the while condition will be false so it will stop immediately. Same for third and all remaining threads.

This is not a problem in general because there is always wait time between the musical events. The problem arise when processing time is greater than the wait time between two events

1 Like

Yes, because sclang’s scheduler is non-preempting and single threaded. In your example, the first forked thread will always increment a to 100 before yielding to the others. So, the threads are neither parallel nor concurrent. For concurrent execution of threads you need to yield manually (with yield).

EDIT: adding to the answer above, wait is the same thing as yield within a routine. also, you cannot yield from the main thread.

and a point of clarification, there is only one real thread in the sclang virtual machine. sclang itself runs several OS-level threads for things like executing callbacks for input events.

I see, thanks!

So, even if I waited between threads (making the execution concurrent), thread safety and safe access would still be guaranteed, as only the one sclang’s scheduler thread would be running anyway at any time, correct?

Yes.

Thread un-safety is a matter of thread A being interrupted by another thread in a way that is not under your control.

In SC, when thread A has control, it keeps control until it explicitly yields. Thread A cannot be interrupted without explicitly giving permission. So there is no uncontrolled interruption, hence no possibility of thread un-safety, and no need for any mutexes in sclang code.

hjh

for the most part, yes. you have the guarantee that almost all operations at the bytecode level are atomic. this includes loading and storing the value of a variable, which is what you mean by “safe access”, i think.

however, you are responsible for making your own code thread safe in a multithreaded environment. for example, if your code yields in the middle of a set of operations that ought to be atomic, perhaps by calling some externally supplied function, your code is no longer thread safe. the next scheduled thread would see a partially executed “atomic” transaction. it is still possible to come up with race conditions in SuperCollider, they are just not of the classic, “two threads loading and storing an int in a loop”, variety.

a concrete example:

// Wrapper around some object that also stores its
// size, calculated once in case it is expensive.
// User code can also set a function to be called when
// the size changes.
MyStorage {
  var <size = 0, <>onSizeUpdate, <storedObject;

  setStored { |newStored|
    var newSize = newStored.size;
    if (size != newSize) {
      onSizeUpdate.value()
      // This call could yield(). Another thread may
      // then observe this object and find that its size is
      // not the same as storedObject.size, breaking
      // thread safety.
    };
    storedObject = newStorage;
  }
}
2 Likes

sorry, i wrote out that reply awhile ago and thought i had already sent it, but when i saw @jamshark70’s reply above i realized it hadn’t gone out of my drafts folder.

“thread safety” is a difficult thing to speak about unambiguously, in part because there are so many different kinds of threads across operating systems and programming languages. in any case, i don’t think it’s accurate to say that there is no need for mutexes in SuperCollider. my previous comment contains an example of a critical section that ought to be protected with one – in fact, it should probably just be rearranged so there is no way the invariant (size != storedObject.size) can be observed in a transient state, but you can’t say that of all possible critical sections.

I do think there’s a concrete difference between a class (as in your example) which provides a user hook – that is, the class is fully aware of giving up complete control over the execution flow – versus a preemptive scheduler wherein the user literally never has complete control over the execution flow (because the scheduler may interrupt at any moment) – but might naively assume that she does.

In the nearly 20 years I’ve been using SC, I can’t recall any cases where a user accidentally stumbled into thread safety problems. I’m willing to bet this is not the normal experience for C++ programmers’ initial attempts at concurrency. It’s possible, as above, to contrive an example, and it’s not impossible for such to happen in the wild. But in practice, it hasn’t come up (which may also be because SC users don’t tend to go that deeply into all the permutations and ramifications of concurrency).

hjh

i agree, for most people’s use cases thread safety doesn’t come up a lot in SC, maybe that’s why you forgot about it. :slight_smile:

i also agree there’s a concrete difference between preemptive and non-preemptive scheduling, but that doesn’t mean mutexes or other synchronization primitives are never necessary in sclang code. i’m not sure i understood your point there.

I was wrong, simple as that. I’m not a computer scientist and I just didn’t think of it. (I hadn’t considered it necessary to issue an explicit retraction.)

hjh

The Routines in sclang are actually coroutines. This is actually made clear in some classlib comments…

Object {
	// coroutine support
	yield {
		_RoutineYield
		^this.primitiveFailed
	}
}

…but in less comp-sci terms in the SC help files.

(Edit: actually the Routine help says, somewhat confusingly

Routines can be used to implement co-routines as found in Scheme and some other languages.

But I really don’t see how SC Routines differ from actual coroutines, so that they would have to be “used to implement” those.)

So as mentioned above the only context switches happen when you explicitly yield (and variations, yieldAndReset, alwaysYield) and when when you call next (and less often synonyms like value/run/resume directly) on a Routine. Those are pretty much the only times when switchToThread is called in the underlying C++ implementation.

Of course those calls can be nested in some other function calls (e.g. embedInStream), so it’s not always obvious where that context-switching happens, but for the most part it is.

(Aside: it’s basically impossible to port the classlib “as is” to a language without coroutine support. See e.g. paper on ScalaCollider .)

Also Routines/Threads have their own Environment, which is actually a little difficult to change from “the outside”, i.e. from another scland Thread. (Stack frames cannot be changed with “hacks” from within SC lang as fas as I know; they can only be inspected by creating copies, e.g. DebugFrame created by getBackTrace.) So only the heap-stored objects are usually “at risk” of being changed from another Thread/Routine, i.e. the local Thread vars and and environment (usually) will be pointing to the same objects after a context switch, but the “contents” of these objects may have changed. But then that’s not really a different problem from when you e.g. call library function that executes on the same Thread as the caller but changes some heap-stored objects.

Perhaps there should be a sclang facility equivalent to “disable interrupts”. After all, the main Thread of the REPL interpreter runs like this, meaning it’s impossible to yield from it without causing an error. So in that example if you could in a kind of pseudocode level::

  setStored { |newStored|
    var newSize = newStored.size;
    atomic {
      if (size != newSize) {
        onSizeUpdate.value()
        // an yield in any function called from there
        // would throw an Exception (say "yielded in atomic")
      };
      storedObject = newStorage;
   } // end atomic
  }

This is a bit inspired from Java’s synchronized keyword, but there’s no lock in this atomic.

Here is a little hacking experiment:

(r = Routine {
	var oldP = thisThread.parent.postln;
	thisThread.instVarPut(\parent, nil); // supa' hack
	thisThread.parent.postln;
	thisThread.instVarPut(\parent, oldP);
	thisThread.parent.postln;
})


r.run
// a Thread
// nil
// a Thread

So far so good for how atomic would be implemented. But if you actually try to yield while parent is nil.

(r = Routine {
	var oldP = thisThread.parent.postln;
	thisThread.instVarPut(\parent, nil); // supa' hack
	thisThread.parent.postln;
	// can't yield here; we're in "interrupts disabled" mode
	\huh.yield;
	thisThread.instVarPut(\parent, oldP);
	thisThread.parent.postln;
})


r.run // a Thread
//a Thread
//nil
//Interpreter has crashed or stopped forcefully. [Exit code: -1073741819]

The issue is that _RoutineYield assumes that a routine always has a non-nil parent. The only check is for the class. So basically Routines are Threads with non-nil parent, as I understand it.

But it’s a simple addition there to throw an error like “yielded with nil parent” to make our atomic hack work as intended. Actually the C++ code has a wee bit of copypasta there, so prRoutineAlwaysYield and prRoutineYieldAndReset would have to be changed similarly to check for nil parent… Interestingly, one of those had such a check but it’s commented out.