πŸ“Œ Related Tech Posts:
Large-scale financial HTS architecture and C++/MFC troubleshooting series:
πŸ‘‰ [C++] HTS Real-Time Data Architecture: Adaptive Throttling & Trailing Edge Flush
πŸ‘‰ [C++] Advanced HTS Real-Time Market Data: Multi-PB Service Parallel Processing

Enterprise financial Home Trading Systems (HTS) operate on massive C++/MFC codebases spanning decades of development. Following the integration of a new high-throughput memory cache module, a critical bug surfaced: closing the main HTS window immediately without launching any trading screens left the main executable permanently hanging as a background zombie process.

This was not a simple memory leak. Rather, it was a structural deadlock caused by module state contamination between MFC Extension DLLs (AFX_EXTENSION_MODULE, new CDynLinkLibrary) and MFC Regular DLLs, colliding with the OS Loader Lock during teardown.

This article details the root cause investigation and demonstrates how refactoring to a pure Win32 DLL architecture eliminated zombie processes and restored clean 0.3-second shutdowns.


1. The Symptom: Why Won't the Process Terminate on Simple Exit?

During routine testing, QA reported a bizarre failure mode:

🚨 QA Defect Report:
"Strange... I didn't open any watchlist or chart windows. I simply launched the main HTS (HtsMain.exe) and immediately closed it, but HtsMain.exe remains permanently stuck in Task Manager!"

Inspection revealed HtsMain.exe lingering at 0% CPU in the background process list. Subsequent launch attempts failed with "Application already running" alerts, forcing users to terminate the process via command-line taskkill.

HTS Main Process Zombie Hang Mechanism
β–² HTS architecture & root cause mechanism: HtsMain.exe βž” CommDriver.dll βž” DataManager.dll

2. System Architecture & Initial Hypotheses

The system consists of several interrelated modules:

  • HtsMain.exe: The main HTS framework host executable.
  • CommDriver.dll: The primary communication driver implemented as an MFC Regular DLL.
  • ScreenWatchList.dll: UI screen DLL rendering watchlist and order books.
  • DataManager.dll (New): A shared memory pool and snapshot cache engine designed to eliminate redundant server queries and prevent UI freezes.

2.1 Hypothesis 1: Background Worker Thread Blocking Exit?

DataManager runs a worker thread to sort packets. However, with zero screens open, the packet queue was idle and empty. Adding explicit shutdown triggers did not resolve the process hang.

2.2 Hypothesis 2: Why Does DataManager Load on Clean Launch?

Investigation revealed that CommDriver.dll was implicitly linked against DataManager.lib. Consequently, loading the communication module automatically loaded DataManager.dll into memory on startup, executing its DllMain regardless of user activity.


3. Root Cause: MFC Module State Contamination & Loader Lock

The architectural culprit lay inside DataManager/dllmain.cpp:

// [Legacy dllmain.cpp - MFC Extension DLL Architecture]
#include "pch.h"
#include <afxdllx.h>

static AFX_EXTENSION_MODULE DataManagerDLL = { NULL, NULL };

extern "C" int APIENTRY
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
    UNREFERENCED_PARAMETER(lpReserved);

    if (dwReason == DLL_PROCESS_ATTACH)
    {
        // 1. Initialize MFC Extension Module
        if (!AfxInitExtensionModule(DataManagerDLL, hInstance))
            return 0;

        // 2. Force injection into the global MFC module chain (Root Cause)
        new CDynLinkLibrary(DataManagerDLL);
    }
    else if (dwReason == DLL_PROCESS_DETACH)
    {
        CDataManager::GetInstance().Stop();
        // 3. Clean up extension module
        AfxTermExtensionModule(DataManagerDLL);
    }
    return 1;
}

Why Did This Trigger a Zombie Process?

  1. Module State Conflict (AFX_MODULE_STATE):
    HtsMain.exe and CommDriver.dll are independent MFC Regular DLL contexts. By calling new CDynLinkLibrary, DataManager.dll forced itself into the host application's shared MFC resource search chain and thread-local state (_AFX_THREAD_STATE).
  2. Abrupt Exit Sequence Breakdown:
    When closing the main frame without open windows, standard window destruction sequences (WM_CLOSE, WM_DESTROY) are bypassed. During this abrupt teardown, AfxTermExtensionModule collided with the OS Loader Lock while the C Runtime (CRT) attempted cleanup, deadlocking the main thread permanently.
  3. Architectural Mismatch:
    DataManager.dll manages pure memory caches and math calculations. It contains zero windows, dialogs, or GDI controls, making an MFC Extension DLL architecture completely unnecessary.
