tech

[English] [Japanese]

81. Safe Asynchronous Processing

With the support of transcompiling from LuneScript to go language, asynchronous processing by LuneScript is now supported.

It has a simple function to statically eliminate data races.

__Runner interface

To do asynchronous processing in LuneScript, implement the __Runner interface.

The __Runner interface is of type: __async is discussed later.

// @lnsFront: skip
pub interface __Runner {
   pub fn run() __async mut;
}

By implementing this interface, you can take advantage of the new built-in functions __run(), __join().

For example, if you execute the following processing, print("hoge:", self.val ); will be processed asynchronously.

// @lnsFront: ok
class Hoge extend (__Runner) {
   let val:int;
   pub fn run() __async mut {
      print("hoge:", self.val );
   }
}

let list:List<Hoge> = [];
for index = 0, 10 {
   let mut hoge = new Hoge(index);
   __run( hoge, __lns.runMode.Sync, "" );
   list.insert( hoge );
}
foreach hoge in list {
   __join( hoge );
}

__run() function

The __run() function is for starting asynchronous execution of the __Runner class.

When asynchronous execution starts, run() method of __Runner class is executed in another thread.

The type of the __run() function is as follows.

// @lnsFront: skip
pub fn __run( runner:__Runner, mode: RunMode, name:str ) : bool
  • The first argument, runner, specifies the __Runner object to run.
  • Specify the following for the second argument mode.

    • __lns.runMode.Sync

      • If the number of running __Runner exceeds a certain number, a new thread will not be started and will be executed here.
    • __lns.runMode.Queue

      • If the number of running __Runners exceeds a certain number, put it in the Runner queue and run it when the running __Runner stops.
    • __lns.runMode.Skip

      • If the number of running __Runner exceeds a certain number, do not run Runner.
      • Returns false if not executed.
  • Specify the name of this asynchronous process in name of the third argument.

When transcompiled to lua, the behavior is as follows:

  • If mode is __lns.runMode.Sync or __lns.runMode.Queue, do not start a new thread and run now.
  • If mode is __lns.runMode.Skip, do not run and return false.

__join() function

The __join() function is a function that waits for the end of __Runner's asynchronous processing.

// @lnsFront: skip
pub fn __join( runner:__Runner )

If you transcompile to lua, do nothing as there is no asynchronous processing.

Restrictions on Constructor Arguments

Constructor arguments for classes that extend __Runner are restricted to the following types.

  • int, real, str, bool, enum
  • immutable types
  • An object of a class that satisfies the following conditions (from v1.6.0)

    • final, has no public members, and all public methods are __noasyc

In other words, the following cases will result in an error.

// @lnsFront: error
class Test {
   pub fn func() __async {
   }
}
final class Foo {
   pub fn func() __noasync {
   }
}
class Hoge extend (__Runner) {
   pub fn __init( test:Test, list:List<int>, foo:Foo ) __async { // error
   }
   pub fn run() __async mut {
   }
}

The error is because the arguments test and list are mutable types.

You need to declare it as immutable like this:

In addition, foo can be passed as mutable because all methods are __noasync.

// @lnsFront: ok
class Test {
   pub fn func() __async {
   }
}
final class Foo {
   pub fn func() __noasync {
   }
}
class Hoge extend (__Runner) {
   pub fn __init( test:&Test, list:&List<int>, foo:Foo ) __async { // ok
   }
   pub fn run() __async mut {
   }
}

__async, __noasyc attributes

If you look at the definition of the run() method in the __Runner interface, you'll see that __async has been added.

This declares the function to be executable asynchronously.

LuneScript divides and manages conventional synchronous processing, which operates with only one thread, and asynchronous processing, which starts and executes a new thread.

To run a function asynchronously, you must declare the function to be asynchronously executable.

That's __async.

On the other hand, traditional synchronization is __noasyc.

Normally, I think sync is the counterpart to async, but//It is daringly set to noasync for the following reasons.

  • Hard to distinguish between async and sync
  • Since the subject is asynchronous processing (async) and synchronous processing is an exception, noasync

