1

I'm shocked to trace this simple code:

#include <thread>

void foo()
{
    for (int i = 0; i < 1000000; ++i) {
        std::this_thread::sleep_for(std::chrono::nanoseconds(1));
    }
}

int main()
{
    std::thread t(foo);
    t.join();
}

Guess what ? sleep_for calls FreeLibrary everytime !

kernel32.dll!_FreeLibraryStub@4()
msvcr120d.dll!Concurrency::details::DeleteAsyncTimerAndUnloadLibrary(_TP_TIMER * timer) Line 707
msvcr120d.dll!Concurrency::details::_Timer::_Stop() Line 111
msvcr120d.dll!Concurrency::details::_Timer::~_Timer() Line 100
msvcr120d.dll!`Concurrency::wait'::`7'::TimerObj::~TimerObj()
msvcr120d.dll!Concurrency::wait(unsigned int milliseconds) Line 155
test826.exe!std::this_thread::sleep_until(const xtime * _Abs_time) Line 137
test826.exe!std::this_thread::sleep_for<__int64,std::ratio<1,1000000000> >(const std::chrono::duration<__int64,std::ratio<1,1000000000> > & _Rel_time) Line 162
test826.exe!foo() Line 6

Why sleep_for had to call FreeLibrary ?

This program will take 2 seconds with boost library, and will take > 3 minutes (lose my patience) with msvcrt (Release mode). I can't imagine.

amanjiang
  • 1,213
  • 14
  • 33
  • 1
    I don't understand how you expected a sleep for _one nanosecond_ to work. – Lightness Races in Orbit Aug 26 '15 at 15:25
  • This is a very implementation-specific thing that none but the Visual C++ standard library developers will know the answer to. – Some programmer dude Aug 26 '15 at 15:27
  • 2
    @JoachimPileborg: Fortunately, some of those developers frequent SO :) – Lightness Races in Orbit Aug 26 '15 at 15:27
  • 2
    A sleep can be expected to release the current time slice. Therefore (assuming 10ms time slices) you want the program to sleep for 2.7 hours. – danielschemmel Aug 26 '15 at 15:29
  • 1
    Fortunately or unfortunately... :-) We also ship almost all of the runtime library source code, so it's often quite straightforward to step through the runtime library code to see what's going on. In this case, the ConcRT code for `Concurrency::wait` and the `_Timer` class have a number of useful comments. – James McNellis Aug 26 '15 at 18:19
  • First, the call to `FreeLibrary` just decrements a reference count. Second, the idea that a program doesn't sleep fast enough is very, very silly. You're basically just seeing the difference between rounding the sleep down to zero or up to one timeslice. – David Schwartz Aug 26 '15 at 19:52
  • @LightnessRacesinOrbit I am not, it's just a test, sometimes the implementation-specific is important, it will affects the performance, especially for concurrent programming. – amanjiang Aug 27 '15 at 02:26
  • @DavidSchwartz Yes, reference count, but it still could unload the library, it's about the design. – amanjiang Aug 27 '15 at 03:13

1 Answers1

5

In Visual C++ 2013, most of the C++ Standard Library concurrency functionality sits atop the Concurrency Runtime (ConcRT). ConcRT is a work-stealing runtime that provides cooperative scheduling and blocking.

Here, Concurrency::wait uses a thread pool timer to perform the wait. It uses LoadLibrary/FreeLibrary to increment the reference count of the module in which the ConcRT runtime is hosted for the duration that the timer is pending. This ensures that the module is not unloaded during the wait.

I'm not a ConcRT expert (not even close), so I'm not 100% sure what is the exact scenario where the ConcRT module could be unloaded here. I do know that we made similar changes to std::thread and _beginthreadex, to acquire a reference to the module in which the thread callback is hosted, to ensure that the module is not unloaded while the thread is executing.

In Visual C++ 2015, the C++ Standard Library concurrency functionality was modified to sit directly atop Windows operating system primitives (e.g. CreateThread, Sleep, etc.) instead of ConcRT. This was done to improve performance, to resolve correctness issues when mixing use of C++ threading functionality with use of operating system functionality, and as part of a more general deemphasization of ConcRT.

Note that on Windows, sleep precision is in milliseconds and a sleep of zero milliseconds generally means "go do other useful work before coming back to me." If you compile your program with Visual C++ 2015, each call to wait_for will in turn call Sleep(0), which "causes the thread to relinquish the remainder of its time slice to any other thread that is ready to run."

James McNellis
  • 348,265
  • 75
  • 913
  • 977
  • Very helpful, this is the answer what I expected, thank you. – amanjiang Aug 27 '15 at 02:23
  • 1
    By the way, "ensure that the module is not unloaded", why don't you leave this to the developers ? Unload a running module - this made no sense. – amanjiang Aug 27 '15 at 02:56
  • @amanjiang: To prevent errors? For convenience? Because "checking" for this manually in general is almost impossible? Why is this not the best way to do it? – Lightness Races in Orbit Aug 27 '15 at 11:28
  • @LightnessRacesinOrbit I don't know if it is the best way, or maybe there is no "best way". I'm just interesting in the reason to make such a change. – amanjiang Aug 27 '15 at 13:11
  • @LightnessRacesinOrbit Well, for std::thread and _beginthreadex, it's not big deal. For sleep_for (and other things), it costs. As the most foundational language facilities, (I'm afraid) it may introduce unnecessary overhead and complexity. – amanjiang Aug 27 '15 at 13:21
  • @LightnessRacesinOrbit So I often use boost for concurrent programs. Not std, std is slow, and it's more complex. For example in dll entry point std::thread will cause dead lock, but boost:thread works. – amanjiang Aug 27 '15 at 13:38
  • 1
    Consider the case where [1] a module is loaded via COM, [2] some COM object inside of the module starts some asynchronous operations via std::async or std::thread, and then [3] all COM objects go out of existence, but the asynchronous operations are still pending or the background threads are still running. The COM runtime may at this time release its reference to the module. If the asynchronous operations or worker threads do not own their own references to the module, the reference count will drop to zero and the loader will unload the module out from under them. – James McNellis Aug 27 '15 at 14:31
  • This problem is exacerbated in Windows Store apps / Universal Windows Platform apps, where most modules are loaded via the COM runtime and that fact is largely "hidden" from the developer by higher level abstractions like C++/CX. We saw a sizable number of crashes due to this problem, which is why we altered `_beginthreadex` to acquire a reference to the module to prevent premature unload (`std::thread` now calls `_beginthreadex`, so it's "fixed" as well). – James McNellis Aug 27 '15 at 14:35
  • 1
    We don't want to leave this up to developers for a few reasons. First, it is rarely obvious where acquiring a reference is _actually_ required. We don't want developers to have to acquire a reference every time they start a thread or a task--if nothing else, that would require platform-specific code all over the place, even in code that should be portable across C++ implementations and operating systems. Additionally, there's no way for Windows Store apps to increment the reference count of a module loaded via the COM runtime (the required APIs are not callable). – James McNellis Aug 27 '15 at 14:37
  • 1
    @JamesMcNellis So this is the reason. I don't know about Windows Store apps, but if a COM object goes out of existence, why not do a sync work to ensure all async operations are finished or cancelled ? If not, it means that although an object is done, but some related code is still running, this is not RAII. – amanjiang Aug 27 '15 at 15:02
  • A related question: http://stackoverflow.com/questions/32252143/stdthread-cause-deadlock-in-dllmain – amanjiang Aug 28 '15 at 01:11
  • 1
    In practice it is not so easy to explicitly wait for all asynchronous operations to complete, especially in highly asynchronous APIs like the new Windows Runtime APIs (it's exceedingly easy to lose track of tasks). Practically no application does this 100% correctly. – James McNellis Aug 28 '15 at 14:10