Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Comments for "https://os.phil-opp.com/async-await/" #768

Closed
utterances-bot opened this issue Mar 27, 2020 · 85 comments
Closed

Comments for "https://os.phil-opp.com/async-await/" #768

utterances-bot opened this issue Mar 27, 2020 · 85 comments

Comments

@utterances-bot
Copy link

utterances-bot commented Mar 27, 2020

This is a general purpose comment thread for the Async/Await post.

Copy link

The thing I was trying to wrap my head around for quite some time, thanks for so detailed explanation!

Two minor points:

  1. The following code in run_ready_tasks:
if !self.waker_cache.contains_key(&task_id) {
    self.waker_cache.insert(task_id, self.create_waker(task_id));
}
let waker = self.waker_cache.get(&task_id).expect("should exist");

- why not use the Entry API instead?
2. "it's both simpler and safer to instead implement the Arc-based Wake trait" - the link on "Wake" leads to wake method on Waker - probably it should be this trait instead?

@jounathaen
Copy link
Contributor

jounathaen commented Mar 27, 2020

The statemachine in https://os.phil-opp.com/async-await/#the-async-await-pattern

    fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
        loop {
            match self { // TODO: handle pinning
                ExampleStateMachine::Start(state) => {}
                ExampleStateMachine::WaitingOnFooTxt(state) => {}
                ExampleStateMachine::WaitingOnFooTxt(state) => {}
                ExampleStateMachine::End(state) => {}
            }
        }
    }

Contains the state ExampleStateMachine::WaitingOnFooTxt twice. Should probably be ExampleStateMachine::WaitingOnBarTxt 😉

edit: Fix in #769

Copy link

Excellent! Looking forward to the next article

@phil-opp phil-opp changed the title https://os.phil-opp.com/async-await/ Comments for "https://os.phil-opp.com/async-await/" Mar 28, 2020
@phil-opp
Copy link
Owner

@Cerberuser Great to hear that the post is useful to you!

Good question! I tried using it, but it leads to borrowing errors here because we need to borrow self again for the create_waker method. Of course we could work around this, for example by destructuring self and defining the create_waker methods in a way that it only needs access to the wake_queue field. However, the variant with expect was shorter and easier to explain, so I found it the better choice for this post.

the link on "Wake" leads to wake method on Waker - probably it should be this trait instead?

Thanks for reporting! Fixed in f32ee7f.

@phil-opp
Copy link
Owner

@jounathaen Thanks for reporting and providing the fix!

@phil-opp
Copy link
Owner

@senseiod I already started working on it :).

Copy link

p2mate commented Mar 29, 2020

I'm surprised that on x86 you need to re-enable interrupts before doing a hlt. On cortex m you can do:
cortex_m::interrupt::free(|_| {
if self.wake_queue.is_empty() {
cortex_m::asm::wfi()
}
})

So the CPU will wake from wfi even with the interrupts disabled. Ofcourse the interrupt handler will only be executed when the interrupts are re-enabled.

@phil-opp
Copy link
Owner

@p2mate Yeah, I think the approach of ARM is definitely better. As far as I know, there is no equivalent to the wfi instruction on x86 unfortunately.

Copy link

thank you , it's very detailed/clear and very useful.

Copy link

Thank you very much for the detailed lesson, and I look forward to continuing.

Copy link

There is a repetition typo: "have to have to" in the first paragraph of
Executors and Wakers

Thanks for a fascinating article.

Copy link

ZoeyR commented Mar 31, 2020

Isn't the uniqueness of TaskId not be guaranteed for zero-sized futures? Box would give the same address for multiple "instances" of the zero sized type.

Edit: See this playground link for an example: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=72f510e1edcb7657ce46becab9179977

Copy link

Great article! Thank you so much :)

@phil-opp
Copy link
Owner

phil-opp commented Apr 1, 2020

Thank you all for the nice feedback :).

@LordSparhawk

There is a repetition typo: "have to have to" in the first paragraph of Executors and Wakers

Thanks for reporting! Fixed in b6d09c8.

@phil-opp
Copy link
Owner

phil-opp commented Apr 1, 2020

@ZoeyR

Isn't the uniqueness of TaskId not be guaranteed for zero-sized futures? Box would give the same address for multiple "instances" of the zero sized type.

Very good catch! While zero-sized futures are rare in practice, we should still ensure that they can be used without problems. I opened #782 and #783 to create task IDs using a static counter instead.

