DEV Community

zzz111
zzz111

Posted on

Understanding Thread Synchronization in Windows C++: A Practical Guide to CRITICAL_SECTION

Understanding Thread Synchronization in Windows C++: A Practical Guide to CRITICAL_SECTION

Introduction

Multithreading is widely used in modern Windows applications, especially in network servers, background services, file processing tools, and real-time applications.

However, when multiple threads access the same shared resource, unexpected problems may occur. These issues are often difficult to reproduce because they depend on thread scheduling and execution timing.

One of the most common problems is a race condition, where the final result depends on the order in which different threads execute.

In this article, we'll explore how to use the Windows CRITICAL_SECTION synchronization mechanism to protect shared resources, along with practical examples and common mistakes.

1. What Is a Race Condition?

Consider an application where multiple threads update a shared counter.

A simplified example might look like this:

volatile LONG g_counter = 0;

DWORD WINAPI WorkerThread(LPVOID)
{
    for (int i = 0; i < 100000; ++i)
    {
        g_counter++;
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

If several threads execute this function simultaneously, the final counter value may be lower than expected.

The reason is that g_counter++ is not an atomic operation.

It typically involves reading the current value, incrementing it, and writing the result back to memory.

Two threads may read the same value before either writes the updated result, causing one increment to be lost.

Also, volatile does not make an operation atomic or provide thread synchronization.

2. Introducing CRITICAL_SECTION

Windows provides CRITICAL_SECTION as a lightweight synchronization primitive for threads within the same process.

A critical section allows only one thread at a time to enter a protected region.

The basic workflow is:

  1. Initialize the critical section.
  2. Enter the critical section before accessing shared data.
  3. Perform the protected operation.
  4. Leave the critical section.
  5. Delete the critical section when it is no longer needed.

Example:

#include <Windows.h>

CRITICAL_SECTION g_cs;
LONG g_counter = 0;

DWORD WINAPI WorkerThread(LPVOID)
{
    for (int i = 0; i < 100000; ++i)
    {
        EnterCriticalSection(&g_cs);

        g_counter++;

        LeaveCriticalSection(&g_cs);
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Before creating worker threads, initialize the synchronization object:

InitializeCriticalSection(&g_cs);
Enter fullscreen mode Exit fullscreen mode

After all worker threads have finished, release it:

DeleteCriticalSection(&g_cs);
Enter fullscreen mode Exit fullscreen mode

It is important not to delete a critical section while another thread is still using it.

3. A Complete Multithreading Example

The following program creates four worker threads and uses a critical section to protect a shared counter.

#include <Windows.h>
#include <stdio.h>

CRITICAL_SECTION g_cs;
LONG g_counter = 0;

DWORD WINAPI WorkerThread(LPVOID)
{
    for (int i = 0; i < 100000; ++i)
    {
        EnterCriticalSection(&g_cs);
        ++g_counter;
        LeaveCriticalSection(&g_cs);
    }

    return 0;
}

int main()
{
    InitializeCriticalSection(&g_cs);

    HANDLE threads[4] = {0};

    for (int i = 0; i < 4; ++i)
    {
        threads[i] = CreateThread(
            NULL,
            0,
            WorkerThread,
            NULL,
            0,
            NULL
        );

        if (threads[i] == NULL)
        {
            printf("CreateThread failed: %lu\n",
                GetLastError());
            break;
        }
    }

    for (int i = 0; i < 4; ++i)
    {
        if (threads[i] != NULL)
        {
            WaitForSingleObject(
                threads[i],
                INFINITE
            );

            CloseHandle(threads[i]);
        }
    }

    printf("Final counter: %ld\n", g_counter);

    DeleteCriticalSection(&g_cs);

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

When all four threads are successfully created and complete their work, the expected result is:

Final counter: 400000
Enter fullscreen mode Exit fullscreen mode

If thread creation fails, the actual result will depend on how many threads were successfully started.

4. Analyzing Multithreaded Debug Logs

In real-world applications, logging is often essential for identifying synchronization issues.

For example, a network service might record the following diagnostic information:

[14:22:01.120] [INFO] Worker thread 1 started
[14:22:01.121] [INFO] Worker thread 2 started
[14:22:01.122] [DEBUG] Shared resource initialized
[14:22:01.123] [DEBUG] Buffer content: KFCFK9uun6llbi2o9hg1jcxwdj3gmm9h0gquwf246fVW50CF
[14:22:01.124] [INFO] Worker thread 1 entered critical section
[14:22:01.125] [INFO] Worker thread 1 released critical section
[14:22:01.126] [INFO] Worker thread 2 entered critical section
[14:22:01.127] [INFO] Worker thread 2 released critical section
[14:22:01.128] [INFO] Shared resource updated successfully
Enter fullscreen mode Exit fullscreen mode

These logs can help developers understand the sequence of operations.

However, timestamps alone do not prove that synchronization is correct.

For more reliable analysis, developers should also inspect thread states, lock ownership, and the code paths accessing shared resources.

5. Common CRITICAL_SECTION Mistakes

Holding a Lock for Too Long

A common performance problem occurs when a thread holds a critical section while performing slow operations.

For example:

EnterCriticalSection(&g_cs);

Sleep(5000);

LeaveCriticalSection(&g_cs);
Enter fullscreen mode Exit fullscreen mode

During the five-second delay, other threads attempting to enter the same critical section will be blocked.

Whenever possible, keep the protected region short.

Forgetting to Release the Lock

If a function returns without calling LeaveCriticalSection, other threads may wait indefinitely.

For complex functions, developers should use structured cleanup logic or an RAII wrapper to ensure that locks are released correctly.

Using Multiple Locks in an Inconsistent Order

Deadlocks may occur when different threads acquire multiple locks in different orders.

For example:

  • Thread A acquires Lock 1 and waits for Lock 2.
  • Thread B acquires Lock 2 and waits for Lock 1.

Neither thread can proceed.

A common solution is to establish a consistent lock acquisition order throughout the application.

6. CRITICAL_SECTION vs Mutex

Both critical sections and mutexes can protect shared resources, but they have different characteristics.

Feature CRITICAL_SECTION Mutex
Synchronization scope Same process Same or different processes
Kernel object handle No ordinary mutex handle Yes
Recursive acquisition Supported Supported
Named synchronization Not supported Supported
Typical use Internal thread synchronization Thread or process synchronization

For synchronization between threads in the same process, CRITICAL_SECTION is often a practical choice.

For synchronization between separate processes, a Windows mutex may be more appropriate.

7. Performance Considerations

Synchronization introduces overhead, especially when many threads compete for the same lock.

A heavily contended critical section can become a bottleneck.

Some practical optimization techniques include:

  • Reducing the amount of work performed while holding a lock.
  • Avoiding unnecessary shared state.
  • Using separate locks for independent resources.
  • Considering atomic operations for simple counters.
  • Measuring lock contention before making architectural changes.

For example, if the only requirement is to increment a shared integer, Windows provides InterlockedIncrement:

LONG g_counter = 0;

InterlockedIncrement(&g_counter);
Enter fullscreen mode Exit fullscreen mode

This performs an atomic increment without requiring an explicit critical section.

However, atomic operations are not a replacement for locks when several related operations must be performed together.

8. Conclusion

Thread synchronization is an essential part of reliable multithreaded programming on Windows.

CRITICAL_SECTION provides a straightforward way to protect shared resources within a process, but developers must use it carefully.

Incorrect synchronization can lead to race conditions, deadlocks, inconsistent data, and difficult-to-reproduce failures.

By keeping critical sections short, following consistent lock acquisition rules, and choosing the appropriate synchronization primitive for each task, developers can build more stable and efficient Windows applications.

For more advanced scenarios, it is also worth exploring reader-writer locks, condition variables, thread pools, and lock-free programming techniques.

Top comments (0)