Skip to Content
Course content

105: Threads with std::thread

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

I've been thinking about how to handle heavy lifting in our applications. Imagine we're building a simple file indexer—something that scans a bunch of "files" (simulated strings) for a specific keyword. If we do this on the main thread, our whole program just freezes until the scan is done. That's a terrible user experience.

The Freeze

I'll start by writing a basic search function that takes a while to run. I'm using std::this_thread::sleep_for here just to simulate a heavy I/O operation or a complex regex search.

#include <iostream>
#include <string>
#include <vector>
#include <thread>
#include <chrono>

void scan_files(std::string keyword) {
    std::cout < "[Worker] Scanning files for: " < keyword < "...\n";
    std::this_thread::sleep_for(std::chrono::seconds(3)); // Simulate work
    std::cout < "[Worker] Found 42 matches for " < keyword < "!\n";
}

int main() {
    std::cout < "Starting indexer...\n";
    scan_files("cpp_tutorial");
    std::cout < "Done!\n";
    return 0;
}

When I run this, it's boring. The program prints "Starting indexer...", hangs for three seconds, and then prints "Done!". If this were a GUI app, the window would be "Not Responding". I want that scan to happen in the background so I can do other things in main().

The Crash and the "Join"

Right, let's throw std::thread at this. I'll wrap that scan_files call in a thread object to kick it off into the background.

int main() {
    std::cout < "Starting indexer...\n";
    
    std::thread worker(scan_files, "cpp_tutorial");
    
    std::cout < "I can do other things while the worker scans!\n";
    return 0;
}

I ran this, and it crashed. Immediately. The console screamed something about std::terminate being called. I forgot a crucial rule: a std::thread object must be either joined or detached before it is destroyed.

Essentially, the worker object in main went out of scope (because the program ended) while the actual OS thread was still running. C++ doesn't know if you want to wait for that thread to finish or let it run wild in the background, so it just kills the whole process to be safe. I'll use .join(), which tells the main thread: "Stop here and wait until the worker is completely finished."

int main() {
    std::cout < "Starting indexer...\n";
    
    std::thread worker(scan_files, "cpp_tutorial");
    
    std::cout < "I can do other things while the worker scans!\n";
    
    worker.join(); // Wait for the worker to finish
    std::cout < "Worker finished. Now we can exit.\n";
    return 0;
}

Now it works. The "I can do other things" message prints immediately, and then the program pauses at worker.join() until the three-second sleep is over.

Passing Data Safely

Now, what if I want to pass a large object to the thread? Let's say a vector of filenames. If I just pass it as a reference, I might run into a disaster where the main thread modifies or deletes the vector while the worker is still reading it.

I noticed that std::thread copies the arguments by value by default. That's actually a safety feature. But if I really want to pass by reference (maybe for performance with a massive dataset), I can't just put &my_vector in the constructor. std::thread handles arguments in a way that requires std::ref to explicitly signal a reference.

Let's look at how that works in practice:

void scan_list(const std::vector<std::string>& files) {
    std::cout < "[Worker] Scanning " < files.size() < " files...\n";
}

int main() {
    std::vector<std::string> my_files = {"doc1.txt", "doc2.txt", "image.png"};
    
    // This would fail to compile or behave unexpectedly:
    // std::thread worker(scan_list, my_files); 
    
    // Use std::ref to pass by reference
    std::thread worker(scan_list, std::ref(my_files));
    
    worker.join();
    return 0;
}

Just a warning: when you use std::ref, you are now responsible for the lifetime of that object. If main returned before the worker finished, the worker would be reading a deleted vector. That's why join() is your best friend here.




📋 Practical Task

Build a Multi-Threaded Log Processor

Create a program that simulates processing three different log files simultaneously.

  • Create a function void process_log(int log_id) that prints "Processing log [id]...", sleeps for a random duration between 1 and 3 seconds (using std::this_thread::sleep_for), and then prints "Log [id] complete!".
  • In your main function, spawn three separate threads, each processing a different log ID (1, 2, and 3).
  • Ensure the main thread prints "All logs are being processed in parallel..." before it begins waiting for the threads.
  • Properly join() all three threads before the program exits to prevent a crash.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.