Skip to Content
Course content

197: ManualResetEvent and AutoResetEvent

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

I've spent a lot of time reviewing junior code, and there is one mistake that pops up almost every time we hit synchronization primitives: the belief that ManualResetEvent and AutoResetEvent are just alternative ways to write a lock statement.

Thinking Events Protect Data Like Locks Do

If you think of these events as "locks," you're going to run into race conditions that are a nightmare to debug. A lock (or a Mutex) is about mutual exclusionβ€”it ensures that only one thread can touch a piece of data at a time. Events, however, are about signaling. They aren't there to stop two threads from crashing into each other; they are there to tell a thread, "Okay, the thing you were waiting for is finally ready."

Imagine you have a ManualResetEvent and you use it to "lock" a shared list. If you call Set() to let a thread in, but forget to call Reset(), every single other thread in your application will just barge right through that gate. You haven't protected your data; you've just built a door that you forgot to close. To protect data, use lock. To coordinate timing, use events.

ManualResetEvent: The Broadcast Signal

Think of a ManualResetEvent as a security gate at a concert. Once the promoter opens the gate (calls Set()), it stays open. Every single person in line (every thread calling WaitOne()) can walk through. They don't have to wait for the gate to be opened again; they just flow through until someone manually shuts the gate (calls Reset()).

I usually use these for "global" states. For example, if you have five different background services that all need to wait until the main configuration file is loaded before they start working, a ManualResetEvent is your best bet. One thread loads the config and calls Set(), and all five services wake up simultaneously.


// The gate starts closed (false)
ManualResetEvent configLoadedEvent = new ManualResetEvent(false);

// Background thread
Task.Run(() => {
    Console.WriteLine("Service A waiting for config...");
    configLoadedEvent.WaitOne(); 
    Console.WriteLine("Service A starting!");
});

// Main thread
Thread.Sleep(2000); // Simulating loading time
Console.WriteLine("Config loaded. Opening the gate!");
configLoadedEvent.Set(); // Everyone waiting is now released

AutoResetEvent: The Turnstile

An AutoResetEvent is completely different. It's not a gate; it's a turnstile. Only one person can go through at a time. As soon as one thread calls WaitOne() and is allowed to pass, the turnstile automatically snaps shut behind them. If another thread is waiting, it has to wait for the signal to be Set() again.

This is incredibly useful for producer-consumer scenarios. If you have a thread that processes a single item from a queue, you don't want every worker thread to wake up when only one item is available. That would cause a "thundering herd" problem where ten threads fight over one piece of data. With AutoResetEvent, only one thread wakes up, takes the item, and the event automatically resets for the next one.


// The turnstile starts closed
AutoResetEvent itemReadyEvent = new AutoResetEvent(false);

// Worker thread
Task.Run(() => {
    while(true) {
        itemReadyEvent.WaitOne(); // Wait for signal
        Console.WriteLine("Worker processed one item.");
        // No need to call Reset(); it happened automatically!
    }
});

// Producer thread
itemReadyEvent.Set(); // Releases ONE thread
Thread.Sleep(1000);
itemReadyEvent.Set(); // Releases the next ONE thread

My rule of thumb? If you're saying "Everyone wake up!", use ManualResetEvent. If you're saying "One of you wake up and handle this," use AutoResetEvent.




πŸ“‹ Practical Task

Exercise: The Multi-Stage System Bootloader

You are building a simulation of a system boot-up sequence. The system has three components: Hardware, Network, and UI.

  • The Hardware and Network modules must both initialize before the UI can start. (Hint: Use a ManualResetEvent to signal the UI that the system is "Ready").
  • Once the UI is running, it will send "Initialization Tasks" (like loading user preferences, checking updates, and syncing cache) to a single Background Worker thread. These tasks must be processed one by one. (Hint: Use an AutoResetEvent to signal the worker that a new task is available).

Requirements:

  1. Create a ManualResetEvent named _systemReady. Start the UI thread, but make it wait on this event.
  2. Simulate the Hardware and Network modules loading (use Task.Delay). Once both are done, call Set() on _systemReady.
  3. Create an AutoResetEvent named _taskAvailable. Create a worker thread that loops and waits for this event to process tasks.
  4. The UI thread, after waking up, should trigger _taskAvailable three separate times to simulate three distinct tasks being queued.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.