If neither __async, __noasync are declared, the default is __noasyc, but we provide a way to handle the default as __async.

Restrictions on functions declared __async

Functions declared __async have the following restrictions:

  • You cannot access a __noasync declared function from within a __async declared function.
  • A mutable variable outside its scope cannot be accessed from within a function declared __async.

A function declared __noasync, on the other hand, has no such restriction.

This is a guard to safely perform asynchronous operations.

Asynchronous processing must consider exclusive control. If you don't do exclusive control where you need it, it becomes a bug.

See here for the necessity of exclusive control.

However, it is very difficult to manually cover all cases where exclusive control is required.

Therefore, LuneScript adopts a method that reduces omissions of exclusion control due to human error by declaring meta information in the grammar and having the compiler check for inconsistencies.

A typical language that takes this approach is Rust.

Rust achieves advanced mutual exclusion by defining strict meta information.

LuneScript does not implement as advanced exclusive control as Rust, but instead adopts relatively simple and easy-to-handle meta information definitions.

By using __asyncLock described later, it is possible to access __noasync from __async.

A function declared __noasync cannot be executed from within a function declared __async.

This means that the following cases will result in an error.

// @lnsFront: error
class Test {
   fn func1() __noasync {
   }
   fn func2() __async {
      self.func1(); // error
   }
}

Above func1 is __noasync and func2 is __async. In this case, __async func2 cannot access __noasync func1.

A mutable variable outside its scope cannot be accessed from within a function declared __async.

This means that the following cases will result in an error.

// @lnsFront: error
let mut list = [ 1, 2 ];
let list2 = [ 1, 2 ];
class Test {
   fn func() __async {
      foreach val in list { // error
         print( val );
      }
      foreach val in list2 { // ok
         print( val );
      }
   }
}

The above func is __async and list is a top-scope mutable variable.

In this case, the mutable list cannot be accessed from the __async func.

On the other hand, list2 is immutable. Immutable variables are accessible from func.

You can also access mutable members from __noasync methods.

__async:__noasyc == N:1

LuneScript is designed so that there are multiple (N) threads that operate asynchronously (__async) and one thread that operates with __noasync.

If there are multiple threads running on __noasync, it's no longer asynchronous, so it's only natural that there's one thread on __noasync.

How to temporarily remove the __async restriction. (__asyncLock)

As mentioned earlier, functions declared __async have limitations.

The ideal is to satisfy this limit in all cases, but in reality there are cases where this is not possible.

So we provide a way to temporarily remove the restriction on functions declared __async.

That's __asyncLock.

For example, use __asyncLock like this:

// @lnsFront: ok
class Test {
   fn func1() __noasync {
   }
   fn func2() __async {
      __asyncLock {
         self.func1(); // ok
      }
   }
}

Because func2 is __async, you cannot access func2, which is __noasync by nature, but within the __asyncLock block the __async restriction is lifted.

Relationship between __asyncLock and __noasync

__asyncLock makes a function declared __async temporarily behave as __noasync.

And as mentioned above, there should be only one thread running __noasync.

So __asyncLock waits until the __noasync thread stops running while the __noasync thread is running, and executes the _asyncLock block after the __noasync thread has stopped running.

Other __asyncLock blocking executions are treated the same as __noasync thread executions.

Nesting __asyncLock across functions

In the next case we are calling func3 -> func2 -> func1.

At this time, func3 and func2 are __asyncLocked, but func2 is already executed as __noasync, so __asyncLock of func2 is executed without blocking.

// @lnsFront: ok
class Test {
   fn func1() __noasync {
   }
   fn func2() __async {
      __asyncLock {
         self.func1();
      }
   }
   fn func3() __async {
      __asyncLock {
         self.func2();
      }
   }
}

__asyncLock across functions can be nested like this.

On the other hand, __asyncLock within the same function cannot be nested. error.

// @lnsFront: error
class Test {
   fn func1() __noasync {
   }
   fn func2() __async {
      __asyncLock {
         __asyncLock { // error
            self.func1();
         }
      }
   }
}

