DEV Community

pythonassignmenthelp.com
pythonassignmenthelp.com

Posted on

How I Debugged My First Multithreaded Program in C++ for a Systems Assignment

You're staring at your assignment, half your IDE windows open, and the clock keeps ticking. The professor said “Add multithreading to your C++ program,” and you kind of get the theory, but now your code is breaking in ways that don’t make sense. Sometimes it runs fine, sometimes it crashes, and sometimes it spits out weird numbers. That sinking feeling? I’ve been there. Debugging my first multithreaded C++ program for a systems assignment was a real eye-opener—and honestly, it taught me more about computers than any lecture.

Why Multithreading Is Tricky

When you write regular C++ code, you’re usually dealing with one thing at a time. But multithreading means you have several parts of your program running at the same time. The thing is, they can bump into each other, mess with the same variables, and create bugs that don’t show up every time you run your program.

For my assignment, I had to create a program that counted word frequencies in a text file, using multiple threads to speed things up. The idea: split the file into chunks, let each thread count words separately, then combine the results. Easy in theory, but in practice? Not so much.

Step 1: Starting Simple (Single Thread)

Before adding threads, I made sure my single-threaded solution worked. This helped isolate bugs that were unrelated to threading.

#include <iostream>
#include <fstream>
#include <map>
#include <string>
#include <sstream>

// Counts words in a file (single thread)
std::map<std::string, int> count_words(const std::string& filename) {
    std::ifstream infile(filename);
    std::map<std::string, int> word_count;
    std::string line;
    while (std::getline(infile, line)) {
        std::istringstream iss(line);
        std::string word;
        while (iss >> word) {
            ++word_count[word]; // Increase count for each word
        }
    }
    return word_count;
}

int main() {
    std::map<std::string, int> wc = count_words("sample.txt");
    for (auto& pair : wc) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }
}
Enter fullscreen mode Exit fullscreen mode

Why start here? If your basic logic is broken, adding threads will only make debugging harder. Always get your serial version working first.

Step 2: Introducing Threads (And the First Bug)

Okay, so I got bold and tried to parallelize the word counting. I split the file into chunks, then launched threads for each chunk. Here’s a simplified version:

#include <iostream>
#include <fstream>
#include <map>
#include <string>
#include <sstream>
#include <vector>
#include <thread>
#include <mutex>

std::mutex mtx; // Protects shared data

void count_words_chunk(const std::vector<std::string>& lines,
                       std::map<std::string, int>& word_count) {
    std::map<std::string, int> local_count;
    for (const auto& line : lines) {
        std::istringstream iss(line);
        std::string word;
        while (iss >> word) {
            ++local_count[word]; // Local counting
        }
    }
    // Merge local_count into shared word_count
    std::lock_guard<std::mutex> lock(mtx);
    for (const auto& pair : local_count) {
        word_count[pair.first] += pair.second;
    }
}

int main() {
    std::ifstream infile("sample.txt");
    std::vector<std::string> all_lines;
    std::string line;
    while (std::getline(infile, line)) {
        all_lines.push_back(line);
    }

    std::map<std::string, int> word_count;
    const int num_threads = 4;
    std::vector<std::thread> threads;
    int lines_per_thread = all_lines.size() / num_threads;

    // Assign chunks to threads
    for (int i = 0; i < num_threads; ++i) {
        int start = i * lines_per_thread;
        int end = (i == num_threads - 1) ? all_lines.size() : (i + 1) * lines_per_thread;
        std::vector<std::string> chunk(all_lines.begin() + start, all_lines.begin() + end);
        threads.emplace_back(count_words_chunk, chunk, std::ref(word_count));
    }

    for (auto& t : threads) {
        t.join(); // Wait for all threads to finish
    }

    for (auto& pair : word_count) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }
}
Enter fullscreen mode Exit fullscreen mode

