DEV Community

loading...

Kinx Library - Isolate (Multi Thread without GIL)

Kray-G
Although I used to be a C++/Boost lover, I was back to C with the spirit of Zen, or the spirit of "simple is the best". Also returning to rock and roll from metal, I'm really into Rolling Stones.
・6 min read

Hello, everybody!

The script language Kinx is published with the concept of Looks like JavaScript, Feels like Ruby, Stable like AC/DC(?).

This time it is an Isolate, which is a native thread library without GIL.

As you can see from the name Isolate, as a thread model, each thread operates independently and does not share the memory. This is chosen to increase safety.

Isolate

Model of multi threading

The memory sharing model of threads like C/C++ is too dangerous and difficult. You have to pay attention much to the race condition trap and ensure deadlock control, but you will still get deadlock easily. The battle between multithreading and safety is still ongoing.

The threading model of Ruby and Python is safe, but its weakness is that GIL (Global Interpreter Lock) gives many limitations for parallelism.

Let's challenge this wall also at Kinx. Ruby hasn't been freed from GIL due to past concerns, but it's likely to move on to the next stage.

So Kinx prepared the mechanism named as Isolate. It's a completely independent native thread. The exchange of information is limited to Integer, Double and String. Therefore, if you want to send an object, you need to prepare the mechanism of serializing and deserializing it. The most easy way is to make it a string and just execute it because the source code of Isolate is given as a string.

But note that only the compile phase is not reentrant. So the compilation phase will be locked and processed in sequence.

Isolate Object

Roughly, the Isolate object is used as follows.

  • Create an Isolate object by new Isolate(src). Not executed yet at this point. src is just a string.
  • Compile & execute with Isolate#run(). The return value is this of the Isolate object.
  • By Isolate#join(), wait for the thread to finish.
  • When the main thread ends, all threads will end with nothing to be cared.
    • Therefore, when controlling the end, synchronize by using a method such as data transfer described later, and correctly join in the main thread.

Example

Creating a new thread

Look at the example first. What is passed to the constructor of Isolate is just a string. It feels good to looks like a program code when write it as a raw string style, but there is a trap on it.

  • A %{...} in a raw string has been recognized as an inner-expression for the raw string itself.

So, you had better avoid to use %{...} inside a raw string.
For example below, using %1% for that purpose and applying the value directly into a string. It is like a little JIT.

var fibcode = %{
    function fib(n) {
        return n < 3 ? n : fib(n-2) + fib(n-1);
    }
    v = fib(%1%);
    var mutex = new Isolate.Mutex();
    mutex.lock(&() => System.println("fib(%1%) = ", v));
};

34.downto(1, &(i, index) => new Isolate(fibcode % i).run())
    .each(&(thread, i) => { thread.join(); });
Enter fullscreen mode Exit fullscreen mode

Locking for printing has been used to avoid to a strange output.

fib(15) = 987
fib(10) = 89
fib(20) = 10946
fib(3) = 3
fib(11) = 144
fib(21) = 17711
fib(4) = 5
fib(9) = 55
fib(23) = 46368
fib(16) = 1597
fib(14) = 610
fib(8) = 34
fib(2) = 2
fib(24) = 75025
fib(26) = 196418
fib(28) = 514229
fib(29) = 832040
fib(7) = 21
fib(30) = 1346269
fib(25) = 121393
fib(5) = 8
fib(13) = 377
fib(12) = 233
fib(19) = 6765
fib(22) = 28657
fib(18) = 4181
fib(17) = 2584
fib(6) = 13
fib(27) = 317811
fib(31) = 2178309
fib(1) = 1
fib(32) = 3524578
fib(33) = 5702887
fib(34) = 9227465
Enter fullscreen mode Exit fullscreen mode

The order might be changed because of a multi thread.

fib(10) = 89
fib(19) = 6765
fib(14) = 610
fib(11) = 144
fib(26) = 196418
fib(17) = 2584
fib(21) = 17711
fib(20) = 10946
fib(9) = 55
fib(13) = 377
fib(28) = 514229
fib(18) = 4181
fib(30) = 1346269
fib(31) = 2178309
fib(7) = 21
fib(3) = 3
fib(8) = 34
fib(4) = 5
fib(25) = 121393
fib(16) = 1597
fib(22) = 28657
fib(23) = 46368
fib(12) = 233
fib(27) = 317811
fib(29) = 832040
fib(15) = 987
fib(2) = 2
fib(5) = 8
fib(1) = 1
fib(6) = 13
fib(32) = 3524578
fib(24) = 75025
fib(33) = 5702887
fib(34) = 9227465
Enter fullscreen mode Exit fullscreen mode

End of thread

The thread will be finished when the Isolate code has been reached at the end.
The returned status code from the thread will be returned as a return code of join.

var r = new Isolate(%{ return 100; }).run().join();
System.println("r = %d" % r);
Enter fullscreen mode Exit fullscreen mode
r = 100
Enter fullscreen mode Exit fullscreen mode

Transfer data - Isolate.send/receive/clear

For simple data transfer you can use Isolate.send(name, data) and Isolate.receive(name). The buffer is distingished by name, the threads are send/receive data by the name.

  • name can be omitted. When it is omitted, it is same as specifying "_main".
  • As data, only Integer, Double, and String is supported.
    • That is why for an object, you should stringify it and it should be reconstructed by receiver.
  • To clear the buffer by Isolate.clear(name).
    • If you do not clear the buffer by Isolate.clear(name), the buffer data will be remaining. It means the same data can be got by Isolate.receive(name) many times.

Mutex

Mutex object is constructed by Isolate.Mutex. By the way, the mutex is distinguished by the name even if it is in the same process.

var m = new Isolate.Mutex('mtx');
Enter fullscreen mode Exit fullscreen mode

By using the same name, the same mutex will be constructed. If you omit the name, the name will be the same as "_main".

Mutex object is used with Mutex#lock(func) method. The callback function of func is called with a locked mutex.

var m = new Isolate.Mutex('mtx');
m.lock(&() => {
    // locked
    ...
});
Enter fullscreen mode Exit fullscreen mode

Condition

You can use a condition variable. That is used with a mutex object together. When passing a locked mutex to Condition#wait(), it waits after mutex is unlocked. In that status, when another thread do the Condition#notifyAll() and the thread can get the lock, to come back from the waiting status.

Condition#notifyOne() is not supported because everybody says 'nobody should use it!'.

var cond = %{
    var m = new Isolate.Mutex('mtx');
    var c = new Isolate.Condition('cond');
    m.lock(&() => {
        var i = 0;
        while (i < 10) {
            System.println("Wait %1%");
            c.wait(m);
            System.println("Received %1%");
            ++i;
        }
        System.println("Ended %1%");
    });
};

var ths = 34.downto(1, &(i, index) => new Isolate(cond % i).run());
System.sleep(1000);
var c = new Isolate.Condition('cond');
16.times(&(i) => {
    System.println("\nNotify ", i);
    c.notifyAll();
    System.sleep(500);
});
ths.each(&(thread) => {
    thread.join();
});
Enter fullscreen mode Exit fullscreen mode

NamedMutex

It is a mutex object with using between processes. To construct it, use Isolate.NamedMutex, but the usage is same as a normal mutex object.

But I do not know if it is good that the name should be Isolate.NamedMutex, because it role is over the Isolate. If you have any idea of that, please let me know. For example, Process.NamedMutex, or System.NamedMutex, or something.

var mtx = Isolate.NamedMutex('ApplicationX');
mtx.lock(&() => {
   ...
});
Enter fullscreen mode Exit fullscreen mode

It is used when you want to exclusive it with other processes.

Data serialization and deserialization

So far, there is no feature of serializing and deserializing data. You do it yuorself. In fact, I hope I want to add some features for that, so I am now thinking about the functionality of that.

Now what you can do is to stringify it and reconstruct it to the object. When it is the JSON object as a simple structure, you can realize it by JSON.stringify and JSON.parse. As an another simple way, you can also put it directly with toJsonString().

var t = %{
    var o = %1%;
    System.println(["Name = ", o.name, ", age = ", o.age].join(''));
};
var o = {
    name: "John",
    age: 29,
};
new Isolate(t % o.toJsonString()).run().join();
Enter fullscreen mode Exit fullscreen mode
Name = John, age = 29
Enter fullscreen mode Exit fullscreen mode

You want to pass data dynamically, you need the code to deserialize it.

var t = %{
    var o;
    do {
        o = Isolate.receive();
    } while (!o);
    System.println("Received message.");
    o = JSON.parse(o);
    System.println(["Name = ", o.name, ", age = ", o.age].join(''));
};
var o = {
    name: "John",
    age: 29,
};
var th = new Isolate(t).run();
Isolate.send(o.toJsonString());
th.join();
Enter fullscreen mode Exit fullscreen mode
Received message.
Name = John, age = 29
Enter fullscreen mode Exit fullscreen mode

Conclusion

To realize a native thread without GIL, I did a lot of things depended on the runtime context and I designed the C function of Kinx should be reentrant. I believe it is really unnecessary to lock by GIL, if there is no mistake and bug...

To tell the truth, I ca not promise I haven't made a mistake. I believe you understand it if you were a developper. But I don't face any problem so far. Of course I will fix it if you report the bug.

Anyway, this is a challenge!

As I wanted to add the functionality of Isolate as a multi-threading for the multi-core, I did it. But it is still on the early stage. Challenge anything!

See you next time.

Discussion (0)