__asyncLock overhead

As mentioned above, __asyncLock does exclusive control.

Use of __asyncLock should be minimized, as exclusive control has overhead.

For example, using __asyncLock inside a for loop adds extra overhead for the loop:

// @lnsFront: ok
class Test {
   fn func1() __noasync {
   }
   fn func2() __async {
      for _ = 1, 10000000 {
         __asyncLock {
            self.func1();
         }
      }
   }
}

In this case, it's better to put __asyncLock outside the for loop. However, there are cases where the scope of exclusion becomes too wide if you put it outside the for loop.

You should carefully decide which ranges to __asyncLock.

Limitations of __asyncLock

__asyncLock has the following restrictions:

  • You cannot return or break from within __asyncLock.

In other words, the following processing is not possible.

// @lnsFront: error
class Test {
   fn func1() __noasync : bool {
      return true;
   }
   fn func2() __async : int {
      __asyncLock {
         if self.func1() {
            return 1; // error
         }
      }
      return 0;
   }
}

If you want to do something like this, write:

// @lnsFront: ok
class Test {
   fn func1() __noasync : bool {
      return true;
   }
   fn func2() __async : int {
      let mut val = 0;
      __asyncLock {
         if self.func1() {
            val = 1;
         }
      }
      return val;
   }
}

default to __async

A function that declares nothing is __noasync.

We provide a way to make this default to __async .

_lune_control default_async_all

If the above is declared at the top of a .lns file, it will default to __async within that .lns file.

software design

The features so far are summarized below.

  • Multiple (N) __async threads and one __noasync thread becomes N:1.
  • __asyncLock blocks while another __noasync thread is running.

From these, to do asynchronous processing in LuneScript you need:

"Basically, __Runner performs __async processing, and __noasync processing is kept to a minimum."

For example, start __Runner immediately after starting with __main(), and wait for the end of that __Runner with __join. It is basic to design.

A mechanism for safe asynchronous control

LuneScript prevents omission of exclusive control by the following.

  • Restrictions on functions declared __async
  • Restrictions on constructors of classes that extend __Runner
// @lnsFront: error
let mut list = [ 1, 2 ];
class Test {
   fn func() __async {
      foreach val in list { //error
         print( val );
      }
   }
}

For example, the above access to list from func() would originally result in a compile error, but if this is not considered an error, while executing the func() method, if another thread updates the value of list, list will reference and modification occur simultaneously, resulting in undefined behavior.

To guard against this, functions declared __async are restricted.

Also, if you run code like:

// @lnsFront: error
class Hoge extend (__Runner) {
   let list:List<int>;
   pub fn __init( list:List<int> ) __async { // error
      self.list = list;

      __run( self, __lns.runMode.Queue, "test" );
   }
   pub fn run() __async mut {
      self.list.insert(1);
   }
}

let mut workList = [1];
let hoge1 = new Hoge( workList );
let hoge2 = new Hoge( workList );

Originally, the type of list in Hoge constructor is mutable, so it will cause a compilation error, but if this is not treated as an error, insert() will occur at the same time due to asynchronous processing of multiple Hoges for the same workList, resulting in undefined behavior. Become.

To guard against this, constructors that extend __Runner are restricted.

imperfect restriction

As mentioned above, LuneScript's exclusive control is incomplete.

As some of you may have already noticed, it is easy to cause indeterminate behavior even if you follow the current restrictions.

For example, list2 accessed from func() in the code below is &List<int> so it is immutable and satisfies the restrictions of __async functions.

// @lnsFront: ok
let mut list = [ 1, 2 ];
let list2 = list;
class Test {
   fn func() __async {
      foreach val in list2 {
         print( val );
      }
   }
}

However, if list is updated from another thread while func is running asynchronously, list2 accessed by func is the same instance as list, resulting in undefined behavior.

The same thing can happen with constructor restrictions.

As you can see, this is an imperfect restriction, but it is a trade-off between the ease of programming development and the strictness of static checks. It is

We will continue to explore ways to improve the strictness of static checks while maintaining ease of programming development.