Key lines to notice:

  • std::mutex mtx; and std::lock_guard<std::mutex> lock(mtx); — these keep threads from updating word_count at the same time.
  • Each thread counts words locally (local_count), then merges them into the shared map.

What happened when I skipped the mutex? Chaos. Sometimes the counts were wrong, sometimes my program crashed. Turns out, updating a shared variable from multiple threads without protection is asking for trouble.

Step 3: Debugging (Finding Where It Breaks)

Here’s the thing: multithreaded bugs are often “heisenbugs”—they vanish when you add print statements, and appear only under certain conditions. My first instinct was to sprinkle std::cout everywhere, but that can actually change the bug.

So, I learned to:

  1. Check the logic in each thread

    Make sure each thread works correctly with its own data, before touching shared variables.

  2. Use local variables as much as possible

    Local variables (like local_count above) aren’t shared, so they’re safe.

  3. Protect all shared data

    Anything that’s updated by multiple threads needs a mutex or similar protection.

  4. Test with different input sizes

    Sometimes bugs only show up when you have lots of data.

If you're stuck on a similar C/C++ project, this resource has helped students work through these concepts and see step-by-step examples.

Practical Debugging Example: Race Condition

Here’s a simple example (not word counting) that shows a race condition—the kind of bug you’ll see in multithreading:

#include <iostream>
#include <thread>

int counter = 0;

void increment_counter() {
    for (int i = 0; i < 1000; ++i) {
        ++counter; // No mutex: unsafe!
    }
}

int main() {
    std::thread t1(increment_counter);
    std::thread t2(increment_counter);

    t1.join();
    t2.join();

    std::cout << "Counter value: " << counter << std::endl; // Should be 2000
}
Enter fullscreen mode Exit fullscreen mode

What's the problem?

You’d expect the result to be 2000, but it’s almost always less. That’s because both threads are updating counter at the same time, sometimes overwriting each other’s work.

Fixing it with a mutex:

#include <iostream>
#include <thread>
#include <mutex>

int counter = 0;
std::mutex mtx;

void increment_counter() {
    for (int i = 0; i < 1000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++counter; // Safe now!
    }
}

int main() {
    std::thread t1(increment_counter);
    std::thread t2(increment_counter);

    t1.join();
    t2.join();

    std::cout << "Counter value: " << counter << std::endl; // Should be 2000
}
Enter fullscreen mode Exit fullscreen mode

Now, the output is always correct. The mutex ensures only one thread can increment the counter at a time.

Common Mistakes Students Make

Honestly, the hardest part of multithreading isn’t writing the code—it’s avoiding these classic mistakes:

1. Forgetting to Protect Shared Data

I see this a lot: students update shared variables from multiple threads without a mutex or other protection. It works fine with small inputs, but fails randomly with bigger ones. Always ask yourself: “Is this variable shared?”

2. Overusing Mutexes (Deadlocks)

It’s tempting to slap a mutex everywhere, but if you lock multiple mutexes in different orders, you can get deadlocks—your program just hangs forever. Keep mutex usage simple, and avoid locking more than one at a time if possible.

3. Assuming Multithreading Will Always Speed Up Your Program

This tripped me up in my algorithms class. Sometimes, the overhead of managing threads and mutexes means your program is slower than the single-threaded version. Test both, and don’t assume parallel means faster.

Key Takeaways

  • Always get your single-threaded version working before adding multithreading.
  • Protect shared data with mutexes, but keep mutex usage simple.
  • Debugging multithreaded code is tricky—bugs can be random and hard to reproduce.
  • Use local variables in your threads as much as possible; only share what's necessary.
  • Test with various input sizes, and check if multithreading actually improves performance.

Closing Thoughts

Multithreaded programming in C++ takes patience and practice, but you’ll get there. Every bug you fix teaches you something new—not just about code, but about how computers actually work. Keep at it, and don’t be afraid to break things as you learn!


Want more C/C++ tutorials and project walkthroughs? Check out https://pythonassignmenthelp.com/programming-help/cpp.

Top comments (0)