Recommended Approach to Locks

In some utilities I previously created using non-Racket languages, I'd track information referenced by multiple routines by using a struct with a R/W lock as a field; then I could create functions that would lock and defer an unlock on the struct as fields were updated without worrying about race conditions.

Is there a recommended method to reproduce this functionality in Racket?

To be clear, I'm looking for a way to save information that multiple processes can access, both to read and write, while preventing a race condition.

Sounds like you want semaphores 18 Concurrency and Synchronization however that page describes many possibilities, so also take a look at the other ones. For semaphores I recommend using call-with-semaphore (when it makes sense / is possible).

Ideally you might be able to avoid having one central piece, so that instead different threads can work on their own pieces of the data independently and then you could use thread sends or channels to collect and merge together the results, but sometimes it is not really avoidable. Async channels also may make sense depending on what you are doing.

1 Like

Thanks! I'll take a look through the link. I'm still learning the language and I wasn't sure if there was a single preferred way to do what I was describing, and this sounds like there's several different but similar options. If that's the case, I feel a little better about my uncertainty!

1 Like

The things that are described there also work together / can be used in combinations.

For example semaphores, channels, async-channels, threads and many other things can be used as syncronizable events, which means that you can use sync to wait for multiple things at the same time and also handle them individually with handle-evt this is useful for threads that collect results from multiple other threads or for threads that are used as "control" threads (listening for multiple inputs / receiving control messages / then possibly creating or terminating worker threads etc.)

Async-channels are useful as work queues because multiple threads can put to and get from them, so they can be used to distribute work or collect results. When you create them with a limited max size they can be useful as a buffer between producers and consumers, for example if your producers are faster then your consumers, an unlimited async-channel would keep growing taking more and more memory if you limit it to number of consumers + a few more (so that consumers always have work items waiting for them) then producers are blocked/paused whenever consumers are too slow.
(Why would you want multiple consumers in a non parallel, concurrent szenario? For example in a webscraper, where every consumer processes a link by downloading it and sends the links it finds to a found-links-queue that is processed by a thread (which has a set of seen links) that adds the link to the work queue if it wasn't seen yet. Downloading is often mostly, concurrently with other downloads, waiting for input.)

When you pair your data so that mostly threads own the data they need to access, then you don't have to worry about other threads while accessing that data. Keep in mind that threads are pretty light weight and you can create many of them and give each what it needs, on construction or for example by receiving it via thread-receive.

Semaphores are useful when you want to make something look atomic to multiple threads, while also allowing them to read/write fairly arbitrary. Doing this may seem like an easy solution (and I also use it), but it is very possible that this creates a lot more contention between the different threads, then is necessary with a less hammer like solution.

The other things I wrote are mostly about trying to avoid that situation. (Because when you can avoid multiple writers things get easier, especially if you also want parallelism not only concurrency, where contention really becomes problematic)
I will add a disclaimer here that I haven't done much parallelism with racket yet.

If you have specific questions about certain code patterns (with examples) and what could be useful there, it is easier to discuss those, then making "general" claims.

4 Likes

To follow up to this excellent answer, I would also point to the quote people say in the Go community: "Don't communicate by sharing memory; instead, share memory by communicating". 1 This advice applies to Racket as well.

4 Likes