Hi,
I've just read grandha's latest/last Nim blog entry: "Goodbye Nim, and good luck" http://gradha.github.io/articles/2015/02/goodbye-nim-and-good-luck.html
Sorry for bringing this up, this news was new to me. The problem as I understand: every thread has their own GC, therefore we cannot send a pointer (e.g., by using a channel) from a thread to another. The reason is simple: it's unclear that who will free() the memory after it is not used and you can't be sure that the other thread is not using the data anymore.
Is it a problem with all GCs (even with Boehm GC)? Can we have a workaround for this? Or a simple solution (e.g., have a global collection for all the shared pointers as keys and the stored value could be how many threads can see this pointer)?
I'm not a GC expert. Could you write me more details or send me links to read?
Oh not that again...
This has never been true and there are lots of workarounds available, starting with casting the ref to ptr and pass it via a channel, using Boehm's GC instead or using manually managed memory and locking instead of channels. It's true that Nim's concurrency design doesn't win beauty contests yet but it's also good enough for version 1 and lots of stuff is in the pipeline for improving it. For example, pony-lang describes a novel efficient GC algorithm to deal with exactly this problem. (Pony uses thread local heaps too.)
I'm surprised to hear that there are a lot of workarounds available. I've asked exactly this on SO, and never got a reply:
Maybe it would make sense to clarify these workarounds there? If these workarounds are too hard to find for the average Nim newbie, it would definitely be a big help. I actually spend some time trying to come up with a specific workaround, but it did not work, which is a separate unresolved question on SO:
In fact, I've already stopped using Nim for one of my projects, because I simply couldn't find a good workaround :(.
The Boehm GC works perfectly fine for this use case if you don't need (hard or soft) real time performance. With the caveat that you give up guarantees of memory safety for shared memory.
@bluenote: There are workarounds, but they require some effort to implement, too much for a Stackoverflow post.
@bluenote: I tried to bring your code (from StackOverflow) alive and make it work. I'm far for any solution. However I found that copying a channel breaks the connection:
import threadpool
import os
var ch: Channel[int]
ch.open()
var ch2 = ch # copy
ch.send(5) # send to the old one
sleep(1000)
echo ch2.tryRecv() # read from the new, prints (dataAvailable: false, msg: 0)
Could you please share some examples how to share a newly created channel with a different thread as @bluenote asked? I tried to do it myself (the code was compiled with and without --gc:boehm every time), but failed to make it work.
@mora: The problem that you have is that variable assignment does a copy [1]. With the Boehm GC, you can use ref and ptr types for shared memory and pass the references between threads. Of course, you then have to ensure that all threads use proper locking for shared objects (this should happen automatically for channels).
[1] Because channels are value types, not reference types. The frequency of value types in Nim is the #1 thing that tends to trip people up; they're used to reference types being the default (or only option) in other languages.
I can't see any usecase when copying (with the current implementation) a channel would do anything useful for anybody. If that is true, then maybe it should give a compile time error.
Just an idea: could we have a class similar to Rust's Arc<T>? It's a reference counting smart pointer. So we would do memory management by hand (using alloc instead of new), have a counter how many instance have copy (we increases this counter at each copy() call and decreases at each destructor call). Of course this counter and a mutex (to protect copy() and destructor calls to this counter) have to be stored on the heap as well. This way we could share memory between threads. Moreover, the Channel should be wrapped to this type by default, which makes the assignments possible.
mora: I can't see any usecase when copying (with the current implementation) a channel would do anything useful for anybody. If that is true, then maybe it should give a compile time error.
The point behind value types is generally not the copying semantics, but avoiding the cost of a level of indirection. This can be relevant if you embed objects inside other objects or if you use local or global variables that you only pass as arguments to other procedures, but never assign.
In the case of channels, you have the additional problem that it's not necessarily clear whether you want a ref or a ptr. Having a value type allows you to use both.
What channels needs is more convenience functionality for channel management.
mora: Just an idea: could we have a class similar to Rust's Arc<T>? It's a reference counting smart pointer.
This doesn't address the underlying problem, which is that it's currently cumbersome and unsafe to deal with objects on the shared heap. Nim's current concurrency model is pretty narrowly focused on either (1) running as a quasi-distributed system where threads are isolated from each other or (2) iterating over arrays. This, unfortunately, leaves out a lot of applications.
In general, Arc<T> and RWArc<T> are also too limited; you want a more general solution.
@bluenote: I am not sure, but it seems to be a problem with trying to pass a closure iterator to another thread; closure environments by their nature are thread-local.
Channels can definitely be passed to other threads as pointers, e.g.:
{.experimental.} # for auto-dereferencing convenience
type SharedChannel[T] = ptr Channel[T]
proc newSharedChannel[T](): SharedChannel[T] =
result = cast[SharedChannel[T]](allocShared0(sizeof(Channel[T])))
open(result[])
proc close[T](ch: var SharedChannel[T]) =
close(ch[])
deallocShared(ch)
ch = nil
proc someThread(ch: (SharedChannel[string], SharedChannel[bool])) {.thread.} =
let (mainChannel, responseChannel) = ch
while true:
let s = mainChannel.recv
if s == nil:
break
echo s
responseChannel.send(true)
responseChannel.send(false)
proc main() =
var
mainChannel = newSharedChannel[string]()
responseChannel = newSharedChannel[bool]()
th: Thread[(SharedChannel[string], SharedChannel[bool])]
createThread(th, someThread, (mainChannel, responseChannel))
for i in 0..10:
mainChannel.send($i)
if not responseChannel.recv:
break
mainChannel.send(nil)
joinThread(th)
close(mainChannel)
close(responseChannel)
main()
@Jehan: I never used allocShared0 before. Do you need to free the memory? Maybe an Arc/RWArc implementation could handle that.
Thank you for the example, I'll play with it when I get home.
mora: Do you need to free the memory?
Yes. I should probably update the example in order to do this.
Edit: changed.
Edit: A clarification. If you use the Boehm GC, you don't need to explicitly deallocate. With the Boehm GC, all ptr and ref references are both garbage-collected. Basically, the Boehm GC gives you a global shared heap.
@Jehan: I don't understand. Are the ptr references garbage-collected? According to the manual: "Traced references are declared with the ref keyword, untraced references are declared with the ptr keyword." If I alloc some memory because I want to pass it to a C function (which will free the memory), then how can I do that? (Sorry for not reading the code, where I could find the answer.)
By the way, I did some tests with valgrind. There is no unfreed memory with and without the deallocShared call. However, we have always unfreed memory if I use --gc:boehm.
Sorry for keep pushing this topic. I think that we need a hell of a tutorial about concurrency patterns (and macros, sequtils, etc).
Thanks again, Peter
If I alloc some memory because I want to pass it to a C function (which will free the memory), then how can I do that?
That surely is a bad idea but if the C code uses Ansi C's free proc, you need to wrap and use C's malloc of course, everything else cannot work.
By the way, I did some tests with valgrind. There is no unfreed memory with and without the deallocShared call. However, we have always unfreed memory if I use --gc:boehm.
Valgrind is worthless with Nim code as it doesn't know about Nim's allocators.
mora: Are the ptr references garbage-collected?
The Boehm GC is a conservative garbage collector; it does not understand the difference between ptr and ref. It will treat every machine word that contains the address of an allocated area of memory as a reference.
I don't see any actual problem with Nim in this area, but rather an issue of people getting a slightly confused by the default memory handling being a little different from what they expected or were used to.
To me the way GC and threads works in Nim makes sense and seems like good engineering.
I also personally think in most (not all) cases keeping the bulk data where it is needed and sending messages between channels is the simplest and in the long wrong leads to the least hassles.
And for other cases you can do what Araq suggested to optimize performance.
A few advantages you do have with Nim are macros and the open code and community. So if there _is some additional/novel abstraction or convenience or GC to make real improvements in these matters and that is a significant concern of yours, the Nim community and language seems to me the place to be, and leaving it makes no sense.
I personally think the extreme type of manual memory management you suggest @mora, no offense, is quite a poor engineering decision, and does not address the 'problem'. My personal opinion is that the authority of Mozilla has caused quite a few people to be somewhat misled by Rust in certain areas (although I am not saying its all bad).