Concurrency and parallelism
Concurrent programs use CPU time efficiently without being blocked on I/O or data synchronization. Parallel programs leverage multi-core CPUs to compute things in parallel faster than sequential programs.
This page describes how to write concurrent and/or parallel programs in Pen.
Built-ins
Pen provides several built-in functions for concurrent and parallel programming.
go
function
The go
built-in function runs a given function concurrently, and possibly in parallel.
future = go(\() number {
computeExpensive(x, y, z)
})
The go
function returns a function of the same type as the given argument. The returned function returns a resulting value of the function execution. In other languages, such functions returning values computed concurrently when they are ready are also known as futures or promises.
The go
function may or may not run a given function immediately depending on its implementation. For example, the standard Os
system package runs the given function in parallel if multiple CPU cores are available.
race
function
The race
built-in function takes multiple lists and merge them into one by evaluating elements in each list concurrently and possibly in parallel. The resulting list contains the elements in the original lists in order of their finished times of computation. Remember that elements in lists are evaluated lazily.
zs = race([[number] xs, ys])
This functionality is similar to concurrent queues in other imperative languages, such as channels in Go. Input lists to the race
function correspond to producers of elements into the queue, and a consumer of the queue is codes that use elements in the output list.
Patterns
Task parallelism
The go
function can run different codes concurrently. For example, the following code runs the functions, computeA
and computeB
concurrently. Runtimes of applications might execute those functions even in parallel if their system packages allow that.
compute = \(x number, y number) number {
z = go(\() number { computeA(x) })
v = computeB(y)
v + z
}
Data parallelism
To run the same computation against many values of the same type, you can use recursion and the go
function.
computeMany = \(xs [number]) [number] {
if [x, ...xs] = xs {
y = go(\() number { foo(x()) })
ys = computeMany(xs)
[number y(), ...ys]
} else {
[number]
}
}
The example above computes things in order of elements in the original list. However, you might want to see output values of concurrent computation in order of their finished times. By doing that, you can start using the output values as fast as possible without waiting for all computation to be completed. In this case, you can use the race
function to reorder elements in the output list by their finished times.
compute = \(xs [number]) [number] {
race([[number] [number x()] for x in computeMany(xs)])
}
If you want to evaluate elements in multiple lists concurrently, you can simply pass the lists as an argument to the race
function. Note that elements in the same lists are not evaluated concurrently although elements in different lists are evaluated concurrently.
compute = \(xs [number], ys [number]) [number] {
race([[number] computeMany(xs), computeMany(ys)])
}