GUI, Browser, Server and Embedded Concurrency

Different environments impose different concurrency rules.

Learning Objectives

Apply main-thread UI rules, UI event loops and dispatching to the UI thread; keep interfaces responsive with background workers and Web Workers.

Compare Node.js server concurrency, thread-per-request and event-driven server models; use async database clients and connection pools; handle saturation, request cancellation and graceful shutdown.

Build embedded concurrency: superloops and cooperative schedulers, ISRs versus task context, interrupt masking and critical sections, RTOS tasks and priorities, semaphores/queues/event flags/mutexes, priority inversion, task notifications, ISR-safe APIs, DMA and cache coherency, ISR/task shared memory, volatile and atomics, memory barriers, watchdogs, deadlines and jitter, WCET, safety-critical review, deterministic shutdown and fault handling.

Plain-Language Explanation

Different environments impose different concurrency rules. A user interface has one thread that draws the screen — block it and the app freezes. A server must juggle thousands of clients without letting any one of them hog resources. A microcontroller reacts to hardware interrupts in microseconds and must meet deadlines every time, not just on average. The same ideas — queues, locks, priorities, cancellation — appear in all three, but with different constraints.

Precise Technical Explanation

Main-thread UI rules UI toolkits (Android, iOS/macOS, Windows, browsers, most desktop GUIs) are single-threaded: widgets may be touched only from the UI (main) thread; long work there freezes rendering and input (Android ANR after ~5 s; browsers flag long tasks over 50 ms).

UI event loops The UI thread runs an event loop: input events, timers, layout, paint (Module 11).

Dispatching updates to the UI thread runOnUiThread/Handler/Dispatchers.Main (Android), DispatchQueue.main/@MainActor (Apple), Dispatcher.Invoke/SynchronizationContext (WPF/WinUI), postMessage back from workers (browsers).

Long-running tasks and responsive interfaces Move work off the UI thread; show progress; allow cancellation; update the UI only with results; chunk work that must touch UI state.

Thread pools, coroutines on background dispatchers, BackgroundService, Android WorkManager for deferrable work surviving process death.

Browser Web Workers Module 17 (JS): workers for CPU work, OffscreenCanvas for rendering off the main thread.

Node.js server concurrency One event loop per process for I/O; worker_threads for CPU; cluster/multiple processes for cores.

Thread-per-request versus event-driven server models Virtual threads/goroutines blocking, simple Memory per connection a thread stack blocking the loop

Measured below with 100 concurrent slow requests.

Async database clients asyncpg, R2DBC, Npgsql async, node-postgres: free the thread while waiting for the database; the connection pool remains the real limit.

Connection pools and saturation When all connections are busy, requests wait for one — queueing delay grows quickly as utilisation approaches 100% (measured below; Module 22 explains why).

Server request cancellation Detect client disconnects (request.is_disconnected, HttpContext.RequestAborted, ctx.Done()) and cancel downstream work.

Graceful server shutdown Stop accepting, finish in-flight requests within a deadline, close pools (Go example in Module 17).

Embedded superloops and cooperative schedulers A superloop (while (1) { poll sensors; update control; handle comms; }) with interrupt handlers setting flags — simple and deterministic if every step is short. Cooperative schedulers run task functions in turn (run-to-completion).

Interrupt service routines (ISR) Hardware events preempt normal code; ISRs must be short, non-blocking, re-entrant-aware, and defer work to tasks.

ISR versus thread/task context ISRs cannot block, cannot take ordinary mutexes, run with interrupts of lower priority masked, and use the interrupt stack; tasks can block and are scheduled by the RTOS.

Interrupt masking and critical sections Disabling interrupts (or raising BASEPRI on Cortex-M) makes a short sequence atomic with respect to ISRs on a single core; keep such sections to microseconds (they add interrupt latency). On multicore MCUs, a spinlock is also needed.

RTOS tasks and priorities Fixed-priority preemptive scheduling: the highest-priority ready task runs; equal priorities may time-slice. Priorities are assigned by deadline/period (rate-monotonic: shorter period → higher priority).

Semaphores, queues, event flags and mutexes in RTOS Binary/counting semaphores (ISR → task signalling), queues (copying data between tasks/ISRs), event groups (wait for combinations of bits), mutexes (with priority inheritance) — FreeRTOS, Zephyr, ThreadX, embOS.

