This post summarises the difficulties that need to be surmounted in order to support multithreaded canister smart contract execution on the Internet Computer. This question has come up in a number of venues so I hope that writing down the current thinking in one place can be helpful.
Background
The canister smart contract model is inspired by the traditional actor model. In the actor model, an application is partitioned into a group of actors. The actors make progress by sending messages to each other and processing messages sent to them. Typically, even though the system may consist of multiple threads, individual actors are designed to run in a single threaded manner. This guarantees that when an actor is executing on a thread, it has exclusive access to its data and no multithreaded synchronisation is needed.
Problem
The subnet blockchain on the Internet Computer is a collection of nodes. When one subnet executes an update message on a canister, then all [honest] nodes on the subnet must execute the message. Further, all nodes must arrive at the same result after the execution finishes i.e. the execution of update messages has to be deterministic across all nodes in a subnet.
Hence, if we allowed a message execution to occur over multiple threads, then we would have to ensure that the execution is still deterministic. As an example, let’s consider a hypothetical function below.
global0 = 0;
fn handler() -> int {
if compare_and_swap(&global_variable, 0, 1) {
return 1;
}
return 0;
}
Let’s say that there are two threads that are both executing the above function concurrently. It is possible that on some nodes, the first thread returns 1 and on some nodes it returns 0. So in order to support multithreaded execution, we will need to address problems like this. We are not aware of any straightforward solution to this problem.
Workarounds
If a canister can only be single threaded, then it means that it can only execute one update message at a given time. This can impact the ability to scale a canister that needs to execute many update messages. There are any number of ways to address this problem.
Partition shared state into shards
One way is to shard the canister state into multiple canisters which can execute concurrently.
So in the above example, one could do something like the following.
Global canister:
global0 = 0;
fn compare_and_swap_global(old_val, new_val) -> bool {
if global0 == old_val {
global0 = new_val;
return true;
}
return false;
}
Shard canisters:
fn handler() {
if global_canister::compare_and_swap_global(0, 1).await {
return 1;
}
return 0;
}
There is a new global canister that manages the shared state. And instead of having a single canister there are now shard canisters that send messages to the global canister to update the shared state.
Embrace the actor model
As discussed in the background section above, the other way to work around the problem is to try to partition the application into smaller actors that can all execute in parallel. An example is discussed in this stackoverflow post where a HTTP service could be decomposed into a set of actors each performing a chunk of the required work.