Skip to content

Latest commit

 

History

History
236 lines (179 loc) · 8.09 KB

Asynchrony_and_Concurrency_Python.md

File metadata and controls

236 lines (179 loc) · 8.09 KB

Asynchrony and Concurrency in Python

🔶 1. What is GIL? What is it for?


Answer:

GIL (Global Interpreter Lock) is a mechanism used in the CPython implementation of Python to prevent multiple native threads from executing Python bytecodes at once. This lock is necessary because CPython's memory management is not thread-safe. GIL ensures that only one thread executes Python bytecode at a time, avoiding conflicts between threads over the Python interpreter's memory.

Example:

import threading

def my_func():
    x = 0
    for i in range(1000000):
        x += 1

# create 2 threads
t1 = threading.Thread(target=my_func)
t2 = threading.Thread(target=my_func)

# start the threads
t1.start()
t2.start()

In this example, even though we have two threads running the same function, GIL will not allow them to run simultaneously, so the overall execution time will be similar to running the function only once on a single thread.

🔶 2. How is a thread different from a process?


Answer:

A thread is a lightweight, independent unit of execution that can run within a process. Threads within a process share the same memory space, making it easy for them to share data and communicate with each other. A process, on the other hand, is a self-contained execution environment that has its own memory space and resources.

🔶 3. Tell us about the race condition and thread safety.


Answer:

A race condition occurs when multiple threads access shared data or resources simultaneously, and the outcome of the program depends on the order in which the threads execute. Thread safety is the property of an application or library that it can handle multiple threads accessing shared data or resources without introducing race conditions.

Example:

import threading

x = 0

def increment():
    global x
    for i in range(1000000):
        x += 1

def decrement():
    global x
    for i in range(1000000):
        x -= 1

# create 2 threads
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=decrement)

# start the threads
t1.start()
t2.start()

# wait for the threads to finish
t1.join()
t2.join()

print(x)

In this example, we have two threads running simultaneously that increment and decrement a shared variable x. The final value of x will depend on the order in which the threads execute, which can lead to race conditions.

🔶 4. What mechanisms for synchronizing access to shared resources do you know?


Answer:

Some mechanisms for synchronizing access to shared resources include:

  • Locks: a mechanism that allows only one thread to execute a critical section of code at a time.
  • Semaphores: a mechanism that allows multiple threads to access a shared resource, but with a limit on the number of threads that can access the resource at the same time.
  • Monitors: a mechanism that allows only one thread to execute a critical section of code at a time, and also provides a mechanism for threads to wait for specific conditions to be met before continuing execution.

Example:

import threading

# Using locks
x = 0
lock = threading.Lock()

def increment():
    global x
    with lock:
        for i in range(1000000):
            x += 1

def decrement():
    global x
    with lock:
        for i in range(1000000):
            x -= 1

# Using semaphores
semaphore = threading.Semaphore(1)

def increment():
    global x
    semaphore.acquire()
    for i in range(1000000):
        x += 1
    semaphore.release()

def decrement():
    global x
    semaphore.acquire()
    for i in range(1000000):
        x -= 1
    semaphore.release()

In this example, we use locks and semaphores to synchronize access to the shared variable x, making sure that only one thread can access the variable at a time, avoiding race conditions.

🔶 5. What mechanisms of interaction of processes do you know?


Answer:

Some mechanisms of interaction of processes include:

  • Inter-process communication (IPC) mechanisms such as pipes, sockets, and message queues
  • Signals, which are used to send simple notifications between processes
  • Shared memory, which allows processes to directly access the same memory space.

🔶 6. What is asynchronous I/O?


Answer:

Asynchronous I/O is a method of input/output operations in which the process does not wait for the I/O operation to complete before continuing execution. This allows for more efficient use of system resources, as the process can perform other tasks while the I/O operation is in progress.

Example:

import asyncio

async def read_file():
    # Open the file in non-blocking mode
    file = open("file.txt", "r", buffering=0)
    # Read the first line of the file
    line = await file.readline()
    print(line)
    file.close()

async def main():
    await read_file()

asyncio.run(main())

In this example, the await file.readline() is an asynchronous I/O operation that reads the first line of the file, the program does not wait for the operation to finish, it can execute the next line while it is reading the file.

🔶 7. What are coroutines? How do they work?


Answer:

Coroutines are a type of lightweight, cooperative threads that allow for concurrency without the need for multiple threads. Coroutines can yield control to other coroutines, allowing them to execute, and then resume execution later. They are implemented using the "yield" keyword and are similar to generators, but with more features for concurrency.

Example:

import asyncio

async def my_coroutine():
    print("Starting coroutine")
    await asyncio.sleep(1)
    print("Ending coroutine")

async def main():
    await my_coroutine()

asyncio.run(main())

In this example, my_coroutine() is a coroutine that uses the await asyncio.sleep(1) to simulate a delay of one second. The await keyword is used to allow other coroutines to run while this coroutine is paused.

🔶 8. What is the use of the async/await construct in Python?


Answer:

The async/await construct in Python is used to write asynchronous code in a synchronous-like fashion. The "async" keyword is used to define an asynchronous function and the "await" keyword is used to call other asynchronous functions from within an asynchronous function.

🔶 9. How is the EventLoop arranged?


Answer:

An EventLoop is a mechanism that allows for the scheduling and execution of asynchronous code. It runs in a single thread and manages the execution of tasks, such as I/O operations, by scheduling them to be executed as soon as their resources become available. EventLoop is an essential part of asyncio library which provides an abstract event-driven asynchronous I/O framework.

Example:

import asyncio

async def my_task():
    print("Starting task")
    await asyncio.sleep(1)
    print("Ending task")

async def main():
    # Create an event loop
    loop = asyncio.get_event_loop()
    # Schedule the task using the loop
    loop.create_task(my_task())
    # Run the loop
    loop.run_forever()

asyncio.run(main())

In this example, we create an event loop using asyncio.get_event_loop() and schedule a task using loop.create_task(my_task()). The event loop is then run using loop.run_forever() which schedules and runs the task as soon as resources become available.

🔶 10. What is the purpose of the async with statement in Python? Provide an example?


Answer

The async with statement is used for managing resources in an asynchronous context, similar to the regular with statement for synchronous code. It's commonly used for working with asynchronous I/O resources that need to be acquired and released safely.

Example:

import asyncio

class AsyncResource:
    async def __aenter__(self):
        print("Acquiring resource asynchronously")
        await asyncio.sleep(1)
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        print("Releasing resource asynchronously")

async def main():
    async with AsyncResource() as resource:
        print("Using async resource")

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