top | item 29641959

(no title)

talex5 | 4 years ago

To clarify that, there are two systems here:

- domainslib schedules all tasks across all cores (like Go).

- eio keeps tasks on the same core (and you can use a shared job queue to distribute work between cores if you want).

Eio can certainly do async IO on multiple cores.

Moving tasks freely between cores has some major downsides - for example every time a Go program wants to access a shared value, it needs to take a mutex (and be careful to avoid deadlocks). Such races can be very hard to debug.

I suspect that the extra reliability is often worth the cost of sometimes having unbalanced workloads between cores. We're still investigating how big this effect is. When I worked at Docker, we spent a lot of time dealing with races in the Go code, even though almost nothing in Docker is CPU intensive!

For a group of tasks on a single core, you can be sure that e.g. incrementing a counter or scanning an array is an atomic operation. Only blocking operations (such as reading from a file or socket) can allow something else to run. And eio schedules tasks deterministically, so if you test with (deterministic) mocks then the trace of your tests is deterministic too. Eio's own unit-tests are mostly expect-based tests where the test just checks that the trace output matches the recorded trace, for example.

The Eio README has more information, plus a getting-started guide: https://github.com/ocaml-multicore/eio/blob/main/README.md

discuss

order

Zababa|4 years ago

Thank you for the clarification. So if I understood correctly, distributing independant jobs that use async IO between core is okay? For example, I recently wrote a program that reads all the files in a folder, calculates a hash of their contents, and then rename them as their hash. I did this in Go. Since all operations are "perfectly independant", I only had to do synchronization for whole program stuff: make sure the program doesn't exit when goroutines are sleeping, and use channels to not exhaust the file descriptors. From what I understand, I can launch every opening-hashing-renaming operation in a separate goroutine, and Go will take care of making everything async and multicore at the same time.

Now let's imagine I want to do the same program in OCaml. I think my options are:

- on current OCaml, thread-based concurrency but no parallelism

- on current OCaml, monadic concurrency (Lwt, Async) but no parallelism

- on multicore OCaml, direct/effect-based (I'm not sure what's the right word) concurrency with eio, which is deterministic. If I want parallelism here, I have to explicitely create and use a shared job queue, while the Go runtime does this implicitly. Since the standard library Queue is not thread safe, I would have to use Mutex to avoid concurrent access.

Is this correct? I've read the eio documentation but it's hard to wrap my head around all of that without examples. I've found the Domain_manager which looks like what I want. For example, I could have the main thread fill the queue and for each core available, I could launch a Domain_manager.run toto, with toto taking jobs from the queue that would be shared between all domains?

talex5|4 years ago

Instead of Stdlib.Queue you can use Eio.Stream, which is thread-safe (and will take care of waking sleeping threads when data becomes available).

The README shows an example of a pool of workers pulling jobs from an Eio.Stream:

https://github.com/ocaml-multicore/eio#example-a-worker-pool

We're still exploring what APIs to provide for this kind of thing, and in particular how to unify domainslib and eio.

mst|4 years ago

I continue to be rather impressed by raku's 'Atomic Int' type plus the atomic operators (and while normally I dislike emoji in identifiers, them using the atom symbol to make the atomic operators stand out when debugging actually seems rather neat in this scenario).

See the example near the end of https://raku-advent.blog/2021/12/01/batteries-included-gener... for what I mean.