-
Notifications
You must be signed in to change notification settings - Fork 30
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
[RFC] Async
API exploration and minimal proposal
#99
Comments
And so it seems you can indeed do it now in a dirty way by wrapping the fiber transfer inside a new fiber inside a function: static addSelfToScheduler() {
var fiber_current = Fiber.current
Scheduler.add( Fn.new {
fiber_current.transfer()
})
} Impressive but seems needlessly complex, prone to error, and potentially harder to debug - when it would be so easy to just make the tiny patch to Scheduler to allow this to happen without all the extra layers. Working implementation using the above hack as a library: |
I'm not sure I understand your idea well, but I'm afraid you don't understand the scheduler. You don't ask the scheduler to resume the current fiber. It does it automatically when it can. You just |
That's always possible, but I'm honestly fairly certain I truly do here. :) Let me explain my understanding and you can point out where you see errors, if any.
Well, we may be getting hung up on what "automatic" means... there is no actual magic here - it's all just C and Wren code working together - asking the Scheduler to do things... Fiber's don't pause and resume themselves (well sorta with call, but we're not using Fiber.call). I wonder if that's the confusion? For simplicity we'll assume only the main Fiber exists when we start, that we're running a script from the console... For a C foreign dispatch here is what happens:
Docs for
At this point things are dead stop. The only thing still "running" [quietly] is the UV event loop inside C. The VM is not running. So how do we get unstuck? That C callback I mentioned earlier... it finally finishes... this kicks of another sequence of events: (see
schedulerResume(freeRequest(request), true);
schedulerFinishResume(); That pushes any necessarily return arguments and then calls one of (passing it the Fiber saved earlier):
And what does resume do? It calls So what I'm talking about here is doing this same thing almost 100% in Wren. IE, we're not going to call any foreign C functions, we're just calling other Wren functions (who then hopefully call async C functions, but that will only return control to them not to us)... we need to get control back to ourselves. Lets look at my example again step by step: // schedule some function
Scheduler.add(fn) We schedule a function... if the queue is empty that means that this will be the very next thing the Scheduler runs when Lets say we SKIP the critical next step as you're suggesting: // schedule myself
// Scheduler.add(Fiber.current) // comment this out! // transfer control to the scheduled function (fn)
Scheduler.runNextScheduled_() The Scheduler transfers control to "some function" by calling Control is never returned to the "caller". You may be missing the nuance of So we fix this as shown earlier: Scheduler.add(fn)
Scheduler.add(Fiber.current)
Scheduler.runNextScheduled_()
Eventually it will and we can resume primary execution. This is what my static await(list) {
while(true) {
if (list.any { |task| task.isRunning }) {
Async.waitForOthers()
} else {
break
}
}
} Every time the main fiber (the one running You can test all this on the current CLI right now just by trying my |
The whole Scheduler system as written now truly "hides" a key part of the scheduling [state] in C callbacks... making it much harder to follow I think. If I was to rewrite the whole thing to be clearer I might actually have At any point you could then print out the list of "suspended" threads - you could then have some sort of "Task manager" Fiber running monitoring everything. This is impossible currently... because once a Fiber is handed to C is "disappears" and the Scheduler loses track of it until C reminds it again. |
You had me thinking about this statement for a moment. This is completely true, but it's only a single possibly way of using the scheduler. This would require your current fiber to go into a tight (or loose) sleep loop - either waking up far more often than is necessary OR not waking up often enough. That's the problem I'm solving, that's what the scheduler should do for us. Here is a tiny example of the problem you can run: import "scheduler" for Scheduler
import "timer" for Timer
// our background process
var f = Fn.new {
Timer.sleep(5000) // pretend this is a file download
System.print("Function done")
}
Scheduler.add(f)
Scheduler.runNextScheduled_()
// control is never returned to us after the function finishes
System.print("Will you ever see this? No.") What we really want to do is "wait until the function finishes, then let me know"... I solved this inside the Scheduler. Here is what I believe you're pointing out/suggesting: import "scheduler" for Scheduler
import "timer" for Timer
// our background process
var f = Fn.new {
Timer.sleep(5000) // pretend this is a file download
System.print("Function done")
}
Scheduler.add(f)
while(true) {
Time.sleep (1000) // sleep for 1000 ms, giving our download time to run
// check some status to see if the function is done
// if done break
}
System.print("Will you ever see this? YES.") So here we sleep, wake up every second, and check the status of the download... solving the problem outside the scheduler.
This approach seems less optimal because it's kind of "building a tiny scheduler" with The two approaches may look quite similar (the loops), and they are - but a key difference is:
|
I was finally able to grasp it all! Thanks. I did understand the scheduler, but not your code. However, I've still got some critiques.
var task = Task.run {
Timer.sleep(1000)
System.print("Task")
}
Scheduler.add { System.print("Non-task") }
Task.await([task]) Will wait, print "Task" then "Non-task" instead of immediately printing "Non-task", waiting, then printing "Non-task".
I suggest, instead, adding the following little piece of code to class Scheduler {
// ...
static all(tasks) {
if (__scheduled == null) __scheduled = []
var callingFiber = Fiber.current
var results = List.filled(tasks.count, null)
var fibers = []
for (i in 0...tasks.count) {
fibers.add(Fiber.new {
results[i] = tasks[i].call()
fibers[i] = null
callingFiber.transfer()
})
}
__scheduled.addAll(fibers)
while (fibers.any {|fiber| fiber != null }) runNextScheduled_()
return results
}
static any(tasks) {
if (__scheduled == null) __scheduled = []
var callingFiber = Fiber.current
var result = null
var done = false
var fibers = tasks.map {|task| Fiber.new {
var myResult = task.call()
if (!done) {
done = true
result = myResult
callingFiber.transfer()
}
} }
__scheduled.addAll(fibers)
runNextScheduled_()
return result
}
} import "scheduler" for Scheduler
import "timer" for Timer
var a = Fn.new {
Timer.sleep(5000)
System.print("Task a")
return "a"
}
var b = Fn.new {
Timer.sleep(1000)
System.print("Task b")
return "b"
}
var c = Fn.new {
Timer.sleep(3000)
System.print("Task c")
return "c"
}
System.print(Scheduler.all([a, b, c]))
// Waits a second, then prints "b"
// Waits two more seconds, then prints "c"
// Waits two more seconds, then prints "a"
// Prints "[a, b, c]" |
Your broadening the discussion a bit I think, which is fine. I was arguing (at a minimum) for a single small primitive to be added to Scheduler to allow people to build more complex schedulers on top of the existing system. :-) My async system was just one such small example of what's possible. The existing scheduler is indeed quite limited in so many ways and I'd love to replace it with something nicer, but I'm not sure that needs to be in core or CLI core. The isn't what I was suggesting here.
This is a bug, not a problem with the approach itself. My first implementation just didn't consider this. As you point out all that's needed is an additional call to If you open an issue against https://github.com/joshgoebel/wren-async-task/blob/main/async-task.wren with a small example case and I'll see if I can fix the bug for you. :-)
It'll get resumed the next time ANY IO event fires of course... the issue is how quickly it will be resumed, which is indeed a bug.
I see what you mean, but I think a Task is a lot more than a Fiber. How much is it depends on what a user is trying to do. Fibers are the low-level concurrency primativees Wren exposes. A Task is higher level abstraction and might include:
I think there is value in the low-level Fiber primitives being quite basic and then allowing much more complex task management built on to of that. I was showing one such simple way to build an async/await system. I'm not sure we need to lock users into one true way. If we're "stuck" with the Scheduler in the CLI (for some time) because it's too hard-wired into the C code to easily replace entirely (via a binary or non-binary library) then I'd like to make sure it supports "just enough" to allow people to build multiple different scheduling systems on top of it. Then lets see what people build, and may the best implementation win (if it's even decided we need a single winner).
Oh you mean there isn't a way to get a return value? That could easily be added to the API. |
Your implementation looks like one viable possibility for building out the higher level pieces. It indeed solves "more of the problem" than mine did because I think you had more of the problem in mind. :-) I think your implementation also proves my larger point here. I see you use: __scheduled.addAll(fibers) With the one change I suggest your implementation also could be built entirely outside the Scheduler. At a minimum - if we do nothing else - we need a clean way to directly add a fiber (not just a function) to the scheduler run queue. I'd be happy if we just agreed on that one thing for starters. :-) |
I purposely omitted errors from the discussion. I'd like to see some different approaches. I imagine dealing with errors very much depends on the use case.
With #3 though we rapidly get into supervision, supervision trees, etc... (the stuff Elixir and Erlang are amazing at) which are things that you could build out on TOP of lower-level primitives. They aren't things I think we need to support in CLI or CLI core on day one. I suppose #3 is really just a variant of #2.
I'd love to tackle this. This is the one thing that may require further changes to Scheduler itself. (vs being able to be built on top)... I would think it involves passing process handles around, not Fibers. So when we sleep a task inside C, C gets a handle to the process (not the fiber)... when it resumes it passes the scheduler the process handle... at that point the scheduler could go "oh wait, this process has been killed/requested to kill" and simply not resume the fiber and lose the reference - which at some point should make it be GCed. And (I have no idea about this part) theoretically if C has a list of process handles (that are waiting callbacks in UV) you could have Scheduler call down into C to say "kill this handle" and perhaps? in some? cases C could simply remove the callback from the UV event loop. This might very much depend on what kind of callback it was though, etc... one imagines there are fewer side effects to cancelling a timer (wake me up in 15) than cancelling a download operation thats' right in the middle of a bunch of network IO. |
IMO it is a problem with the approach itself. I was unable to find a way to fix it.
I don't understand.
Yup, this is what I meant.
I'm not sure about "dependencies/relationships to other tasks" (what you mean). About the others, they can, and probably should, be external to the scheduler. This is something to have an external library for. But in your code,
I don't see this is as a contradiction to what I said. Quite the opposite. The CLI provides the most basic async functionalities - running fibers in parallel, and people can build whatever abstractions they want, or need, on top of it.
You are right. As opposed to the other points, this one is really something that can be integraed easily into your solution (in fact, before I built my solution, I hacked this into yours). I just wanted to note it.
I see this as no more than an optimization. You can easily add a fiber to the scheduler now, with
I didn't mean generally "errors in the scheduler". That's nice to have, but not very important. What I meant is combination of successful and failed fibers in
Yeah, I've though about something like that.
I don't fear this. Cancelling a network request is not worse, or better, than cancelling file I/O. The thing I do fear of is uncompleted code. For example, what if we do have to operations that one of them need to succeed (
What if we finished the network request (and thus cancelling the second fiber) after opening the file, but before closing it? It would not be closed until the process finished. That makes me fear we need a whole another set of methods for uncancellable fibers, and that starts to become an exponential number of methods (errors x uncancellable). ConclusionI understand you think that this does not need to be in the CLI. But I do not agree. The scheduler is only halfway completed without this. Even if this can be coded into an external library (and it cannot, because |
I think this requires some small amount of support in Scheduler to do nicely. But it wouldn't have to be super complex: static addSelfToScheduler() {
Scheduler.add(Fiber.current, {skipTicks: 1 })
} The second argument being how many Scheduler "ticks" to skip... and a sentinel would have to be added to the queue so that the scheduler could tell when it had "completed" a full loop of all processes. So we would add ourselves to the current run loop but then when our tick count is > 0 we'd skip calling I should note that your solution still goes much further by integrating everything (intentionally I assume). You're building task/parent dependencies into the callbacks themselves, where-as I'm not. So you only wake up the "parent" when a child completes, where-as my simple approach wakes the parent many times. (the suggestion here would only wake it once per scheduler tick) These are all tradeoffs. I think the system I proposed is simpler and more modular. I think it's easier to understand each of the small components in isolation (though perhaps this depends on one's programming background?). The bug you found could be quite annoying, but if that were resolved I think both of these approaches would work well in real-life for many uses cases. Also note one could add dependencies to the Scheduler (at the data level)... without baking them into the loops or callbacks themselves. IE using it might look something like static await(list) {
list.each { |task| Threads.addDependency(Threads.current, task) }
while (true) {
if (list.any { |task| task.isRunning }) {
aSync.waitForDependents()
}
}
} This difference here again still being we're woken each time AFTER a dependent thread runs on the queue (which can be super useful in a lot cases, just not this example)... for example, one could imagine a system where we aren't just blindly waiting but where we are tracking the progress of 10 different downloads and printing the progress to the screen constantly. That's why I was trying to focus on smaller building blocks - that you could solve many types of problems with. I think you're trying to solve a narrower problem (and hence doing it better because your solution is focusing on ONLY that problem). |
For example adding a progress callback (to track the status of those downloads) to my system would be a single LOC: static await(list, progressCallback) {
while (true) {
if (list.any { |task| task.isRunning }) {
if (progressCallback != null) progressCallback.call()
aSync.waitForDependents()
}
}
} |
Also the original problem could be solved another way with an var task = Task.run {
Timer.sleep(1000)
System.print("Task")
}
Scheduler.addAndStart { System.print("Non-task") }
Task.await([task]) That might even be the better default behavior, though I could imagine arguments on both sides. Since we don't have true concurrency there may be cases where this ordering really matters. |
Do you have any thoughts on the specifics of my actual minimal proposal here:
If there any harm in making it possible to do that without the extra fiber/fn hoops you have to jump thru now to accomplish it? |
My head doesn't grasp it. Sorry. |
I think (I'm not an expert, but I know some things) that real OS schedulers have the idea of a loop thru the FULL process list... the scheduler interrupts periodically (since modern OSes are preemptive) and the loops over all the processes to see if they need some time on the CPU or not... then rinse repeat. But this "infinite" bug can't happen because you're just processing the task queue literally each "tick". It's impossible for a process to be called twice. We could of course build that... but right now we just have a simple queue... so you could do the same thing by adding a "STOP" opcode to the queue. Say here is the queue:
The problem is we get to the end and remove await, but await runs and adds itself back, infinite loop. So what you do instead. Before await runs:
After await runs:
This might be a terrible way to actually do it though and it might be simpler to just have a List and use |
I don't agree. Your system is indeed easier to work with, for the programmer of the CLI. For the user, all it wants is to run something in parallel. That's it. And I'm not sure yours being more modular; js will prove which have (almost) exactly the same system as mine. Furthermore, IMHO, mine is more natural: you wake up when you need to. If I want some task to finish, I want to wake up when it finished. That's it.
What's the point?
Indeed. But smaller building blocks have a problem: they're small. And I'm afraid yours are too small. I prefer larger building blocks, especially when I'll probably never want to get deeper (try to describe a situation only your smaller blocks can solve).
You're calling the progress callback after each I/O event? What's the point? It will probably have just one for one network request. You probably want to call it each X time. Here's how I would do it: class FiberProgress {
static withProgress(fiber, callback, progressTime) {
return Fn.new {
return Scheduler.any([
fiber,
Fn.new {
while (true) {
Timer.sleep(progressTime)
callback.call()
}
},
])
}
}
}
var result = FiberProgress.withProgress(
Fn.new { Network.get("https://google.com") },
Fn.new { System.print("Progress...") },
1000
)
It does not solve the bug, and I also don't see value in this addition:
I don't see value in it, but I see a lot of hard. You can ruin the scheduler completely with that. |
A simpler way will just to check if we have items in the queue. If yes, |
It's equally easy for just a user. We both have the exactly same API.
That provides no way for users to track tasks as they progress or update the UI, etc... as I already pointed out.
There are two use cases (just off the top of my head) here:
The code you showed only does one thing, on purpose. The code I showed easily allows either (by adding a single callback). What I'm suggesting is more flexible - because I want to give implementers choices.
Obviously matter of opinion. :-) Though I did already say that Async being built into Scheduler might be OK. :-)
That is a pretty large assumption/leap there... wanting to track the progress of many running tasks seems a pretty common problem in many systems.
Really? A download on a socket would wake many times as data is received. Every download tool I know shows the status of the download as the progress bar fills up.
Yes, with your implantation you'd be forced to make the callback one of the tasks itself... (which seems weird to me at a glance, but perhaps it's not that unusual) I already pointed out the problem with this though. Now you're waiting LONGER than you have to when things are complete, whatever the value of your sleep is.
I was more suggesting it as a workaround that fixes your example. It does indeed fix your example does it not?
This is making assumptions about how things must be used. I'd rather avoid doing that and assume there are different ways a implementor might want to accomplish a thing using the low-level primitives. I'm not sure how that is an argument against
How? I'm not sure I follow. Are you saying your real objection is this piece of code? while(true) {
Scheduler.add(Fiber.current)
Scheduler.nextScheduled()
} I'd say this is simply buggy code and just as bad as writing the following and then wondering why you're stuck in an infinite loop.
It should be understood (and documented) that placing the current fiber at the end of the run queue in a loop is going to result in an infinite loop. I don't find that surprising. |
Great idea. This would be another great addition to the current minimal Scheduler. I think we could add both:
So then it's easy to write: if (Scheduler.hasPending) Scheduler.add(Fiber.current)
Scheduler.next() |
HA, I may have changed my mind - I need to noodle more before responding. I keep coming back to this is the user's responsibility. I get the feeling you think this is bad: // give other items on the queue a chance to run before we resume
addSelfToScheduler()
Scheduler.runNextScheduled_() Really that's just a So this pattern is super useful. It's only bad if you have an infinite loop. Your proposal of allowing processes to check what the scheduler is doing internally is mixing concerns. If my code wants to yield (allowing other things to run) it should simple yield. It shouldn't need to FIRST check with the scheduler and then change it's behavior based the scheduler state. That is unnecessary complexity. We should find a way that avoids the NEED to do this. |
I think feel like partially we're talking about different things here and that is complicating the whole discussion. I want to build some useful lower-level primitives (yes it is possible to shoot yourself in the foot, just like with Fibers) and you want to build useful higher level primitives. There is room for both, it's not either or. And it matters not one bit to the users at the top-most layer. You can build the higher level stuff on top of the lower-level. You may think the middle layers aren't necessary, I think they add value... we may just have to agree to disagree on that point. :-)
I'd think perhaps scheduler could try and protect people from doing ridiculous things, sure, BUT since we don't have // safe
Scheduler.yield()
// -potentially- unsafe
Scheduler.add(Fiber.current)
Scheduler.nextScheduled() You might suggest that |
Right. I was too focused on the details, sorry 😺
It does, as I already pointed out.
Ditto.
Ditto.
Try to show a case where it doesn't hold.
I meant higher-level protocol, like HTTP. Per data progress is not something that cannot be done, there's a whole bag of patterns in the js world.
Weird? I find it pretty natural. You have two tasks: making a network request and reporting progress. Separating them makes a lot of sense.
No you don't. This code can be left as-is with cancellation, or cancel by hand pretty easily.
If you use var task = Task.run {
Timer.sleep(1000)
System.print("Task")
}
Scheduler.add { System.print("Non-task") } // `add()` cannot be replaced with `addAndStart()` anymore
System.print("After `add()`")
Task.await([task])
I'm sorry but I can't. Do you have some more concrete example? I can easily imagine both cases:
These needs are pretty much covered by the new APIs.
Yes and no. We seem to disagree about the most basic question: How much low-level does the API need to be? Let me bring an example. Do you think This is definitely something that a lot of abstractions can be built on top of. Still it is an implementation detail and I (hope) you don't want to expose it. On the other hand, it's all about the language. I think we'll both agree that C++'s The scheduler is a low level component. Messing with it is not something to be done. Just like you can't, and shouldn't, interrupt the event loop in JS.
It goes again to exposing a pointer to the contents of list. Why not just expose |
I needed to read this comment before responding! 😶
|
Well, sure if you're imagining we're also adding cancellation. :-) Then you'd just cancel the "watching" thread vs having to wait for it...
I think I'm thinking a bit more of "supervision" of the running processes.. where you want the "parent" in child... you don't want to have one fo the children babysitting the others. What if that thread dies or errors? (but I supposed that's cheating since I said we weren't talking about errors). That's why it initially truck me as odd... but I guess updating UI really has nothing to do with supervisions, those are likely separate concerns since the UI could ALSO die and need to be restarted, etc...
But by design. It's all about what order you WANT the task queued (in the current scheduler)
I feel like this distinction only exists at all here because we're not preemptive. A thread runs until it yields... so run order is HUGE. I think you're imagining a more complex scheduler where this doesn't matter (like most complex schedulers) and that would be good in some ways. I'm imagining the limits of the current scheduler...
I've learned that just because I can't think of a thing doesn't make it not so. I wasn't saying you are wrong, only that I often try to avoid that type of thinking when it can be avoided.
I feel like you're mixing different things here. And it all depends on HOW we're thinking about these things. I don't think "what kind of language" matters so much. There are higher level abstractions in C++ and lower-level ones, same for Wren. You can build and use both in many languages (obviously the language determines HOW close to the hardware you get). Scheduler is lower-level (cause it's so minimal) but it's not the lowest level (in Wren). Fibers are. Wren code shouldn't try or expect to mess with Fiber internals, but the Scheduler is (in some ways) just Wren code. I just want to be able to work with the Scheduler to build more complex things, not "mess with it" per se... there should be an easy generic way to say to the scheduler "run other things, then wake me later"... without resorting to a fixed time loop.
I wasn't sure if the 0 might break or have weird behavior on the C/UV side because I haven't tested it.
I'm aware. :-) It's a common pattern in other places too. In coop multitasking yield often is king.
Actually yield might be the wrong name them... since there are two different things here (which bugs me too):
Most often I would say you want to yield all the way down to UV to allow it's callbacks to fire as soon as they can etc... ie in a highly concurrent system doing many different things the long the Wren VM holds the CPU without throwing back to UV is bad. |
This feels inconsistent to how I'm thinking about it. You say Scheduler is low level while wanting to add all these higher level concepts to it ( If possible (and not difficult) it would be nice if people could build different higher level Schedulers. See what works, see what doesn't. Find out what level of complexity feels correct for Wren Core, vs the CLI, vs external outside libraries. |
There may be another philosophical difference here (which we're discussing in other threads). I think (within reason) the more things someone can accomplish without recompiling the CLI is "good". IE, the CLI in a perfect world just "runs Wren" and includes some useful libraries. The more it becomes a "complex runtime environment with many assumptions already baked in" the worse in some ways. Wren doesn't have a Scheduler... so I feel like (again in a perfect world) that a scheduler should just be another library I could load into the CLI... those libraries (by necessity) building on top of C + Wren's lower-lever Fiber primitives. What I like about the current Scheduler is that it almost allows you to build these very complex higher level abstractions on top of it. I think it'd be better if we removed the "almost" part. |
I just wanted to say that not always you cannot always replace
I don't think so.
But now it can't be avoided, because I doubt it 😃
Every language has abstractions and low level stuff, but as higher as the language, the less low level stuff it exposes. Is the fiber low level? No. Fibers are not used only to create schedulers: they're not even always used in the context of asynchronous code. Are fibers low-level when we're talking about async stuff in the CLI? Perhaps. But I don't think they are: they are just part. It's somewhat comparable to Is the scheduler low level? Neither so. It can be used to build higher-level stuff (like progressing and so), but it is also high level enough for some stuff. "Low level" is a relative term, and the scheduler is not "low level" as in "not useful on its own, only as a building block for other abstractions". I just want it to perform even more things.
http://docs.libuv.org/en/v1.x/timer.html#c.uv_timer_start:
Like I said, to me, the scheduler is not low level. Another option is to bake them into
I would completely agree if only it was possible. The scheduler is deeply integrated with the rest of the CLI, because of the fact that it uses libuv and asynchronouty. Decoupling it is not possible. Now, the only question is if we should open its internals. I think we should not, and instead build meaningful abstractions on top of them.
Why I like about the current scheduler is that it almost allows you to build an async app. I think it'd be better if we removed the "almost" part 😄 I don't refuse a |
Let's agree to strongly agree on that LOL.
I think we may disagree on what "open it's internals" means... taking a thing is already possible and just making it less dirty... is that truly "opening the internals"...? I mean maybe you're technically right but it doesn't feel like "opening the internals" much to me if it only allows existing things to be done more cleanly.
Someone has to build the libraries for users to plugin in then not care about. ;-) |
Wait, why does it almost again? I'm pretty sure the Meanwhile my example requires getting dirty by running an intermediate fiber (and function) for no other reason than to switch to yet another fiber... ie, your solution can be built without compromises - but mine can't. I think both approaches should be possible. |
First, no, because we're not exposing Second, even if we could, we still cannot build an app without them. We can include them in the app, but that means the CLI is not complete.
I don't understand. |
Well I think you mean "[exposing] by name" but if we add So if we add
I was only pointing out you have to jump thru ugly hoops (IMHO) to put a fiber directly onto the queue (put the fiber within a fiber within a function)... where-as your solution has no ugly hoops - merely private API usage. |
Private API usage is way worse than ugly hacks. |
I was playing around with fibers and wanted to start to build something like an async framework but realized we may be missing just the tiniest infrastructure to make that possible. Following is the larger context - skip to just the end if you'd like my proposal.
Here is the idea I started with:
I would like to just put those tasks into the scheduler and then wait for them to all complete asynchronously.
The only core thing missing seems to be
Scheduler.add(fiber)
. You can add new functions to the Scheduler, but you cannot ask it to resume the current Fiber later. Perhaps you could nest the transfer inside a wrapper function insider yet another Fiber (the oneadd
creates) - but ugh... that sounds sounds like pain for no reason.We need a way to be able to insert the current fiber into the queue of Fibers eligible for resumption later. This isn't super helpful on it's own but typically what you would do is first insert a Fiber ahead of you - so that Fiber would run and then when it slept you would be next to resume control, ie:
So let's add that, it's a 4 line patch to
static add(_)
:Perfect, now we can build basic Async support on this alone.
You'll see
run
looks like exactly what we described above.wait
just inserts us at the end of the scheduling queue and then suspends (trusting the Scheduler to resume us later whenrunNextScheduled_()
is called). This assumes the function we are calling will do so at some point (use any of the asyncIO
calls,Timer.sleep
, etc.)And on then top of this foundation you can build higher level abstractions. Given the following small
Task
class the sample code at the very beginning now works.The Proposal
Minimally
add(fiber)
to the Scheduler APIThis is the foundational thing needed for building these patterns on top of the existing Scheduler. If this was added then I think many types of async patterns could be explored outside the scope of the CLI to see what works best.
If we take it a step further
Async
class to wrap up common async patternsPeripheral
I think higher level abstractions - such as the
Task
class shown here could be provided by libraries outside of the CLI Core - again all depending on how minimal we wish to keep CLI.CC @ChayimFriedman2 Related to our prior discussion.
The text was updated successfully, but these errors were encountered: