interthread
"The basic idea behind an actor is to spawn a self-contained task that performs some job independently of other parts of the program. Typically these actors communicate with the rest of the program through the use of message passing channels. Since each actor runs independently, programs designed using them are naturally parallel."
- Alice Ryhl
For a comprehensive understanding of the underlying
concepts and implementation details of the Actor Model,
it's recommended to read the article Actors with Tokio
by Alice Ryhl ( also known as Darksonn ).
This article not only inspired the development of the
interthread crate but also serves as foundation
for the Actor Model implementation logic in it.
What is the problem ?
To achieve parallel execution of individual objects within the same program, it is challenging due to the need for various types that are capable of working across threads. The main difficulty lies in the fact that as you introduce thread-related types, you can quickly lose sight of the main program idea as the focus shifts to managing thread-related concerns. It involves using constructs like threads, locks, channels, and other synchronization primitives. These additional types and mechanisms introduce complexity and can obscure the core logic of the program.
Moreover, existing libraries like actix, axiom,
designed to simplify working within the Actor Model,
often employ specific concepts, vocabulary, traits and types that may
be unfamiliar to users who are less experienced with
asynchronous programming and futures.
Solution
The actor macro - when applied to the
implementation block of a given "MyActor" object,
generates additional types and functions
that enable communication between threads.
A notable outcome of applying this macro is the
creation of the MyActorLive struct ("ActorName" + "Live"),
which acts as an interface/handle to the MyActor object.
MyActorLive retains the exact same public method signatures
as MyActor, allowing users to interact with the actor as if
they were directly working with the original object.
Examples
Filename: Cargo.toml
[dependencies]
interthread = "1.0.1"
oneshot = "0.1.5"
Filename: main.rs
// <- this is it
// uncomment to see the generated code
//#[interthread::example(path="src/main.rs")]
An essential point to highlight is that when invoking
MyActorLive::new, not only does it return an instance
of MyActorLive, but it also spawns a new thread that
contains an instance of MyActor in it.
This introduces parallelism to the program.
The code generated by actor takes
care of the underlying message routing and synchronization,
allowing developers to rapidly prototype their application's
core functionality. This fast sketching capability is
particularly useful when exploring different design options,
experimenting with concurrency models, or implementing
proof-of-concept systems. Not to mention, the cases where
the importance of the program lies in the result of its work
rather than its execution.
The same example can be run in
tokio,
async-std,
and smol,
with the only difference being that the methods will
be marked as async and need to be awaited for
asynchronous execution.
Examples
Filename: Cargo.toml
[dependencies]
interthread = "1.0.1"
tokio = { version="1.28.2",features=["full"]}
Filename: main.rs
// <- one line )
async
The actor macro is applied to an impl block, allowing it to be used with both structs and enums to create actor implementations.
Examples
Filename: Cargo.toml
[dependencies]
interthread = "1.0.1"
oneshot = "0.1.5"
Filename: main.rs
;
;
# interthread
> "The basic idea behind an actor is to spawn a
self-contained task that performs some job independently
of other parts of the program. Typically these actors
communicate with the rest of the program through
the use of message passing channels. Since each actor
runs independently, programs designed using them are
naturally parallel."
> - Alice Ryhl
For a comprehensive understanding of the underlying
concepts and implementation details of the Actor Model,
it's recommended to read the article
by Alice Ryhl .
This article not only inspired the development of the
`interthread` crate but also serves as foundation
for the Actor Model implementation logic in it.
## What is the problem ?
To achieve parallel execution of individual objects
within the same program, it is challenging due
to the need for various types that are capable of
working across threads. The main difficulty
lies in the fact that as you introduce thread-related types,
you can quickly lose sight of the main program
idea as the focus shifts to managing thread-related
concerns.
It involves using constructs like threads, locks, channels,
and other synchronization primitives. These additional
types and mechanisms introduce complexity and can obscure
the core logic of the program.
Moreover, existing libraries like
Filename: main.rs
// <- this is it
// uncomment to see the generated code
//#[interthread::example(path="src/main.rs")]
An essential point to highlight is that when invoking
MyActorLive::new, not only does it return an instance
of MyActorLive, but it also spawns a new thread that
contains an instance of MyActor in it.
This introduces parallelism to the program.
The code generated by actor takes
care of the underlying message routing and synchronization,
allowing developers to rapidly prototype their application's
core functionality. This fast sketching capability is
particularly useful when exploring different design options,
experimenting with concurrency models, or implementing
proof-of-concept systems. Not to mention, the cases where
the importance of the program lies in the result of its work
rather than its execution.
The same example can be run in
tokio,
async-std,
and smol,
with the only difference being that the methods will
be marked as async and need to be awaited for
asynchronous execution.
Examples
Filename: Cargo.toml
[dependencies]
interthread = "1.0.1"
tokio = { version="1.28.2",features=["full"]}
Filename: main.rs
// <- one line )
async
The actor macro is applied to an impl block, allowing it to be used with both structs and enums to create actor implementations.
Examples
Filename: Cargo.toml
[dependencies]
interthread = "1.0.1"
oneshot = "0.1.5"
Filename: main.rs
;
;
Outputs
Thread A - Dog Tango says: Woof!
Thread B - Cat Kiki says: Meow!
Thread MAIN - Cat Kiki says: Meow!
Thread MAIN - Dog Tango says: Woof!
The crate also includes a powerful macro called example that can expand the actor macro, ensuring that users always have the opportunity to visualize and interact with the generated code. Which makes actor 100% transparent macro .
Using actor in conjuction with example empowers users to
explore more and more advanced techniques and unlock the full potential of
parallel and concurrent programming, paving the way for
improved performance and streamlined development processes.
Examples
Filename: Cargo.toml
[dependencies]
interthread = "1.0.1"
tokio = { version="1.28.2",features=["full"]}
Filename: main.rs
use ;
;
async
Outputs (on my machine )
Task 34 awake now!
Task 23 awake now!
Task 25 awake now!
Task 24 awake now!
Task 5 awake now!
...
Task 59 awake now!
Task 42 awake now!
Task 57 awake now!
Task 58 awake now!
Task 55 awake now!
60 in total.
The above example demonstrates a more advanced usage of the actor macro, showcasing its flexibility and capabilities. In this example, we explore non-blocking behavior that doesn't modify the state of the object or return any type.
To modify the state we'll need to use some additional types "shared state types" or "thread-safe types".
Examples
use ;
use ;
;
async
Outputs ( on my machine )
Total tasks - 60
To return a type from the task, we will use a 'channel'
from crate oneshot.
Tokio offers its own version of oneshot.
Filename: Cargo.toml
[dependencies]
interthread = "1.0.1"
tokio = { version="1.28.2",features=["full"]}
Examples
use ;
use ;
use ;
;
// we use argument `id`
async
The id argument is particularly useful when working with multiple instances of the same type, each/some serving different threads. It allows for distinct identification and differentiation between these instances, enabling more efficient and precise control over their behavior and interactions.
The following example serves as a demonstration of the flexibility provided by the actor macro. It showcases how
easy is to customize and modify various aspects of the code generation process.
Examples
Filename: Cargo.toml
[dependencies]
interthread = "1.0.1"
oneshot = "0.1.5"
Filename: main.rs
use mpsc;
use actor;
// this is initial macro
// #[actor(channel=2,file="src/main.rs",edit(script(imp(play))))]
// will change to
// we have the code of `play` component
// using `edit` in conjuction with `file`
// Initiated By : #[actor(channel=2,file="src/main.rs",edit(script(imp(play))))]
The provided example serves as a glimpse into the capabilities of the actor macro, which significantly reduces the amount of boilerplate code required for interthread actors. While the example may not be immediately comprehensible, it demonstrates how the macro automates the generation of essential code, granting developers the freedom to modify and manipulate specific parts as needed.
If you like this project, please consider making a small contribution.
Your support helps ensure its continued development
Join interthread on GitHub for discussions!
Please check regularly for new releases and upgrade to the latest version!
Happy coding!