There is some discussion of my thoughts on multi-threaded Emacs here. Read that first. This project gives us a unique opportunity to make the interpreter thread safe. I am generally of two minds about how to handle multi-threading.
idea 1 - CSP all the way
CSP or "communicating sequential processes" is the most flexible way to implement parallelism. This is the approach used by languages like go and clojure. You essentially create first-class channels that can be passed around to other threads, and then only communicate through those channels. This let you build a topology of "communicating processes" that can sequence with each other as desired. It is very simple, flexible, and powerful.
However, you run into the issue that this method could not be made backwards compatible with existing GNU Emacs runtime. That runtime has no ability to suspend and resume threads of execution in the way that would be needed to mimic CSP. You could in theory do this with existing Emacs threading API, but is very unstable and will crash your Emacs, so it is not ready to be used. However if the threading library was ever made robust, then you could in theory model CSP in GNU Emacs.
Idea 2 - coroutine style
GNU Emacs supports stackless coroutines (called generators), which allow for basic concurrency. If you limited your multi-threading API, you could have it map to coroutines as a polyfill. This would mean you would not have to write code twice, once for multi-threading, and once for not. I imagine it looking something like this.
(let ((num 0)
(go (goroutine (lambda (x) (yield (1+ x))))))
(setq num (resume go num))
(message "%d" num) ;; prints 1
(setq num (resume go (+ num 2)))
(message "%d" num)) ;; prints 4
This would limit thread to only communicating with the thread that spawned them, and only yielding values in the top level function (since Emacs coroutines are stackless). But you would not need an explicit channel type, and it would be easy to create polyfill version of goroutine, yield, and resume so that the same code would work in GNU Emacs and a multi-threaded Emacs. I don't like this solution as well as just using CSP. But maybe the backwards compatibility would be worth it.
why not use async/await for the backend?
I don’t think using Tokio or any other async await platform would be worthwhile. Fundamentally, they don’t offer any feature that you wouldn’t already get with threads. Plus async brings its own problems.
First is that you have to introduce function coloring into codebase. Everything needs to have two versions. Plus async code is messier and harder to reason about. We are not writing a web server, so we don’t need that complexity.
The only real advantage of async is lower resources usage. Green threads take significantly less space compared to system threads. But this is only really a concern when you want thousands of threads or are memory constrained.
The other advantage is that they can switch context a little faster because they pin threads to cores. However I see this as a disadvantage. In tokio you need to tell the runtime upfront if something is going to be compute heavy or IO heavy. But since the threads are in elisp, we can’t know that ahead of time. Many modern CPU’s have both performance cores and efficiency cores. The kernel will try to move compute heavy tasks to the p-cores and IO heavy tasks to the e-cores. But tokio can’t do that because the threads are pinned!
Also async only supports cooperative multitasking, which means a task that doesn’t play by the rules can starve others. But OS threads are preemptive, so that can’t happen.
What should be shared and what should be thread-local?
Things that are going to change only rarely such as functions can be shared between threads. However most other things like variable bindings and properties will be thread local. However to better support integration with existing elisp, thread local types will be copied to a thread as needed. This ensures that users don't have to worry about "is this variable defined in this thread or not", but still protects us from data races and other nasty concurrency bugs.
how to handle variable bindings?
As described in the previous section, the current idea is have variable bindings and symbol properites copied to other threads "on demand". Essentially when a thread first tries to access a variable that it does not have defined locally, it will send a message to the it's parent thread and ask for the current value via a channel. The parent thread will copy send a message back with the value, or message indicating that the variable is undefined. The child thread will wait until it receives a message back before continuing execution.
how to handle errors
When an error occurs in another thread, what do you do? Any channels connected to that thread would become poisoned, and would signal an error if you tried to read from them. But what about threads that don’t communicate with the main thread. Maybe you wanted to delete a file in the background or something. Raising it randomly back to the mail thread seems really disruptive. But it could also be a foot gun if it just silently ignores the error. Maybe it just prints it as s message.
There is some discussion of my thoughts on multi-threaded Emacs here. Read that first. This project gives us a unique opportunity to make the interpreter thread safe. I am generally of two minds about how to handle multi-threading.
idea 1 - CSP all the way
CSP or "communicating sequential processes" is the most flexible way to implement parallelism. This is the approach used by languages like go and clojure. You essentially create first-class channels that can be passed around to other threads, and then only communicate through those channels. This let you build a topology of "communicating processes" that can sequence with each other as desired. It is very simple, flexible, and powerful.
However, you run into the issue that this method could not be made backwards compatible with existing GNU Emacs runtime. That runtime has no ability to suspend and resume threads of execution in the way that would be needed to mimic CSP. You could in theory do this with existing Emacs threading API, but is very unstable and will crash your Emacs, so it is not ready to be used. However if the threading library was ever made robust, then you could in theory model CSP in GNU Emacs.
Idea 2 - coroutine style
GNU Emacs supports stackless coroutines (called generators), which allow for basic concurrency. If you limited your multi-threading API, you could have it map to coroutines as a polyfill. This would mean you would not have to write code twice, once for multi-threading, and once for not. I imagine it looking something like this.
This would limit thread to only communicating with the thread that spawned them, and only yielding values in the top level function (since Emacs coroutines are stackless). But you would not need an explicit channel type, and it would be easy to create polyfill version of
goroutine,yield, andresumeso that the same code would work in GNU Emacs and a multi-threaded Emacs. I don't like this solution as well as just using CSP. But maybe the backwards compatibility would be worth it.why not use async/await for the backend?
I don’t think using Tokio or any other async await platform would be worthwhile. Fundamentally, they don’t offer any feature that you wouldn’t already get with threads. Plus async brings its own problems.
First is that you have to introduce function coloring into codebase. Everything needs to have two versions. Plus async code is messier and harder to reason about. We are not writing a web server, so we don’t need that complexity.
The only real advantage of async is lower resources usage. Green threads take significantly less space compared to system threads. But this is only really a concern when you want thousands of threads or are memory constrained.
The other advantage is that they can switch context a little faster because they pin threads to cores. However I see this as a disadvantage. In tokio you need to tell the runtime upfront if something is going to be compute heavy or IO heavy. But since the threads are in elisp, we can’t know that ahead of time. Many modern CPU’s have both performance cores and efficiency cores. The kernel will try to move compute heavy tasks to the p-cores and IO heavy tasks to the e-cores. But tokio can’t do that because the threads are pinned!
Also async only supports cooperative multitasking, which means a task that doesn’t play by the rules can starve others. But OS threads are preemptive, so that can’t happen.
What should be shared and what should be thread-local?
Things that are going to change only rarely such as functions can be shared between threads. However most other things like variable bindings and properties will be thread local. However to better support integration with existing elisp, thread local types will be copied to a thread as needed. This ensures that users don't have to worry about "is this variable defined in this thread or not", but still protects us from data races and other nasty concurrency bugs.
how to handle variable bindings?
As described in the previous section, the current idea is have variable bindings and symbol properites copied to other threads "on demand". Essentially when a thread first tries to access a variable that it does not have defined locally, it will send a message to the it's parent thread and ask for the current value via a channel. The parent thread will copy send a message back with the value, or message indicating that the variable is undefined. The child thread will wait until it receives a message back before continuing execution.
how to handle errors
When an error occurs in another thread, what do you do? Any channels connected to that thread would become poisoned, and would signal an error if you tried to read from them. But what about threads that don’t communicate with the main thread. Maybe you wanted to delete a file in the background or something. Raising it randomly back to the mail thread seems really disruptive. But it could also be a foot gun if it just silently ignores the error. Maybe it just prints it as s message.