Python: Asyncio Vs. Threading – Which One To Choose?

With streams and subprocesses, Asyncio offers coroutine-based concurrency for non-blocking I/O. For blocking I/O processes, threading offers concurrency on a thread-based basis.

In this article, find out what each means, their differences and similarities, and in the end, gather all the information and find out which one you would like to use instead.

Advertising links are marked with *. We receive a small commission on sales, nothing changes for you.

Asyncio Introduction

Python: Asyncio Vs. Threading - Which One To Choose?

When using async, we cycle over the executing blocks of code one at a time. Therefore, even though you may call typical (synchronous) functions from an async application, your program must be designed around async.

What you will need to make your software async is listed below:

  1. To make your function declarative available, prefix them with the async keyword.
  2. Add it when calling async methods (without the await keyword, they won’t execute).
  3. You may launch async functions asynchronously by creating tasks from them. Wait for them to complete it as well.
  4. Start the asynchronous portion of your application by calling asyncio.run.

Here is an example of a straightforward async program that executes two functions asynchronously:

import asyncio

async def do_first():
    print("Running do_first block 1")
    ...

    # Release execution
    await asyncio.sleep(0)

    print("Running do_first block 2")
    ...

async def do_second():
    print("Running do_second block 1")
    ...
    
    # Release execution
    await asyncio.sleep(0)

    print("Running do_second block 2")
    ...

async def main():
    task_1 = asyncio.create_task(do_first())
    task_2 = asyncio.create_task(do_second())
    await asyncio.wait([task_1, task_2])

if __name__ == "__main__":
    asyncio.run(main())

Threading Introduction

In threading, one line of code is run at a time, but the line that is run changes continuously. Using a threading library, we construct a few threads, start them, and then wait for them to complete (using join, for example).

A sample of such a program is shown here:

import threading

def do_first():
    print("Running do_first line 1")
    print("Running do_first line 2")
    print("Running do_first line 3")
    ...

def do_second():
    print("Running do_second line 1")
    print("Running do_second line 2")
    print("Running do_second line 3")
    ...

def main():
    t1 = threading.Thread(target=do_first)
    t2 = threading.Thread(target=do_second)

    # Start threads
    t1.start(), t2.start()

    # Wait threads to complete
    t1.join(), t2.join()

if __name__ == "__main__":
    main()

The Mikael Koli’s output was as follows:

Running do_first line 1
Running do_second line 1
Running do_first line 2
Running do_second line 2
Running do_second line 3
Running do_first line 3

As we can see, it switched between the lines from each of the routines it ran. It randomly executed some of the code from these routines.

Take note that the lines of code were not executed simultaneously. True concurrency has yet to be present here.

Differences Between Asyncio & Threading In Python

Let’s go through some of the most significant differences between the Asyncio and Threading modules.

1. Asynchronous Programming Vs. Procedural Programming

Asynchronous programming is the main emphasis of Asyncio.

In this programming paradigm, calls are made and either completed immediately or later.

You may verify the progress of a job or obtain results later by using a handle on the asynchronous tasks. Using callbacks, you may react to events as they happen.

As opposed to threading, which is typically used in procedural and object-oriented programming, this is distinct.

Calls to the function are made, and the outcomes are obtained. The execution of these may occur wholly in a new thread or not, without delegation to a later line.

This fundamental distinction between the two modules may account for all other differences.

2. Thread-Based Vs. Coroutine-Based Concurrency

The asyncio module’s primary goal is to provide and enable concurrency based on coroutines.

Because coroutines are just functions, they are a simple kind of concurrency.

Cooperative multitasking is made possible by carefully selecting the moments when coroutines pause and using an event loop to manage the context shift between them.

The threading module’s primary goal is to facilitate concurrency that uses threads.

These are more powerful concurrency units. The underlying operating system creates, controls, and manages threads.

The operating system decides which threads are active and for how long and manages the thread switching. In Python, they are represented by threading.Thread object.

3. No GIL Vs. GIL

The Global Interpreter Lock, or GIL, does not influence the scope of coroutines in the asyncio module.

This lock makes sure that the Python interpreter’s internals are thread-safe.

Since every coroutine in an event loop runs on its thread, the idea of a GIL could be more useful in coroutines.

The GIL applies to threads in the threading module.

At any given moment, only one thread may communicate with the Python interpreter’s internals.

Unless it is eliminated under certain circumstances, such as while blocking I/O and in some third-party libraries, this restriction is often present.

Similarities Between Asyncio & Threading In Python

Let’s examine some key similarities between the Asyncio and Threading classes.

1. Concurrency Is Used With Both Modules

Concurrency is meant for both the asyncio and threading modules.

Several issues call for concurrency, running code, or operations concurrently or out of sequence.

These issues can often be solved in Python, at a high level, via coroutines or threads.

2. The Two Modules Are Both Appropriate For I/O Tasks

The asyncio module was created especially for non-blocking I/O workloads.

It provides non-blocking I/O with streams for programming sockets or networks and non-blocking I/O with subprocesses for executing system instructions.

I/O-bound activities fit the threading module and threads in particular. This is due to the global interpreter lock, which prohibits more than one thread from operating concurrently.

The lock is freed when threads do I/O, such as with files, sockets, and peripheral devices.

3. Both Provide Access To The Same Synchronization Primitives

The synchronization primitives supported by the threading module and the asyncio module are essentially the same.

Units of concurrency like coroutines and threads may be synchronized and coordinated using synchronization primitives.

Coroutines and threads can both employ synchronization primitives that have the same classes and API.

4. Both Provide Support For The Same Safe Queue Data Structures

A typical data structure for concurrency is queues.

The queue can be expanded or produced by tasks and retrieved or consumed by other activities.

They provide secure data sharing and communication amongst concurrent units.

Both the threading and asyncio modules provide safe queues.

5. Both May Experience Deadlocks and Race Conditions

Concurrency failure models can affect both threads and coroutines.

These are a subset of programming errors that only occur in concurrent programs.

When two or more concurrent units access a sensitive region of code, known as a critical section, race conditions arise, leaving the code in an erratic or inconsistent state. This may result in data loss, corruption, and strange behavior.

A deadlock results when one unit of concurrency waits for an event that will never happen. They can occur when there are problems with timing, coordination, and cyclic waiting.

Coroutines and threads can both experience deadlocks and race situations.

All in all, both of them are good, now you have to decide with which one you want to work with.

Advertising links are marked with *. We receive a small commission on sales, nothing changes for you.