Implementing a Oneshot Channel in Rust
A oneshot channel is a communication primitive where a sender can send a single value, and a receiver can wait to receive that value. Once the value is sent, the channel is considered "closed" and cannot be used again. This is useful for scenarios where you need to signal the completion of a task or return a result from an asynchronous operation exactly once.
Problem Description
Your task is to implement a oneshot channel in Rust. This channel should allow a single sender and a single receiver to communicate.
Key Requirements:
- Sender: A
Sender<T>struct that holds a reference to the channel's internal state. It should have asend(value: T)method. - Receiver: A
Receiver<T>struct that holds a reference to the channel's internal state. It should have arecv()method. - Single Send/Receive: The channel must ensure that a value can only be sent once. Subsequent
sendcalls should ideally panic or have no effect (though panicking is preferred for clarity in this exercise). Similarly, arecvcall should only yield the value once. - Blocking Receive: The
recv()method should block the current thread until a value is sent. - Error Handling: If
recv()is called after the sender has dropped without sending a value, it should return an error. Ifsend()is called after a value has already been sent, it should panic.
Expected Behavior:
- When
send(value)is called, thevalueis stored internally. - When
recv()is called and a value has been sent, it returnsOk(value). - When
recv()is called and the sender has been dropped without sending a value, it returnsErr. - If
send()is called a second time, the program should panic. - The
SenderandReceivershould beSendandSyncifTisSendandSync, enabling their use across threads.
Edge Cases:
- Dropping the
Senderbefore sending a value. - Dropping the
Receiverbefore receiving a value. - Calling
recv()multiple times on the sameReceiver. - Calling
send()multiple times on the sameSender.
Examples
Example 1:
// Assume we have a `OneshotChannel::new()` function that returns (Sender<i32>, Receiver<i32>)
let (sender, receiver) = OneshotChannel::new();
// In a separate thread (for demonstration of blocking):
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(100));
sender.send(123);
});
// In the main thread:
let received_value = receiver.recv();
// Expected Output:
// Ok(123)
Explanation: The sender sends the value 123 after a short delay. The receiver blocks until the value is available and then returns it wrapped in Ok.
Example 2:
let (sender, receiver) = OneshotChannel::new::<String>();
// Sender is dropped without sending
drop(sender);
// Receiver tries to receive
let result = receiver.recv();
// Expected Output:
// Err(ChannelClosedError)
Explanation: The sender is dropped before sending any data. When the receiver attempts to recv, it encounters the Err indicating the channel was closed without a value.
Example 3:
let (sender, receiver) = OneshotChannel::new();
sender.send(42);
// Trying to send again should panic
// sender.send(100); // This line would cause a panic
let received_value = receiver.recv();
// Expected Output (if the second send was commented out):
// Ok(42)
Explanation: The first send succeeds. If a second send were attempted, it would panic. The recv successfully retrieves the initially sent value.
Constraints
- The channel should be able to hold any type
Tthat implementsSend. - The implementation should be thread-safe, allowing
SenderandReceiverto be moved to different threads. - Performance is a consideration; avoid unnecessary copying or complex synchronization primitives if simpler ones suffice.
- The channel should not require any external crates beyond Rust's standard library.
Notes
- You will need to define your own error type for when
recv()is called on a closed channel. - Consider using
std::sync::Mutexorstd::sync::atomicoperations for managing the shared state of the channel. - The
Arc(Atomic Reference Counter) will likely be crucial for sharing ownership of the channel's internal state between the sender and receiver, and potentially across threads. - Think about how to signal the receiver that a value has arrived without it constantly polling. A condition variable might be useful here.
- When the
SenderorReceiveris dropped, its destructor should correctly clean up any internal state or signal the other end if necessary. - The
SenderandReceivershould be distinct types, enforcing the oneshot nature at the type level.