Skip to Content
Course content

136: Mutexes and Locking with C11 Threads

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Why can't I just use a global variable for my counter?

I get this question a lot. It seems intuitive: if you have a global int total_requests, and ten threads increment it, you should end up with ten. But C doesn't work that way at the hardware level. An operation like total_requests++ isn't a single step; it's actually three: load the value from memory into a register, increment it, and store it back.

If Thread A loads the value (say, 5) and is then interrupted by Thread B, which also loads 5, increments it to 6, and stores it... Thread A still thinks the value is 5. It increments its local register to 6 and stores it. You just lost an entire increment. We call this a race condition, and it's the absolute worst kind of bug because it's non-deterministic. Your code might work perfectly on your laptop but crash or give wrong totals on a production server with more cores.

What's the actual workflow for using mtx_t?

In C11, we use the mtx_t type from threads.h. Think of a mutex (short for mutual exclusion) as a physical key to a room. If you have the key, you can enter the critical section of your code. If someone else has it, you have to wait in line until they come out and hand it over.

Here is how you actually implement it. I'll use a simple shared counter for a simulated web server:

#include <stdio.h>
#include <threads.h>

int request_count = 0;
mtx_t count_mutex;

int handle_request(void* arg) {
    // Lock the mutex before touching the shared variable
    if (mtx_lock(&count_mutex) == thrd_success) {
        request_count++; 
        mtx_unlock(&count_mutex);
    }
    return 0;
}

int main() {
    // Initialize the mutex
    if (mtx_init(&count_mutex, mtx_plain) != thrd_success) {
        return 1; // Initialization failed
    }

    thrd_t t1, t2;
    thrd_create(&t1, handle_request, NULL);
    thrd_create(&t2, handle_request, NULL);

    thrd_join(t1, NULL);
    thrd_join(t2, NULL);

    printf("Total requests: %d\n", request_count);

    // Always clean up your resources
    mtx_destroy(&count_mutex);
    return 0;
}

Notice that I used mtx_plain. That's the standard, non-recursive mutex. The most important part here is that the time spent holding the lock is tiny. Never put a printf or a heavy network call inside a lock if you can avoid it, or you'll turn your multi-threaded program into a single-threaded one because every thread is just waiting in line.

How do I stop my program from hanging forever?

You've probably heard the term "deadlock." This usually happens in one of two ways. First, the "forgotten unlock." If your function returns early due to an error but you forgot to call mtx_unlock, every other thread that ever tries to touch that variable will hang indefinitely. I highly recommend keeping your lock/unlock pairs as close together as possible.

The second way is the "deadly embrace." This happens when Thread A locks Mutex 1 and waits for Mutex 2, while Thread B has locked Mutex 2 and is waiting for Mutex 1. Neither can move.

If you're in a situation where you can't afford to wait, C11 gives you mtx_trylock. Instead of putting the thread to sleep, it returns immediately. If it gets the lock, it returns thrd_success; if it's already locked, it returns thrd_busy. This lets you do other work or try again later instead of freezing your entire application.




📋 Practical Task

Implement a Thread-Safe Shared Log Buffer

You are building a logging system where multiple worker threads write messages to a shared global string buffer. Without synchronization, the messages will overlap and corrupt the text.

  • Create a global char log_buffer[1024] and a global int buffer_index = 0.
  • Initialize a mtx_t mutex to protect these two variables.
  • Write a function void write_log(const char* msg) that locks the mutex, appends the message to the buffer using strcat or sprintf (while ensuring you don't exceed the 1024 limit), updates the buffer_index, and then unlocks the mutex.
  • Spawn three threads that each call write_log five times with different messages.
  • Join the threads and print the final buffer to the console to verify that no messages were mangled.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.