Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nb architectural issues #15

Open
mqudsi opened this issue Dec 26, 2018 · 1 comment
Open

nb architectural issues #15

mqudsi opened this issue Dec 26, 2018 · 1 comment

Comments

@mqudsi
Copy link

mqudsi commented Dec 26, 2018

I believe there is a problem with the design of nb as it only allows for "one level" of asynchronicity, which is glossed over via the stubbed out definitions for the sample APIs in the documentation (such as serial_send and serial_receive).

If you were to actually flesh out those examples into real-world code, a problem arises wherein a nb function can really only decide that it would block once and only once. The moment stateful work commences that is predicated on the values that were passed in to the function, it becomes impossible to "early out" for the remainder of the function while still supporting a level of abstraction in which the function itself does not need to know anything about the caller.

For example, take the following pseudocode:

fn send_something(&mut self, bytes: &[u8]) -> nb::Result<(), !> {
    let result = self.already_sending_something.compare_exchange(false, true, Ordering:..);
    if result != Ok(false) {
        // Awesome, we avoided blocking when the sender was busy
        return Err(nb::Error::WouldBlock);
    }

    for i in 0..bytes.len() {
        // Start sending by loading this byte into the send register
        self.send_register.load(bytes[i]);

        // We could poll until the byte has been sent
        // while !self.send_register.is_empty() {};

        // But why don't we avoid blocking instead?
        if !self.send_register.is_empty() {
            return nb::Error::WouldBlock;
        }
    }

    return Ok(());
}

This code can be adapted to store i internally (saving state) so that the next time it is called after self.send_register.is_empty() returns false, it can pick up where it left off instead of starting over from zero, no problem. But how does it know that it is being called again by the same line of code/state?

@eldruin
Copy link
Member

eldruin commented Feb 4, 2019

I understand the highlighted problem but for the example above I would say that to me "send something" sounds too "blocking", and then a non-blocking implementation gets weird.
I would say that in this example the "forgetfulness of nb" could be passed on to the API. i.e.:
Whoever processes something asynchronously could have a queue of uncompleted work. Then I would split the method something like enqueue_something_for_send(). and process_send_queue() (or hopefully some better names than that). This would break the dependency to the function argument.
To put this into some pseudocode, given an imaginary queue:

fn enqueue_for_send(&mut self, bytes: &[u8]) {
    // you may need to return some error in case the queue is full or something like that
    self.send_queue.push(bytes);
}

fn process_send_queue(&mut self)-> nb::Result<(), !> {
    let result = self.already_sending_something.compare_exchange(false, true, Ordering:..);
    if result != Ok(false) {
        // Awesome, we avoided blocking when the sender was busy
        return Err(nb::Error::WouldBlock);
    }

    let next_byte = self.send_queue.pop();
    if let Ok(byte) = next_byte {
        self.send_register.load(byte);
        return Err(nb::Error::WouldBlock);
    }
    else {
        return Ok(()); // queue was empty
    }
}

This could be further improved to handle situations where the same byte should be retried because an error occurred or avoid the last process_send_queue() call, etc.

This would enable scenarios where two different loops enqueue and actually send data, as well as scenarios where one thread does it all (enqueue once, block on process_send_queue())

If you are interested in checking if a particular data piece was already sent, the code could also be extended to return a token to the element inserted in the send queue. Then the user can check later on, or even provide the token with a function to also process the queue but I think the whole thing starts getting more complicated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants