Async
Say you have some function that takes a long time to run, and you'd like to be able to do other things in the meantime while that function resolves. Normally, you'd have to run the function and just wait until it completes before continuing.
fn someFunction() -> int {
// this function takes a long time to resolve
}
fn main() {
int result = someFunction() // have to wait for this to complete
}
There might be other smaller tasks that could be done in the meantime, but execution gets blocked, even if the result may not be needed until much later. One way you could get around this is to simply call the function later when it's needed, but you're simply delaying the wait until later, even if you had all the necessary inputs for the function early on.
Futures
This is where async comes in. You can use the async keyword to tell a function call to start execution immediately in the background, while the rest of your code continues as normal. This creates a Future, which is signified by a * at the end of the type (e.g. int*). This Future will hold the return value of the function, but since it can't be known when the function has completed execution, you have to await the Future to block execution until it has a value, and then get the value out.
fn someFunction() -> int {
// this function takes a long time to resolve
}
fn main() {
int* result = async someFunction() // start executing the function here
// do some other work while `someFunction` runs in the background
int value = await result // block execution until `someFunction` resolves
}
This allows you to do work while your slow, expensive function executes in the background simultaneously. Note that the function itself was not marked as async, but rather the function call was. This avoids the problem of function coloring in other languages, where once a function is marked as async, every other function that calls it must also be marked as async.
You can also have functions that return value-error tuples store their return value in a Future:
fn someFunction() -> (int, error) {
// do something
}
fn main() {
(int, error)* future = async someFunction()
int value = await future catch err {
return err
}
}
Mutex
When dealing with asynchronous code, sometimes multiple threads need to be able to access the same value at the same time. If one thread is reading from a value while another happens to be writing to it at the same time, you can end up with data races that can lead to unpredictable behavior.
This is where mutual exclusion (Mutex) can come in. By creating a lock around a value, you can guarantee that only one thread can access it at a time, and other threads must wait their turn.
You can declare a Mutex by adding ^ to the end of the type:
int[]^ numbers = [1, 2, 3, 4, 5]
Importantly, Mutexes cannot be declared as mutable. So the following would not be allowed:
mut int[]^ numbers = [1, 2, 3, 4, 5] // error: cannot declare Mutex as mutable
In order to make a Mutex mutable, you must first lock the value so that no other thread can access it, using the lock keyword. By forcing a lock for mutability, we can ensure that there will be no data races.
lock numbers
// type of `numbers` is now `mut int[]^`
While the Mutex is locked, you are free to mutate the value however you'd like. During this time, other threads cannot read from the variable, and only the thread that has the lock can write to or read from it. Once you're done, you can unlock the Mutex with the unlock keyword, which will return it to its previous immutable state. If you forget to unlock, it will be done automatically at the end of the function call. However, you should not rely on this, and it is recommended that you manually unlock as soon as you do not need the lock anymore.
unlock numbers
// type of `numbers` is now `int[]^`
While it's immutable, any number of threads can freely read from the Mutex, however none can write to it.
An example of using Mutexes with async would be:
int[]^ numbers = [1, 2, 3, 4, 5]
fn add1() -> bool {
lock numbers
// `numbers` is `mut int[]^` now
for i in numbers {
numbers[i] = numbers[i] + 1
}
return true
}
fn add2() -> bool {
lock numbers
// `numbers` is `mut int[]^` now
for i in numbers {
numbers[i] = numbers[i] + 2
}
unlock numbers
return true
}
bool* first = async add1()
bool* second = async add2()
if not await first or not await second {
panic("Something went wrong")
}
assert(numbers == [4, 5, 6, 7, 8])
In this example, add1 and add2 run concurrently, but whichever function gets to the lock numbers instruction first would gain a lock on the numbers array. If we assume that add2 gets the lock first, then add1 will be unable to get the lock to the array to run its lock numbers instruction, and would have to wait its turn until add2 released it with its unlock numbers instruction. In this example, the order of operations would look something like this:
- declare
numbers = [1, 2, 3, 4, 5] - call
add1 - call
add2 add2attempts and succeeds gaining lock onnumbersadd1attempts and fails gaining lock onnumbersadd2adds 2 to each value innumbers.numbers = [3, 4, 5, 6, 7]add2releases lock onnumbersadd1attempts and succeeds gaining lock onnumbersadd1adds 1 to each value innumbers.numbers = [4, 5, 6, 7, 8]add1implicitly releases lock onnumbersat the end of the function scope.
Of course, the order that this executes in may not be exactly this, but it should give you an idea of how Mutexes behave.