Difference in Concurrency Aspects in Python

Difference in Concurrency Aspects in Python

One may imagine that Python only just introduced these notions or capabilities, given that we’re hearing a lot of new trends regarding asynchronous operations and concurrency with the release of Python 3.

Many newcomers might believe using asyncio is the only practical approach to performing concurrent and asynchronous activities. This article will discuss how we can achieve concurrency and its benefits or drawbacks in Python.

Threads and Multithreading

Threads have been in Python for a very long time. As a result, we may perform multiple operations at once thanks to threads.

Unfortunately, CPython, a typical mainline Python version, still uses the global interpreter lock (GIL), which makes multi-threaded applications—common nowadays’s method of implementing parallel processing—less than ideal.

Python introduced GIL to make CPython’s memory handling more manageable integrations with C (for example, the extensions).

The GIL is a locking mechanism in that the Python interpreter runs only one thread simultaneously. Python’s byte code can only ever be executed by one thread simultaneously.

Example Code:

import threading
import time
import random


def worker(num):
    sec = random.randrange(1, 5)
    time.sleep(sec)
    print("I am thread {}, who slept for {} seconds.".format(num, sec))


for i in range(3):
    t = threading.Thread(target=worker, args=(i,))
    t.start()

print("Completed!")

Output:

Completed!
I am thread 1, who slept for 3 seconds.
I am thread 3, who slept for 2 seconds.
I am thread 4, who slept for 4 seconds.

Processes and Multiprocessing

Multiprocessing makes use of many CPUs. We can effectively conduct several tasks simultaneously since each CPU operates in parallel. For jobs that are CPU-bound, multiprocessing is what you want to use.

Python introduces the multiprocessing module to achieve parallelism, which will feel very similar if you have used threading.

Example Code:

import multiprocessing
import time
import random


def worker(num):
    sec = random.randrange(1, 5)
    time.sleep(sec)
    print("I am process {}, who slept for {} seconds.".format(num, sec))


for i in range(3):
    t = multiprocessing.Process(target=worker, args=(i,))
    t.start()

print("Completed")

Output:

Completed
I am process 1, who slept for 1 seconds.
I am process 2, who slept for 2 seconds.
I am process 0, who slept for 3 seconds.

Instead of multithreading, we are using multiple processes running on different cores of your CPU, which makes our Python script faster.

Asynchronous and asyncio

In synchronous operations, the tasks are carried out in sync, one after the other. However, jobs may begin in asynchronous operations entirely independently of one another.

One async task might start and continue to run while the execution switches to another activity. On the other hand, asynchronous tasks often execute in the background and don’t block (make the implementation wait for completion).

Along with other valuable features, asyncio offers an event loop. The event loop monitors various I/O events, switches to ready tasks, and pauses tasks awaiting I/O.

As a result, we don’t waste time on unfinished projects.

Example Code:

import asyncio
import datetime
import random


async def my_sleep_func():
    await asyncio.sleep(random.randint(0, 5))


async def displayDate(num, loop):
    endTime = loop.time() + 60.0
    while True:
        print("Loop: {} Time: {}".format(num, datetime.datetime.now()))
        if (loop.time() + 1.0) >= endTime:
            break
        await my_sleep_func()


loop = asyncio.get_event_loop()

asyncio.ensure_future(displayDate(1, loop))
asyncio.ensure_future(displayDate(2, loop))

loop.run_forever()

If we walk through the snippet of code above:

  • We have an async function displayDate, which takes a number and the event loop as parameters.
  • The said function has an infinite loop that stops after 60 seconds. But during these 60 secs, it repeatedly prints out the time and takes a nap.
  • The await function can wait on other async functions to complete.
  • We pass the function to the event loop (using the ensure_future function).
  • We start running the event loop.

Whenever the await call is made, asyncio understands that the function will probably need some time. When asyncio notices that halted function’s I/O is ready, it resumes the process.

Now, the point is, what do we need to use among the three forms of concurrency? We can take note of the following to help in our decision-making:

  • Use multiprocessing for CPU Bound operations.
  • Use multithreading for I/O Bound, Fast I/O, and Limited Number of Connections.
  • Use Asynchronous IO for I/O Bound, Slow I/O, and many connections.
  • asyncio/await works on Python 3.5 and later.

We can also refer to the pseudocode below:

if io_bound:
    if io_very_slow:
        print("Use asyncio")
    else:
        print("Use multithreading")
else:
    print("multiprocessing")
Marion Paul Kenneth Mendoza avatar Marion Paul Kenneth Mendoza avatar

Marion specializes in anything Microsoft-related and always tries to work and apply code in an IT infrastructure.

LinkedIn

Related Article - Python Threading

Related Article - Python Async