Producer-consumer model

Keywords: IE

The producer-consumer model is a classic multi-threaded design model, which provides a good solution for multi-threaded collaboration. The producer-consumer model is not one of the 23 OO (Object Oriented) design patterns of GOF, but a non-OO design pattern.

In the producer-consumer mode, there are usually two types of threads: the producer thread responsible for submitting user requests and the consumer thread responsible for specifically processing producer-submitted requests (user requests).

In the producer-consumer model, the producer and the consumer do not communicate directly, but first submit the task to the shared buffer, and then the consumer gets the task from the shared buffer and processes it.

The core of the producer-consumer model is the shared memory buffer. As a bridge between producer and consumer, memory buffer avoids direct communication between producer and consumer, thus decoupling producer and consumer. It is necessary to use memory buffer here. With memory buffer, the producer does not have to wait for the consumer to receive the task, but directly puts the task into the buffer to start the next task of "production". Consumers are similar. This enables producers and consumers to be independent and concurrent, alleviates the time lag in execution speed between producers and consumers, and supports the uneven situation of busyness.

Realization

Here we first define the data model PCDada for interaction between producers and consumers, which is used in all subsequent implementations.

/**
 * Created by zhangcs on 17-4-16.
 */
public final class PCData {

    private int intData;

    public PCData( int intData){
        this.intData = intData;
    }

    public int getIntData() {
        return intData;
    }

    @Override
    public String toString(){
        return "data:" + intData;
    }

}

1. Using synchronized lock and wait(), notify() method to realize

The implementation of wait() and notify() methods is relatively simple. Firstly, buffer is defined. List is used for convenience; secondly, buffer size is defined. When buffer is full or buffer is empty, producer and consumer are blocked respectively.

/**
 * Created by zhangcs on 17-4-16.
 * The producer inserts the task into the buffer
 */
public class Producer implements Runnable {

    // Buffer space maximum capacity
    private final int MAX_SIZE = 10;

    // Counter
    // Because the self-incremental operation of int is not thread-safe, Atomic Integer is used here.
    private static AtomicInteger counter = new AtomicInteger();

    private List<PCData> list;

    public Producer( List<PCData> list){
        this.list = list;
    }

    @Override
    public void run() {
        synchronized (list){

            while ( list.size() > MAX_SIZE){
                System.out.println( "Buffer full, producer blocked");
                try{
                    // Production is blocked due to unsatisfactory conditions
                    list.wait();
                } catch( InterruptedException e){
                    e.printStackTrace();
                }

            }

            // Create tasks and add buffers
            PCData data = new PCData( counter.incrementAndGet());
            list.add( data);
            System.out.println( data + " Join the queue");

            list.notifyAll();
        }

    }

    public int getMAX_SIZE(){
        return MAX_SIZE;
    }

}
/**
 * Created by zhangcs on 17-4-16.
 * Consumers get tasks from buffers and process them
 */
public class Consumer implements Runnable {

    private List<PCData> list;

    public Consumer( List<PCData> list){
        this.list = list;
    }

    @Override
    public void run() {
        synchronized (list){
            while( true){
                while ( list.size() <= 0){
                    System.out.println("Buffer no task, consumer blocking");
                    try{
                        // Consumption congestion due to unsatisfactory conditions
                        list.wait();
                    } catch( InterruptedException e){
                        e.printStackTrace();
                    }
                }

                // consumption
                PCData data = list.get( 0);
                list.remove( 0);
                System.out.println( "Consumer Consumption" + data );

                list.notifyAll();
            }
        }
    }

}

When using, pass the same buffer LinkedList to producer and consumer respectively.

@Test
public void test() throws InterruptedException {

    // Buffer
    List<PCData> list = new LinkedList<>();

    // Create multiple producers
    Producer producer1 = new Producer( list);
    Producer producer2 = new Producer( list);
    Producer producer3 = new Producer( list);
    Producer producer4 = new Producer( list);

    // Creating Consumers
    Consumer consumer = new Consumer( list);

    // Create thread pools and execute producers and consumers
    ExecutorService service = Executors.newCachedThreadPool();
    service.execute( producer1);
    service.execute( producer2);
    service.execute( producer3);
    service.execute( producer4);
    service.execute( consumer);

    Thread.sleep( 10 * 1000);

    service.shutdown();

}

