top of page
Buscar

Multithreading, Parallelism, and Concurrency



This article is the first in a series on multithreading, concurrency, and parallelism. The goal is to explain and exemplify these concepts clearly to facilitate understanding.


Let's start by explaining what a process is

A process is an instance of a running program that the operating system manages. It is created when a program is loaded into memory and executed. The operating system coordinates the execution of multiple processes, ensuring that each one receives CPU time and access to necessary resources, as well as managing communication and synchronization between them to ensure efficient and stable system operation.


Given this introduction, let's delve into the world of threads.


What is the difference between single-threaded and multi-threaded?

Single-threaded programming is essentially how we first learn to code, where there is only one sequence of code being executed at a time, meaning one process running at a time. In a single-threaded environment, there is no parallelism, and the program cannot perform multiple tasks simultaneously. Single-threaded programming is easier to implement and manage but can limit efficiency in systems with multi-core processors.


Multi-threading is an execution model where a process or program works with multiple threads simultaneously. In this model, the workload is distributed among the various processor cores, executing tasks independently and in parallel. This allows the system to perform multiple operations simultaneously, making better use of system resources, improving performance, and enhancing the responsiveness of complex applications. However, multi-threaded programming is more complex to implement and maintain.


Below is a simple example of single-threaded and multi-threaded code.


Single-threaded:

