- The threads in Windows They share memory within the same process, but each has its own stack and context, which requires protecting access to global data.
- CreateThread, WaitForSingleObject, mutex and CRITICAL_SECTION are the basic pieces for creating, waiting for and synchronizing threads in C with the Win32 API.
- Passing parameters through LPVOID and using shared structures allows multiple threads to reuse the same code while working on different data.
- Patterns such as shared counter and producer-consumer show the need for critical sections and condition variables to avoid race conditions.
If you program in C on Windows, sooner or later you're going to need perform multiple tasks at the same timeHandling interface events, processing data in the background, updating the console, etc. That's where threads come in. Although in GNU/Linux we usually use pthread And in POSIX, in Windows the story changes: here the star is the Win32 API And, when we talk about "pure and simple" C, functions like CreateThread, WaitForSingleObjectmutexes, critical sections, and condition variables.
In this guide you will see, calmly but without beating around the bush, How to create, manage, and synchronize threads in C for WindowsWe'll start with what a thread is, continue with basic examples of creation, parameter passing, and return of results, and finish by delving into critical sections, mutexes, and a classic of concurrency: the producer-consumer pattern. CRITICAL_SECTION y CONDITION_VARIABLEAll of this is done using WinAPI and taking into account the peculiarities of Windows compared to other systems.
What is a thread in Windows and how does it differ from a process?
A thread is, to put it simply, the minimum unit of execution that the operating system scheduler puts into operation on the CPU. When Windows allocates processor time, it doesn't do so between processes, it does so between WirelessEach process, in turn, can have one or more active threads.
In Windows, threads of the same process share the same address spaceThe shared memory includes executable code, global data, dynamic memory (heap), and most open resources (file handles, sockets, synchronization objects, etc.). Each thread has its own stack and register context, but all share the same memory.
A process, on the other hand, is a isolated instance of a program: has its own space of virtual memoryIts handle table, its set of threads, etc. Creating processes is considerably more expensive than creating threads because the system has to set up that entire isolated environment. Creating threads reuses much of the process's environment.
On multi-core machines, Windows can run multiple parallel wiresOne per logical core. If you have a processor with 4 cores, there can be 4 threads actually running at the same time. The remaining threads ready to run receive small "pieces" of CPU time through context switches.
What are threads used for in C applications on Windows?
The use of threads allows us, mainly, two very interesting things: on the one hand accelerate There of execution distributing work among several centers and, on the other hand, to make our application do several things at once without freezing. For example, a console application can continue responding to the keyboard while another thread updates the time in a corner of the screen.
In the context of Windows console games or tools, threads are very useful for separate game logic, user input, and background tasksYou can have one thread continuously checking the keyboard status, another updating animations or colors on screen, and the main one managing the main game loop.
It is also common to use threads in more "serious" desktop or server programs. process requests in parallel, do background work while the interface remains fluid or take advantage of I/O wait times to keep the CPU busy with other tasks.
Windows thread model: HANDLE, input function, and associated resources
When you create a thread in Windows using the classic API, the system returns a HANDLEThat is, a "handle" or opaque object with which you can wait for the thread, consult it, or close it; tools such as Process Hacker They allow you to inspect those threads. That handle is not the thread itself, but a reference to the thread object maintained by the operating system.
The function that the thread executes must conform to a specific signature. In the Windows API, this standard form is commonly used:
DWORD WINAPI ThreadFunc(LPVOID arg);
That LPVOID arg It's a generic pointer that allows you pass any data as a parameter Within the thread, you cast that pointer to the appropriate type, from an integer or a structure to a complex structure with multiple fields.
When the thread ends, the function returns a DWORD with the thread's exit code. From the main thread, you can retrieve that value later using the function GetExitCodeThread while the handle remains open.
Internally, Windows reserves for each thread their own stack (by default it is usually around 1 MB, although it can be adjusted in CreateThreadand internal structures such as the TEB (Thread Environment Block). Furthermore, it maintains all the information necessary for context switches: registers, instruction pointers, FPU state, etc.
Thread creation and context switching costs in Windows
Creating threads is lighter than creating processes, but it's not free. Every time you call CreateThreadWindows must allocate stack, create TEB, register thread in scheduler and return a valid HANDLE. If you need thousands of short-lived threads, that overhead becomes noticeable, and it makes sense to consider thread pools.
In addition, there is the cost of the context changesEach time the scheduler passes execution from one thread to another, it has to save the registers of the outgoing thread and restore those of the incoming thread, update the stacks, and so on. With many threads ready, the system spends more time switching contexts and moving data in the cache than executing useful code.
If you use Microsoft's standard C library, it's worth knowing that the CRT API offers features such as _beginthreadexwhich correctly initialize the per-threaded runtime library. Direct use of CreateThread in threads that call CRT functions (for example printf, malloc) can cause leaks or unusual behavior if care is not taken; furthermore, techniques such as DLL injection They usually resort to remote threads created by the API.
Creating threads in C with Windows: CreateThread step by step
The core function for launching threads in C with WinAPI is CreateThreadWhen called, the thread starts executing almost immediately (unless otherwise specified with flags). Let's look at the key parameters you need to master on a daily basis:
- lpThreadAttributesOptional security structure. In most educational or desktop scenarios, it is passed
NULLand it is ignored. - dwStackSize: initial stack size. If you pass 0, Windows uses the default size defined in the executable.
- lpStartAddress: thread input function address, for example
ThreadFunc. - lpParameter: pointer to the data you want to pass to the thread. It is received as
LPVOIDin the function. - dwCreationFlagsThis flag controls how the thread is created. With 0, the thread starts running immediately. With other flags, you can create threads that are initially suspended.
- lpThreadId: pointer to a
DWORDwhere Windows will write the thread numeric identifierIt is useful for debugging or for certain specific operations.
The function returns a HANDLE On the thread. If the value is different from NULLIt has been created correctly and the thread function code can now be running in parallel to the main thread.
To make the main thread wait for a secondary thread to finish, it is very convenient to use WaitForSingleObjectYou pass it the thread's HANDLE and a maximum waiting time. If you use the constant INFINITEThe main thread will remain blocked until the secondary thread finishes:
WaitForSingleObject(threadHandle, INFINITE);
Typically, the main thread launches one or more other threads, does its own work, and just before finishing the process or using the results from those threads, Please wait for its completion with this function.
Basic execution order control: Sleep and checks
Since the Windows scheduler is free to interrupt and resume threads at almost any time, it is sometimes convenient to introduce artificial waiting Sleep For educational purposes only. For example, if a secondary thread only executes one printf which is completed immediately, may seem to disregard the waiting order because the exit appears very close together.
Adding a call to Sleep(ms) Within the secondary thread, we force a visible pause, which clearly demonstrates that the main thread actually waits for the secondary thread to finish when we use WaitForSingleObjectThis is very useful when you're learning and want to see the intertwining of messages multi-threaded console.
Passing parameters to a thread and returning results
One of the keys of the programming with threads is how to pass input data and how to retrieve resultsWith the classic signature DWORD WINAPI ThreadFunc(LPVOID data) It is always done through the generic parameter LPVOID.
For example, imagine you want to launch a thread that adds two integers. From the main thread you can allocate memory for an array of ints with two positions, copy the operands there and pass the array address to CreateThread cast a LPVOIDWithin the thread, you do the reverse casting to int* And now you have access to the numbers.
To return a result From within the thread, there are several options. One is to use a protected global variable if there are other threads involved, but this isn't particularly elegant. Another, much cleaner option, is to also use dynamic memory or shared structures.
- You reserve an array or structure with space for operands and result.
- The thread reads the operands, calculates the sum, and writes the result to the same memory block.
- The main thread, once
WaitForSingleObjecthas confirmed that the thread has ended, Read the result from that memory.
Always remember that All global process variables are visible to all threadsThis facilitates the exchange of information, but also opens the door to a host of career conditions if access is not properly protected.
Get the ID assigned by Windows to a thread
In addition to the HANDLE that returns CreateThreadWindows assigns a numeric identifier (a DWORD) to each thread. If you're interested in knowing that ID, you just need to pass the address of a DWORD in the sixth parameter of CreateThread.
For example, you declare a DWORD threadId;and when calling CreateThread you pass it to him &threadIdWhen the function returns successfully, that variable contains the thread ID. This information is useful for debugging and logging. logs or for some advanced API functions that work with IDs instead of handles.
Multiple threads with the same function: reusing code
One of the advantages of the thread model is that the same input function can be used for many different threadsThe trick is that each thread receives a different block of data as a parameter, so they all execute the same code but work on their own data.
For example, you can define a structure with the information that each thread needs To display the time in a different position on the console: X and Y coordinates and an exit flag. In the main thread, you create two instances of that structure, with different coordinates, and launch two threads, passing the address of each structure to CreateThread.
Within the function of casting the thread LPVOID to a pointer to your structure, and from there each thread It uses its own coordinates and its own exit flagThe code is exactly the same, but the behavior is different for each thread thanks to that custom data.
Thus, with a single function you can display the time simultaneously in several places on the screeneach controlled by a different thread, waiting for the second to change, obtaining the time with GetLocalTime and refreshing the text with its own coordinates.
Synchronization, critical sections, and race conditions
When multiple threads access shared resources, it is mandatory to talk about critical sections and synchronization. A critical section is any stretch of code where a thread reads or writes data that can be used by other threads at the same time, such as a global variable, a linked list, a buffer, or even the console cursor.
If you allow two threads to modify a shared structure simultaneously, the result can be completely unpredictable. For example, if 300 threads increment the same global counter by 1, the logical outcome would be 300, but this isn't guaranteed. mutual exclusionYou might encounter 298, 295, or any other value. Several sums overlap.
In the case of the Windows console, a typical problem is that the cursor is uniqueIf one thread places the cursor at certain coordinates and is then interrupted, another thread can change the coordinates and write text. When the first thread resumes execution and writes its own text, it will do so where the second thread left off, not where it intended. The result: text in incorrect or jumbled positions.
To avoid these disasters, synchronization mechanisms are used such as mutexes, critical sections, and condition variableswhich allow serializing access to shared resources and ensuring that only one thread is in the critical section at a time.
Mutex in Windows: mutual exclusion for simple critical sections
Un mutex (from the English “mutual exclusion”) is a synchronization object that can only be in possession of one thread at a timeWhile one thread holds the mutex locked, any other thread that attempts to acquire it will have to wait.
In Windows, a mutex is created with CreateMutexwhich returns a HANDLE like any other system object. That mutex can have a global name; if you create it with a name, other threads and even other processes can open it with OpenMutexThe basic idea is:
- Create the mutex once at the start of the program.
- Before entering a critical section, wait for the mutex
WaitForSingleObject. - Execute the code that accesses the shared resource.
- By the end, release the mutex
ReleaseMutex.
If a thread reaches the mutex waiting stage and another thread is already in the critical section, the system blocks it until the mutex is released. This ensures that the code between acquiring and releasing the mutex executes effectively. as if it were atomic regarding other threads.
In console applications, it is very common to encapsulate screen access in a function that Request the mutex, trace the text, and release it.For example, a function TrazarTexto(x, y, texto) could:
- Open or receive the HANDLE of the shared mutex.
- Call to
WaitForSingleObjectfor get It. - Place the cursor with
SetConsoleCursorPositionand write withprintf. - Release the mutex with
ReleaseMutexand close your own HANDLE if applicable.
This ensures that even if multiple threads write to the console, the complete writing sequences (position + text) are protected and not mixed up. When the program is finished, don't forget close the mutex HANDLE with CloseHandle to free up resources.
Classic example: 300 threads incrementing a counter
A very useful example to understand critical sections is to launch hundreds of threads incrementing a global counterWithout synchronization, the final counter rarely matches the number of threads launched, because several sums overlap.
If you declare a global integer and create 300 threads, each adding 1 to that variable and terminating, you'll see that the result can be, for example, 298. That shows that there are race conditions: two threads read the old value, increment it each on their own and write the same value, losing a sum.
If you introduce a mutex around the increment operation, that is, each thread does WaitForSingleObject, increments the counter and then ReleaseMutexAll threads "queue up" to perform their sum. Only one thread is executing that fragment at any given time, and now, when the program finishes, the counter is 300.
This pattern of acquiring a mutex just before touching the shared data and releasing it as soon as you finish is the essence of the well-protected critical sections and the basis of secure concurrent programming.
Producers and consumers with CRITICAL_SECTION and ConditionVariable
When you need something more sophisticated than a simple counter, traditional concurrency patterns come into play, such as the one that producer-consumerIn Windows, in addition to mutexes, you have lighter and more specific mechanisms such as CRITICAL_SECTION y CONDITION_VARIABLE, perfect for coordinating multiple threads within the same process.
Imagine a scenario with 150 producer threads and 150 consumer threads sharing a buffer of only 10 elements. Each producer generates items for the buffer, sleeping for a random amount of time between 0 and 50 ms, and each consumer reads items from the buffer at its own pace. There is no guarantee about the execution order of the threads, but we do need the buffer to be... Do not fill with more than 10 elements or read when empty.
To solve this, three basic pieces are used:
-
An
CRITICAL_SECTIONthat protects access to the buffer and its indices. - An condition variable for producersto wake them up when there is free space.
- An condition variable for consumersto wake them up when items are available.
The general scheme of the producers is:
- Enter the critical section with
EnterCriticalSection. - While the buffer is full, sleep the thread with the appropriate condition variable: call
SleepConditionVariableCSassociating the condition to the critical section. - When there is space, produce the item, insert it into the buffer, and update the indexes.
- Exit the critical section with
LeaveCriticalSectiony target consumers that there are elements throughWakeConditionVariableoWakeAllConditionVariable.
Consumers follow a mirror logic:
- They enter the critical section.
- While the buffer is empty, they wait in the consumer condition.
- When there are elements, they consume one, updating the buffer state.
- They abandon the critical section and They wake up the producers to indicate that space has been freed up.
This pattern ensures that no producer thread writes to the buffer when it's full (it will be blocked until a consumer frees up space) and that no consumer attempts to read from the empty buffer (it will be idle until a new item is available). Furthermore, all of this is achieved with protected access to the buffer for ourselves thanks to the CRITICAL_SECTION.
Threads and development environment: notes on Visual Studio and C#
Although we are focusing on C and WinAPI here, it is very common to find examples in the Windows ecosystem. Visual C# and .NET using the namespace System.ThreadingThe general idea is the same: create threads, launch a function that runs in parallel, update a progress bar or a graphical control while the main thread continues to respond.
In C#, for example, it's typical to create a Windows Forms application with a secondary thread that manipulates a progress bar or changes colors, while the main thread responds to button events. The logic is similar: one thread executes a loop with Thread.Sleep To avoid overloading the CPU, one thread changes values while another sends messages or handles clicks. In this case, synchronization of access to UI controls must also be carefully managed, but the philosophy of "one thread not blocking the other" is exactly the same.
The important thing is that, both with Thread in C# as with CreateThread o _beginthreadex In C, you're playing by the same rules as concurrency, critical sections, and shared resourcesJust change the library you're working with.
Taken as a whole, the Windows threading model allows you to create a simple thread that prints messages with Sleep even to building complex architectures with dozens of producer and consumer threads coordinated with mutexes, critical sections and condition variables; the key is to understand well what each thread shares, when to synchronize and how to fearlessly use the WinAPI functions to control the creation, waiting and termination of each one.
Passionate writer about the world of bytes and technology in general. I love sharing my knowledge through writing, and that's what I'll do on this blog, show you all the most interesting things about gadgets, software, hardware, tech trends, and more. My goal is to help you navigate the digital world in a simple and entertaining way.
