How to sleep right and wrong

    Not so long ago, a good article ran through us about the terrible state of performance of modern software ( original in English , Habré translation ). This article reminded me of one anti-pattern of code, which occurs quite often and generally works somehow, but leads to a slight loss of performance here and there. Well, you know, a trifle, which fix the hands will not reach. The only trouble is that a dozen of such “trifles” scattered in different places of the code begin to lead to problems like “I have the last Intel Core i7, and the scrolling is jerking.”

    I'm talking about the misuse of the Sleep function.(case may differ depending on programming language and platform). So what is Sleep? Documentation answers this question very simply: it is a pause in the execution of the current thread for a specified number of milliseconds. It should be noted the aesthetic beauty of the prototype of this function:

    voidSleep(DWORD dwMilliseconds);

    Only one parameter (extremely clear), no error codes or exceptions - always works. There are very few such nice and clear features!

    More respect for this feature, when you read how it works
    Функция идёт к планировщику потоков ОС и говорит ему «мы с моим потоком хотели бы отказаться от выделенного нам ресурса процессорного времени, сейчас и ещё на вот столько-то миллисекунд в будущем. Отдайте бедным!». Слегка удивлённый подобной щедростью планировщик выносит функции благодарность от имени процессора, отдаёт оставшийся кусок времени следующему желающему (а такие всегда найдутся) и не включает вызвавший Sleep поток в претенденты на передачу ему контекста выполнения на указанное количество миллисекунд. Красота!

    What could possibly go wrong? That programmers use this wonderful feature is not for what it is intended.

    And it is intended for software simulation of some external, defined by something real, pause process.

    Correct example number 1


    We write the application "clock", in which once a second you need to change the number on the screen (or the position of the arrow). The Sleep function here fits perfectly: we really have nothing to do for a clearly defined period of time (exactly one second). Why not sleep?

    The correct example number 2


    We write the controller of the moonshine apparatus of the bread machine . The operation algorithm is defined by one of the programs and looks like this:

    1. Go to mode 1.
    2. Work in it for 20 minutes
    3. Switch to mode 2.
    4. Work in it for 10 minutes
    5. Shut down.

    Here, too, everything is clear: we work with time, it is set by the technological process. Using Sleep is acceptable.

    And now we will look at examples of wrong use of Sleep.

    When I need some example of incorrect C ++ code, I go to the code repository of the text editor Notepad ++. Its code is so terrible that any anti-pattern will definitely be there, I even wrote an article about it . Do not let me down nopadpadik ++ this time! Let's see how Sleep is used in it.

    Bad example number 1


    When starting, Notepad ++ checks if another instance of its process is already running and, if so, looks for its window and sends it a message, and closes itself. To detect its other process, a standard method is used - the global named mutex. But the following code was written to search for windows:

    if ((!isMultiInst) && (!TheFirstOne))
    {
        HWND hNotepad_plus = ::FindWindow(Notepad_plus_Window::getClassName(), NULL);
        for (int i = 0 ;!hNotepad_plus && i < 5 ; ++i)
        {
            Sleep(100);
            hNotepad_plus = ::FindWindow(Notepad_plus_Window::getClassName(), NULL);
        }
        if (hNotepad_plus)
        {
        ...
        }
        ...
    }


    The programmer who wrote this code tried to find the window of the already running Notepad ++ and even envisaged a situation when two processes were launched literally at the same time, so the first one has already created a global mutex, but has not yet created an editor window. In this case, the second process will wait for the creation of the window "5 times 100 ms". As a result, we either do not wait at all, or lose up to 100 ms between the moment of the actual creation of the window and the exit from Sleep.

    This is the first (and one of the main) antipatterns using Sleep. We are not waiting for the onset of the event, but "for some milliseconds, suddenly you are lucky." We are waiting so much that, on the one hand, it doesn’t annoy the user very much, but on the other hand, we have a chance to wait for the event we need. Yes, the user may not notice a pause of 100 ms at the start of the application. But if such a practice "to wait for a little from the bald" is accepted and permissible in the project - it can end up with the fact that we will wait at every step for the most petty reasons. Here it is 100 ms, there is another 50 ms, and here it is 200 ms - and now our program already “for some reason slows down a few seconds”.

    In addition, it is just aesthetically unpleasant to see code that works for a long time, as it could work quickly. In this particular case, you could use the functionSetWindowsHookEx , subscribing to the HSHELL_WINDOWCREATED event - and get a notification about creating a window instantly. Yes, the code becomes a bit more complicated, but literally 3-4 lines. And we win up to 100 ms! And most importantly, we no longer use the functions of unconditional waiting where waiting is not unconditional.

    Bad example number 2


    HANDLE hThread = ::CreateThread(NULL, 0, threadTextTroller, &trollerParams, 0, NULL);
    int sleepTime = 1000 / x * y;
    ::Sleep(sleepTime);

    I didn’t really understand what exactly and how long this code was waiting for in Notepad ++, but I often saw the common “start a stream and wait” antipattern. People are waiting for different things: the beginning of work of another stream, the receipt of some data from it, the end of its work. There are two bad points here:

    1. Multithreaded programming is necessary in order to do something multithreaded. Those. the launch of the second thread assumes that we will continue to do something in the first one, at this time the second thread will do other work, and the first one, having finished its work (and, perhaps, having waited a little longer), will receive its result and somehow use it. If we start to “sleep” immediately after launching the second stream - why is it needed at all?
    2. You need to wait correctly. For proper waiting, there are proven practices: the use of events, wait-functions, call callbacks. If we are waiting for the code to start working in the second thread, start an event for this and signal it in the second thread. If we are waiting for the completion of the second thread, C ++ has a wonderful thread class and its join method (or, again, platform-specific methods like WaitForSingleObject and HANDLE in Windows). It’s just silly to wait for work to be done in another thread “some milliseconds”, because if we don’t have a real-time OS, no one will give you any guarantee for how long that second thread will start or go to a certain stage of its work.

    Bad example number 3


    Here we see a background thread that sleeps waiting for some events.

    classCReadChangesServer
    {
    ...
    voidRun(){
            while (m_nOutstandingRequests || !m_bTerminate)
            {
                ::SleepEx(INFINITE, true);
            }
        }
        ...
    voidRequestTermination(){
            m_bTerminate = true;
        ...
        }
        ...
        bool m_bTerminate;
    };
    

    It must be admitted that it is not Sleep that is used here, but SleepEx , which is more intelligent and can interrupt waiting for certain events (such as the completion of asynchronous operations). But it does not help at all! The fact is that the while loop (! M_bTerminate) has every right to work indefinitely, ignoring the RequestTermination () method called from another thread, resetting the m_bTerminate variable to true. About the causes and consequences of this, I wrote in a previous article . To avoid this, you should use something that is guaranteed to work correctly between threads: an atomic, event, or something like that.

    Yes, formally, SleepEx is not to blame for the problem of using a regular Boolean variable to synchronize threads; this is a separate error of another class. But why did it become possible in this code? Because at first the programmer thought “I need to sleep here,” and then I wondered how long and on what condition to stop doing it. And in the right scenario, he even should not have had a first thought. The thought “should expect an event” should have appeared in my head - and from that moment the thought would have already worked towards choosing the right data synchronization mechanism between threads, which would eliminate both the Boolean variable and the use of SleepEx.

    Bad example number 4


    In this example, we will take a look at the backupDocument function, which serves as an “autosave”, useful in case of an unexpected editor crash. By default, she sleeps for 7 seconds, then gives the command to save the changes (if any).

    DWORD WINAPI Notepad_plus::backupDocument(void * /*param*/)
    {
        ...
        while (isSnapshotMode)
        {
            ...
            ::Sleep(DWORD(timer));
            ...
            ::PostMessage(Notepad_plus_Window::gNppHWND, NPPM_INTERNAL_SAVEBACKUP, 0, 0);
        }
        return TRUE;
    }

    The interval is changeable, but not in this trouble. Any spacing will be both too large and too small. If we type one letter per minute - there is no point in sleeping for just 7 seconds. If we copy-paste 10 megabytes of text from somewhere - there is no need to wait another 7 seconds after that, it is large enough to initiate a backup immediately (all of a sudden we cut it out from somewhere and it was not there, and the editor will crash in a second).

    Those. by simple expectation, we replace the missing more intelligent algorithm here.

    Bad example number 5


    Notepad ++ can “type text” - i.e. Emulate text input by a person, pausing between letters. It seems to be written as an "Easter egg", but you can think of some working application of this feature ( fool Upwork, yeah ).

    int pauseTimeArray[nbPauseTime] = {200,400,600};
    constint maxRange = 200;
    ...
    int ranNum = getRandomNumber(maxRange);
    ::Sleep(ranNum + pauseTimeArray[ranNum%nbPauseTime]);
    ::SendMessage(pCurrentView->getHSelf(), SCI_DELETEBACK, 0, 0);

    The trouble here is that the code contains an idea of ​​some kind of “average person”, pausing 400-800 ms between each key pressed. Ok, maybe it's “average” and normal. But you know, if the program I use makes some pauses in my work simply because they seem beautiful and appropriate to her, this does not at all mean that I share her opinion. I would like to be able to customize the duration of these pauses. And, if in the case of Notepad ++ this is not very critical, then in other programs I sometimes encountered settings like “update data: often, normal, rare”, where “often” was not for me often enough, and “rare” was not enough seldom. Yes, and "normal" was not normal. Such functionality should give the user the ability to accurately specify the number of milliseconds, which he would like to wait before performing the desired action. With the obligatory opportunity to enter "0". Moreover, 0 in this case should not even be passed as an argument to the Sleep function, but simply exclude its call (Sleep (0) does not actually return instantly, but returns the remaining piece of the time slot issued by the scheduler to another thread).

    findings


    With the help of Sleep, it is possible and necessary to carry out waiting when it is an unconditionally defined waiting for a specific period of time and there is some logical explanation for why it is: “according to the technical process”, “time is calculated using this formula”, “so much waiting said the customer. " Waiting for some events or synchronization of threads should not be implemented using the Sleep function.

    Also popular now: