DllMain and the Loader Lock — The Real Reason You're Told to "Do Nothing in DLL Initialization"
· Go Komura · Windows, DLL, Windows Development, C++, Troubleshooting, Multithreading, Win32 API
“The app hangs at startup, but only in a particular environment.” “When we load our own DLL, LoadLibrary sometimes never returns.” “It deadlocks only at the timing of a service start.” — Follow investigations like these far enough and, more often than not, you arrive at the same place. The DLL’s initialization code — that is, DllMain.
Microsoft’s documentation warns about DllMain in an unusually strong tone. Do not call LoadLibrary. Do not synchronize with other threads. Do not call User, Shell, or COM functions. The ideal DllMain is an empty stub — why is the language this strong? The reason concentrates in a single internal mechanism, the loader lock. Aimed at developers who write DLLs, plug-ins, and C++/CLI wrappers on Windows, this article explains, from primary sources, how the loader lock works, the structure that makes a deadlock hold, and the safe design and investigation procedure.
1. The Bottom Line First
DllMainis called while holding the loader lock, a shared lock of which there is exactly one per process. So calling, fromDllMain, work that tries to take the loader lock (directly or indirectly) creates the possibility of a deadlock, or of a crash from touching a DLL that has not yet been initialized.1- Calling
LoadLibrary/FreeLibraryis forbidden. It creates a circular load-order dependency and can cause initialization code to run against a DLL whose own initialization has not yet run.2 - Synchronizing with other threads is also forbidden. DLL notifications are serialized, so waiting inside
DllMainfor a thread to start or exit leaves that thread itself stopped waiting for the loader lock, and you deadlock.23 - What you can safely call is, in practice, only a subset of Kernel32.dll. And official documentation states plainly that “a complete list of safe functions does not exist”. User, Shell, and COM functions load other components and cause access violations.2
- In a DLL linked with the CRT, the same restrictions apply to constructors and destructors of globals. They run as a de facto part of
DllMain.2 - The correct design is “defer”. Do what initialization you can at compile time (statically); defer what you cannot until first use. That is the official best practice.1
- C++/CLI mixed DLLs are especially dangerous. To avoid running MSIL under the loader lock,
DllMainand its call tree must be compiled native.4
2. When and How DllMain Is Called
DllMain is the entry point the OS loader calls when a DLL enters or leaves a process or a thread. There are four notifications.
| Notification | Timing |
|---|---|
| DLL_PROCESS_ATTACH | When the DLL is loaded into the process |
| DLL_THREAD_ATTACH | When a new thread is started in the process |
| DLL_THREAD_DETACH | When a thread exits normally |
| DLL_PROCESS_DETACH | When the DLL is unloaded, or when the process exits |
Two facts are easy to miss. First, every time a single thread is created, DllMain of every already-loaded DLL is called with DLL_THREAD_ATTACH. In other words DllMain is not “something that runs once when my DLL is loaded”; it is code that keeps being called for the process’s thread activity. If you do not need that, you can stop it by calling DisableThreadLibraryCalls inside DLL_PROCESS_ATTACH (do not call it from a DLL linked with the static CRT).5
Second, in a DLL linked with the CRT (the C/C++ runtime), constructors and destructors of global and static C++ objects run, via the CRT’s entry point, as a part of DllMain.2 Even if you think “our DllMain is empty, so we are safe”, a global object with elaborate initialization is the same as running that work in DllMain.
flowchart TB
accTitle: The four timings at which DllMain is called
accDescr: DLL_PROCESS_ATTACH runs on DLL load; DLL_THREAD_ATTACH and DETACH run on every already-loaded DLL at each thread start and exit in the process; DLL_PROCESS_DETACH runs on unload or process exit; and constructors of static objects also run inside this via the CRT
load["DLL load"] --> pa["DLL_PROCESS_ATTACH"]
pa --> ta["DLL_THREAD_ATTACH (on every thread start)"]
ta --> td["DLL_THREAD_DETACH (on every thread exit)"]
td --> pd["DLL_PROCESS_DETACH (on unload or exit)"]
pa -.-> crt["Construction of static objects also runs here"]
Figure 1: DllMain is called not only at load time but on every thread start and exit, and initialization of static objects also runs as part of that.
3. The Loader Lock — One Lock That Serializes Every Notification
Why are the restrictions on DllMain alone this severe? The answer is in the loader’s structure.
To keep a series of operations — DLL load, unload, and the various notifications — consistent, the OS loader serializes the work with a single loader lock per process. And the important point is that DllMain is called while this loader lock is held.1 For as long as you are inside DllMain, every other DLL load in that process, and every start-of-thread notification, waits for this lock to be released.
From that structure, the reasons for the prohibitions follow one after another.
- You must not call
LoadLibrarybecause it creates loader-lock reentrancy, or a circular load-order dependency. It can also result in calling a function on a DLL whose initialization has not yet finished.2 - Synchronizing with other threads is dangerous because the thread you are waiting on has moments when it needs the loader lock (notifications at start and exit, calls to
GetModuleHandle-family APIs, and so on). You hold the loader lock and wait for the other side; the other side waits for the loader lock — the classic lock-order inversion.6 - User, Shell, and COM functions are dangerous because they load other system components internally. You touch a component before it is initialized, or after it has been torn down, and you get an access violation.2
sequenceDiagram
accTitle: Why waiting for a thread inside DllMain deadlocks
accDescr: DllMain, holding the loader lock, waits for a worker thread to exit, but the worker that is trying to exit waits for the loader lock to be released so it can receive DLL_THREAD_DETACH, so they wait on each other and deadlock
participant L as Loader(holds lock)
participant D as DllMain
participant W as Worker
L->>D: DLL_PROCESS_DETACH
D->>W: Request exit and wait
W->>W: Finish work, then exit
Note over W: Exit notify needs the lock
Note over D,W: DllMain holds lock, W waits
Figure 2: “DllMain waits for a thread to exit” is a structural deadlock, because thread exit itself needs the loader lock.
The point is that this is not the kind of thing that “happens if you are unlucky”; it is structurally guaranteed to hold. The documentation tells you to treat the loader lock as the top of the lock hierarchy the app defines (the one taken first). Inside DllMain you already hold that top-level lock, so any act of going on from there to wait for something else is dangerous — that is a useful way to remember it.6
flowchart TB
accTitle: Lock-order inversion between the loader lock and a private lock
accDescr: DllMain, holding the loader lock, goes to take a private lock, while a worker, holding that private lock, goes to take the loader lock for GetModuleHandle or similar, so the acquire order inverts and they deadlock
d["DllMain: holding the loader lock"] --> dg["Goes to take private lock G"]
w["Worker: holding private lock G"] --> wl["Goes to take the loader lock"]
dg -.-> dead["Deadlock from inverted acquire order"]
wl -.-> dead
wl -.-> api["GetModuleHandle and similar require it internally"]
Figure 3: Even an innocuous API such as GetModuleHandle requires the loader lock internally, so an order inversion with a private lock can hold.
Also, calling CreateThread from inside DllMain itself is not recommended. The created thread needs the loader lock to process the DLL_THREAD_ATTACH notification, so it cannot start running until the DllMain that is currently executing returns and releases the lock. Therefore waiting inside DllMain for that thread to start or finish is an immediate deadlock. There is a lifetime problem as well — if, after DllMain returns, the DLL is unloaded while a thread that has not yet started running is still left behind, the thread’s start address still points at already-freed code and you crash.3
4. Two Landmines C++ Developers Step On Easily
Landmine 1: Dynamic initialization of global objects. As Chapter 2 said, constructors of static objects run under the DllMain restrictions. Reading a configuration file, standing up a logging facility, initializing COM, starting a thread — the moment you put a global in a DLL whose constructor does that kind of work, you are executing “things you must not do in DllMain”. Constant initialization that is fixed at compile time (anything you can make constexpr) is safe; initialization that involves a function call should be deferred.
flowchart TB
accTitle: The path by which initializing a global object becomes a landmine
accDescr: The loader lock is taken on DLL load, and constructors of global objects run via the CRT entry point, so LoadLibrary, thread synchronization, and COM initialization inside those constructors are executions of DllMain prohibitions
load["DLL load (loader lock acquired)"] --> crt["CRT entry point"]
crt --> ctor["Constructor of a global object"]
ctor --> ng1["LoadLibrary-equivalent work"]
ctor --> ng2["Starting a thread and waiting for it to finish"]
ctor --> ng3["Using COM or User32"]
ng1 -.-> risk["All of these fall under DllMain prohibitions"]
ng2 -.-> risk
ng3 -.-> risk
Figure 4: Even “DllMain is empty, so we are safe” revives the same danger the moment you have a global with elaborate initialization.
Landmine 2: C++/CLI (mixed assemblies). In a configuration that wraps a native DLL with C++/CLI (the shape covered in the wrapper article), there is a danger of running MSIL (managed code) under the loader lock. Running MSIL can trigger CLR initialization or the load of another assembly. The compiler emits warning C4747 on code where DllMain executes MSIL directly, but it cannot detect indirect execution through a function in another module. Compile DllMain and the functions called from it as native with #pragma unmanaged, or use a configuration that does not have a DllMain at all.4
flowchart TB
accTitle: Whether MSIL execution under the loader lock can be detected
accDescr: Code where DllMain executes MSIL directly can be detected by the compiler with warning C4747, but indirect execution through a function in another module cannot, so you have to prevent it by reviewing the call tree and insisting on a native compile
d2["Calls from DllMain"] --> dir["Execute MSIL directly"]
d2 --> ind["Execute via another module"]
dir --> c47["Detectable with warning C4747"]
ind --> nc["The compiler cannot detect it"]
nc -.-> rv["Prevent with review and #pragma unmanaged"]
Figure 5: C4747 only protects you against direct execution. Indirect paths can be caught only by review.
5. The Correct Design — Make “Defer” the Default Policy
The official best-practice recommendation is clear.1
- Finish what initialization you can at compile time (statically). First ask whether a dynamic initialization can be replaced with a static one.
- Defer the rest until first use. As long as first use happens from an ordinary API that is called after the DLL has finished loading, the initialization runs outside the loader lock and you can safely use almost the whole Windows API. For exclusion on first access you can use
INIT_ONCE(one-time initialization) or C++ magic statics (function-local statics). Deferral is not a panacea, though — if that first access itself is made fromDllMainor a static initializer, the initializer still runs under the loader lock and you are back under the same restrictions. - Make an exception only for failures you must detect early. You may have a requirement that a broken configuration file should make the load itself fail. Even then, keep it to the minimum of “try and fail immediately”.
- Consider
DisableThreadLibraryCallsin DLL_PROCESS_ATTACH. If the DLL does not use thread notifications, you can remove the notification cost itself (except when using the static CRT or static TLS).5 - Inspect with Application Verifier. Many of the dangerous calls inside
DllMainare ones Application Verifier will detect at run time.1
flowchart TB
accTitle: Design guidance for DLL initialization
accDescr: First consider whether initialization can be compile-time static initialization; if not, the default is to defer to first use, and leave in DllMain only the minimum that must be detected early as a load failure
q1{"Can it be decided at compile time?"} -->|"yes"| s["Make it static initialization"]
q1 -->|"no"| q2{"Must the failure be detected at load time?"}
q2 -->|"no"| lazy["Defer to first use (the default)"]
q2 -->|"yes"| min["Do only the minimum in DllMain"]
lazy -.-> once["Exclude with INIT_ONCE or a function-local static"]
Figure 6: The decision order is “can it be static → can it be deferred”, and what you leave in DllMain is only the minimum that must be detected early.
Whether to apply DisableThreadLibraryCalls can be decided mechanically with the following branch.
flowchart TB
accTitle: Whether to call DisableThreadLibraryCalls
accDescr: Do not call it from a DLL linked with the static CRT; if static TLS is in effect the call itself fails so you do not call it; if neither applies and the DLL does not use thread notifications, call it in DLL_PROCESS_ATTACH, checking the return value, to cut the notification cost
q1{"Linked with the static CRT?"} -->|"yes"| no2["Must not call it"]
q1 -->|"no"| q2{"Using static TLS?"}
q2 -->|"yes"| eff["The call fails anyway (FALSE)"]
q2 -->|"no"| q3{"Are thread notifications needed?"}
q3 -->|"no"| yes["Call it in ATTACH (check the return value)"]
q3 -->|"yes"| keep["Do not call it; handle the notifications"]
Figure 7: The three conditions of static CRT, static TLS, and whether notifications are needed decide uniquely whether you should call it.
For stopping threads at unload, official documentation gives a concrete protocol. Rather than “wait” for worker threads to exit in DLL_PROCESS_DETACH (on an unload via FreeLibrary), the shape is (1) signal exit with an event, (2) the thread side folds its work down to a consistent state, signals back, and enters an infinite wait, (3) the DllMain side confirms the consistent state and then folds the thread with TerminateThread.3 It looks rough, but it is documented as the realistic answer inside the constraint “you must not wait for a thread’s natural exit inside DllMain”.
sequenceDiagram
accTitle: Protocol for stopping a thread at unload
accDescr: DllMain signals the worker thread to exit with an event; the worker folds its work down to a consistent state, signals back, and enters an infinite wait; DllMain confirms the consistent state and then terminates the thread
participant D as DllMain (DETACH handling)
participant W as Worker thread
D->>W: Signal exit with an event
W->>W: Fold work down to a consistent state
W->>D: Signal consistency complete and wait forever
D->>W: Terminate with TerminateThread
Note over D,W: No wait for natural exit, so no deadlock
Figure 8: Instead of “wait for a natural exit”, “wait for a consistency signal and then cut it off” avoids a collision with the loader lock.
As a matter of first principles, the safest design is to avoid owning threads in a DLL that can be unloaded, and to keep thread ownership on the EXE side.
DLL_PROCESS_DETACH at process exit is the opposite: doing nothing and returning is the ideal. By this point every other thread has already been forcibly terminated, and you cannot rely on the state of dependent DLLs or the runtime either. Elaborate work here only causes deadlocks and crashes. Data that must be persisted should be written out in the app’s own shutdown path; do not depend on this notification.3
6. How to Investigate When You Hit It
Loader-lock hangs have a recognizable fingerprint.
Look at the stacks in a hang dump. Take a dump of the frozen moment and inspect each thread’s stack. If you find a pair of a thread waiting on a lock inside ntdll.dll loader functions (the family whose names start with Ldr) and a thread waiting on something else inside DllMain or a static initializer (dynamic initializer), you are almost certain. A thread stopped in the middle of a LoadLibrary call is another typical character.
flowchart TB
accTitle: The fingerprint of a loader-lock hang
accDescr: In a hang dump, if you find both a thread waiting on a lock inside ntdll loader functions and a thread waiting on something else inside DllMain or a static initializer, you can treat it as a loader-lock deadlock with near certainty
dump["Hang dump"] --> t1["Thread waiting on a lock in Ldr-family functions"]
dump --> t2["Thread waiting inside DllMain or a static initializer"]
t1 --> pair{"Both present?"}
t2 --> pair
pair -->|"yes"| conf["Almost certainly a loader-lock deadlock"]
pair -->|"no"| other["Investigate as a hang of another kind"]
Figure 9: Loader-lock hangs have the recognizable fingerprint “waiting in Ldr + waiting inside DllMain”.
Suspect the “timing-dependent” character. A loader-lock deadlock holds only at the moment a DLL load coincides with a thread start or exit. Repro conditions such as “occasionally at startup”, “only on a particular machine”, and “only when run as a service” are signs of this kind of problem.
Run preventive inspection. Enable Application Verifier and run your tests, and you can detect dangerous calls inside DllMain at run time.1 For C++/CLI, do not ignore warning C4747; in reviews of functions reachable from DllMain, add the angle of “functions that call LoadLibrary indirectly” (COM initialization, some CRT features, delay-loaded imports, and so on) to the review checklist, and you will catch accidents before you ship them. The first call of a delay-loaded import becoming LoadLibrary internally is an easy point to miss.
7. Summary
DllMainis called while holding the loader lock (one per process, the lock that serializes every DLL notification). Every restriction follows from that.- The core of the prohibitions is “do not call
LoadLibrary/FreeLibrary”, “do not synchronize with other threads”, and “do not call functions that depend on a DLL other than Kernel32”. Constructors and destructors of static objects that run via the CRT fall under the same restrictions. - The basic design policy is deferral. Make static the initialization you can make static; defer the rest to first use. Use
DisableThreadLibraryCallsand Application Verifier. - Stopping threads at unload follows the official protocol (signal → confirm consistency → terminate). DLL_PROCESS_DETACH at process exit is ideally empty.
- In C++/CLI, running MSIL under the loader lock is a landmine of its own. Insist on a native compile of the
DllMaincall tree.
The DllMain restrictions look, at first, like an unreasonable list of prohibitions. But once you hold the single point that “it is called while holding the loader lock, the top-level lock”, every prohibition is a restatement of the same principle. Remember it as a principle and, when you meet an edge case that is not in the documentation, you should still be able to ask the right question: “Is this work I am allowed to do while holding the lock?”
Related Articles
- How Windows DLL Name Resolution Works - Search Order and SxS
- Calling Native DLLs from C#: C++/CLI Wrapper vs P/Invoke
- Practical Multithreading Best Practices: C++ Edition — Eliminating Accidents by Structure with RAII and jthread
- Spurious Wakeups — Why Condition Variables Wake “Without Being Notified” and How to Wait Correctly on Windows
- Reading Crash Dumps with WinDbg + SOS — A Practical Guide to Analysis After Collection
- COM STA/MTA Fundamentals - Threading Models and How to Avoid Hangs
Related Consulting Areas
KomuraSoft LLC handles root-cause investigation of hangs and deadlocks at startup or DLL load time (dump analysis), design reviews around DllMain and static initialization, and remediating C++/CLI wrappers and plug-in DLLs toward a safe initialization design. You can consult us even at the hard-to-reproduce stage of “it only hangs on startup in a particular environment”.
- Bug Investigation & Root-Cause Analysis
- Technical Consulting & Design Review
- Windows Application Development
- Contact Us
References
-
Microsoft Learn, Dynamic-Link Library Best Practices. On DllMain being called while the loader lock is held, so that the functions you can call are severely restricted; the ideal DllMain being an empty stub and initialization being deferred as far as possible; the recommendation of compile-time static initialization; doing only the minimum for failures that must be detected early; and detecting typical DllMain mistakes with Application Verifier. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, DllMain entry point. On performing only simple initialization and termination in the entry point; why you must not call LoadLibrary / FreeLibrary (circular load order and use of a DLL before initialization or after termination); Kernel32.dll being guaranteed already loaded, so that you can call it in the range that does not load other DLLs; there being no exhaustive list of safe functions; User, Shell, and COM functions causing access violations; DLL notifications being serialized, so that communication with other threads or processes causes deadlocks; and the same restrictions applying to constructors and destructors of static objects when the CRT is linked. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Dynamic-Link Library Best Practices - Best Practices for Synchronization. On the structure that deadlocks if you wait for a thread to exit inside DllMain (the DLL_THREAD_DETACH notification of thread exit needs the loader lock); the protocol for stopping a thread at unload (signal with an event, confirm a consistent state, then terminate); DLL_PROCESS_DETACH at process exit having the other threads already forcibly terminated and no guarantee of address-space consistency, so that the ideal handler is empty; and creating a thread in DllMain leaving notifications queued with initialization incomplete and causing problems. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Initialization of Mixed Assemblies. On not executing MSIL under the loader lock; not compiling DllMain and its call tree to MSIL and dealing with that via #pragma unmanaged; warning C4747 being emitted when DllMain tries to execute MSIL directly, but indirect execution through another module not being detectable; and dynamic initializers of static objects being able to cause the same problem. ↩ ↩2
-
Microsoft Learn, DisableThreadLibraryCalls function (libloaderapi.h). On disabling DLL_THREAD_ATTACH / DLL_THREAD_DETACH notifications to reduce overhead at thread create and destroy; not calling it from a DLL linked with the static CRT; and the optimization not being performed when static TLS (thread_local or __declspec(thread)) is in effect. ↩ ↩2
-
Microsoft Learn, Dynamic-Link Library Best Practices - Deadlocks Caused by Lock Order Inversion. On defining a lock hierarchy and always acquiring in the same order; the loader acquiring the loader lock before calling DllMain, so that the loader lock should sit at the top of the lock hierarchy; observing acquire order between APIs that take the loader lock indirectly, such as GetModuleFileName, and private locks; and a concrete example of a deadlock from lock-order inversion. ↩ ↩2
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
The Win32 Thread Pool API — Concurrency Without Creating Threads, via CreateThreadpoolWork
Are you spawning CreateThread calls all over your native code? This article explains the Win32 thread pool API redesigned in Vista — the ...
What "Not Responding" Really Is — How Windows Decides an App Has Hung, and How to Design Apps That Don't
Windows' "Not Responding" is a mechanism in which the OS judges that a window has not retrieved a message for 5 seconds and replaces it w...
Spurious Wakeups — Why Condition Variables Wake "Without Being Notified" and How to Wait Correctly on Windows
A condition variable's wait can return even when no notification has arrived (a spurious wakeup). This article explains, from the Windows...
Named Pipes in Practice — Windows' Standard IPC from Design to Security
A practical guide to named pipes, Windows' standard inter-process communication. This article organises, from primary sources, the choice...
Apps That Break on Resume from Sleep — How Windows Power Events Work and How to Build Business Apps That Survive Them
You opened the laptop and the business app's connections were dead — the cause is a design that never accounted for sleep. This article c...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Frequently Asked Questions
Common questions about the topic of this article.
- Can you really do nothing at all in DllMain?
- "Do nothing" is not hyperbole; it is the official design stance, and Microsoft itself says that the ideal DllMain is a near-empty stub. What is safe is a subset of Kernel32.dll functions — Kernel32 is guaranteed to be loaded by the time DllMain runs — in the range that does not load other DLLs. Creating a critical section or a mutex, and using TLS, are examples of what you can do. Conversely, LoadLibrary/FreeLibrary, synchronizing with other threads, and calling functions in User32, Shell, COM, and the like are forbidden because they cause deadlocks and access violations. Initialization you are unsure about should not be done in DllMain; defer it until the first time it is used.
- Do constructors of C++ globals (static objects) also fall under the DllMain restrictions?
- They do. When the DLL is linked with the CRT (the C++ runtime), constructors and destructors of global and static objects run, via the entry point the CRT provides, as a de facto part of DllMain. That means calling LoadLibrary from a constructor, starting another thread and waiting for it to finish, initializing COM, and so on all carry the same danger as doing those things in DllMain. For a global object with non-trivial initialization, keep a pointer and construct it on first access, or use a function-local static, so that the work runs outside DllMain.
- Should I call DisableThreadLibraryCalls?
- Conditionally, yes. If the DLL does not need DLL_THREAD_ATTACH/DETACH notifications, calling DisableThreadLibraryCalls in DLL_PROCESS_ATTACH stops the per-thread-create and per-thread-exit notifications and reduces overhead in a process that creates threads frequently. There are two exceptions. Do not call it from a DLL linked with the static CRT (the static CRT needs the thread notifications). And if static TLS via thread_local or __declspec(thread) is in effect, the call itself fails and returns FALSE, so make a habit of checking the return value. Use it on a typical DLL that uses the dynamically linked CRT, after you have confirmed that nothing depends on the thread notifications.
- Why does a C++/CLI (mixed managed) DLL hang at startup?
- The typical cause is trying to run MSIL (managed code) while the loader lock is held. In a C++/CLI mixed assembly, if DllMain, functions called from it, or dynamic initializers of globals are compiled to MSIL, CLR initialization or the load of another assembly can be required under the loader lock, and that can deadlock. The compiler emits warning C4747 when DllMain itself tries to execute MSIL directly, but it cannot detect indirect execution through another module. The mitigation is to compile DllMain and its call tree as native with #pragma unmanaged — or not to have a DllMain at all.
- May I clean up resources in DLL_PROCESS_DETACH?
- The answer changes between "process exit" and "unload via FreeLibrary". On DLL_PROCESS_DETACH at process exit, the other threads have already been terminated, and there is no guarantee that the address space is still consistent, so cleanup such as freeing memory is actually dangerous; official guidance is that "the ideal handler is empty". Write out any data that must be persisted in the app's own shutdown path, and here essentially do nothing and return. On an unload via FreeLibrary, the process continues, so you do need complete cleanup — stopping threads, closing handles, and so on. Waiting for a thread to exit inside DllMain deadlocks, though, so you must follow the official protocol: signal, wait until a consistent state, and finish the work outside DllMain.