Copy link

guswynn commented Apr 10, 2020

https://doc.rust-lang.org/std/future/trait.Future.html doesn't seem to suggest that when you are returning ready that you must use the waker to say you are ready.

I have found future impl's (like https://docs.rs/futures-util/0.3.4/src/futures_util/future/future/map.rs.html#42) that pass the cx down to a lower future, but don't actually call wake themselves. Are they assuming a lower future will call wake?

Should an executor have a timeout after which if it hasn't gotten a .wake() from a task, it polls the future anyways?

@phil-opp
Copy link
Owner

@guswynn

https://doc.rust-lang.org/std/future/trait.Future.html doesn't seem to suggest that when you are returning ready that you must use the waker to say you are ready.

I'm not sure if I understand your question. You don't need to do anything with the waker if you're returning Poll::Ready because the task can continue execution in this case. It is only paused after a future returned Poll::Pending. In that case, the asynchronous task must wake the Waker to signal that the task should be continued.

I have found future impl's (like https://docs.rs/futures-util/0.3.4/src/futures_util/future/future/map.rs.html#42) that pass the cx down to a lower future, but don't actually call wake themselves. Are they assuming a lower future will call wake?

Yes, exactly. Only the futures at the lowest level need to handle the Waker. Futures that call poll on an inner future can rely on the fact that the inner future will perform the wakeup in case it returns Poll::Pending.

Should an executor have a timeout after which if it hasn't gotten a .wake() from a task, it polls the future anyways?

This might be a good idea for robustness, but normally you should be able to rely on the waker notification.

@hismito
Copy link

hismito commented Apr 19, 2020

Thanks for the great post.

In "The Problem with Self-Referential Structs"

element: 0x1001a, // address of the last array element

should this address be 0x1001c to match the diagram ?

edit: array in diagram starts at 0x10014, the last element sits at 0x10014 + 4 * 2 = 0x1001c.
if that's true, the diagram itself might need to be updated to 0x1001c as well.

Copy link
Contributor

Isn’t going from cooperative multitasking to preemptive multitasking as simple and straightforward here as locking each Task with a Mutex? I mean, that’s really all threads really are: cases where each new process instance is locked down.

Copy link
Contributor

^ Oops, I meant adding a MutexGuard attribute to the Executor struct. Isn’t that really the only necessary step here, or is there something else?

@phil-opp
Copy link
Owner

@realKennyStrawn93 I'm not quite sure what you mean with adding a MutexGuard to the Executor. Preemptive multitasking means that each task can be interrupted at arbitrary points in time, not just when it returns Poll::Pending. This means that a task might be paused between any two assembly instructions. Since it needs to continue exetution normally when it is resumed, the OS must ensure that the complete CPU state including all registers and stack content is exactly the same as when the task was paused. This imposes some challenges and e.g. requires a separate stack per thread.

@phil-opp
Copy link
Owner

@hismito Good catch! You're right that it should be 0x1001c instead of 0x1001a. Fixed in a7a5757.

Copy link

Mandragorian commented Apr 25, 2020

Why do we use an Arc, instead of an Rc wrapper? Everything runs on the same thread so how do atomic operations help us?

It might have something to do with the asynchronicity of interrupts but I am not sure how this is relevant. Then again, I have gotten quite rusty, no pun intended, with low level asynchronous programming.

edit: I now realize that Arc is probably used because of how Wake uses it in the blanket implementation.

@phil-opp
Copy link
Owner

@Mandragorian The Waker type of the core library implements Sync, so we can't create a Waker that uses the non-Sync Rc type internally, even if we don't use the Wake trait.

Apart from the type system requirements, using a waker type that isn't Sync would lead to race conditions in our code even though we're not using multiple threads yet. The reason is, as you assumed, that interrupts are asynchronous. So they might arrive right when the main thread modifies the reference count. If this modification wasn't atomic, a data race would occur if the interrupt handler modifies the reference count too (which it does when it calls wake on the AtomicWaker).

Copy link

mtnygard commented May 3, 2020

The code block under "The Wake Trait" says it goes in simple_executors.rs. Should that be executors.rs?

Copy link

If I understand correctly, you push the task ID to wake_queue when the Future wakes up, and then move the ID from wake_queue to task_queue in the run() method. Why use indirect wake_queue? It will be much clearer if we push the task ID directly to task_queue when the Future wakes up.

@phil-opp
Copy link
Owner

