Angelscript & Async/Await

Started by
2 comments, last by gjl 3 years, 1 month ago

Hello all,

Lately I've been trying to introduce a bit of async into my scripts - I need this for various IO (resource loading, network messaging) and compute heavy operations (ray casting, pathfinding). Currently the concurrency support via addons is pretty limited and mainly focused on script-script interaction, not script-C++, which is needed for real concurrency. My idea was to try and mimic async/await mechanism for Angelscript - at least as far as it's possible with what language offers currently.

I wonder if anyone tried it so far, and has some learnings and things worth sharing? At this moment I've got a working proof of concept that tells me I'm not doing something unreasonable or completely stupid. Once I refactor it I will try sharing though it's tightly coupled with my thread pool/job system, as it requires it on C++ side, but I'm sure this can be easily implemented differently, I'm just utilising it because it's safe way to get async executing with pre/post execution happening on main thread, so all synchronization is done outside of thread after job is done.

Here is the API I'm aiming for:

async<T> - a future holding value of type T
T async.get() - returns value if available or waits
async<T> async.await() - suspends script execution and resumes once value is available, I return templated async because it's then possible to chain await().get() to instantly wait & retrieve value

async.whenAll(list of async handles) - waits until all async handles complete
async.whenOne(list of async handles) - waits until at least one handle completes

There are some rough edges, and ideally it should be supported in language (proper keywords for instance rather than function calls), but I think I managed to replicate the overall experience of async/await quite well, even if on the low level that's not really what happens and it's not so seamless - requiring plugging threading of your own.

Also got problem with whenAll/whenOne as they require common handle - I think I may use ref or something because this may be a mix of multiple async objects where T is different.. like async<int>, async<bool> etc.

I think the most important limitation in my POC currently is that async functions can only come from C++ because it requires function that kicks off a safe async job (self-contained, not touching shared state, yielding some result at the end of execution). Normally we'd use annotation async for a method like in C# but that'll introduce concurrency into scripts and for my purposes I don't really need it as long as I can “async-ize” certain C++ calls.

So currently I have:

async<int> @asyncLoad()

which runs specific async job. This is how it looks on C++ side:

CScriptAsync* loadAsync()
{
   asIScriptContext* ctx = asGetActiveContext();
   if (ctx)
   {
      asIScriptEngine* engine = ctx->GetEngine();
      asITypeInfo* type = engine->GetTypeInfoByDecl("async<int>");    
      ScriptEngine& manager = ScriptEngine::getScriptEngineFromPointer(engine);
      CScriptAsync* async = new CScriptAsync(type);
      // this pushes this job to thread pool for execution, once it's done
      // the ScriptAsyncJob will set the value on async object and mark it
      // as completed
      manager.runAsyncJob(std::move(std::make_unique<ScriptLoadAsyncJob>(async)));
      return async;
   }   
}

This simple mock schedules ScriptLoadAsyncJob which is dummy async job that just waits and then sets value onCScriptAsync that was passed to this job. This is a sign that computation completed, and elsewhere ScriptManager will pick this up (it keeps a list of suspended CScriptAsync objects and checks their completion, and if it's true it will resume these script contexts).

How it looks on script side:

async<int>@ value = asyncLoad()
// here we can do some processing because function `asyncLoad` is fired on C++ side 
// in async way, returning async<int> promise
print("This is processing while waiting for asyncLoad result")

// Now we decide we want to force wait for the value as we're done processing - this
// will suspend the script and wait until C++ signals the value is ready
value.await();

print("Waited and got: " + formatInt(value.get()));

// Now slightly different usage where we wait for value immediately and it converts 
// straight to int via chainging await() and get() - ugly but works!
int nextValue = asyncLoad(i).await().get();
print("Then got: " + formatInt(nextValue));

And this works surprisingly well and as I'd expect. When such code is executed we get this output:

This is processing while waiting for asyncLoad result

and then we get info that script has been suspended. Meanwhile on C++ side the job is sleeping a couple of seconds simulating some heavier workload and once completing it signals to async object that it's value is now available, which then resumes the script and continues execution where it stopped:

Waited and got: 1234

Which means script resumed and completed execution while having access to the value that was generated in parallel. Then after another while:

Then got: 42532

As the second async op completes.

All this of course does not hold main thread in waiting and the entire game loop keeps running.
I'll continue cleaning this up and see where it goes - I will also try to isolate as much code as possible and post here in case anyone would find this useful - it's pretty simple really and most difficult is actually wiring it into async job execution on C++ side so it's safe.


Where are we and when are we and who are we?
How many people in how many places at how many times?
Advertisement

This is definitely interesting.

I haven't seen anyone implement any code that can be easily shareable as a generic async add-on. Maybe you'll be the first ?

AngelCode.com - game development and more - Reference DB - game developer references
AngelScript - free scripting library - BMFont - free bitmap font generator - Tower - free puzzle game

Looks great!

This topic is closed to new replies.

Advertisement