Priority inversion in real-time systems Module 5; RTOS mutexes implement priority inheritance; semaphores used as locks do not.

Lightweight direct-to-task signals (FreeRTOS xTaskNotify) — faster than semaphores for one-to-one signalling.

Only special variants may be called from ISRs (xQueueSendFromISR, xSemaphoreGiveFromISR with pxHigherPriorityTaskWoken and portYIELD_FROM_ISR).

DMA and cache-coherency considerations Clean/invalidate caches around DMA buffers, align them to cache lines (Memory course, Modules 8 and 16).

Shared memory between ISR and tasks Single-writer variables, volatile for compiler visibility, atomics for multi-word or read-modify-write updates, ring buffers with one writer per index, or short critical sections.

volatile and atomics in embedded systems volatile makes the compiler perform every access (needed for flags set in ISRs and for registers) but not atomicity — a 32-bit counter on an 8-bit MCU tears; x++ is still read-modify-write. Use C11 atomics where supported, or mask interrupts.

Memory barriers and hardware-specific requirements Cortex-M needs DSB/ISB after some system-register writes and before sleeping; DMA and multicore parts need barriers; follow the vendor's reference manual.

Watchdogs and deadlock recovery A hardware watchdog resets the system if software stops "kicking" it — the last line of defence against deadlocks and hangs; kick it only from a place that proves all critical tasks are alive.

Real-time deadlines and jitter Hard real-time: missing a deadline is a failure; soft: degraded quality. Jitter (variation in start/finish times) matters for control loops.

Worst-case execution time WCET bounds each task's execution time (measurement plus analysis); schedulability tests (rate-monotonic bound, response-time analysis — computed below) check that all deadlines are met.

Safety-critical design and review Static allocation, bounded loops, no unbounded blocking, priority-inheritance mutexes, watchdogs, MISRA C, ISO 26262/IEC 61508 processes, timing analysis.

Deterministic shutdown and fault handling Defined safe states (outputs off), fault handlers that record context and reset safely, brown-out handling.

Embedded caution: the C program below is a *host simulation* on Linux: a POSIX timer signal stands in for a hardware timer interrupt and sigprocmask for interrupt masking. Real targets differ — use your MCU's reference manual and RTOS documentation; this is not a portable API.

Why It Matters in Real Systems

Frozen apps, overloaded servers and missed control deadlines are all concurrency failures specific to their environment; the same principles adapted correctly prevent them.

Key Terms

Main/UI thread The only thread allowed to touch UI objects Bare-metal infinite loop calling each job in turn Interrupt service routine Critical section (embedded) Code run with interrupts masked Shorter period → higher priority Worst-case execution time

Hardware timer that resets a stuck system

Syntax and API Overview

The specification's RTOS-style pseudocode (illustrative, not a portable API):

Queue capacity bounds how far the sensor may run ahead; the send timeout decides what happens when the control task falls behind (drop, overwrite, or report a fault); ControlTask usually has the higher priority because its output deadline is tighter; a missed ReceiveFromQueue timeout is a detectable deadline miss; if an ISR produces samples it must use the ISR-safe send variant; actual API names and rules are RTOS-specific (FreeRTOS xQueueSend, Zephyr k_msgq_put, …).

Task SensorTask:
    while true:
        sample = ReadSensor()
        SendToQueue(sensorQueue, sample, timeout)
        Sleep(period)

Task ControlTask:
    while true:
        sample = ReceiveFromQueue(sensorQueue, timeout)
        if sample available:
            UpdateControlOutput(sample)

Complete Code Example

Thread-per-request versus event-driven servers and connection-pool saturation (CPython 3.13.1, local sockets; one real run):

An embedded-style superloop with a timer "ISR", a single-producer/single-consumer ring buffer, an interrupt-masked critical section and a rate-monotonic schedulability check — host simulation on Linux (GCC 11.2; a POSIX interval-timer signal plays the timer interrupt):

import asyncio
import threading
import time
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


class Slow(BaseHTTPRequestHandler):
    peak = 0
    live = 0
    lock = threading.Lock()

    def do_GET(self):
        with Slow.lock:
            Slow.live += 1
            Slow.peak = max(Slow.peak, Slow.live)
        time.sleep(0.2)                                    # a blocking 200 ms "database call"
        with Slow.lock:
            Slow.live -= 1
        self.send_response(200); self.end_headers(); self.wfile.write(b"ok")

    def log_message(self, *args):
        pass