@mtnygard Good catch, fixed in 199c3b4.

@phil-opp
Copy link
Owner

@SnowyCoder Great to hear that you like the blog!

Is there a reason why in the executor you splitted the tasks and the waker_cache? Since each task once running should have a waker I would've written them as a single BTreeMap<TaskId, (Task, Optional<Waker>)> (or a struct with two fields).

It's been a while since I wrote that code, so I don't remember my exact reasons for using a separate waker cache anymore. I know that I changed the executor design a few times while writing this post, so maybe the reasons for keeping it separate don't exist anymore with the current design.

Is it possible to create the keyboard queue without dynamic allocation? It just seems more efficient since the space requirements don't change (although I imagine it's harder to implement)

Yes, this should be possible. The ArrayQueue implementation in crossbeam only does a single allocation in it's new method, which could be replaced with a static array. However, this would require modifying the crossbeam code, which was not worth the effort to me.

It's also worth mentioning that there is ongoing work on adding support for heap allocations in constants in Rust itself, which might at some point allow to mark the ArrayQueue::new function as const without removing the allocation. Then we could directly initialize a static at compile time and the Rust compiler would automatically transform the allocation to a static memory region.

@RMuskovets
Copy link

RMuskovets commented Oct 25, 2020 via email

@phil-opp
Copy link
Owner

@RMuskovets This is only about const and static items. So you might be able to do static FOO: &String = &String::from("test") at some point. In this case, the compiler would intercept the allocation and replace it with normal static memory.

Normal let statements are not effected by this. So let foo = String::from("test") will still use the dynamic allocator.

@RMuskovets
Copy link

Oh, that's really cool! But will it work in environment like lazy_static?
And for me this is a bit unclear: if I declare a const in a function (if it doesn't result in a compile error...), will it still use dynamic allocations? I mean something like this:

fn function() {
    const foo: &String = &String::from("test");
}

@phil-opp
Copy link
Owner

It doesn't matter where you define a const in your code, it is always evaluated at compile time. So if allocations in constants are permitted at some time, your code example should work and not do any actual allocations.

Lazy static evaluates expressions lazily at runtime, so it would still perform a normal dynamic allocation at runtime.

Copy link

Hey, super duper amazing series. You'r AWESOME. Can't wait for more. :)

Btw, in:

[dependencies.crossbeam-queue]
version = "0.2.1"

it does not work if you pump it up to the newest "0.3.0" version as I first did. Just a warning if someone wastes any time on it like me.

Copy link

@Gabriel-Alves-Cunha That's because Crossbeam changed its pop() api from Ok(val) to Some(val). Just change that and it should work on ver. 0.3.x

@ethindp
Copy link

ethindp commented Dec 3, 2020 via email

@phil-opp
Copy link
Owner

@Gabriel-Alves-Cunha

Hey, super duper amazing series. You'r AWESOME. Can't wait for more. :)

Thanks a lot, I'm glad that you like it :).

@ethindp

Tasks are only paused at .await points if they can't directly continue. If a task doesn't need to wait, it just runs till it finishes. Only if an awaited future returns Poll::Pending, the task pauses itself and returns control to the executor.

Copy link

I would just like to point out that instructions::enable_interrupts_and_hlt as used in the post is deprecated and probably should be changed to instructions::enable_and_hlt.
Thanks for all your hard work in making this series!

@phil-opp
Copy link
Owner

@brightly-salty Thanks for reporting! Yes I'm aware of that and I try to update it when I have time.

@ethindp
Copy link

ethindp commented Dec 28, 2020 via email

@bjorn3
Copy link
Contributor

bjorn3 commented Dec 28, 2020

You will need to implement Future on a type and then .await a value of that type. Make sure to use the Waker that is passed in through the context that you get as argument to tell the executor to run your task again when it is ready.

Copy link

Excellent!

Copy link

mkbanchi commented Jan 29, 2021

Hi and congratulations for the post! Hi found a little error in the paragraph 'The Problem with Self-Referential Structs'.
After moving struct WaitingOnWriteState at address 0x10024, the last array element will be at address 0x1002c and not at address 0x1002a.

@phil-opp
Copy link
Owner

Good catch, thanks! Fixed in cd8e139.

Copy link

TANDEXX commented Feb 9, 2021

Try to make disk support next mbr or gpt and finally: exfat, fat32 fat16 and fat12 file systems support. This can be very good!!!

@phil-opp
Copy link
Owner

