0

I've been using threadpool in Nim and have encountered the requirement that spawned functions cannot accept a mutable argument. However, I want to pass a proc a Lock, which in turn has to be mutable, according to the type of acquire. The only way to get around this I've found is by having the lock mutable and declared in global scope, so I don't have to pass it into the function I spawn.

But I'd really rather avoid that. I had the idea of using a pointer - so the lock could be mutable, but the pointer itself isn't - to get around this, but it looks like pointers aren't really first-class objects in Nim. I tried just declaring the parameter of waitLock to be ref (line 3), and I still get the complaint that acquire must be passed a var Lock and not a ref Lock here. And it looks like dereferencing pointers is also done automatically, so there's no way around it...? Is there any way to get around using dynamic scoping and have the lock be explicitly passed into the proc? Is it with good reason that I can't do what I want? Or have I just missed the dereference operator in some manual? What would the cleanest way to implement this be?

import os, threadpool, locks

proc waitLock(lock: ref Lock): void =
  acquire lock
  echo "Child thread has lock!"
  release lock

var lock: Lock
initLock lock

spawn waitLock(lock)
acquire lock
echo "Parent thread has lock!"
release lock

sync()
deinitLock lock

1 Answers1

0

What would the cleanest way to implement this be?

Use global locks. It's true that globals are considered poor style when they reduce encapsulation and make code less easy to reason about, but things like Channels, Locks, and Thread objects are semantically global, so imho these criticisms don't apply

Is it with good reason that I can't do what I want?

yes. it is inherently unsafe for a thread to mutate a parameter, so it's proper for Nim to disallow passing a var parameter to a thread in general.

Why is that any different from mutating a global?

Nim's memory model is a bit different. Quoting from the manual:

Each thread has its own (garbage collected) heap, and sharing of memory is restricted to global variables. This helps to prevent race conditions. GC efficiency is improved quite a lot, because the GC never has to stop other threads and see what they reference.

This also means that GC objects (anything containing a ref,string, or seq) can't be passed between threads, even if global or wrapped in a Channel or SharedList.
Quoting from passing channels safely:

Note that when passing objects to procedures on another thread by pointer (for example through a thread's argument), objects created using the default allocator will use thread-local, GC-managed memory. Thus it is generally safer to store channel objects in global variables (as in the above example), in which case they will use a process-wide (thread-safe) shared heap.

However, it is possible to manually allocate shared memory for channels using e.g. system.allocShared0 and pass these pointers through thread arguments

This restriction is lifted when using --gc:orc/--gc:arc, and the new Isolated permits safe copy-free moving of sub-graphs between threads. A new implementation of Channels which uses this mechanism will be in the next release (whatever's after 1.4.6)

pointers aren't really first-class objects...no way around dereferencing pointers..Have I just missed the dereference operator?

While Nim encourages the use of ref(traced references) for safety and ease, as a systems language of course pointers (untraced references) are fully supported.

To get an untraced reference from a mutable object, you use addr, and the syntax for dereferencing a ptr is the same as for a ref: []

The bit of the manual you were overlooking is here

Here's that syntax at play in your example:

import os, threadpool, locks

proc waitLock(lock: ptr Lock): void =
  acquire lock[]
  echo "Child thread has lock!"
  release lock[]

var lock: Lock
initLock lock

spawn waitLock(lock.addr)
acquire lock
echo "Parent thread has lock!"
release lock

sync()
deinitLock lock
shirleyquirk
  • 1,527
  • 5
  • 21
  • Thanks a lot, very comprehensive! I have a few questions still. Why is using a global lock better? I see a lot of people saying that globals are to be avoided and I can see why. What's different about thread sync structures? You wrote "it is inherently unsafe for a thread to mutate a parameter". How is this worse than threads mutating globals? – LemongrabThree May 07 '21 at 10:16
  • glad i could help. yes, 'globals considered harmful' is conventional wisdom, but imho an exception should be made for things like locks,channels,thread objects that are, well, global. this applies doubly so for Nim code, i'll update my post to discuss why. – shirleyquirk May 07 '21 at 10:25
  • 1
    Is this doable with `ref`, too? – LemongrabThree May 07 '21 at 10:43
  • great question: no. (unless using gc:arc/orc) I hope i've explained that above, please tell me if there's something i can clarify :-) – shirleyquirk May 07 '21 at 10:59