class Server(ThreadingHTTPServer):
    request_queue_size = 128                               # listen backlog (socketserver's default is only 5)


def threaded_server(clients):
    srv = Server(("127.0.0.1", 0), Slow)
    threading.Thread(target=srv.serve_forever, daemon=True).start()
    url = f"http://127.0.0.1:{srv.server_address[1]}/"
    t0 = time.perf_counter()
    with ThreadPoolExecutor(clients) as pool:
        list(pool.map(lambda _: urllib.request.urlopen(url).read(), range(clients)))
    elapsed = time.perf_c
…
thread-per-request: 100 concurrent 200 ms requests in 0.49 s; peak concurrent handler threads 100
event-driven:       100 concurrent 200 ms requests in 0.27 s on 1 event-loop thread (threads alive: 1)
pool of 20 connections, 20 requests x 50 ms: total 0.06 s, median wait    0 ms, max wait    0 ms
pool of  5 connections, 20 requests x 50 ms: total 0.25 s, median wait  123 ms, max wait  185 ms
#include <signal.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/time.h>
#include <unistd.h>

#define RB_SIZE 16u                                    /* power of two */
static volatile uint16_t rb[RB_SIZE];
static atomic_uint head, tail;                         /* ISR writes head, main loop writes tail */
static volatile sig_atomic_t ticks;                    /* single-writer flag/counter from the "ISR" */
static uint32_t uptime_ms_lo, uptime_ms_hi;            /* a 64-bit value split in two words: needs a critical section */
static unsigned dropped;

static void timer_isr(int sig)                          /* stands in for a hardware timer interrupt */
{
    (void)sig;
    ticks++;
    unsigned h = atomic_load_explicit(&head, memory_order_relaxed);
    unsigned t = atomic_load_explicit(&tail, memory_order_acquire);
    if (h - t < RB_SIZE) {
        rb[h % RB_SIZE] = (uint16_t)(1000 + ticks);     /* "ADC sample" */
        atomic_store_explicit(&head, h + 1, memory_order_release);   /* publish after writing the slot */
    } else {
        dropped++;                                       /* queue full: count, never block in an ISR */
    }
    if (++upt
…
superloop consumed 200 samples from the timer ISR via an SPSC ring buffer (dropped 0)
uptime read inside a masked critical section: 201 ms (ticks seen 201)
RM utilisation 0.550 vs Liu-Layland bound 0.780 for 3 tasks -> schedulable under rate-monotonic priorities

Line-by-Line Explanation

Thread-per-request: ThreadingHTTPServer starts a thread per connection; the blocking 200 ms handlers run concurrently, one thread each. The listen backlog is raised to 128 — with socketserver's default of 5, a burst of 100 connections overflows the accept queue, clients' connection attempts are dropped and retried, and the run takes seconds instead of a fraction of a second (a real-world tuning knob: somaxconn, backlog).

Event-driven: one asyncio thread serves all 100 connections; each handler suspends during its wait.

Pool saturation: with 20 connections nobody waits; with 5 connections, 20 requests queue — the median and maximum waits grow by multiples of the 50 ms query time.

Superloop: the "ISR" writes a slot then publishes head with release; the main loop reads head with acquire, consumes slots and publishes tail — single-writer indices, no locks. A full buffer drops samples rather than blocking (ISRs must never block).

Critical section: the two-word uptime would tear if the ISR fired between reading the low and high words; masking the signal (disabling the "interrupt") makes the read atomic.

Rate-monotonic: total utilisation 0.2 + 0.2 + 0.15 = 0.55 ≤ 0.780 — schedulable.

Interleaving or State Trace

Ring buffer: head = tail = 0; ISR writes rb[0], head = 1; ISR writes rb[1], head = 2; main loop reads head = 2 (acquire), consumes rb[0], rb[1], sets tail = 2 (release) — the ISR's capacity check uses this tail.

Correctness and Safety Analysis

Server: every request's waiting is bounded by the pool and timeouts; the event loop never blocks. Embedded: single-writer indices + acquire/release (or volatile + barriers on targets without atomics) make the ring buffer safe; multi-word state is read with interrupts masked; ISRs never block.

Common Mistakes and Misconceptions

Updating UI widgets from background threads.

Unbounded thread-per-request servers under load.

Assuming async removes database limits (the pool still saturates).

Long critical sections with interrupts disabled; blocking calls in ISRs; non-ISR-safe RTOS APIs in ISRs.

volatile used as if it provided atomicity.

Failure Modes and Debugging Steps

UI freezes: profile the main thread (long tasks, StrictMode, Main Thread Checker). Server latency spikes: pool wait time metrics. Embedded: trace tools (Tracealyzer, SystemView), GPIO toggling + logic analyser for timing, watchdog reset reason registers, stack high-water marks.

Performance and Scalability Implications

Thread-per-request costs memory per connection; event-driven costs discipline; pools cap throughput at connections / query time; on MCUs, interrupt latency is bounded by the longest masked section.

Security Considerations

UI threads should not process untrusted large inputs synchronously (freeze = DoS); servers need connection limits and timeouts; embedded systems need watchdogs and safe states against fault injection.

Platform and Runtime Caveats

The embedded example is a Linux host simulation; Cortex-M interrupt masking uses __disable_irq()/BASEPRI, and C11 atomics may be unavailable or emulated on small cores. FreeRTOS, Zephyr and ThreadX APIs differ.

Real-World Applications

Android/iOS apps, VS Code (Electron main/renderer/worker processes), nginx and Node servers, connection-pool tuning in Spring/HikariCP, automotive ECUs, drones and industrial controllers on RTOSes.

Best Practices

Keep UI and event-loop threads free; post results back.

Bound server concurrency and pool waits; cancel on disconnect; shut down gracefully.

ISRs: short, non-blocking, ISR-safe APIs, defer work.

Protect multi-word ISR/task data with masking or atomics; use priority-inheriting mutexes; run schedulability analysis; keep a watchdog.

Exercises

E1 · Beginner. Why must an Android app not perform network I/O on the main thread?

Answer: The main thread runs the UI event loop; blocking it freezes the interface (and Android forbids it, throwing NetworkOnMainThreadException).

E2 · Intermediate. A service has 10 database connections and queries take 20 ms. What is the maximum throughput, and what happens beyond it?

Answer: 10 / 0.02 s = 500 queries/s. Beyond that, requests queue for connections and latency rises without bound unless the queue is limited and excess requests are rejected.

E3 · Advanced challenge. Why is the uptime read wrapped in a critical section while the ring-buffer indices are not?

Answer: Each index is a single word written by exactly one side and published with release/acquire, so reads cannot tear and ordering is guaranteed. The 64-bit uptime is two separate words updated by the ISR; without masking, the main loop could read the low word before a carry and the high word after it.

Quiz

? Which server model uses one thread to serve many concurrent connections?
+ Event-driven (async) :: The loop multiplexes connections.
- Thread-per-request :: One thread each.
- Process-per-request :: One process each.
- Fork-per-request :: One process each.

? What may an ISR safely do in an RTOS?
+ Signal a task with an ISR-safe API and return quickly. :: E.g. xSemaphoreGiveFromISR.
- Block on a mutex until data is available. :: ISRs must not block.
- Call printf for debugging in production. :: Slow and often unsafe.
- Disable interrupts for 100 ms. :: Ruins interrupt latency.

? Which are true? (select all that apply)
+ UI widgets should be updated only from the UI thread. :: Most toolkits are single-threaded.
+ A connection pool caps throughput at roughly connections / query time. :: Little's Law.
- volatile makes x++ atomic on an MCU. :: It only forces memory accesses.
+ A watchdog resets a system that stops kicking it. :: Last-resort recovery.

Summary

UIs keep one thread for drawing and push work elsewhere; servers trade thread-per-request simplicity against event-driven efficiency, with connection pools as the true limit; embedded systems combine superloops or RTOS tasks with short ISRs, masked critical sections, single-writer buffers, priority-inheriting mutexes, schedulability analysis and watchdogs. The principles are shared; the constraints are environment-specific.

Further Reading

Android Developers: "Processes and threads", "Keep your app responsive".

Phil Koopman, *Better Embedded System Software*.

Richard Barry, *Mastering the FreeRTOS Real Time Kernel*.

C. L. Liu & James Layland, "Scheduling Algorithms for Multiprogramming in a Hard-Real-Time Environment" (1973).