@TANDEXX We might cover disk access and file systems at some point, but there are some other topics that I plan to cover before that (threads, processes, userspace, etc).

@tux7k
Copy link

tux7k commented Feb 11, 2021

maybe in the future you could do guis?? 🤔

@phil-opp
Copy link
Owner

I'm currently working on a new edition of the blog, which will use a pixel-based framebuffer for display output. This makes it possible to set individual pixels and also create some GUI if you like. For the blog, I plan to only cover a simple shell and some games at the beginning, but as soon as we have support for userspace processes, a GUI might make sense. (This won't happen in the near future though.)

Copy link

TANDEXX commented Feb 23, 2021

I have problem while compiling system. this throws linking with cc failed with many arguments and next output is:
= note: /bin/ld: /home/jonatan/code/rs/blogOs/target/debug/deps/blog_os-dcd5841984a347cd.3zk56vg27gs3kpga.rcgu.o: in function '_start':
/home/jonatan/.cargo/registry/src/github.com-1ecc6299db9ec823/bootloader-0.9.12/src/lib.rs:35: multiple definition of '_start'; /usr/lib/gcc/x86_64-linux-gnu/8/../../../x86_64-linux-gnu/Scrt1.o:(.text+0x0): first defined here
/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/8/../../../x86_64-linux-gnu/Scrt1.o: in function '_start':
(.text+0x12): undefined reference to '__libc_csu_fini'
/bin/ld: (.text+0x19): undefined reference to '__libc_csu_init'
/bin/ld: (.text+0x20): undefined reference to 'main'
/bin/ld: (.text+0x26): undefined reference to '__libc_start_main'
collect2: error: ld returned 1 exit status
only one error and one warning. I installed needed target and rust nightly and I am on linux mx (debian like system). What is wrong? (I replaced ` characters with ')

@phil-opp
Copy link
Owner

You need to compile the project for a bare-metal target, e.g. by passing a --target argument.

@ethindp
Copy link

ethindp commented Feb 23, 2021 via email

Copy link

nanoadam commented Mar 1, 2021

When I run the kerenl, I get the warning: WARNING: Scancode queue uninitialized. I tried looking for mistakes but couldn't find any.

Here is my github repo: /~https://github.com/nanoproductions/dwn-os

Thanks!

Copy link

ethindp commented Mar 2, 2021

Okay, so I'm trying to make it possible to spawn tasks when the executor is not owned by any function. As it is right now, I have it like this:

    let mut executor = Executor::new();
    executor.spawn(AsyncTask::new(gdt::init()));
    executor.spawn(AsyncTask::new(interrupts::init_stage2()));
    executor.spawn(AsyncTask::new(pci::init()));
    executor.spawn(AsyncTask::new(rtc::init()));
    executor.run();

However, not only does .run() consume the executor, but its blocking too. If I store it in a mutex, then that mutex will be held forever. So you won't then be able to spawn anything. I could store it in an atomic, but that's not very adequate either.
I thought of altering the .run() method of the executor to be called on an interrupt, and making it not loop at all, but that presents me with a problem -- the interrupt will be hung until run() completes, and only Rust knows when that'll be. But I definitely need to be able to do this, because I need to be able to call async code from non-async functions. I'm just not sure how to do it. Thoughts?

@tux7k
Copy link

tux7k commented Mar 5, 2021

I'm currently working on a new edition of the blog, which will use a pixel-based framebuffer for display output. This makes it possible to set individual pixels and also create some GUI if you like. For the blog, I plan to only cover a simple shell and some games at the beginning, but as soon as we have support for userspace processes, a GUI might make sense. (This won't happen in the near future though.)

Wow, thanks

Copy link

TANDEXX commented Mar 21, 2021

so I rewrited this to basic shell and here is download link: https://drive.google.com/file/d/1uDwCqCpfBzrz93v9AGXWjQ2xp2aVAitF/view?usp=sharing. messages displayed before modification are not deleted and this isn't finished because my linux on pendrive was broken and finally I wanted to make my own system (file system also was broken and I writed my own system in virtual machine because I just can't write .bat files and I know only the: sh, javascript, bash, python (very low) and php as script languages)

Copy link
Contributor

I didn't understand why we need handle multiple tasks in executor. Is it possible to combine all futures in one, for example using join_all? I think one task executer is way simpler, with same result.

Repository owner locked and limited conversation to collaborators Jun 12, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests