Is the basic concept of structured concurrency more about programming language design, or applicable to how a program is structured? I can do something like this in JS:
async function doTasks(tasks = [])
// Do some concurrent stuff:
let results = await Promises.all(tasks.map((task) => new Promise((resolve, reject) => {
workerpool.exec('mytask', [task])
.then(function(result) {
resolve(result)
})
.catch(function(err) {
reject(err)
})
})))
// Function pauses here until all tasks resolve.
// do stuff with results
}
The workerpool exists outside the scope of this function, is that kind of the crux of the distinction, or does it come down to how threads / workers are managed (by the language / first class constructs) that outlines the definition of structured concurrency?
The example above feels like a similar pattern to what I've seen in discussions of structured concurrency but as with most things I feel like there's an aha moment where I'll get what all the fuss is about.
I think the simplest expression of the idea of structured concurrency that I've come across is this: The scope of any concurrent process is the same as the lexical scope of the function that created it. This has profound implications.
Let's say, for example that I call `doTasks()` with 10 separate tasks. Before any of them can complete, one of them fails. What happens to the other 9? In your example, they are "leaked" because they will continue running, even though the scope that was awaiting their results in gone. Using a "worker pool" as in the example, it might go something like this:
async function doTasks(tasks = []) {
let workerpool = new WorkerPool();
try {
for (let task of tasks) {
workerpool.exec("my task", task);
}
let results = await workerpool.all();
// do stuff with results
} finally {
// no matter the outcome, nothing outlives this scope.
await workerpool.destroy();
}
}
It should be noted that cancellation, i.e. the ability to "destroy" a concurrent task is a necessary primitive for structured concurrency. You can think of it as the equivalent of automatically releasing memory at the end of a lexical scope.
This helps complete the picture, thanks. In my current use-case I am batching up precomputing some expensive calculations in a genetic algorithm which all needs to be finished before I can run the competition. If one of the phenotypes throws an error Promises.all will catch it and my program will continue and I can handle the error, but as for my workers, yup they will leak - and I can't stop that using promises / async / await without leaking some internals of my workers with some shared semaphore or something, which is so 1990's. So yours this was a perfect example that helped bring it all together.
Funnily enough after this thread I did some digging around and came across effection as well, I'd never come across the term "structured concurrency" before but it feels like a natural next step on what I'm trying to accomplish, so I'll definitely be keen to see more development in this area and maybe some first class language constructs at some point.
Thanks for taking the time to give an example and explain the concept in relatable terms.
In my opinion it's mostly a pattern where you make sure that all sub-tasks are fully contained within child tasks and never outlive them. You can model that patten in most programming languages - e.g. as you do in JS.
However there can be environments which have structured concurrency baked in and thereby enforce the pattern. Kotlin coroutines are one example here.
So I guess my example resembles some form of structured concurrency but the threads are not encapsulated in the parent's closure. I still need to structure my promises and await / async logic to deal with the edge cases and make sure the program doesn't get stuck or leave threads hanging. That makes sense, thanks.
So I glossed over this thread when I first saw it, and something drew me back.
My first thought when reading the description is that it reminds me of the 'with' semantics you see for resource management, where the API gives you an resource that lives for the lifetime of the closure, and then is destroyed or returned to a pool.
That's is a refinement of RAII, where the API may initialize the connection or may allocate an existing one to you that is no longer in use (serial acquisition).
This feels like a refinement of that for thread lifetimes, which are probably not a bad thing.
I think I'm getting the picture from this and other replies. To get close to a sense of structured concurrency I've had to lean on a plugin that handles worker threads (or would have to write one myself), and tie this in with async/await language features, and add some checks to handle thread lifetime and error states.
It would be really nice to have a first class language feature to wrap all this up, it sounds like that's what structured concurrency as a concept is aiming towards.
The example above feels like a similar pattern to what I've seen in discussions of structured concurrency but as with most things I feel like there's an aha moment where I'll get what all the fuss is about.