2. Using Lock lock and await() and signal() methods

Using await() and signal() methods can achieve the effect that producers and consumers can share the lock object or use the lock object separately, which is similar to the above method. Here's an example of shared locks

/**
 * Created by zhangcs on 17-4-17.
 * The producer inserts the task into the buffer
 */
public class Producer implements Runnable {

    // Buffer size
    private final int MAX_VALUE = 10;

    // Carrier/Buffer
    private List<PCData> list;

    // Counter
    private static AtomicInteger counter = new AtomicInteger();

    // lock
    private final Lock lock;
    // Conditional variable with full buffer
    private final Condition full;
    // Conditional variable with empty buffer
    private final Condition empty;

    // Locks are introduced here, so that producers and consumers share the same lock.
    public Producer( List<PCData> list, Lock lock, Condition full, Condition empty){
        this.list = list;

        this.lock = lock;
        this.full = full;
        this.empty = empty;
    }

    @Override
    public void run() {

        // Lock up
        lock.lock();

        while( list.size() >= MAX_VALUE){
            System.out.println( "Buffer full, producer blocked");
            try{
                // Buffer full, blocking threads
                full.await();
            }catch( InterruptedException e){
                e.printStackTrace();
            }
        }

        // production
        PCData data = new PCData( counter.incrementAndGet());
        list.add( data);
        System.out.println( data + " Join the queue");

        full.signalAll();
        empty.signalAll();

        // Release lock
        lock.unlock();

    }

}
/**
 * Created by zhangcs on 17-4-17.
 * Consumers get tasks from buffers and process them
 */
public class Consumer implements Runnable {

    private List<PCData> list;

    // lock
    private final Lock lock;
    // Conditional variable with full buffer
    private final Condition full;
    // Conditional variable with empty buffer
    private final Condition empty;

    // Locks are introduced here, so that producers and consumers share the same lock.
    public Consumer( List<PCData> list, Lock lock, Condition full, Condition empty){
        this.list = list;

        this.lock = lock;
        this.full = full;
        this.empty = empty;
    }

    @Override
    public void run() {

        while( true){

            // Lock up
            lock.lock();

            while( list.size() <= 0){
                System.out.println("Buffer no task, consumer blocking");
                try{
                    // Consumption congestion due to unsatisfactory conditions
                    empty.await();
                } catch( InterruptedException e){
                    e.printStackTrace();
                }
            }

            // consumption
            PCData data = list.get( 0);
            list.remove( 0);
            System.out.println( "Consumer Consumption" + data );

            full.signalAll();
            empty.signalAll();

            // Release lock
            lock.unlock();
        }
    }
}

Common buffers and locks are passed in when used

@Test
public void test() throws InterruptedException {

  // Creating Buffers
  List<PCData> list = new LinkedList<>();
  // Create locks
  Lock lock = new ReentrantLock();
  Condition full = lock.newCondition();
  Condition empty = lock.newCondition();

  // Creating Producers
  Producer producer1 = new Producer( list, lock, full, empty);
  Producer producer2 = new Producer( list, lock, full, empty);
  Producer producer3 = new Producer( list, lock, full, empty);

  // Creating Consumers
  Consumer consumer = new Consumer( list, lock, full, empty);

  // Create thread pools and execute producer and consumer threads
  ExecutorService service = Executors.newCachedThreadPool();
  service.execute( producer1);
  service.execute( producer2);
  service.execute( producer3);
  service.execute( consumer);

  Thread.sleep( 10 * 1000);

  service.shutdown();
}

3. BlockingQueue implementation using blocking queue

There are five common BlockingQueue implementation classes, which can be selected according to the requirements.