public class MonoThread {
	public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 1; i <= 5; i++) {
            printNumbers();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Total time: " + (endTime - startTime) + " ms");
    }

    public static void printNumbers() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(i);

            try {
                Thread.sleep(500); // Simulates a time-consuming operation
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Multi-threaded:

public class MultiThread {
	public static void main(String[] args) {
 	   long startTime = System.currentTimeMillis();

        Thread thread1 = new Thread(new NumberPrinter());
        Thread thread2 = new Thread(new NumberPrinter());
        Thread thread3 = new Thread(new NumberPrinter());

        thread1.start();
        thread2.start();
        thread3.start();

        try {
            thread1.join();
            thread2.join();
            thread3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Total time: " + (endTime - startTime) + " ms");
    }
}

class NumberPrinter implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(i + " - " + Thread.currentThread().getName());

            try {
                Thread.sleep(500); // Simulates a time-consuming operation
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Parallelism

Parallelism refers to the simultaneous execution of multiple tasks. This is only possible when a system has multiple processors or CPU cores that can execute different parts of a program simultaneously. In a parallel system, several tasks are actually performed at the same time. This is possible in multi-threaded or distributed systems, where different threads or processes run on different cores or machines.


Let me explain it more clearly.

Imagine you are organizing a dinner with three courses: an appetizer, a main course, and a dessert. If only you are responsible for preparing all the dishes (single-threaded), you will have to do one task at a time, first preparing the appetizer, then the main course, and finally the dessert. This way, the dishes will take longer to be ready, as you can only start the next dish after finishing the one you are working on.


Now, consider that you have two friends who have come to help you (multi-threaded). Each person can work on a different dish at the same time: one prepares the appetizer, another the main course, and the other the dessert. As a result, all the dishes are ready almost simultaneously, significantly reducing the total preparation time.


Just like in this example, parallelism is like having multiple people (or processing units) working on different parts of a larger task at the same time, increasing efficiency and speeding up the completion of the work.


Got it? Now it's time to put this into code.

Let's build a code with two threads, each with its task.

In this example, we have two processes executing two different tasks simultaneously.

class Parallelism {
public static void main(String[] args) {

        // Create the first thread that prints messages
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread 1 - Message");

                try {
                    Thread.sleep(500); // Pauses for 500 milliseconds
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // Create the second thread that prints messages
        Thread thread2 = new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("Thread 2 - Message");

                try {
                    Thread.sleep(500); // Pauses for 500 milliseconds
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // Start the threads
        thread1.start();
        thread2.start();

        // Wait for the threads to finish
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Parallel execution completed.");
    }
}

Benefits of Parallelism

  • Improved Performance: By dividing tasks into smaller parts executed simultaneously, the total processing time can be significantly reduced.

  • Faster Response: Parallel applications tend to have a faster response to user requests, as multiple tasks can be processed simultaneously.

  • Energy Efficiency: Efficient use of hardware resources through parallelism can also result in energy savings in computing systems.


Challenges of Parallelism

  • Coordination and Synchronization: Properly managing the concurrent execution of tasks and ensuring the consistency of shared data can be complex and error-prone.

  • Communication Overhead: Sometimes, the communication and synchronization between threads or processes can introduce overhead that reduces the efficiency of parallelism.

  • Limited Scalability: Not all tasks can be efficiently parallelized, and scalability may be limited by factors such as task dependencies or available hardware resources.


In summary, parallelism in multi-threaded programming is a powerful technique for improving the performance and efficiency of computing systems, but it requires careful understanding and appropriate design to be successfully implemented.


Concurrency

Concurrency in multi-threaded programming is the ability of multiple threads to perform operations simultaneously, sharing common resources and data. This means that we have multiple tasks being executed simultaneously, competing to access the same resource, whether it is a variable, a list, or even a database. This can lead to serious problems if not managed correctly.


Continuing our dinner example, imagine you and your friends are preparing the appetizer, main course, and dessert. There will come a time when more than one person will need to use the oven, which in this case, we can say is the resource being shared. Each dish requires its temperature and time in the oven, so only one dish can be in the oven at a time. At this point, there is competition for the use of the oven, and for the dishes to be prepared correctly, synchronization is necessary for using the resource.


Let's look at a practical example.

Below is a code where three threads essentially add a counter to a list.

In this example, the list and the variable i are the resources that the threads are competing to access.

The resources are not synchronized, so the threads can access them simultaneously, which can lead to serious issues, causing the number not to be incremented correctly, nor the list to be added correctly.

class Concurrency {
    static List<Integer> list = new ArrayList<>();
    static int i = 0;

    public static void main(String[] args) {

        MyRunnable runnable = new MyRunnable();
        Thread thread0 = new Thread(runnable);
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread0.start();
        thread1.start();
        thread2.start();
    }

    public static class MyRunnable implements Runnable {
        @Override
        public void run() {
            list.add(i++);
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + ":" + i + ":" + list);
        }
    }
}

Example of incorrect output that can be generated:

Thread-1:2:[0, 1]
Thread-0:1:[0, 1]
Thread-2:3:[0, 1, 2]

Due to the lack of synchronization, we can see that thread 1 and thread 2 printed the same counters in the list but had different counters.


Problems that Concurrency Can Cause

  • Race Conditions: Occur when two or more threads try to modify the same resource simultaneously without proper synchronization. This can result in inconsistent results due to the unpredictable interleaving of thread operations.


  • Deadlocks: A deadlock happens when two or more threads are blocked indefinitely, waiting for resources held by other blocked threads. This can occur when threads acquire resources in a specific order and try to acquire resources in the opposite order, blocking each other.


  • Unordered Concurrency: In certain situations, the order of thread execution can affect the program's behavior unpredictably. Depending on synchronization and thread management, the program's result may vary.


  • Data Consistency Issues: Without appropriate synchronization mechanisms, such as locks or atomicity, read and write operations may not be consistent across different threads. This can lead to inconsistent or incorrect data values.


Synchronization and Concurrency Management Techniques that Can Be Adopted

  • Synchronized Methods and Blocks: In Java, synchronized is used to create synchronized blocks or methods, ensuring that only one thread executes a critical section of code at a time.


  • Reentrant Locks: Reentrant locks, such as ReentrantLock in Java, offer greater flexibility and control over synchronization, allowing more complex conditions for locking and unlocking shared resources.


  • Atomic Classes: Atomic classes, such as AtomicInteger or AtomicReference in Java, ensure atomic read and write operations without the need for explicit synchronization.


  • Use of Volatile Variables: Volatile variables in Java ensure that all threads see the latest version of the variable value, avoiding local caching in thread caches.


  • Utilization of Thread-Safe Data Structures: Use thread-safe collections, such as ConcurrentHashMap or CopyOnWriteArrayList, which are designed to allow safe operations in multi-threaded environments.


Let's leave these problems and synchronization techniques for another article to avoid making this one too tiring.


Conclusion

Multithreading is most used where batch processing is required or when the workload can be distributed across multiple processes. It is necessary to be careful when implementing and maintaining systems that have multi-threaded code. If implemented correctly and in projects where they are truly needed, they can bring numerous benefits.


 
 
 

Comments


  • Linkedin
  • GitHub

© 2024 by Thalles Vieira All Rights Reserved

Subscribe for me!

Thanks for submitting!

bottom of page