async
Asynchronous workers and tasks
Import
_ <- fat.async
Types
The async library introduces the Worker type.
Worker
The Worker is a simple wrapper around an asynchronous operation.
Constructor
| Name | Signature | Brief |
|---|---|---|
| Worker | (task: Method, wait: Number) | Builds a Worker in standby mode |
The Worker constructor takes two arguments:
- task: The method to be executed asynchronously (the method may not take arguments directly, but you may curry those in using two arrows on the definition
-> ->). - wait (optional): A timeout in milliseconds. If the task does not finish within this time, it is cancelled.
Prototype members
| Name | Signature | Brief |
|---|---|---|
| start | <> Worker | Begin the task |
| cancel | <> Void | Cancel the task |
| await | <> Worker | Wait for task completion |
| isDone | <> Boolean | Check if the task has completed |
| hasStarted | Boolean | Set by start method |
| hasAwaited | Boolean | Set by await method |
| isCanceled | Boolean | Set by cancel method |
| result | Any | Set by await method |
Standalone methods
| Name | Signature | Brief |
|---|---|---|
| atomic | (op: Method): Any | Execute the operation atomically |
| unchecked | (op: Method): Any | Suppress warning of risky operation |
| selfCancel | <> Void | Terminate the execution of the thread |
| processors | <> Number | Get the number of processors |
Usage notes
Worker instances are mapped to system threads on a one-to-one basis and get executed as per the system's scheduling. This implies that their execution may not always be immediate. To wait for the result of a Worker, employ the await method.
Unlike in other contexts, in asynchronous code, the task: Method executes without access to the scope in which it is created. It can only access properties that have been 'curried' -> -> into its execution scope or those that are directly accessible in the global scope.
The global memory limit is shared by all Workers, but a completely new context, including a separate stack, is allocated for each one. However, in the event of an irrecoverable or fatal error, such as memory or stack exhaustion by one of the Workers, the interpreter will be halted and all threads terminated.
to keep maximum performance, avoid using text interpolation within asynchronous tasks
Examples
async <- fat.async
math <- fat.math
time <- fat.time
# Define a slow task
slowTask = (seconds: Number): Text -> -> {
time.wait(seconds * 1000)
'done'
}
# Start the task as a Worker
worker = Worker(slowTask(5)).start
# Get the worker result
result1 = worker.await.result # blocks until task is done
# Start a task with timeout
task = Worker(slowTask(5), 3000).start # task should timed out
# Get the task result
result2 = task.await.result # blocks until task is done or timeout occurs
the
awaitmethod will raiseAsyncErrorif the task times out before completion
atomic
The atomic wrapper is a critical tool for ensuring thread safety and data integrity in concurrent programming. When multiple workers or asynchronous tasks access and modify shared resources, race conditions can occur, leading to unpredictable and erroneous outcomes. The atomic operation addresses this issue by guaranteeing that the method it wraps is executed atomically. This means the entire operation is completed as a single, indivisible unit, with no possibility of other threads intervening partway through for the same operation. This is particularly important for operations such as incrementing a counter, updating shared data structures or files, or performing any action where the order of execution matters:
async.atomic(-> file.append(logFile, line))
While atomic operations are a powerful tool for ensuring consistency, it's important to be mindful of the potential for contention it introduces. Contention occurs when multiple threads or tasks attempt to execute an operation simultaneously, leading to potential performance bottlenecks as each thread waits its turn. Overuse or unnecessary use of atomic operations can significantly degrade the performance of your application by reducing concurrency. Keep only the critical section of code that absolutely requires atomicity enclosed as an atomic operation.
under the hood, atomic operations are fundamentally guarded by a single global
mutex
Async in Web Build
When using fry built with Emscripten (for example, when using FatScript Playground), the platform's limited support for multi-threading affects the Worker implementation. To maximize cross-platform code compatibility, Worker tasks execute inline and block the main thread when the start method is called. This approach compromises the advantages of asynchronous execution but allows a consistent implementation across platforms in many scenarios.