CACS355 Network Programming

Network ProgrammingUnit 313 min read

Multithreading in Network Programming: Threads, Synchronization & Concurrency

Unit 3 of Network Programming explores how multithreading improves scalability and efficiency in client-server applications by handling multiple client requests concurrently, covering thread creation, synchronization, and real-world Java implementations for high-performance networking.

TAKEAWAYS:

  • Multithreading allows a server to handle multiple clients simultaneously, improving throughput and responsiveness.
  • Java’s Thread and Runnable interfaces enable lightweight concurrency for network operations.
  • Synchronization mechanisms (synchronized, ReentrantLock) prevent race conditions in shared resources.
  • Thread pools (ExecutorService) optimize resource usage by reusing threads instead of creating new ones.
  • Network applications like eSewa and Daraz use multithreading to manage thousands of concurrent transactions.
  • Deadlocks and livelocks are critical issues in multithreaded networking; proper design avoids them.

1. Introduction to Multithreading in Network Programming

Network applications often face challenges like:

  • Single-threaded servers block on I/O (e.g., waiting for client requests), wasting CPU cycles.
  • Scalability limits: A single thread can only handle one client at a time, leading to slow responses under load.

Multithreading solves this by:

  • Running multiple threads concurrently to handle multiple clients.
  • Offloading blocking operations (e.g., reading/writing sockets) to threads while the main thread processes other tasks.

1.1 Why Multithreading for Networking?

Consider a Daraz server during a sale:

  • Thousands of users place orders simultaneously.
  • A single-threaded server would process orders one by one, causing delays.
  • A multithreaded server assigns a thread per client, processing orders in parallel.

Visualization:


2. Thread Basics in Java

Java provides two ways to create threads:

  1. Extending Thread class (not recommended for networking due to inflexibility).
  2. Implementing Runnable interface (preferred for better design).

2.1 Thread Lifecycle

A thread transitions through states:

  1. New: Created but not started.
  2. Runnable: Ready to run (may be waiting for CPU).
  3. Running: Executing.
  4. Blocked/Waiting: Waiting for I/O or synchronization.
  5. Terminated: Execution completed.
stateDiagram-v2
    [*] --> New
    New --> Runnable: start()
    Runnable --> Running: CPU scheduled
    Running --> Blocked: I/O wait
    Blocked --> Runnable: I/O completes
    Running --> Terminated: finish()
    [*] --> Terminated

2.2 Creating a Thread for Networking

Example: A simple TCP server handling multiple clients using Runnable.

// ServerThread.java
public class ServerThread implements Runnable {
    private Socket clientSocket;
    public ServerThread(Socket socket) {
        this.clientSocket = socket;
    }
    public void run() {
        try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
             BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                out.println("Echo: " + inputLine);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Trace Table:

Step Action Thread State Socket State
1 Thread created New Closed
2 start() called Runnable Closed
3 accept() returns client socket Running Connected (client)
4 readLine() Blocked (I/O) Connected (client)
5 Data received Running Connected (client)
6 writeLine() Blocked (I/O) Connected (client)
7 Client disconnects Terminated Closed

3. Thread Synchronization

When multiple threads access shared resources (e.g., a shared ArrayList of client connections), race conditions occur:

  • Thread A reads a value, Thread B modifies it, Thread A writes the old value → data corruption.

3.1 Synchronization Mechanisms

Mechanism Description Example Use Case
synchronized Locks a method/block to allow one thread at a time. Shared counter for client connections.
ReentrantLock More flexible than synchronized (supports fairness, tryLock). High-contention scenarios (e.g., NEPSE).
volatile Ensures visibility of changes across threads (no atomicity). Flags for server shutdown.

Example: Synchronized Counter

public class ClientCounter {
    private int count = 0;
    public synchronized void increment() {
        count++;
    }
    public int getCount() {
        return count;
    }
}

Race Condition Without Synchronization:

sequenceDiagram
    participant Thread1
    participant Thread2
    participant SharedVar
    Thread1->>SharedVar: read count (5)
    Thread2->>SharedVar: read count (5)
    Thread1->>SharedVar: write count (6)
    Thread2->>SharedVar: write count (6)
    Note over Thread1,Thread2: Lost update! Final count is 6, not 7

With Synchronization:

sequenceDiagram
    participant Thread1
    participant Thread2
    participant SharedVar
    Thread1->>SharedVar: synchronized block (lock acquired)
    Thread2->>SharedVar: waits for lock
    Thread1->>SharedVar: read count (5)
    Thread1->>SharedVar: write count (6)
    Thread1->>SharedVar: unlock
    Thread2->>SharedVar: synchronized block (lock acquired)
    Thread2->>SharedVar: read count (6)
    Thread2->>SharedVar: write count (7)

4. Thread Pools (ExecutorService)

Creating threads dynamically is inefficient. Instead, use a thread pool:

  • Reuses threads for multiple tasks.
  • Limits resource usage (e.g., 100 threads max).

Example: Fixed Thread Pool for a Server

ExecutorService executor = Executors.newFixedThreadPool(10);
while (true) {
    Socket clientSocket = serverSocket.accept();
    executor.submit(new ServerThread(clientSocket));
}

Advantages:

  • Avoids thread creation overhead.
  • Prevents resource exhaustion (e.g., NTC’s call center handling 10,000 concurrent calls).

Disadvantages:

  • Threads may starve if tasks are not distributed fairly.
  • Requires tuning (e.g., pool size = CPU cores × 2).

5. Real-World Applications

5.1 eSewa: Handling Concurrent Transactions

  • Idea: Uses multithreading to process thousands of payment requests simultaneously.
  • How:
    • Each transaction is assigned a thread from a pool.
    • Synchronization ensures no double-spending (race condition prevention).
  • Worked Example: Suppose 500 users initiate payments in 1 second. A single-threaded server would take 500 seconds. A multithreaded server with 100 threads processes all in ~5 seconds.

5.2 Pathao: Ride Dispatching

  • Idea: Multithreading manages driver availability and ride assignments.
  • How:
    • One thread handles driver sign-ins.
    • Another thread assigns rides to available drivers.
    • Synchronization ensures no double-booking.

6. Common Pitfalls and Solutions

Pitfall Cause Solution
Deadlock Threads wait for each other’s locks. Use tryLock() with timeouts.
Livelock Threads repeatedly yield without progress. Add random delays in retry logic.
Starvation Some threads never get CPU time. Use fair locks (ReentrantLock(true)).
Memory Leaks Threads hold references to objects. Use weak references or WeakHashMap.

Example Deadlock:

// Thread 1
lock1.lock();
lock2.lock(); // Deadlock if Thread 2 holds lock2

// Thread 2
lock2.lock();
lock1.lock(); // Deadlock if Thread 1 holds lock1

Solution: Acquire locks in a fixed order.

lock1.lock();
lock2.lock(); // No deadlock if order is consistent

7. Comparison: Single-Threaded vs. Multithreaded Servers

Feature Single-Threaded Server Multithreaded Server
Throughput Low (1 request at a time) High (N requests concurrently)
Resource Usage Low (1 thread) High (N threads)
Scalability Poor (bottleneck at CPU) Good (add more threads)
Complexity Simple High (synchronization, deadlocks)
Example Use Case Small-scale apps (e.g., local chat) Large-scale apps (e.g., Daraz, eSewa)

8. Exam Tips

  1. Define Key Terms:

    • Thread: Lightweight process that runs concurrently.
    • Race Condition: Undefined behavior due to unsynchronized access.
    • Thread Pool: Reusable threads for efficient task scheduling.
  2. Code Questions:

    • Always show thread creation (Thread or Runnable).
    • Include synchronization (e.g., synchronized block).
    • Demonstrate thread pool usage (ExecutorService).
  3. Real-World Tie-Ins:

    • Link multithreading to NEPSE stock trading (concurrent order processing) or WhatsApp (handling millions of messages).
  4. Common Mistakes to Avoid:

    • Forgetting to close() sockets in threads (leaks resources).
    • Not handling InterruptedException (threads can be interrupted).
    • Overusing threads (prefer thread pools).
  5. Problem-Solving Approach:

    • For a multithreaded server question:
      1. Identify shared resources (e.g., client list).
      2. Choose synchronization (e.g., synchronized method).
      3. Use a thread pool for scalability.
      4. Handle exceptions gracefully.

9. Worked Example: Prime Number Checker

Task: Write a multithreaded TCP server/client to check if a number is prime.

Server Code

public class PrimeServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(1234);
        ExecutorService executor = Executors.newFixedThreadPool(5);
        while (true) {
            Socket clientSocket = serverSocket.accept();
            executor.submit(new PrimeChecker(clientSocket));
        }
    }
}

class PrimeChecker implements Runnable {
    private Socket clientSocket;
    public PrimeChecker(Socket socket) { this.clientSocket = socket; }
    public void run() {
        try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
             PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
            int num = Integer.parseInt(in.readLine());
            boolean isPrime = isPrime(num);
            out.println(isPrime ? "Prime" : "Composite");
        } catch (IOException e) { e.printStackTrace(); }
    }
    private boolean isPrime(int n) {
        if (n <= 1) return false;
        for (int i = 2; i <= Math.sqrt(n); i++) {
            if (n % i == 0) return false;
        }
        return true;
    }
}

Client Code

public class PrimeClient {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("localhost", 1234);
        PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        out.println(17); // Test number
        System.out.println(in.readLine()); // Output: "Prime"
        socket.close();
    }
}

Trace Table for Server Thread:

Step Action Thread State Socket State Shared Resource (Prime Check)
1 Thread created New Closed -
2 start() called Runnable Closed -
3 accept() returns client socket Running Connected (client) -
4 readLine() Blocked (I/O) Connected (client) -
5 Data received (17) Running Connected (client) Input: 17
6 isPrime(17) Running Connected (client) Check divisors 2-4
7 writeLine("Prime") Blocked (I/O) Connected (client) Output: "Prime"
8 Client disconnects Terminated Closed -

10. Advanced: Thread-Local Storage

For network applications, thread-local storage (ThreadLocal) avoids shared state:

  • Each thread has its own copy of a variable (e.g., client ID).
  • Used in NTC’s call routing system to track each caller’s session.
ThreadLocal<Socket> clientSocket = new ThreadLocal<>();
// Set in ServerThread constructor:
clientSocket.set(socket);
// Use in run():
Socket sock = clientSocket.get();

11. Summary Diagram

Based on the TU BCA syllabus for Network Programming (CACS355), unit 3.

Discussion

Loading…