Some of the trickiest bugs to hung down are those involving concurrency and non-deterministic timing. In these cases, stepping through a debugger is not at all what you want, since you may actually change the timing just by trying to observe.
To see the nature of the race condition, just put some print statements in some strategic locations and then see the interleaving, out of order, duplicate invocations etc that are causing the trouble. It's hard to see this type of stuff with a debugger.
Yes, but you can repeat your print-debug-loop once a second, maybe even faster. Hit play and look at the output. Hit play again and see if it changed. It may or may not turn up the concurrency issue.
Stepping through with a debugger will take you at least a minute per cycle, won't turn up the concurrency issue, and will spend a great deal of your daily concentration budget.
I think in this case, because everyone brings up multithreaded examples when saying a debugger isn’t useful, maybe print debugging can lead you towards the path of where to use a debugger efficiently.
I personally think if you can’t use a debugger in a multithreaded codebase, the architecture is bad or one doesn’t understand the code. So yeah, full circle, if print debugging helps one learn the code better, that is only a positive.
I’m so amused about how debuggers have become a debate around here. “Printf vs debugger” is like “emacs vs vi” right now, and it really shouldn’t be. Sometimes I put a breakpoint AT my printf statement.
>printing also will likely impact timing and can change concurrent behaviour as well.
I've had a bug like that and the intuitive way to handle it turned out to be entirely sufficient.
The bug (deep in networking stack, linux kernel on embedded device) was timing sensitive enough that printk() introduced unsuitable shifts.
Instead I appended single-character traces into pre-allocated ring buffer memory. The overhead was down to one memory read and two memory writes, plus associated TLB misses if any; not even a function call. Very little infra was needed, and the naive, intuitive implementation sufficed.
An unrelated process would read the ring buffer (exposed as /proc/ file) at opportune time and hand over to the developer.
tl;dr know which steps introduce significant processing, timing delays, or synchronization events and push them out of critical path
>I appended (...) traces into (...) memory. (...) An unrelated process would read (...) at opportune time and hand over to the developer.
I did something similar to debug concurrent treatments in Java, that allows to accumulate log statements in thread-local or instance-local collections and then publish them with possibly just a lazySet():
Print logging is pretty good for concurrency IMO because it doesn't stop the program and because it gives you a narrative of what happened.
If you have a time travel debugger then you can record concurrency issues without pausing the program then debug the whole history offline, so you get a similar benefit without having to choose what to log up front.
These have the advantage that you only need to repro the bug once (just record it in a loop until the bug happens) then debug at your leisure. So even rare bugs are susceptible.
I have also seen the print statements added for debugging alter the timing with the same effect on more than one occasion, appearing to “fix” the issue.
This is the exact realisation that made me take a second look at FP around 10 years ago. I haven't looked back since. I certainly couldn't debug concurrency issues in imperative code when I was young and sharp, but at least I tried. Now that I'm old, if I get a concurrency issue, I'll just file a ticket and grab a coffee instead.
For concurrency issues you don't want a debugger or printing as both are terrible for this, you want a library designed to specifically detect these issues, I have a custom one but many other people use valgrind etc.
This depends a lot on your stack. If we're talking about concurrency issues in a multi-threaded systems-level program, you're probably right and I can't speak to that. But as a web developer when I talk about concurrency issues I'm usually talking about race conditions between network requests and/or user input, and print works fine for those. The timings at fault are large enough that the microscopic overhead of print doesn't change them meaningfully.
To see the nature of the race condition, just put some print statements in some strategic locations and then see the interleaving, out of order, duplicate invocations etc that are causing the trouble. It's hard to see this type of stuff with a debugger.