<|im_start|>system
You are a helpful AI assistant.<|im_end|>
<|im_start|>user
I have an embedded application written in Rust (so using #![no_std]). I am using the embassy (https://embassy.dev/) as my async runtime. In my program I have two tasks: one data acquisition task, and a data processing task. I want the data gathered in the acquisition task to find its way inside the data processing task. Ideally, I want to only react on new data, as shown in the example below. What are some idiomatic ways to do so?
Here's a rough idea of how my code looks like:
```rust
#[embassy_executor::task]
async fn data_acquisition(mut used_peripherals: UsedPeripherals) {
    let data = sample_data(&mut used_peripherals).await;
    // Now I want to access the data in another task.
}

#[embassy_executor::task]
async fn data_processing() {
    // Somehow wait for new data.
    // Do stuff with the received data.
}
```<|im_end|>
<|im_start|>assistant
One way to accomplish this is to use a channel to pass data between the two tasks. Here's an example of how you could modify your code to use a channel:
```rust
use embassy::channel::{mpmc, Receiver};
use embassy_executor::task;

#[task]
async fn data_acquisition(mut used_peripherals: UsedPeripherals, tx: mpmc::Sender<Data>) {
    loop {
        let data = sample_data(&mut used_peripherals).await;
        if tx.try_send(data).is_err() {
            // Handle the case where the channel is full
            // (i.e. the data processing task isn't keeping up).
        }
    }
}

#[task]
async fn data_processing(mut rx: Receiver<Data>) {
    while let Some(data) = rx.recv().await {
        // Do stuff with the received data.
    }
}
```

In this example, mpmc::Sender and Receiver are used to create a multi-producer, multi-consumer channel that can be used to pass data between tasks. The data_acquisition task sends data over the channel using try_send, which returns an error if the channel is full. The data_processing task receives data from the channel using recv, which blocks until new data is available.

Note that the try_send method is used instead of send because send would block if the channel is full, which could cause the data acquisition task to miss new data from the peripherals. Also, the while let Some(data) = rx.recv().await loop is used to continuously process new data as it becomes available.<|im_end|>
<|im_start|>user
Could you rewrite this response and make sure all code is put into code blocks. Your response was difficult to read.<|im_end|>
<|im_start|>assistant
Sure!

```rust
use embassy::channel::{mpmc, Receiver};
use embassy_executor::task;

#[task]
async fn data_acquisition(mut used_peripherals: UsedPeripherals, tx: mpmc::Sender<Data>) {
    loop {
        let data = sample_data(&mut used_peripherals).await;
        if tx.try_send(data).is_err() {
            // Handle the case where the channel is full
            // (i.e. the data processing task isn't keeping up).
        }
    }
}

#[task]
async fn data_processing(mut rx: Receiver<Data>) {
    while let Some(data) = rx.recv().await {
        // Do stuff with the received data.
    }
}
```

In this example, `mpmc::Sender` and `Receiver` are used to create a multi-producer, multi-consumer channel that can be used to pass data between tasks. The `data_acquisition` task sends data over the channel using `try_send`, which returns an error if the channel is full. The `data_processing` task receives data from the channel using `recv`, which blocks until new data is available.

Note that the `try_send` method is used instead of `send` because `send` would block if the channel is full, which could cause the data acquisition task to miss new data from the peripherals. Also, the `while let Some(data) = rx.recv().await` loop is used to continuously process new data as it becomes available.<|im_end|>
<|im_start|>user
I see.  Thank you.  Could you please list the pros and cons of each of these two approaches and provide an example of each?<|im_end|>
<|im_start|>assistant
Sure! I'll provide you with the pros and cons of each approach, and then an example of each.

**Channels**

Pros:

1. Decouples tasks: Channels provide a clear separation between tasks, which makes your code more modular and easier to maintain.
2. Backpressure: Channels can be bounded, meaning they have a fixed size. If the buffer is full, the sender task will be forced to wait, providing backpressure to prevent overloading the system.
3. Synchronization: Channels allow tasks to wait for new data without busy-waiting or polling, making your application more efficient.

Cons:

1. Overhead: Channels introduce some overhead in terms of memory and performance.
2. Complexity: Using channels might make your code more complex, especially when handling errors or closing channels.

Example:

```rust
use embassy::executor::Spawner;
use embassy::time::Duration;
use embassy::util::mpsc;
use embassy::util::Signal;
use embassy_nrf::gpio::NoPin;

#[embassy_executor::task]
async fn data_acquisition(sender: mpsc::Sender<u32, 4>, signal: Signal<NoPin>) {
    loop {
        signal.wait().await;
        let data = sample_data().await;
        sender.send(data).await.unwrap();
    }
}

#[embassy_executor::task]
async fn data_processing(receiver: mpsc::Receiver<u32, 4>) {
    loop {
        let data = receiver.recv().await.unwrap();
        // Do stuff with the received data.
    }
}

fn main() {
    let (sender, receiver) = mpsc::channel();
    let signal = Signal::new();

    Spawner::spawn(data_acquisition(sender, signal.clone())).unwrap();
    Spawner::spawn(data_processing(receiver)).unwrap();
}
```

**Shared Variables**

Pros:

1. Lightweight: Shared variables have minimal overhead in terms of memory and performance.
2. Simplicity: Shared variables can be easier to use, especially for small amounts of data.

Cons:

1. Coupling: Shared variables can lead to tighter coupling between tasks, making your code harder to maintain.
2. Synchronization: Manual synchronization is required, which can introduce complexity and potential bugs.
3. No backpressure: Shared variables do not provide any built-in backpressure mechanism.

Example:

```rust
use core::sync::atomic::{AtomicU32, Ordering};
use embassy::executor::Spawner;
use embassy::time::Duration;
use embassy::util::Signal;
use embassy_nrf::gpio::NoPin;

static DATA: AtomicU32 = AtomicU32::new(0);

#[embassy_executor::task]
async fn data_acquisition(signal: Signal<NoPin>) {
    loop {
        signal.wait().await;
        let data = sample_data().await;
        DATA.store(data, Ordering::Relaxed);
    }
}

#[embassy_executor::task]
async fn data_processing() {
    loop {
        let data = DATA.load(Ordering::Relaxed);
        // Do stuff with the received data.
        embassy::time::Timer::after(Duration::from_secs(1)).await;
    }
}

fn main() {
    let signal = Signal::new();

    Spawner::spawn(data_acquisition(signal.clone())).unwrap();
    Spawner::spawn(data_processing()).unwrap();
}
```

Please note that these examples are simplified and may need to be adjusted based on your specific requirements.<|im_end|>