So ChaiScript is thread-safe, but is it coroutine-safe?


#1

Regarding these two cases:

  • single thread single fiber per ChaiScript object
  • single thread multiple fiber per ChaiScript object

For those unfamiliar with the term “fiber”, it is also commonly known under the name “green thread” / “user thread” and “coroutine”. I choose “fiber” because examples below will be using boost::fiber since it is highlevel enough to produce self-explanatory oneliners.

First case should not cause any problems, as from the perspective of the ChaiScript object, it is merely a blocking call.

However, second case can’t be answered without knowledge about ChaiScripts internals, and before I spend alot of time reading through it only to maybe end up missing something, I hope you guys could help me out. Lets go with an example

var f = fun(){ sleep(1000); };
spawn(f);

with implementations:

chai.add(fun( [] ( const std::function<void()> & f )
    {
        boost::fibers::fiber(f).detach(); // launches the fiber with f as entry
    }),
    "spawn"
);

chai.add(fun( [] ( int ms )
    {
        boost::this_fiber::sleep_for(std::chrono::milliseconds(ms));
    }),
    "sleep"
);

(This is example code. Actual implementations will of course make sure that no fiber outlives the ChaiScript object)

How does ChaiScript handle its internal stacks / execution contexts ? Does a function ( like f ) automatically create its own standalone stack, or does the ChaiScript engine detect that f is being called from a different thread ( it offers async ), and thus creates a stack for that thread ? Something different yet ? And whichever it is, what does it mean for a fiber. That is what I’d like to discuss and find out here.

If the engine detects that it’s in a different thread, would extending it to also detect a different fiber suffice ?


#2

It seems like you are mixing terms here. There is no thing as coroutine-safe. The fibers are not concurrently executed but yield at defines positions in program flow. They are cooperatively scheduled. There is no context switch and no undefined state of data like in a thread. ChaiScript will work in fibers.


#3

No concurrent execution doesn’t automatically make everything foolproof. Infact, nothing of my question implied concurrency. Did you maybe just assume it because it’s physically standing near “thread-safe” ?

  • Fibers have their own stack, so depending on how ChaiScript internals are composed, that might already cause a nuclear disaster. Standard ChaiScript is completely fiber unaware. How does the engine keep track of its script execution ? I don’t know, but just for example if it’s some thread_local state, and then suddenly have the same thread jump back and forth might ( depending on how the ChaiScript engine is designed ) have horrible consequences.
  • ChaiScript locking a (std::)mutex around a location on which we yield/suspend will cause supernovae in a multithreaded singlefiber per ChaiScript object environment in which there still is no concurrency per ChaiScript object, but fibers might be resumed by a different thread, not to speak of other stuff that ChaiScript declares thread_local.

While point 2 was not part of the question, it does underline the fact that you can’t simply say that everything is going to be fine just because there is no concurrent execution. And finding possible landmines ( and what to do about them ) is what I wanted this thread to be about.


Anyhow, in my journey through ChaiScript internals I have not (yet) found such landmines for single thread multiple fiber environment ( same thread resuming and suspending ), but I’ve extended Thread_Storage to detect different fibers and generally removed/replaced all fiber unaware threading features. While I have not yet had time to read through everything that gets created via Thread_Local, I figured making the rest of ChaiScript believe it’s in a different kernel thread can’t be all that wrong, since multithreading is supported.

So far it runs just fine, but I have not yet read through enough ChaiScript code or run my fiber enabled ChaiScript enough to rule out any slumbering UB or the likes.


#4

You are way outside of my experience here with fibers/coroutines and that kind of thing, but I can tell you that I don’t do anything “tricky.”

The stack management is all done through the C++ call stack itself, and thread safety is managed almost entirely with the thread_local objects you’ve already found, and a few locks for the state that is shared between threads.

-Jason