CodeGym /Courses /C# SELF /The Shared Resources Problem

The Shared Resources Problem

C# SELF
Level 56 , Lesson 0
Available

1. Introduction

In a multithreaded application a shared resource is anything that two or more threads can access at the same time. This can be:

  • A variable (for example, a global counter or a list).
  • An object (for example, a collection of users).
  • A file or a network socket.
  • Any data structure modified by different threads.

In our console apps we’ll most often run into variables and objects that are "shared" between threads.

Analogy

Imagine two people trying to write something into the same notebook at the same time without agreeing on turns. In the best case you get a messy entry, in the worst case someone overwrites someone else's data. In programming it's exactly the same, only these “people” are threads.

Briefly about typical resources with data races

In the table below are the most common resources that are dangerous for concurrent access from different threads:

Resource Problem groups Example
Variables of type int Incorrect increment/decrement Counters, indexes
Shared collections Loss/corruption of items, exceptions Shared orders list
Objects Inconsistent state changes Flags, properties
Files Data corruption, incorrect read/write Log files, configuration

2. Race condition: how does it show up?

Example: Visit counter

Say we want to count how many times a user clicked a button (or, in our example, how many times different threads incremented a variable). Simple version of the code:


int counter = 0;

void Increment() {
    counter++;
}

Now we create two threads, each calling Increment() 100,000 times:


using System;
using System.Threading;

class Program
{
    static int counter = 0;

    static void Increment()
    {
        for (int i = 0; i < 100_000; i++)
        {
            counter++;
        }
    }

    static void Main()
    {
        Thread t1 = new Thread(Increment);
        Thread t2 = new Thread(Increment);

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine($"Expected: 200000, got: {counter}");
    }
}

How many times should counter logically be incremented? 200000! But if you run this code multiple times you'll almost certainly see different numbers: 185000, 192500, 198765… Why?

3. Why counter++ is not an atomic operation?

How counter++ actually works

In C# and other high-level languages the program is translated into a set of machine instructions. Unfortunately the operator counter++ does not turn into one magical "add 1 to variable" command. Here's what actually happens:

  1. The thread READS the value from memory (counter).
  2. Increases that value by 1 (in a CPU register).
  3. Writes the new value back to memory (counter).

If two threads do this at almost the same time they can both read the same old value, increment it, and both write the result back, losing one increment.

Race scenario

Say counter was 1000. Both threads read that value (step 1), both increment locally to 1001 (step 2), and then both write back 1001 (step 3). Horrible: one increment is simply lost!

Race visualization

Time Thread 1 Thread 2 Value of counter
1 Read 1000 1000
2 Read 1000 1000
3 Increment to 1001 Increment to 1001 1000 (not written yet)
4 Write 1001 1001
5 Write 1001 1001

As a result, two increments only increased the value by 1!

4. A few more examples: "invisible bugs"

What if the race condition doesn't involve numbers?

Now imagine several threads adding items to the same list:


using System;
using System.Collections.Generic;
using System.Threading;

class Program
{
    static List<int> numbers = new List<int>();

    static void AddNumbers()
    {
        for (int i = 0; i < 10000; i++)
        {
            numbers.Add(i);
        }
    }

    static void Main()
    {
        Thread t1 = new Thread(AddNumbers);
        Thread t2 = new Thread(AddNumbers);

        t1.Start();
        t2.Start();

        t1.Join();
        t2.Join();

        Console.WriteLine($"Expected: 20000, got: {numbers.Count}");
    }
}

This code can also give different results on each run: sometimes the program will crash (exception), sometimes you'll see fewer items than expected.

Why? Because the collection List<T> is not thread-safe out of the box. So when two threads call Add at the same time the internal structure of the list can get corrupted.

5. Atomicity of operations

What is an atomic operation?

An operation is atomic if it executes entirely without being interruptible by another thread in the middle. It's like a "transaction": either all of it happens or none of it.

  • Assignment operations for int like myVar = 42; are atomic on most platforms (unless it's a huge object).
  • But counter++ is not atomic — it's three separate actions.

Special atomic operations

In .NET there are special classes for atomic operations: for example, Interlocked. We'll look at this approach in upcoming lectures.

Example of an atomic increment using Interlocked.Increment:


using System.Threading;

int counter = 0;
Interlocked.Increment(ref counter); // atomic operation!

6. Why catching a race condition is hard?

Race conditions are dangerous because:

  • They may appear only under high load.
  • They are not caught 100% of the time — maybe 5% or even 0.01% of runs.
  • They fail "randomly" and happen where nobody expects them.

How to recognize the problem?

If each run of your program gives different (and incorrect) results, you should suspect a data race.

Programmer jokes

"If a bug appears rarely and is fixed by adding Thread.Sleep(50) — you have bigger problems than it seems."

7. Useful nuances

Synchronization

To protect critical sections (parts of code that work with shared resources) you need to synchronize them. But that's a topic for later lectures. Right now the main thing is to learn to notice and explain the problem.

Typical mistakes by beginners

Many beginners think: “I have counter++ — what could go wrong?” Unfortunately, once you have more than one thread, everything can go wrong! Even seemingly simple things: reading and writing variables, adding items to a list, changing object state and much more.

The place of data races in real development

In modern multithreaded applications (for example, in server APIs, web request handling, games and mobile apps) there are almost always shared resources. Without synchronization data races lead to incorrect order processing, crashes, memory leaks and huge debugging headaches.

In interviews for middle/senior positions you'll definitely be asked: "What is a race condition? How to avoid it?" If you can give the examples above — and explain the mechanics — recruiters will be happy!

2
Task
C# SELF, level 56, lesson 0
Locked
Race Condition Detection Example with an Integer Counter
Race Condition Detection Example with an Integer Counter
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION