Concepts / Managing Shared State Between Threads

Managing Shared State Between Threads

Multiple threads can be coordinated by tracking how many of them are still running in a shared counter named threadsRunning.

  • Programming

Why a Program Needs to Know When Work Is Done

When a program starts several threads to do work at the same time, the rest of the program often needs to know one simple fact: have all of them finished? Without an answer to that question, the program cannot decide when to stop a busy indicator, reset a button, or release the objects that hold the threads. Thread synchronization is the practice of managing multiple threads so that they complete their tasks without interfering with each other or with the main flow of the program.

This article follows one common approach described in the source material. Instead of asking each thread to report directly to the interface, each finishing thread calls a single method named threadFinished. That method updates a shared counter, and only the thread that brings the counter to zero performs the final cleanup. As you read, keep one question in mind: at what point does the program know the work is over, and what has to happen before that point for the answer to be correct?

The Shared Counter threadsRunning

Definition

Multiple threads can be coordinated by tracking how many of them are still running in a shared counter named threadsRunning. The counter records how many threads have not yet finished, and every finishing thread updates that same record.

Because the record is shared, every thread reads and writes the same value. The source describes the variable as the number of threads still running. Each call to threadFinished begins by subtracting one from threadsRunning. Since the counter starts at the number of threads launched and drops by one for each thread that finishes, its value at any moment tells you how much work remains.

Start: 3three threads runningAfter first finish: 2two threads runningAfter second finish:1one thread runningAfter third finish: 0all threads finishedone thread finishesone thread finishesone thread finishes
After each thread finishes, what is the value of threadsRunning, and at what point does it reach zero?

The diagram uses three threads so the countdown is easy to follow. The counter moves one step for each finishing thread, and the only value that matters for the completion decision is the final one, zero. The diagram is a simplified model: it shows one ordered sequence of finishes, while the real program may have finishes happening close together. That interleaving is what makes the order of operations, covered next, so important.

The Decrement Comes First

Each finishing thread performs its update before it performs any test. In the source's method, the first statement subtracts one from threadsRunning. Only after that does the method check whether the value has reached zero. This ordering is the single most important fact for reading the completion logic, and it is where many learners lose track of the flow.

python
Notes on the code
  • The first line is an update: it changes the shared counter.
  • The if line is the test: it is the synchronization decision.
  • The indented lines are reactions: they run only when the test passes.

A useful way to read any line of this method is to ask whether it is an update, a test, or a reaction to the test. The decrement is an update. The if statement is the test. Everything inside the braces is a reaction that runs only when the test passes. Sorting lines this way makes it easier to see where a result diverges from what you expected.

Tracing the Completion Path

What do you think happens?

Three threads finish one after another. When the completion branch runs, what value does threadsRunning hold?

  • 3, because the counter never changes

  • 1, because the last thread does not decrement

  • 0, because the branch runs only when the decrement brings the value to zero

Reveal answer
Answer:

0, because the branch runs only when the decrement brings the value to zero.

The source states that only the call that brings the value to zero triggers the completion branch. Every thread decrements first, so the counter reaches zero exactly once, on the last finish.

threadFinishedcalleda thread finishesDecrementthreadsRunningupdatethreadsRunning == 0?the all-finished testMethod endsremaining threads continueStart button resettext Start Again, enabledrunning set to falsereactionworkers set to nulldiscards the thread arraySelector re-enabledthread count selectorfirst stepthen testnoyesthenthenthen
When a thread finishes, which steps run in order, and which line decides whether all threads are done?

Follow the flowchart from top to bottom. The thread calls threadFinished, the counter is decremented, and then the zero test runs. The test is the line that makes the all-finished decision. If the count is not zero, the method ends and the other threads keep working. If the count is zero, the reactions run in the order the source describes: the start button is relabeled and enabled, running is set to false, workers is set to null, and the thread count selector is re-enabled.

The Zero Test as the Synchronization Signal

The check threadsRunning == 0 is the synchronization decision. The source states the rule plainly: when threadsRunning equals 0, all threads have finished. The check does not ask whether a particular thread is done. It asks only whether the shared count has reached its end. This is why the count is the mechanism and the check is the signal.

Resetting the Interface After Completion

The cleanup branch is where thread status reaches the user. The source describes two interface elements that depend on completion. The start button is relabeled and enabled so the user can start again, and the thread count selector is enabled so the user can change how many threads to run. Both changes happen only inside the branch that runs when every thread has finished. Before that point, the interface is still showing work in progress.

threadsRunningreaches 0all-finished decision trueStart buttontext changed to StartAgain, enabledrunning flagset to falseworkers arrayset to null, thread objectsreleasedThread count selectorre-enabled for the usersignalssignalssignalssignals
Once the all-finished decision is true, which interface elements receive the signal, and what does each one change to?

The diagram shows the same branch from the data side. One condition, the counter reaching zero, sends a signal that touches four places. Two of them are visible to the user: the start button and the thread count selector. The other two are program state: the running flag and the workers array. None of these four changes happens while threads are still finishing, because all of them sit inside the branch.

Tracing a Result That Goes Wrong

Worked example

What happens if the test runs before the decrement

Suppose a developer reorders the method so the zero test runs before the decrement, with two threads still running when the last one finishes. Does the completion branch run at the right time?

  1. 1
    Start state

    threadsRunning is 1, because one thread is still working and the other has finished.

  2. 2
    Test first

    The zero test reads 1. The condition is false, so the completion branch does not run.

  3. 3
    Decrement second

    The method subtracts one, so threadsRunning becomes 0. Nothing in the method checks again.

  4. 4
    Result

    The counter reached zero but the branch never ran. The start button stays disabled, the selector stays locked, and workers is never cleared.

Result

The completion decision diverged from what was expected. The fix is to restore the order: decrement first, then test.

Mistakes When Reading Completion Logic

  • Common mistake

    Assuming every thread runs the cleanup

    Why it fails

    Only the call that brings the value to zero triggers the completion branch. Other threads finish their own call and simply return.

    Do this instead

    Read the if statement as a gate that most calls fail and only the last call passes.

  • Common mistake

    Thinking the test asks whether a specific thread is done

    Why it fails

    The check asks only whether the shared count has reached zero. It does not identify which thread finished last.

    Do this instead

    Treat threadsRunning as the signal and the zero test as the reading of that signal.

  • Common mistake

    Reading the decrement and the test as one step

    What it looks like

    Swapping the two lines so the test happens before the subtraction.

    Why it fails

    The test then sees the value before the last finish is recorded, so the completion branch can be skipped.

    Do this instead

    Keep the decrement first, as the source describes, and confirm that order whenever a completion result looks wrong.

  • Common mistake

    Expecting the interface to update before all threads finish

    Why it fails

    The start button, running flag, workers array, and selector are all changed only inside the branch that runs after the count reaches zero.

    Do this instead

    Before the branch runs, the interface should still show work in progress.

Practice Tracing the Counter

MEDIUM

Four threads are started. Trace threadsRunning through the four finishes. For each finish, write the value after the decrement and say whether the completion branch runs. Then say which single change would fix a version of the method where the test is placed before the decrement.

Hints
  • Start at 4 and subtract one for each finishing thread.
  • The completion branch runs only on the call that produces zero.
  • If the test comes first, it compares a value that is one higher than the one the decrement will leave behind.

Key Takeaways

  • A shared counter named threadsRunning tracks how many threads are still working, and every finishing thread updates it.
  • Each finishing thread decrements the counter first, and only the call that brings the value to zero triggers the completion branch.
  • The check threadsRunning == 0 is the all-finished decision, and the branch it guards resets the start button, sets running to false, clears workers, and re-enables the thread count selector.
  • When a completion result is wrong, check the order of the decrement and the zero test before anything else.