MFC Extension DLL vs Pure Win32 DLL Comparison
β–² DllMain refactoring: Legacy MFC Extension DLL vs. Pure Win32 DLL

4. Solution: Refactoring to a Pure Win32 DLL

The fix was straightforward: strip away all MFC macros and rewrite the module entry point as a standard pure Win32 DLL.

4.1 Refactored dllmain.cpp

// [Clean Pure Win32 DllMain Implementation]
#include <windows.h>
#include "DataManager.h"

// Eliminates AFX_EXTENSION_MODULE & CDynLinkLibrary to prevent module state pollution
BOOL APIENTRY DllMain(HMODULE hModule,
                      DWORD  ul_reason_for_call,
                      LPVOID lpReserved)
{
    UNREFERENCED_PARAMETER(hModule);
    UNREFERENCED_PARAMETER(lpReserved);

    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        // Disable thread library calls to eliminate unnecessary lock overhead
        ::DisableThreadLibraryCalls(hModule);
        break;

    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        break;

    case DLL_PROCESS_DETACH:
        // Explicitly terminate background worker threads cleanly
        CDataManager::GetInstance().Stop();
        break;
    }
    return TRUE;
}

4.2 Build Configuration Adjustments

  • Header Isolation: Removed <afxdllx.h> and pch.h in favor of standard <windows.h>.
  • Encoding Preservation: Maintained CP949 (Windows-949) encoding for native Korean comments.
  • DisableThreadLibraryCalls: Prevents thread attach/detach overhead and avoids lock contention under heavy trading loads.

5. Verification & Production Results

Comprehensive validation was performed across build targets and production runtime environments:

5.1 6-Target MSBuild Verification

  • DataManager (Debug / Release)
  • CommDriver (Debug / Release)
  • ScreenWatchList (Debug / Release)
  • Result: 0 Errors, 0 Warnings across all targets.

5.2 Runtime Validation

Deploying the refactored DLL to the live HTS environment:

  1. Launch HtsMain.exe and log in.
  2. Immediately click X on the main frame without opening any screens.
  3. Monitor Task Manager.
πŸŽ‰ Validation Result:
Upon clicking close, HtsMain.exe fully terminates in 0.3 seconds without lingering in the background process list.
Zero zombie processes occurred across dozens of consecutive rapid start-stop cycles.
Build Verification and HTS Runtime Exit Results
β–² Full MSBuild verification & 0.3-second clean exit timeline

6. Engineering Takeaways for Large-Scale C++ Systems

Architecture Dimension Legacy MFC Extension DLL Refactored Pure Win32 DLL
Module Chain Registration Injected via new CDynLinkLibrary None (Isolated)
Exit Reliability Prone to Loader Lock deadlocks Clean 0.3s shutdown via explicit Stop()
Thread Overhead DllMain triggered on every thread creation 0% with DisableThreadLibraryCalls
Recommended Scope Exporting MFC CWnd/CDialog classes only Memory caches, network drivers, computation modules
  1. Strip MFC Dependencies from Non-UI Modules:
    MFC Extension DLLs are designed specifically for sharing C++ GUI classes across modules. Pure computational or caching components should strictly use pure Win32 DLLs to avoid module state collisions.
  2. Be Wary of CDynLinkLibrary:
    Its automatic injection into global resource chains can create brittle teardown sequences and exit deadlocks during sudden application termination.
  3. Always Use DisableThreadLibraryCalls:
    Unless a DLL explicitly requires per-thread state initialization, disabling these notifications removes unneeded OS synchronization bottlenecks.

7. Frequently Asked Questions (FAQ)

Q1. What is the fundamental difference between MFC Extension and Regular DLLs?
Regular DLLs maintain an independent CWinApp instance and private module state (AFX_MODULE_STATE). Extension DLLs have no private state; they attach to the caller's module chain via CDynLinkLibrary.
Q2. Can MFC-based clients seamlessly link against pure Win32 DLLs?
Yes. Standard C++ classes and C exports (__declspec(dllexport)) adhere to standard Win32 ABI conventions, ensuring 100% interoperability regardless of host framework.
Q3. Why did the hang only reproduce upon immediate window exit?
Normal workflows teardown UI objects gradually through standard WM_DESTROY channels. Rapid exits collapse framework layers quickly, triggering a race condition between CDynLinkLibrary destruction and the Loader Lock.