Packages

A library which spawns workers to run a function in parallel.

Current section

Files

Jump to
working_actors src working_actors.gleam
Raw

src/working_actors.gleam

//// working_actors is a simple library for spawning a group of actors
//// as workers to run a specific function with one of a list of input
//// arguments and collect the function output into a new list.
////
//// Note that the output list is not necessarily in the same order as
//// the input list, so if it is necessary you should return a record with
//// both the input argument and the output from the worker function.
////
import gleam/dict
import gleam/erlang/process
import gleam/list
import gleam/otp/actor
import gleam/result
//
// Worker Process
//
type WorkerMessage(a, b) {
DoWork(input: a, reply_to: process.Subject(ResponseMessage(b)))
Shutdown
}
fn worker_handle_message(
state: fn(a) -> b,
message: WorkerMessage(a, b),
) -> actor.Next(fn(a) -> b, WorkerMessage(a, b)) {
case message {
DoWork(input, reply_to) -> {
let reply = state(input)
actor.send(reply_to, SubmitWork(reply, process.self()))
actor.continue(state)
}
Shutdown -> actor.stop()
}
}
type ResponseMessage(b) {
SubmitWork(fn_respose: b, pid: process.Pid)
}
//
// Worker Functionality
//
/// Spawn `n_workers` worker processes, each ready to run the work
/// function `work_function`. A list of arguments to the `work_function`
/// is provided as the `tasks` list. The functions will be called in
/// parallel until the `work_function` has been run with every argument
/// in the `tasks` list. A new list of the function return values will
/// be returned when all the tasks have run. This list will be in an
/// arbitrary order.
pub fn spawn_workers(
n_workers: Int,
tasks: List(a),
work_function: fn(a) -> b,
) -> List(b) {
let workers =
list.range(1, n_workers)
|> list.map(fn(_) {
actor.new(work_function)
|> actor.on_message(worker_handle_message)
|> actor.start
|> result.map(fn(started) { #(started.pid, started.data) })
})
|> result.values
|> dict.from_list
let idle_workers = workers |> dict.values
use_workers(tasks, workers, idle_workers, [], process.new_subject())
}
fn use_workers(
tasks: List(a),
workers: dict.Dict(process.Pid, process.Subject(WorkerMessage(a, b))),
idle_workers: List(process.Subject(WorkerMessage(a, b))),
function_responses: List(b),
reply_to: process.Subject(ResponseMessage(b)),
) -> List(b) {
// io.println("Task List Length: " <> int.to_string(list.length(tasks)))
// io.println(
// "Idle Worker List Length: " <> int.to_string(list.length(idle_workers)),
// )
// io.println("Workers Running: " <> int.to_string(dict.size(workers)))
// io.println("")
case tasks, idle_workers, dict.size(workers) {
[first_task, ..rest_tasks], [first_worker, ..rest_workers], _ -> {
actor.send(first_worker, DoWork(first_task, reply_to))
use_workers(
rest_tasks,
workers,
rest_workers,
function_responses,
reply_to,
)
}
[], [first_worker, ..rest_workers], _ -> {
actor.send(first_worker, Shutdown)
let new_workers =
dict.filter(workers, keeping: fn(_, v) { v != first_worker })
use_workers([], new_workers, rest_workers, function_responses, reply_to)
}
[first_task, ..rest_tasks], [], _ -> {
case process.receive_forever(reply_to) {
SubmitWork(fn_response, pid) -> {
let assert Ok(worker_subject) = dict.get(workers, pid)
actor.send(worker_subject, DoWork(first_task, reply_to))
use_workers(
rest_tasks,
workers,
[],
[fn_response, ..function_responses],
reply_to,
)
}
}
}
[], [], 0 -> function_responses
[], [], _ -> {
case process.receive_forever(reply_to) {
SubmitWork(fn_response, pid) -> {
let assert Ok(worker_subject) = dict.get(workers, pid)
actor.send(worker_subject, Shutdown)
let new_workers = dict.delete(workers, pid)
use_workers(
[],
new_workers,
[],
[fn_response, ..function_responses],
reply_to,
)
}
}
}
}
}