If you chase two hares, you won't catch a single one.
(Russian folk proverb)
Perhaps the biggest drawback of multithreading is the additional barriers to successfully debugging subtle bugs. The operating system does the switching of threads, and it does it according to its own understanding, without asking us when to execute which thread and when to pause. These decisions of the system depend on external factors (work with other applications, the network, the state of the swap file on the disk, etc., etc. - unless the weather on Mars is in this list). The situation is aggravated in the case of multi-core processors, where multiple threads can actually run completely in parallel. Therefore, with multiple executions of the same application with the same data, even if you are trying to reproduce the entire sequence of pressed buttons, there is no certainty. that you can accurately reproduce the desired event. This is especially unpleasant if the desired event is a repetition of an error that occurred under the same conditions in your program.
The error that can be reproduced is easy to fix. It is often enough to stop the program and go into single-step debugging mode shortly before the situation in which the crash occurred. The bad thing about elusive mistakes is that they are almost impossible to fix as long as they remain elusive. In the case of a multi-threaded application, many errors that are easy to catch in a normal single-threaded case turn into elusive ones.
Sometimes in such cases it helps to write code like this when multithreading is optional. For example, with the conditional compilation symbol, you specify whether to build your application for multi-threaded or single-threaded work. And then the code uses conditional branching, driven by conditional compilation symbols. And depending on whether your conditional compilation symbol is defined or not, the threads are run or not, and in the case of a single thread, all actions in the application are executed sequentially.
Unfortunately, this path is not only difficult from the start (since instead of developing and debugging one application, you actually have to create two different applications and debug them separately), but it is not always useful for debugging purposes. The error that occurs with enviable regularity in a multithreaded case, when multiple threads are disconnected, suddenly disappears.
For KOL, I came up with a replacement for streams with pseudo-streams, in which the application does not basically change its behavior, but in fact becomes single-threaded. To turn streams into pseudo-streams, just add the PSEUDO_THREADS conditional compilation symbol to the project options and build the application. For each pseudo-thread, except for the main thread (represented by the global variable MainThread), a block of memory is allocated to store the stack. The block size is 1 MB by default, but can be changed by setting the PseudoThreadStackSize variable to the desired value.
Pseudo-Threads, like regular streams, can be started (Resume), suspended (Suspend), and switched. The only difference is that pseudo-thread switching is not managed by the operating system, which now considers the entire application to be single-threaded, but by the main pseudo-thread. Switches now occur automatically in just a few places: in the Applet.ProcessMessage method, in the Sleep procedure, and in the WaitForMultipleObjects and WaitForSingleObject functions. Of course, to extend the functionality of these three API functions, if the PSEUDO_THREADS symbol is defined, the KOL module declares its own versions of these functions that can call the MainThread.NextThread method when the current pseudo-thread has nothing else to do.
Thus, without changing the application code, the multi-threaded application becomes single-threaded. The streams are preserved, but in a somewhat truncated form. For debugging purposes, this model can be extremely useful, as pseudo-streams continue to "emulate" (mostly) the behavior of streams. Although, without observing some rules, such a model may not work. Namely:
•Critical sections should not be used to control exclusive access to shared resources: the thread is now one, and no control will actually be performed. It will be much more useful to use semaphores for the same purpose, for example: they will work successfully for both real threads and pseudo-threads;
•You should not use the multimedia timer to organize pseudo-stream switching. If you try to call the MainThread.SwithToThread or NextThread method directly from the multimedia timer event handler, the application will simply break, since the call will actually be made from a really separate thread created by the system for each active multimedia timer. If this action is performed by sending a message (SendMessage), this message will still be processed only in the message handler, i.e. only when the main pseudo-thread receives control, so there is no special sense in such a switch;
•You should not take advantage of the fact that when working with window objects (when creating them) outside the main thread, such windows usually do not appear on the screen, remaining invisible. Or, if you create message handlers in additional threads in order to work with its own window objects in each thread, then in the case of pseudo-threads, when there is only one real thread, this model will most likely not work (only the loop last started).
•And, conversely, moving on to pseudo-threads, remember that in the case of normal threads with window objects, work usually only happens in the main thread. And if in the process of working with pseudo-streams, you start to change the code and directly work with window methods and messages, then this can further prevent the return from pseudo-streams to regular streams.
In fact, the transition to pseudo-streams is not a sufficient condition to ensure that all events occurring in the application are accurately repeated (for example, for debugging purposes). In addition to switching streams by the system, timers, both regular and multimedia, as well as messages from the mouse and keyboard, are still elements of randomness. But at a certain stage of execution, the probability of a repetition of events increases significantly, which means that the chances of localizing the source of the error increase. And in principle, it becomes possible to log all events affecting the operation of the application, and then, on subsequent launches, reproduce them one-to-one. But you will have to do it with your own code.