Workers

The fundamental threading primitive is a worker. All workers are created from a function, and they run on either GPU or CPU, applying that function to data passed to them. If the function returns that data can be read back from the worker.

A simple example of logging in a background CPU thread is

cpu fn do_log(str string) {
    print_ln("log: ", str)
}

let w = cpu do_log

send(w, [string] { "line 1" })
send(w, [string] { "line 2" })

Here the cpu keyword is used to convert the function to a CPU-side worker, and send is a special function that ships data to the worker. In cases like this where the function has no parameters it will act very similar to a normal thread creation, dispatching the function in the background.

However if the function returns data it can be read back from the worker.

fn square(val i64) i64 {
    return val * val
}

cpu fn main() {
    let w = cpu square

    send(w, [i64] { 1, 2, 3 })

    print_ln(receive(w))
    print_ln(receive(w))
    print_ln(receive(w))
}

Or if you want to wait for all data to be processed, you can drain the worker.

fn square(val i64) i64 {
    return val * val
}

cpu fn main() {
    let w = cpu square

    send(w, [i64] { 1, 2, 3 })

    let rets = drain(w)
    print_ln(rets[0])
    print_ln(rets[1])
    print_ln(rets[2])
}

or more idiomatically

fn square(val i64) i64 {
    return val * val
}

cpu fn main() {
    let w = cpu square

    send(w, [i64] { 1, 2, 3 })
    for ret: drain(w) {
        print_ln(" ", ret)
    }
}

Please note that workers are non-blocking, they are a threading primitive, not a synchronisation primitive.

Channels can be created GPU-side with the gpu primitive, to dispatch the above on the GPU is a trivial change, and perhaps the most distinguishing aspect of Eyot's design

fn square(val i64) i64 {
    return val * val
}

cpu fn main() {
    let w = gpu square

    send(w, [i64] { 1, 2, 3 })
    for ret: drain(w) {
        print_ln(" ", ret)
    }
}

Please note that although any function can be passed to the cpu keyword for conversion to a CPU-side worker, only location independent functions (those not tagged with cpu) can be passed to the gpu keyword for conversion to a GPU-side worker.

Not all work is done on vectors of data with no other parameters of course, often you pass uniform parameters to GPU kernels. Worker functions in Eyot need to take a single parameter, which becomes the input type of the worker. Structs can be passed of course, but would not be an idiomatic way of passing constant parameters to a worker.

A better solution in Eyot is the partial application of functions. For example in the following you partially apply the two parameter function multiply, converting it to the single parameter triple function, and then send it values.

fn multiply(lhs, rhs i64) i64 {
    return lhs * rhs
}

cpu fn main() {
    let triple = partial multiply(_, 3)
    let w = gpu triple

    send(w, [i64] { 1, 2, 3 })
    for ret: drain(w) {
        print_ln(" ", ret)
    }
}

This is a lot more convenient than passing a struct in for the purpose of providing context to a function.