PistonDevelopers / dyon

A rusty dynamically typed scripting language

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

In-type concurrency design

bvssvni opened this issue · comments

Sender/receiver channels is a common pattern for concurrency programming. Another common programming pattern is the actor model, where entities communicate by ids. Dyon combines these two approaches by treating functions as entities that can communicate with other processes. With other words, you do not have to change your algorithm to support concurrent programming. You can also use this technique for debugging your program, by printing out all input that is sent to a function.

Dyon uses the in keyword for creating receiver channels. The sender channel can be any loaded function.

Example:

fn main() {
    a := in log
    log(1)
    b := in log
    log(2)
    c := in log
    log(3)
    report(a)   // prints `123`
    report(b)   // prints `23`
    report(c)   // prints `3`
}

// Dummy sink.
fn log(x) {}

fn report(a: in[[f64]]) {
    loop {
        x := next(a)
        if x == none() {break}
        x := unwrap(x)
        print(x[0])
    }
    println("")
}

When the log function is called, Dyon checks a flag whether any channels are subscribing. If there is at least one subscriber, Dyon pushes the arguments into an array and sends a copy to each recipient.

Here is another example, combining in and go:

/*
This example shows how to process data chunkwise by reading
at slower intervals.
*/

fn main() {
    // Create a log receiver channel.
    log := in log
    // Create a channel for signaling when the task is done.
    done := in done
    // Create a new thread that works on the task.
    th := go run()
    loop {
        // Read the done signal first to avoid data race.
        done_val := next(done)
        // Print out received log.
        loop {
            x := next(log)
            if x == none() {break}
            print(unwrap(x))
        }
        println("")
        // Break the loop when done.
        if done_val != none() {break}
        sleep(0.5)
    }
    // Wait for the thread to finish.
    _ := join(thread: th)
}

fn log(x: f64) {}
fn done() {}

// Runs task.
fn run() -> bool {
    for i 100 {
        log(i)
        sleep(0.1)
    }
    done()
    return true
}

A typical output from the example above:

[0][1][2][3][4]
[5][6][7][8][9]
[10][11][12][13][14]
[15][16][17][18][19]
[20][21][22][23][24]
[25][26][27][28][29]
[30][31][32][33][34]
[35][36][37][38][39]
[40][41][42][43]
[44][45][46][47][48]
[49][50][51][52][53]
[54][55][56][57][58]
[59][60][61][62][63]
[64][65][66][67][68]
[69][70][71][72][73]
[74][75][76][77][78]
[79][80][81][82][83]
[84][85][86][87]
[88][89][90][91][92]
[93][94][95][96][97]
[98][99]

Notice that the number of received messages varies, because the receiver thread sleeps at different intervals than the sender thread.

You can also receive current objects:

fn main() {
    log := in add
    ~ x := 0
    add(1)
    println(unwrap(next(log)))  // prints `[1, 0]`
    add(4)
    println(unwrap(next(log)))  // prints `[4, 1]`
    add(3)
    println(unwrap(next(log)))  // prints `[3, 5]`
}

fn add(a: f64) ~ mut x: f64 {x += a}

The type of the receiver channel is in, with an optional inner type e.g.in[[str]]. The inner type created with the in <fn> syntax is an array. For example, if you subscribe to a function foo(a: f64, b: f64), then the type in[[f64]] can be used. If the arguments have different types, you can use in[[]], in[[any]] or just in.

When a receiver channel runs out of scope, sending new messages fails. Dyon cleans up unused channels by defragmenting the subscriber list and truncating it. This prevents memory leaks when creating and destroying lot of channels.

This also means that the order of receiving events can change:

abc      // the subscriber list contains `a`, `b` and `c`
xbc      // `a` runs out of scope and no longer receive messages
cbx      // `c` and the broken `a` is swapped
cb        // the subscriber list is truncated

Notice how c and b changed order in the example above.

When subscribing on a function, all input from all threads is received. To separate different tasks, one must pass in an identifier as an argument to the function.

Design:

  1. Use Rust's receiver/sender channels passing Dyon variables for easy integratation
  2. The in type has an optional inner type e.g. in[[f64]]
  3. Extensible for other kinds of sender/receiver patterns

Rules:

  • All loaded functions can be turned into send-channels
  • An in-type channel is created by in foo where foo is a loaded function
  • External functions and intrinsic functions can not be turned into send-channels