# Unit 3: Concurrency

`async` and `await` are key concepts in Python that facilitate writing asynchronous code. They were introduced in Python 3.5 and have become central in writing efficient and non-blocking code, especially in the context of I/O-bound and high-level structured network code.

## Synchronous and Asynchronous

Synchronous programming is simpler and more straightforward, best suited for linear tasks and CPU-bound operations. Asynchronous programming, on the other hand, allows for concurrent operations, making it ideal for tasks that involve waiting for external operations, thus improving efficiency and scalability in specific scenarios like web servers and UI applications.

<figure><img src="https://4188609209-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fhp4eambztJcKKLa7ysKG%2Fuploads%2FcrOoSA2zn9KoGVxZiCdR%2Fsync-async-01.png?alt=media&#x26;token=d697cd92-7f6e-48d7-a100-c4e5ee93ebd2" alt=""><figcaption><p>Fig. 1. Synchronous and Asynchronous. Source: Internet</p></figcaption></figure>

| Synchronous Programming                                                                                                                                                                                                                                         | Asynchronous Programming                                                                                                                                                                                                                                                         |
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <p><strong>Sequential Execution</strong></p><p>In synchronous programming, tasks are executed one after another. Each task must complete before the next one starts. This is a straightforward and intuitive way of writing code.</p>                           | <p><strong>Concurrent Execution</strong></p><p>Asynchronous programming allows multiple tasks to be processed concurrently. It enables a task to be paused (usually I/O operations) and resume only when the result is ready, without blocking the execution of other tasks.</p> |
| <p><strong>Blocking Operations</strong></p><p>If a task involves waiting (e.g., for file I/O, network requests), the entire program is blocked or waits until the operation completes. This can lead to inefficiency, especially in I/O-bound applications.</p> | <p><strong>Non-Blocking Operations</strong></p><p>In async programming, the program can continue to run other tasks while waiting for other operations to complete. This is especially beneficial for I/O-bound and high-level structured network code.</p>                      |
| <p><strong>Ease of Understanding</strong></p><p>Because of its sequential nature, synchronous code is generally easier to understand and debug. The flow of control is linear and predictable.</p>                                                              | <p><strong>Complexity</strong></p><p>Writing and understanding asynchronous code can be more complex than synchronous code. It requires handling of callbacks, promises, or async/await patterns, which can be less intuitive.</p>                                               |
| <p><strong>Use Cases</strong></p><p>Synchronous programming is well-suited for tasks that are CPU-bound or when tasks need to be executed in a specific order without any requirement for scaling.</p>                                                          | <p><strong>Use Cases</strong></p><p>Asynchronous programming is ideal for applications that require high scalability and responsiveness, such as web servers, UI applications, and networked programs.</p>                                                                       |

Key differences:

* **Blocking vs. Non-Blocking:** Synchronous programming is blocking, while asynchronous programming is non-blocking.
* **Control Flow:** Synchronous code has a straightforward, top-to-bottom control flow. Asynchronous code, however, can be more complex due to callbacks and event loops.
* **Scalability:** Asynchronous programming is more scalable, particularly for I/O-bound and network-bound operations.
* **Resource Utilization:** Asynchronous programming can make better use of system resources, while synchronous programming may underutilize resources during wait times.

## `async`/`await` Keywords

### `async` Keyword

In Python, the `async` keyword is used to declare a function as an "asynchronous function." Such functions, known as "async functions," are able to "pause" their execution without blocking the entire thread by yielding control back to the event loop.

```python
async def fetch_data():
    # perform some asynchronous operations
```

An async function defines a "coroutine" which is a type of function that can suspend its execution before reaching `return`, and it can indirectly pass control back to the event loop. This suspension of execution is done using the `await` keyword.

### `await` Keyword

Within an async function, you use the `await` keyword before a function that returns an "awaitable" object (like another async function). This tells Python to pause the async function until the awaitable is resolved. This pause is non-blocking; it allows other async functions to run in the meantime.

```python
async def fetch_data():
    data = await some_async_io_operation()
    return data
```

When an async function hits an `await` keyword, it pauses and yields control back to the event loop, which can then go on to execute other tasks. Once the awaited operation is completed, the event loop resumes the execution of the async function from the point it was paused. In other words, Python will know that it can go and do something else in the meanwhile (like receiving another request).

{% hint style="info" %}
For `await` to work, it has to be inside a function that supports this asynchronicity. To do that, you just declare it with `async def`.
{% endhint %}

***

## Exercises

Write an `async` function called `fetch_data` that takes a source name (string) and a delay (float).

* It should print `"{source name}: Fetching..."`,
* Asynchronously wait for the given delay,
* Then print `"{source name}: Done!"` and return the source name.

Write a main `async` function that launches three `fetch_data` tasks **concurrently** using `asyncio.gather`. After all tasks are complete, print the list of source names that have finished.

**Expected Output:**

```
SourceA: Fetching...
SourceB: Fetching...
SourceC: Fetching...
SourceB: Done!
SourceC: Done!
SourceA: Done!
['SourceA', 'SourceB', 'SourceC']
```

*(The order of "Done!" may vary depending on delay values)*
