I was reading Dominik's excellent book Nim in Action and I reached the part where he mentions:
When a thread crashes with an unhandled exception, the application will crash with it. It doesn't matter whether you read de value of FlowVar or not.
Down the paragraph he states that:
This behavior will change in a future version of Nim, so that exceptions aren't raised unless you read the value of the FlowVar.
I have searched the blog on releases and I haven't found any reference to a solution. May be I missed it or maybe it is solved.
Any information about it would be appreciated.
The behavior is still the same, unless it changed very recently.
See also https://github.com/nim-lang/Nim/issues/15167 . ;-)
If you use multithreading, I strongly recommend you not to use exceptions in your code.
Use Option or Result[T, cstring] from https://github.com/status-im/nim-stew/blob/1db43c72/stew/results.nim instead and ensure that no exceptions can happen or are all handled in the proc that will be distributed by tagging your proc with {.raises: [].} or only allow unrecoverable errors with``{.raises: [Defect].}``. You can use {. push raises:[].} to apply that to the whole file.
The reason why is that Nim exceptions are ref object and so involve the thread-local GC, which is ... thread-local. If your thread dies to an exception, the GC is also dead and so you are likely accessing undefined memory.
For the same reason, Result[T, cstring] is preferable to Result[T, string] as C-string will not involve a GC-managed type be shared across threads with the memory owner dying to an exceptions.
This might change with --gc:arc and/or in C++ mode if Nim exceptions uses C++ exceptions handling (without allocating for the message).
It's easy to wrap the .thread proc like so:
var errorChannel: Channel[string]
open errorChannel
proc myThreadProc() {.thread.} =
try:
stuffThatCanFail()
except:
errorChannel.send getCurrentExceptionMsg()
createThread(th, myThreadProc)
joinThread(th)
# check for errors: ...
It works, no reason to fear exceptions.
If you use multithreading, I strongly recommend you not to use exceptions in your code.
I think that's overly extreme...
ensure that no exceptions can happen or are all handled in the proc that will be distributed
That's the same as C++ (and some other languages) — an uncaught exception is a fatal error, so a top-level function must not let any escape. Pretty standard stuff, not a reason not to use exceptions.
The reason why is that Nim exceptions are ref object and so involve the thread-local GC, which is ... thread-local. If your thread dies to an exception, the GC is also dead and so you are likely accessing undefined memory.
I don't understand this statement. Who is accessing undefined memory? Not the throwing thread; it's gone. If the exception object is passed to some other thread (the one that created the dead one?) then the runtime must be taking steps to make the object valid in the other thread, since this situation implies that the original thread is gone.
Also, with ARC/ORC isn't it now legal to move a ref from one thread to another?
That's the same as C++ (and some other languages) — an uncaught exception is a fatal error, so a top-level function must not let any escape. Pretty standard stuff, not a reason not to use exceptions.
But it's not the case in Java, C#, Go because of thread-safe GC and it's not the case of Javascript, Python and scripting languages in general because they don't support threads. Threads have already many ways to shoot yourself in the foot, we might as well have basic protection and then remove it as needed.
I don't understand this statement. Who is accessing undefined memory? Not the throwing thread; it's gone. If the exception object is passed to some other thread (the one that created the dead one?) then the runtime must be taking steps to make the object valid in the other thread, since this situation implies that the original thread is gone.
When an exception is thrown, and another thread try to access the error message for example that thread will segfault.
Also, with ARC/ORC isn't it now legal to move a ref from one thread to another?
Yes.
That said, technically Nim channels can handle "moving" refs by deep copying today and using a hidden RTTI field.
But it's not the case in Java, C#, Go because of thread-safe GC and it's not the case of Javascript, Python and scripting languages in general because they don't support threads.
Why do you think Python doesn't support threads? :-)
When an exception is thrown, and another thread try to access the error message for example that thread will segfault.
But ... how did the surviving thread get ahold of that error message? There would have to be something built into the exception runtime that sent it from the dead thread to the one that created it. In that case, the runtime must be responsible for doing that in a way that doesn't cause heap errors, e.g. by copying it into the destination heap, otherwise it's a runtime bug.
Why do you think Python doesn't support threads? :-)
Most curious, so Python solved the Global Interpreter Lock? AFAIK Python supported multiprocessing but not multithreading.
But ... how did the surviving thread get ahold of that error message? There would have to be something built into the exception runtime that sent it from the dead thread to the one that created it. In that case, the runtime must be responsible for doing that in a way that doesn't cause heap errors, e.g. by copying it into the destination heap, otherwise it's a runtime bug.
It got the ref address but crashed when dereferencing it. Si it actually doesn't get the error message.
Most curious, so Python solved the Global Interpreter Lock? AFAIK Python supported multiprocessing but not multithreading.
It's complicated. Python had threads for ages. The Global Interpreter Lock (GIL) limits whether threads can actually run in parallel (instead of only concurrently). So technically, you can have multiple threads in Python, but for CPU-bound code in Python you won't be able to get the speed benefit you might expect from using threads. On the other hand, as far as I know, many C extensions for Python release the GIL when their code runs, so in these cases you actually have parallelism.
People use multiprocessing with Python to work around the GIL implication for CPU-bound code, but of course this causes additional cost for serialization/deserialization, so multiprocessing may not be practical in some cases where non GIL-limited multithreaded code would be a good solution. Fortunately, in many cases multiprocessing does help. :-)
ensure that no exceptions can happen or are all handled in the proc that will be distributed by tagging your proc with {.raises: [].} or only allow unrecoverable errors with``{.raises: [Defect].}``
Thanks for the idea!
I'm now trying this in my thread code. I was already catching a custom exception (inheriting from CatchableError) and when I used raises: [], the compiler told me there could be an XmlError s as well. Ok, then I changed the code to catch CatchableError (which IMHO is acceptable under the circumstances where the except clause occurs). After that, the compiler told me the code could still raise Defect. As https://nim-lang.org/docs/manual.html#effect-system-exception-tracking and https://nim-lang.org/docs/system.html#Defect (and the text further down there) indicate, Defect is for low-level errors that probably shouldn't be caught. That's fine with me, so I changed the raises pragma to raises: [Defect].
However, now the compiler tells me that the code could still raise Exception. Since Exception is the base class of both Defect and CatchableError, I don't want to allow it in the raises pragma. As I understand it, this would defeat the purpose of the pragma since this would mean the same as not having the pragma and its resulting compiler checks at all.
I tried using the {.effects.} pragma to find out from where Exception may be raised, but I "only" get a list of possible exceptions, but nothing about their possible origins.
In case it helps, the proc where I want to add the pragma is https://hg.sr.ht/~sschwarzer/vppdiff/browse/src/vppdiff/convertvpp.nim#L167 ( convertedThreadWrapper). (The currently pushed code only catches ConversionError, not CatchableError, but I'm still stuck if I catch CatchableError, see the previous paragraphs.)
What do you recommend?
We usually use at the top level of each file {.push raises: [].} or {.push raises: [Defect].}
@sschwarzer, unfortunately the exception tracking has not been touched for a while and it's only recently that we pushed it at Status (and so upstream) since it was very helpful to track error due to unhandled exceptions especially in async code. Also it's very helpful for auditors to eliminate a whole class of errors (along the other excellent tools Nim provides: https://status-im.github.io/nim-beacon-chain/auditors-book/02.3_correctness_distinct_mutability_effects_exceptions.html)
I think the best would be to ask for better stacktraces for the effect system. {.gcsafe.} and {.noSideEffect.} are somewhat similar (but a bit better) in that you need to add those at each level to find the offending side effect. In contrast, generics or concepts give you a whole tacktrace of the problematic instantiation.