Implementation class Explain
ArrayBlockingQueue Based on arrays, producers and consumers share the same lock object for data acquisition, which means that they cannot run in real parallel. At the time of creation, it can control whether the internal lock of the object is fair or not, and default to unfair lock.
LinkedBlockingQuque Based on linked list, when the internal data cache list reaches the maximum cache capacity, it will block. Producers and consumers use different locks to control it.
PriorityBlockingQueue Priority-based blocking queues block consumers only when there is no consumable data. Note: Producers cannot produce data faster than consumers, otherwise time will run out of all available heap memory space.
DelayQueue Elements can be retrieved from the queue only when the specified delay time has arrived. There is no size limit, so when inserting data into a queue, it will never block, only when the data is retrieved.
SynchronousQueue There is no buffer waiting queue, no data buffer space inside (its isEmpty() method always returns true), so data elements may exist only when consumers obtain them. Similar to the one-hand delivery scenario in real life, data is transmitted directly between the paired producers and consumers, and does not buffer the data into the data queue. SynchronousQueue is generally used for thread pools, such as Executors.newCachedThreadPool() using SynchronousQueue.

For the specific use of each BlockingQueue implementation class, refer to the API yourself. Here's a direct example

/**
 * Created by zhangcs on 17-4-16.
 * The producer inserts the task into the buffer
 */
public class Producer implements Runnable {

    private volatile boolean isRunning = true;
    private BlockingQueue<PCData> queue;
    // Because the incremental operation of int is not atomic, Atomic Integer is used here for counting.
    private static AtomicInteger integer = new AtomicInteger();

    public  Producer( BlockingQueue<PCData> queues){
        this.queue = queues;
    }

    @Override
    public void run() {
        PCData data = null;
        try{
            while ( isRunning) {
                Thread.sleep( 500);
                data = new PCData( integer.incrementAndGet());
                System.out.println( data + " Join the queue");
                if( !queue.offer( data, 2, TimeUnit.SECONDS)){
                    System.out.println( data + "Failure to join");
                }
            }
        }catch( InterruptedException ie){
            ie.printStackTrace();
            Thread.currentThread().interrupt();
        }
    }

    public void stop(){
        isRunning = false;
    }

}
/**
 * Created by zhangcs on 17-4-16.
 * Consumers get tasks from buffers and process them
 */
public class Consumer implements Runnable {

    private BlockingQueue<PCData> queue;

    public Consumer( BlockingQueue<PCData> queue){
        this.queue = queue;
    }

    @Override
    public void run() {
        try{
            while (true){
                PCData data = queue.take();
                if ( data != null){
                    System.out.println( "Consumer Consumption" + data);
                    // Waiting for 2000 ms to simulate the difference in execution speed between producers and consumers
                    Thread.sleep( 2 * 1000);
                }
            }
        }catch( InterruptedException ie){
            ie.printStackTrace();
            Thread.currentThread().interrupt();
        }
    }
}

Create a buffer when used and pass it to both producers and consumers

public static void main( String[] args) throws InterruptedException {

    // Buffer Establishment
    BlockingQueue<PCData> queue = new LinkedBlockingQueue<>(10);

    Producer producer1 = new Producer( queue);  // Establishing Producers
    Producer producer2 = new Producer( queue);
    Consumer consumer1 = new Consumer( queue);  // Establishing Consumers

    // Running producers and consumers with thread pools
    ExecutorService service = Executors.newCachedThreadPool();
    service.execute( producer1);
    service.execute( producer2);
    service.execute( consumer1);

    Thread.sleep( 20 * 1000);

    // Stop Producers
    producer1.stop();
    producer2.stop();

    Thread.sleep( 13000);

    service.shutdown();
}

4. Pipeline method

The above methods are essentially the same, they are based on thread concurrency and then use locks to synchronize. Unlike pipelines, pipelines are process-based concurrency. The use of pipelines does not need to worry about thread safety, memory allocation and other issues, which is conducive to reducing development costs and debugging costs. Pipeline method has obvious advantages, but its disadvantages are also obvious. It is only suitable for one-to-one situation between producers and consumers, and can not communicate across machines. It is not practical in actual scenarios.

Posted by whitehat on Mon, 08 Jul 2019 12:05:54 -0700