# 8. Coroutines

These snippets demonstrate the use of coroutines for multitasking and cooperative execution, including sending data, handling exceptions, and managing state. They also illustrate how coroutines can be integrated with `asyncio` for asynchronous programming.

***

**1. Basic Coroutine Example**

```python
def simple_coroutine():
    print("Coroutine started")
    x = yield
    print(f"Received: {x}")

coro = simple_coroutine()
next(coro)         # Start the coroutine
coro.send(42)      # Output: Coroutine started \n Received: 42
```

***

**2. Coroutine with Multiple `yield` Statements**

```python
def multi_yield_coroutine():
    print("Coroutine started")
    x = yield "First yield"
    print(f"Received: {x}")
    y = yield "Second yield"
    print(f"Received: {y}")

coro = multi_yield_coroutine()
print(next(coro))  # Output: Coroutine started \n First yield
print(coro.send(10))  # Output: Received: 10 \n Second yield
coro.send(20)         # Output: Received: 20
```

***

**3. Coroutine for Accumulating Values**

```python
def accumulator():
    total = 0
    while True:
        value = yield total
        total += value

coro = accumulator()
next(coro)              # Start the coroutine
print(coro.send(10))    # Output: 10
print(coro.send(5))     # Output: 15
print(coro.send(20))    # Output: 35
```

***

**4. Coroutine for Data Filtering**

```python
def data_filter(threshold):
    while True:
        value = yield
        if value > threshold:
            print(f"Value {value} is above the threshold {threshold}")

coro = data_filter(10)
next(coro)              # Start the coroutine
coro.send(5)            # No output
coro.send(15)           # Output: Value 15 is above the threshold 10
coro.send(8)            # No output
```

***

**5. Coroutine for String Processing**

```python
def string_processor():
    result = ""
    while True:
        string = yield result
        result += string.upper()

coro = string_processor()
next(coro)                    # Start the coroutine
print(coro.send("hello"))     # Output: HELLO
print(coro.send(" world"))    # Output: HELLO WORLD
```

***

**6. Coroutine with Exception Handling**

```python
def exception_handling_coroutine():
    try:
        while True:
            value = yield
            print(f"Received: {value}")
    except ValueError as e:
        print(f"Caught exception: {e}")

coro = exception_handling_coroutine()
next(coro)                # Start the coroutine
coro.send(10)             # Output: Received: 10
coro.throw(ValueError, "An error occurred!")  # Output: Caught exception: An error occurred!
```

***

**7. Coroutine with Closing Behavior**

```python
def closing_coroutine():
    try:
        while True:
            value = yield
            print(f"Processing: {value}")
    except GeneratorExit:
        print("Coroutine closed")

coro = closing_coroutine()
next(coro)         # Start the coroutine
coro.send(42)      # Output: Processing: 42
coro.close()       # Output: Coroutine closed
```

***

**8. Coroutine for Logging Messages**

```python
def logger():
    print("Logger started")
    while True:
        message = yield
        print(f"LOG: {message}")

log_coro = logger()
next(log_coro)         # Start the coroutine
log_coro.send("Start processing")  # Output: LOG: Start processing
log_coro.send("Process complete")  # Output: LOG: Process complete
```

***

**9. Coroutine for Producing Fibonacci Numbers**

```python
def fibonacci_coroutine():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

coro = fibonacci_coroutine()
for _ in range(6):
    print(next(coro), end=" ")  # Output: 0 1 1 2 3 5
```

***

**10. Coroutine with Asynchronous Behavior Using `asyncio`**

```python
import asyncio

async def async_coroutine():
    print("Coroutine started")
    await asyncio.sleep(1)
    print("Coroutine resumed")

asyncio.run(async_coroutine())  # Output: Coroutine started \n Coroutine resumed
```

***
