107
behavioral concurrency

Master-Slave

Reference Wikipedia ↗
Master-Slave — sequence diagram
Plate 107 sequence diagram

The Master-Slave pattern is a concurrency model where one thread (the master) distributes work to multiple other threads (slaves). The master thread typically manages the tasks, assigns them to available slaves, and aggregates the results. Slaves operate independently, processing their assigned tasks without direct communication with each other, and reporting back to the master upon completion. This pattern is useful for parallelizing computationally intensive tasks and improving performance.

This pattern enhances scalability and responsiveness. By offloading tasks to slaves, the master thread remains free to handle other requests or manage the overall system. The slaves can run on separate cores or even separate machines, further increasing the processing capacity. However, the master becomes a single point of failure, and efficient task distribution is crucial to avoid resource contention and ensure optimal utilization of the slave threads.

Usage

The Master-Slave pattern is widely used in scenarios that involve parallel processing and data distribution, including:

  • Database Replication: A primary database server (master) replicates its data to one or more read-only replica servers (slaves). Reads are often directed to the slaves to reduce load on the master.
  • Distributed Computing: Frameworks like Hadoop and Spark utilize a master-slave architecture to distribute data and computation across a cluster of machines.
  • Image and Video Processing: Dividing a large image or video into smaller chunks and processing them concurrently on multiple worker threads.
  • Game Development: Utilizing multiple threads to handle different aspects of the game world, such as AI, physics, and rendering.

Examples

  1. Apache Hadoop: Hadoop utilizes a Master-Slave architecture. The NameNode is the master, managing the file system metadata and coordinating data processing. DataNodes are the slaves, storing the actual data blocks and performing computations as instructed by the NameNode. Hadoop’s MapReduce framework further leverages this pattern to distribute processing tasks.

  2. Redis (Master-Replica Replication): Redis, a popular in-memory data store, supports master-slave (now more commonly referred to as master-replica) replication. The master node receives all write operations, and the replica nodes asynchronously replicate the data. Reads can be distributed to the replicas to improve performance and availability. If the master fails, one of the replicas can be promoted to become the new master.

Specimens

15 implementations
Specimen 107.01 Dart View specimen ↗

The Master-Slave pattern (also known as Leader-Follower) involves one object (the Master) holding the primary data and control, while other objects (the Slaves) synchronize their state with the Master. Changes are made to the Master, and these changes are then propagated to the Slaves. This ensures data consistency across multiple instances.

The Dart implementation uses a Subject class as the Master, holding the data and a list of Observer (Slave) instances. The Subject notifies its observers whenever its state changes using a simple callback mechanism. This approach leverages Dart’s support for functional programming with the use of functions as first-class citizens, making the observer list and notification process concise and readable. The use of a StreamController provides a more robust and reactive approach for larger, more complex scenarios.

// subject.dart
import 'dart:async';

class Subject {
  String _data = '';
  final StreamController<String> _controller = StreamController();

  String get data => _data;

  void setData(String newData) {
    _data = newData;
    _controller.sink.add(_data);
  }

  void addObserver(Observer observer) {
    _controller.stream.listen(observer.update);
  }
}

// observer.dart
abstract class Observer {
  void update(String data);
}

// main.dart
import 'subject.dart';
import 'observer.dart';

void main() {
  final subject = Subject();

  final observer1 = ObserverImplementation(id: 1);
  final observer2 = ObserverImplementation(id: 2);

  subject.addObserver(observer1);
  subject.addObserver(observer2);

  subject.setData('Initial Data');
  subject.setData('Data Updated!');
}

class ObserverImplementation implements Observer {
  final int id;
  ObserverImplementation({required this.id});

  @override
  void update(String data) {
    print('Observer $id received update: $data');
  }
}