mirror of https://github.com/nocturn9x/giambio.git
Updated README, added debugging utility and simplified example
This commit is contained in:
parent
a5764255ac
commit
d60a372af5
263
README.md
263
README.md
|
@ -190,44 +190,34 @@ To demonstrate this, have a look a this example
|
||||||
```python
|
```python
|
||||||
import giambio
|
import giambio
|
||||||
|
|
||||||
async def countdown(n: int):
|
async def child():
|
||||||
print(f"Counting down from {n}!")
|
print("[child] Child spawned! Sleeping for 2 seconds")
|
||||||
while n > 0:
|
await giambio.sleep(2)
|
||||||
print(f"Down {n}")
|
print("[child] Had a nice nap!")
|
||||||
n -= 1
|
|
||||||
await giambio.sleep(1)
|
async def child1():
|
||||||
print("Countdown over")
|
print("[child 1] Child spawned! Sleeping for 2 seconds")
|
||||||
return 0
|
await giambio.sleep(2)
|
||||||
|
print("[child 1] Had a nice nap!")
|
||||||
|
|
||||||
async def countup(stop: int):
|
|
||||||
print(f"Counting up to {stop}!")
|
|
||||||
x = 0
|
|
||||||
while x < stop:
|
|
||||||
print(f"Up {x}")
|
|
||||||
x += 1
|
|
||||||
await giambio.sleep(2)
|
|
||||||
print("Countup over")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
start = giambio.clock()
|
start = giambio.clock()
|
||||||
async with giambio.create_pool() as pool:
|
async with giambio.create_pool() as pool:
|
||||||
pool.spawn(countdown, 10)
|
pool.spawn(child)
|
||||||
pool.spawn(countup, 5)
|
pool.spawn(child1)
|
||||||
print("Children spawned, awaiting completion")
|
print("[main] Children spawned, awaiting completion")
|
||||||
print(f"Task execution complete in {giambio.clock() - start:2f} seconds")
|
print(f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
giambio.run(main)
|
giambio.run(main)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
There is a lot going on here, and we'll explain every bit of it step by step:
|
There is a lot going on here, and we'll explain every bit of it step by step:
|
||||||
|
|
||||||
- First, we imported giambio and defined two async functions: `countup` and `countdown`
|
- First, we imported giambio and defined two async functions: `child` and `child1`
|
||||||
- These two functions do exactly what their name suggests, but for the purposes of
|
- These two functions will just print something and then sleep for 2 seconds
|
||||||
this tutorial, `countup` will be running twice as slow as `countdown` (see the call
|
|
||||||
to `await giambio.sleep(2)`?)
|
|
||||||
- Here comes the real fun: `async with`? What's going on there?
|
- Here comes the real fun: `async with`? What's going on there?
|
||||||
As it turns out, Python 3.5 didn't just add async functions, but also quite a bit
|
As it turns out, Python 3.5 didn't just add async functions, but also quite a bit
|
||||||
of related new syntax. One of the things that was added is asynchronous context managers.
|
of related new syntax. One of the things that was added is asynchronous context managers.
|
||||||
|
@ -261,34 +251,18 @@ exceptions in giambio always behave as expected
|
||||||
Ok, so, let's try running this snippet and see what we get:
|
Ok, so, let's try running this snippet and see what we get:
|
||||||
|
|
||||||
```
|
```
|
||||||
Children spawned, awaiting completion
|
[child] Child spawned!! Sleeping for 2 seconds
|
||||||
Counting down from 10!
|
[child 1] Child spawned!! Sleeping for 2 seconds
|
||||||
Down 10
|
[child] Had a nice nap!
|
||||||
Counting up to 5!
|
[child 1] Had a nice nap!
|
||||||
Up 0
|
[main] Children execution complete in 2.01 seconds
|
||||||
Down 9
|
|
||||||
Up 1
|
|
||||||
Down 8
|
|
||||||
Down 7
|
|
||||||
Up 2
|
|
||||||
Down 6
|
|
||||||
Down 5
|
|
||||||
Up 3
|
|
||||||
Down 4
|
|
||||||
Down 3
|
|
||||||
Up 4
|
|
||||||
Down 2
|
|
||||||
Down 1
|
|
||||||
Countup over
|
|
||||||
Countdown over
|
|
||||||
Task execution complete in 10.07 seconds
|
|
||||||
```
|
```
|
||||||
|
|
||||||
(Your output might have some lines swapped compared to this)
|
(Your output might have some lines swapped compared to this)
|
||||||
|
|
||||||
You see how `countup` and `countdown` both start and finish
|
You see how `child` and `child1` both start and finish
|
||||||
together? Moreover, even though each function slept for about 10
|
together? Moreover, even though each function slept for about 2
|
||||||
seconds (therefore 20 seconds total), the program just took 10
|
seconds (therefore 4 seconds total), the program just took 2
|
||||||
seconds to complete, so our children are really running at the same time.
|
seconds to complete, so our children are really running at the same time.
|
||||||
|
|
||||||
If you've ever done thread programming, this will feel like home, and that's good:
|
If you've ever done thread programming, this will feel like home, and that's good:
|
||||||
|
@ -358,20 +332,191 @@ then giambio will switch less frequently, hurting concurrency. It turns out that
|
||||||
for that is calling `await giambio.sleep(0)`; This will implicitly let giambio kick in and do its job,
|
for that is calling `await giambio.sleep(0)`; This will implicitly let giambio kick in and do its job,
|
||||||
and it will reschedule the caller almost immediately, because the sleep time is 0.
|
and it will reschedule the caller almost immediately, because the sleep time is 0.
|
||||||
|
|
||||||
### Mix and match? No thanks
|
|
||||||
|
|
||||||
You may wonder whether you can mix async libraries: for instance, can we call `trio.sleep` in a
|
### A closer look
|
||||||
giambio application? The answer is no, we can't, and there's a reason for that. Giambio wraps all
|
|
||||||
your asynchronous code in its event loop, which is what actually runs the tasks. When you call
|
In the above section we explained the theory behind async functions, but now we'll inspect the magic behind
|
||||||
`await giambio.something()`, what you're doing is sending "commands" to the event loop asking it
|
`giambio.run()` and its event loop to demistify _how_ giambio makes this whole async thing happen. Luckily for us,
|
||||||
to perform a certain thing in a given task, and to communicate your intent to the loop, the
|
giambio has some useful tooling that lets us sneak peak inside the machinery of the library to better help us
|
||||||
primitives (such as `giambio.sleep`) talk a language that only giambio's event loop can understand.
|
understand what's going on, located at `giambio.debug.BaseDebugger`. That's an abstract class that we can customize
|
||||||
|
for our purposes and that communicates with the event loop about everything it's going, so let's code it:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Debugger(giambio.debug.BaseDebugger):
|
||||||
|
"""
|
||||||
|
A simple debugger for this test
|
||||||
|
"""
|
||||||
|
|
||||||
|
def on_start(self):
|
||||||
|
print("## Started running")
|
||||||
|
|
||||||
|
def on_exit(self):
|
||||||
|
print("## Finished running")
|
||||||
|
|
||||||
|
def on_task_schedule(self, task, delay: int):
|
||||||
|
print(f">> A task named '{task.name}' was scheduled to run in {delay:.2f} seconds")
|
||||||
|
|
||||||
|
def on_task_spawn(self, task):
|
||||||
|
print(f">> A task named '{task.name}' was spawned")
|
||||||
|
|
||||||
|
def on_task_exit(self, task):
|
||||||
|
print(f"<< Task '{task.name}' exited")
|
||||||
|
|
||||||
|
def before_task_step(self, task):
|
||||||
|
print(f"-> About to run a step for '{task.name}'")
|
||||||
|
|
||||||
|
def after_task_step(self, task):
|
||||||
|
print(f"<- Ran a step for '{task.name}'")
|
||||||
|
|
||||||
|
def before_sleep(self, task, seconds):
|
||||||
|
print(f"# About to put '{task.name}' to sleep for {seconds:.2f} seconds")
|
||||||
|
|
||||||
|
def after_sleep(self, task, seconds):
|
||||||
|
print(f"# Task '{task.name}' slept for {seconds:.2f} seconds")
|
||||||
|
|
||||||
|
def before_io(self, timeout):
|
||||||
|
print(f"!! About to check for I/O for up to {timeout:.2f} seconds")
|
||||||
|
|
||||||
|
def after_io(self, timeout):
|
||||||
|
print(f"!! Done I/O check (timeout {timeout:.2f} seconds)")
|
||||||
|
|
||||||
|
def before_cancel(self, task):
|
||||||
|
print(f"// About to cancel '{task.name}'")
|
||||||
|
|
||||||
|
def after_cancel(self, task):
|
||||||
|
print(f"// Cancelled '{task.name}'")
|
||||||
|
```
|
||||||
|
|
||||||
|
To use our debugger class, we need to pass it to `giambio.run()` using
|
||||||
|
the `debugger` keyword argument, like so:
|
||||||
|
|
||||||
|
```python
|
||||||
|
...
|
||||||
|
if __name__ == "__main__":
|
||||||
|
giambio.run(main, debugger=Debugger())
|
||||||
|
```
|
||||||
|
|
||||||
|
__Note__: Note that we passed an _instance_ (see the parentheses?) **not** a class
|
||||||
|
|
||||||
|
Running that modified code will produce a lot of output, and it should look something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
## Started running
|
||||||
|
-> About to run a step for 'main'
|
||||||
|
>> A task named 'child' was spawned
|
||||||
|
>> A task named 'child1' was spawned
|
||||||
|
[main] Children spawned, awaiting completion
|
||||||
|
<- Ran a step for 'main'
|
||||||
|
-> About to run a step for 'child'
|
||||||
|
[child] Child spawned!! Sleeping for 2 seconds
|
||||||
|
<- Ran a step for 'child'
|
||||||
|
# About to put 'child' to sleep for 2.00 seconds
|
||||||
|
-> About to run a step for 'child1'
|
||||||
|
[child 1] Child spawned!! Sleeping for 2 seconds
|
||||||
|
<- Ran a step for 'child1'
|
||||||
|
# About to put 'child1' to sleep for 2.00 seconds
|
||||||
|
[... 2 seconds pass ...]
|
||||||
|
# Task 'child' slept for 2.01 seconds
|
||||||
|
# Task 'child1' slept for 2.01 seconds
|
||||||
|
!! About to check for I/O for up to 0.00 seconds
|
||||||
|
!! Done I/O check (timeout 0.00 seconds)
|
||||||
|
-> About to run a step for 'child'
|
||||||
|
[child] Had a nice nap!
|
||||||
|
<< Task 'child' exited
|
||||||
|
-> About to run a step for 'child1'
|
||||||
|
[child 1] Had a nice nap!
|
||||||
|
<< Task 'child1' exited
|
||||||
|
-> About to run a step for 'main'
|
||||||
|
<- Ran a step for 'main'
|
||||||
|
-> About to run a step for 'main'
|
||||||
|
[main] Children execution complete in 2.01 seconds
|
||||||
|
<< Task 'main' exited
|
||||||
|
## Finished running
|
||||||
|
```
|
||||||
|
|
||||||
|
As expected, this prints _a lot_ of stuff, but let's start going trough it:
|
||||||
|
- First, we start the event loop: That's the call to `giambio.run()`
|
||||||
|
```
|
||||||
|
## Started running
|
||||||
|
```
|
||||||
|
- After that, we start running the `main` function
|
||||||
|
```
|
||||||
|
-> About to run a step for 'main'
|
||||||
|
```
|
||||||
|
- When we run `main`, that enters the `async with` block and spawns our children,
|
||||||
|
as well as execute our call to `print`
|
||||||
|
```
|
||||||
|
>> A task named 'child' was spawned
|
||||||
|
>> A task named 'child1' was spawned
|
||||||
|
[main] Children spawned, awaiting completion
|
||||||
|
```
|
||||||
|
- After that, we hit the end of the block, so we pause and wait for our children
|
||||||
|
to complete: That's when we start switching, and `child` can now run
|
||||||
|
```
|
||||||
|
<- Ran a step for 'main'
|
||||||
|
-> About to run a step for 'child'
|
||||||
|
[child] Child spawned!! Sleeping for 2 seconds
|
||||||
|
```
|
||||||
|
- We're now at `await giambio.sleep(2)` inside `child`, and that puts it to sleep
|
||||||
|
```
|
||||||
|
<- Ran a step for 'child'
|
||||||
|
# About to put 'child' to sleep for 2.00 seconds
|
||||||
|
```
|
||||||
|
- Ok, so now `child` is asleep while `main` is waiting on its children, and `child1` can now execute,
|
||||||
|
so giambio switches again and runs that
|
||||||
|
```
|
||||||
|
-> About to run a step for 'child1'
|
||||||
|
[child 1] Child spawned!! Sleeping for 2 seconds
|
||||||
|
```
|
||||||
|
- Now we hit the call to `await giambio.sleep(2)` inside `child1`, so that also goes to sleep
|
||||||
|
```
|
||||||
|
<- Ran a step for 'child1'
|
||||||
|
# About to put 'child1' to sleep for 2.00 seconds
|
||||||
|
```
|
||||||
|
- Since there is no other work to do, giambio just waits until it wakes up the two children,
|
||||||
|
2 seconds later
|
||||||
|
```
|
||||||
|
# Task 'child' slept for 2.01 seconds
|
||||||
|
# Task 'child1' slept for 2.01 seconds
|
||||||
|
```
|
||||||
|
- Even though we're not doing any I/O here, giambio doesn't know that, so it
|
||||||
|
does some checks (and finds out there is no I/O to do)
|
||||||
|
```
|
||||||
|
!! About to check for I/O for up to 0.00 seconds
|
||||||
|
!! Done I/O check (timeout 0.00 seconds)
|
||||||
|
```
|
||||||
|
- After 2 seconds have passed giambio wakes up our children and runs them until completion
|
||||||
|
```
|
||||||
|
-> About to run a step for 'child'
|
||||||
|
[child] Had a nice nap!
|
||||||
|
<< Task 'child' exited
|
||||||
|
-> About to run a step for 'child1'
|
||||||
|
[child 1] Had a nice nap!
|
||||||
|
<< Task 'child1' exited
|
||||||
|
```
|
||||||
|
- As promised, once all children exit, the parent task resumes and runs until it exits. This also
|
||||||
|
causes the entire event loop to exit because there is nothing else to do
|
||||||
|
```
|
||||||
|
-> About to run a step for 'main'
|
||||||
|
<- Ran a step for 'main'
|
||||||
|
-> About to run a step for 'main'
|
||||||
|
[main] Children execution complete in 2.01 seconds
|
||||||
|
<< Task 'main' exited
|
||||||
|
## Finished running
|
||||||
|
```
|
||||||
|
|
||||||
|
So, in our example, our children run until they hit a call to `await giambio.sleep`, then execution control
|
||||||
|
goes back to `giambio.run`, which drives the execution for another step. This works because `giambio.sleep` and
|
||||||
|
`giambio.run` (as well as many others) work together to make this happen: `giambio.sleep` can pause the execution
|
||||||
|
of its children task and ask `giambio.run` to wake him up after a given amount of time
|
||||||
|
|
||||||
|
__Note__: You may wonder whether you can mix async libraries: for instance, can we call `trio.sleep` in a
|
||||||
|
giambio application? The answer is no, we can't, and this section explains why. When you call
|
||||||
|
`await giambio.sleep` that function talks a language that only `giambio.run` can understand.
|
||||||
Other libraries have other private "languages", so mixing them is not possible: doing so will cause
|
Other libraries have other private "languages", so mixing them is not possible: doing so will cause
|
||||||
giambio to get very confused and most likely just explode spectacularly badly
|
giambio to get very confused and most likely just explode spectacularly badly
|
||||||
|
|
||||||
|
|
||||||
TODO: I/O
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
This is a relatively young project and it is looking for collaborators! It's not rocket science,
|
This is a relatively young project and it is looking for collaborators! It's not rocket science,
|
||||||
|
|
|
@ -22,7 +22,7 @@ from . import exceptions
|
||||||
from .traps import sleep, current_task
|
from .traps import sleep, current_task
|
||||||
from .objects import Event
|
from .objects import Event
|
||||||
from .run import run, clock, wrap_socket, create_pool, get_event_loop, new_event_loop
|
from .run import run, clock, wrap_socket, create_pool, get_event_loop, new_event_loop
|
||||||
|
from .util import debug
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"exceptions",
|
"exceptions",
|
||||||
|
@ -34,5 +34,6 @@ __all__ = [
|
||||||
"create_pool",
|
"create_pool",
|
||||||
"get_event_loop",
|
"get_event_loop",
|
||||||
"current_task",
|
"current_task",
|
||||||
"new_event_loop"
|
"new_event_loop",
|
||||||
|
"debug"
|
||||||
]
|
]
|
||||||
|
|
|
@ -44,6 +44,7 @@ class TaskManager:
|
||||||
task.parent = self.loop.current_task
|
task.parent = self.loop.current_task
|
||||||
self.loop.tasks.append(task)
|
self.loop.tasks.append(task)
|
||||||
self.tasks.append(task)
|
self.tasks.append(task)
|
||||||
|
self.loop.debugger.on_task_spawn(task)
|
||||||
|
|
||||||
def spawn_after(self, func: types.FunctionType, n: int, *args):
|
def spawn_after(self, func: types.FunctionType, n: int, *args):
|
||||||
"""
|
"""
|
||||||
|
@ -53,8 +54,10 @@ class TaskManager:
|
||||||
assert n >= 0, "The time delay can't be negative"
|
assert n >= 0, "The time delay can't be negative"
|
||||||
task = Task(func(*args), func.__name__ or str(func))
|
task = Task(func(*args), func.__name__ or str(func))
|
||||||
task.parent = self.loop.current_task
|
task.parent = self.loop.current_task
|
||||||
|
task.sleep_start = self.loop.clock()
|
||||||
self.loop.paused.put(task, n)
|
self.loop.paused.put(task, n)
|
||||||
self.tasks.append(task)
|
self.tasks.append(task)
|
||||||
|
self.loop.debugger.on_task_schedule(task, n)
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
return self
|
return self
|
||||||
|
@ -66,5 +69,4 @@ class TaskManager:
|
||||||
except BaseException:
|
except BaseException:
|
||||||
self.tasks.remove(task)
|
self.tasks.remove(task)
|
||||||
for to_cancel in self.tasks:
|
for to_cancel in self.tasks:
|
||||||
await to_cancel.cancel()
|
await to_cancel.cancel()
|
||||||
print("oof")
|
|
|
@ -24,6 +24,7 @@ from timeit import default_timer
|
||||||
from .objects import Task, TimeQueue
|
from .objects import Task, TimeQueue
|
||||||
from socket import SOL_SOCKET, SO_ERROR
|
from socket import SOL_SOCKET, SO_ERROR
|
||||||
from .traps import want_read, want_write
|
from .traps import want_read, want_write
|
||||||
|
from .util.debug import BaseDebugger
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from .socket import AsyncSocket, WantWrite, WantRead
|
from .socket import AsyncSocket, WantWrite, WantRead
|
||||||
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
|
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
|
||||||
|
@ -44,11 +45,16 @@ class AsyncScheduler:
|
||||||
A few examples are tasks cancellation and exception propagation.
|
A few examples are tasks cancellation and exception propagation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, debugger: BaseDebugger = None):
|
||||||
"""
|
"""
|
||||||
Object constructor
|
Object constructor
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# The debugger object. If it is none we create a dummy object that immediately returns an empty
|
||||||
|
# lambda every time you access any of its attributes to avoid lots of if self.debugger clauses
|
||||||
|
if debugger:
|
||||||
|
assert issubclass(type(debugger), BaseDebugger), "The debugger must be a subclass of giambio.util.BaseDebugger"
|
||||||
|
self.debugger = debugger or type("DumbDebugger", (object, ), {"__getattr__": lambda *args: lambda *args: None})()
|
||||||
# Tasks that are ready to run
|
# Tasks that are ready to run
|
||||||
self.tasks = deque()
|
self.tasks = deque()
|
||||||
# Selector object to perform I/O multiplexing
|
# Selector object to perform I/O multiplexing
|
||||||
|
@ -71,10 +77,7 @@ class AsyncScheduler:
|
||||||
Returns True if there is work to do
|
Returns True if there is work to do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.selector.get_map() or any([self.paused,
|
if any([self.paused, self.tasks, self.events, self.selector.get_map()]):
|
||||||
self.tasks,
|
|
||||||
self.events
|
|
||||||
]):
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -97,23 +100,25 @@ class AsyncScheduler:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
if self.done():
|
if self.done():
|
||||||
|
# If we're done, which means there is no
|
||||||
|
# sleeping tasks, no events to deliver,
|
||||||
|
# no I/O to do and no running tasks, we
|
||||||
|
# simply tear us down and return to self.start
|
||||||
self.shutdown()
|
self.shutdown()
|
||||||
break
|
break
|
||||||
elif not self.tasks:
|
elif not self.tasks:
|
||||||
if self.paused:
|
# If there are no actively running tasks
|
||||||
# If there are no actively running tasks
|
# we try to schedule the asleep ones
|
||||||
# we try to schedule the asleep ones
|
self.awake_sleeping()
|
||||||
self.awake_sleeping()
|
# The next step is checking for I/O
|
||||||
if self.selector.get_map():
|
self.check_io()
|
||||||
# The next step is checking for I/O
|
# Try to awake event-waiting tasks
|
||||||
self.check_io()
|
self.check_events()
|
||||||
if self.events:
|
# Otherwise, while there are tasks ready to run, well, run them!
|
||||||
# Try to awake event-waiting tasks
|
|
||||||
self.check_events()
|
|
||||||
# While there are tasks to run
|
|
||||||
while self.tasks:
|
while self.tasks:
|
||||||
# Sets the currently running task
|
# Sets the currently running task
|
||||||
self.current_task = self.tasks.popleft()
|
self.current_task = self.tasks.popleft()
|
||||||
|
self.debugger.before_task_step(self.current_task)
|
||||||
if self.current_task.cancel_pending:
|
if self.current_task.cancel_pending:
|
||||||
self.do_cancel()
|
self.do_cancel()
|
||||||
if self.to_send and self.current_task.status != "init":
|
if self.to_send and self.current_task.status != "init":
|
||||||
|
@ -124,6 +129,7 @@ class AsyncScheduler:
|
||||||
method, *args = self.current_task.run(data)
|
method, *args = self.current_task.run(data)
|
||||||
self.current_task.status = "run"
|
self.current_task.status = "run"
|
||||||
self.current_task.steps += 1
|
self.current_task.steps += 1
|
||||||
|
self.debugger.after_task_step(self.current_task)
|
||||||
# Data has been sent, reset it to None
|
# Data has been sent, reset it to None
|
||||||
if self.to_send and self.current_task != "init":
|
if self.to_send and self.current_task != "init":
|
||||||
self.to_send = None
|
self.to_send = None
|
||||||
|
@ -136,12 +142,14 @@ class AsyncScheduler:
|
||||||
self.current_task.status = "cancelled"
|
self.current_task.status = "cancelled"
|
||||||
self.current_task.cancelled = True
|
self.current_task.cancelled = True
|
||||||
self.current_task.cancel_pending = False
|
self.current_task.cancel_pending = False
|
||||||
|
self.debugger.after_cancel(self.current_task)
|
||||||
self.join() # TODO: Investigate if a call to join() is needed
|
self.join() # TODO: Investigate if a call to join() is needed
|
||||||
except StopIteration as ret:
|
except StopIteration as ret:
|
||||||
# Coroutine ends
|
# Coroutine ends
|
||||||
self.current_task.status = "end"
|
self.current_task.status = "end"
|
||||||
self.current_task.result = ret.value
|
self.current_task.result = ret.value
|
||||||
self.current_task.finished = True
|
self.current_task.finished = True
|
||||||
|
self.debugger.on_task_exit(self.current_task)
|
||||||
self.join()
|
self.join()
|
||||||
except BaseException as err:
|
except BaseException as err:
|
||||||
self.current_task.exc = err
|
self.current_task.exc = err
|
||||||
|
@ -156,8 +164,10 @@ class AsyncScheduler:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO: Do we need anything else?
|
# TODO: Do we need anything else?
|
||||||
|
self.debugger.before_cancel(self.current_task)
|
||||||
self.current_task.throw(CancelledError)
|
self.current_task.throw(CancelledError)
|
||||||
|
|
||||||
|
|
||||||
def get_running(self):
|
def get_running(self):
|
||||||
"""
|
"""
|
||||||
Returns the current task
|
Returns the current task
|
||||||
|
@ -187,7 +197,11 @@ class AsyncScheduler:
|
||||||
# Sleep until the closest deadline in order not to waste CPU cycles
|
# Sleep until the closest deadline in order not to waste CPU cycles
|
||||||
while self.paused[0][0] < self.clock():
|
while self.paused[0][0] < self.clock():
|
||||||
# Reschedules tasks when their deadline has elapsed
|
# Reschedules tasks when their deadline has elapsed
|
||||||
self.tasks.append(self.paused.get())
|
task = self.paused.get()
|
||||||
|
slept = self.clock() - task.sleep_start
|
||||||
|
task.sleep_start = None
|
||||||
|
self.tasks.append(task)
|
||||||
|
self.debugger.after_sleep(task, slept)
|
||||||
if not self.paused:
|
if not self.paused:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -196,13 +210,16 @@ class AsyncScheduler:
|
||||||
Checks and schedules task to perform I/O
|
Checks and schedules task to perform I/O
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.tasks or self.events: # If there are tasks or events, never wait
|
if self.tasks or self.events and not self.selector.get_map():
|
||||||
|
# If there are either tasks or events and no I/O, never wait
|
||||||
timeout = 0.0
|
timeout = 0.0
|
||||||
elif self.paused: # If there are asleep tasks, wait until the closest
|
elif self.paused:
|
||||||
# deadline
|
# If there are asleep tasks, wait until the closest deadline
|
||||||
timeout = max(0.0, self.paused[0][0] - self.clock())
|
timeout = max(0.0, self.paused[0][0] - self.clock())
|
||||||
else:
|
elif self.selector.get_map():
|
||||||
timeout = None # If we _only_ have I/O to do, then wait indefinitely
|
# If there is *only* I/O, we wait a fixed amount of time
|
||||||
|
timeout = 1 # TODO: Is this ok?
|
||||||
|
self.debugger.before_io(timeout)
|
||||||
for key in dict(self.selector.get_map()).values():
|
for key in dict(self.selector.get_map()).values():
|
||||||
# We make sure we don't reschedule finished tasks
|
# We make sure we don't reschedule finished tasks
|
||||||
if key.data.finished:
|
if key.data.finished:
|
||||||
|
@ -213,6 +230,7 @@ class AsyncScheduler:
|
||||||
# Get sockets that are ready and schedule their tasks
|
# Get sockets that are ready and schedule their tasks
|
||||||
for key, _ in io_ready:
|
for key, _ in io_ready:
|
||||||
self.tasks.append(key.data) # Resource ready? Schedule its task
|
self.tasks.append(key.data) # Resource ready? Schedule its task
|
||||||
|
self.debugger.after_io(timeout)
|
||||||
|
|
||||||
def start(self, func: types.FunctionType, *args):
|
def start(self, func: types.FunctionType, *args):
|
||||||
"""
|
"""
|
||||||
|
@ -221,8 +239,10 @@ class AsyncScheduler:
|
||||||
|
|
||||||
entry = Task(func(*args), func.__name__ or str(func))
|
entry = Task(func(*args), func.__name__ or str(func))
|
||||||
self.tasks.append(entry)
|
self.tasks.append(entry)
|
||||||
|
self.debugger.on_start()
|
||||||
self.run()
|
self.run()
|
||||||
self.has_ran = True
|
self.has_ran = True
|
||||||
|
self.debugger.on_exit()
|
||||||
if entry.exc:
|
if entry.exc:
|
||||||
raise entry.exc from None
|
raise entry.exc from None
|
||||||
|
|
||||||
|
@ -254,8 +274,10 @@ class AsyncScheduler:
|
||||||
Puts the caller to sleep for a given amount of seconds
|
Puts the caller to sleep for a given amount of seconds
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
self.debugger.before_sleep(self.current_task, seconds)
|
||||||
if seconds:
|
if seconds:
|
||||||
self.current_task.status = "sleep"
|
self.current_task.status = "sleep"
|
||||||
|
self.current_task.sleep_start = self.clock()
|
||||||
self.paused.put(self.current_task, seconds)
|
self.paused.put(self.current_task, seconds)
|
||||||
else:
|
else:
|
||||||
self.tasks.append(self.current_task)
|
self.tasks.append(self.current_task)
|
||||||
|
|
|
@ -43,6 +43,7 @@ class Task:
|
||||||
joined: bool= False
|
joined: bool= False
|
||||||
cancel_pending: bool = False
|
cancel_pending: bool = False
|
||||||
waiters: list = field(default_factory=list)
|
waiters: list = field(default_factory=list)
|
||||||
|
sleep_start: int = None
|
||||||
|
|
||||||
def run(self, what=None):
|
def run(self, what=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -22,6 +22,7 @@ from .core import AsyncScheduler
|
||||||
from .exceptions import GiambioError
|
from .exceptions import GiambioError
|
||||||
from .context import TaskManager
|
from .context import TaskManager
|
||||||
from .socket import AsyncSocket
|
from .socket import AsyncSocket
|
||||||
|
from .util.debug import BaseDebugger
|
||||||
from types import FunctionType, CoroutineType, GeneratorType
|
from types import FunctionType, CoroutineType, GeneratorType
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ def get_event_loop():
|
||||||
raise GiambioError("no event loop set") from None
|
raise GiambioError("no event loop set") from None
|
||||||
|
|
||||||
|
|
||||||
def new_event_loop():
|
def new_event_loop(debugger: BaseDebugger):
|
||||||
"""
|
"""
|
||||||
Associates a new event loop to the current thread
|
Associates a new event loop to the current thread
|
||||||
and deactivates the old one. This should not be
|
and deactivates the old one. This should not be
|
||||||
|
@ -50,23 +51,25 @@ def new_event_loop():
|
||||||
try:
|
try:
|
||||||
loop = thread_local.loop
|
loop = thread_local.loop
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
thread_local.loop = AsyncScheduler()
|
thread_local.loop = AsyncScheduler(debugger)
|
||||||
else:
|
else:
|
||||||
if not loop.done():
|
if not loop.done():
|
||||||
raise GiambioError("cannot set event loop while running")
|
raise GiambioError("cannot set event loop while running")
|
||||||
else:
|
else:
|
||||||
thread_local.loop = AsyncScheduler()
|
thread_local.loop = AsyncScheduler(debugger)
|
||||||
|
|
||||||
|
|
||||||
def run(func: FunctionType, *args):
|
def run(func: FunctionType, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Starts the event loop from a synchronous entry point
|
Starts the event loop from a synchronous entry point
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(func, (CoroutineType, GeneratorType)):
|
if isinstance(func, (CoroutineType, GeneratorType)):
|
||||||
raise RuntimeError("Looks like you tried to call giambio.run(your_func(arg1, arg2, ...)), that is wrong!"
|
raise GiambioError("Looks like you tried to call giambio.run(your_func(arg1, arg2, ...)), that is wrong!"
|
||||||
"\nWhat you wanna do, instead, is this: giambio.run(your_func, arg1, arg2, ...)")
|
"\nWhat you wanna do, instead, is this: giambio.run(your_func, arg1, arg2, ...)")
|
||||||
new_event_loop()
|
elif not isinstance(func, FunctionType):
|
||||||
|
raise GiambioError("gaibmio.run() requires an async function as parameter!")
|
||||||
|
new_event_loop(kwargs.get("debugger", None))
|
||||||
thread_local.loop.start(func, *args)
|
thread_local.loop.start(func, *args)
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,15 +79,20 @@ def clock():
|
||||||
loop
|
loop
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return thread_local.loop.clock()
|
try:
|
||||||
|
return thread_local.loop.clock()
|
||||||
|
except AttributeError:
|
||||||
|
raise GiambioError("Cannot call clock from outside an async context") from None
|
||||||
|
|
||||||
|
|
||||||
def wrap_socket(sock: socket.socket) -> AsyncSocket:
|
def wrap_socket(sock: socket.socket) -> AsyncSocket:
|
||||||
"""
|
"""
|
||||||
Wraps a synchronous socket into a giambio.socket.AsyncSocket
|
Wraps a synchronous socket into a giambio.socket.AsyncSocket
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
return thread_local.loop.wrap_socket(sock)
|
return thread_local.loop.wrap_socket(sock)
|
||||||
|
except AttributeError:
|
||||||
|
raise GiambioError("Cannot wrap a socket from outside an async context") from None
|
||||||
|
|
||||||
|
|
||||||
def create_pool():
|
def create_pool():
|
||||||
|
@ -95,5 +103,5 @@ def create_pool():
|
||||||
try:
|
try:
|
||||||
return TaskManager(thread_local.loop)
|
return TaskManager(thread_local.loop)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise RuntimeError("It appears that giambio is not running, did you call giambio.async_pool()"
|
raise GiambioError("It appears that giambio is not running, did you call giambio.create_pool()"
|
||||||
" outside of an async context?") from None
|
" outside of an async context?") from None
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
"""
|
||||||
|
Copyright (C) 2020 nocturn9x
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
|
@ -0,0 +1,197 @@
|
||||||
|
"""
|
||||||
|
Tooling for debugging giambio
|
||||||
|
|
||||||
|
Copyright (C) 2020 nocturn9x
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from giambio.objects import Task
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
|
||||||
|
class BaseDebugger(ABC):
|
||||||
|
"""
|
||||||
|
The base for all debugger objects
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_start(self):
|
||||||
|
"""
|
||||||
|
This method is called when the event
|
||||||
|
loop starts executing
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_exit(self):
|
||||||
|
"""
|
||||||
|
This method is called when the event
|
||||||
|
loop exits entirely (all tasks completed)
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_task_schedule(self, task: Task, delay: Union[int, float]):
|
||||||
|
"""
|
||||||
|
This method is called when a new task is
|
||||||
|
scheduled (not spawned)
|
||||||
|
|
||||||
|
:param task: The Task object representing a
|
||||||
|
giambio Task and wrapping a coroutine
|
||||||
|
:type task: class: giambio.objects.Task
|
||||||
|
:param delay: The delay, in seconds, after which
|
||||||
|
the task will start executing
|
||||||
|
:type delay: int
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_task_spawn(self, task: Task):
|
||||||
|
"""
|
||||||
|
This method is called when a new task is
|
||||||
|
spawned
|
||||||
|
|
||||||
|
:param task: The Task object representing a
|
||||||
|
giambio Task and wrapping a coroutine
|
||||||
|
:type task: class: giambio.objects.Task
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def on_task_exit(self, task: Task):
|
||||||
|
"""
|
||||||
|
This method is called when a task exits
|
||||||
|
|
||||||
|
:param task: The Task object representing a
|
||||||
|
giambio Task and wrapping a coroutine
|
||||||
|
:type task: class: giambio.objects.Task
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def before_task_step(self, task: Task):
|
||||||
|
"""
|
||||||
|
This method is called right before
|
||||||
|
calling its run() method
|
||||||
|
|
||||||
|
:param task: The Task object representing a
|
||||||
|
giambio Task and wrapping a coroutine
|
||||||
|
:type task: class: giambio.objects.Task
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def after_task_step(self, task: Task):
|
||||||
|
"""
|
||||||
|
This method is called right after
|
||||||
|
calling its run() method
|
||||||
|
|
||||||
|
:param task: The Task object representing a
|
||||||
|
giambio Task and wrapping a coroutine
|
||||||
|
:type task: class: giambio.objects.Task
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def before_sleep(self, task: Task, seconds: Union[int, float]):
|
||||||
|
"""
|
||||||
|
This method is called before a task goes
|
||||||
|
to sleep
|
||||||
|
|
||||||
|
:param task: The Task object representing a
|
||||||
|
giambio Task and wrapping a coroutine
|
||||||
|
:type task: class: giambio.objects.Task
|
||||||
|
:param seconds: The amount of seconds the
|
||||||
|
task wants to sleep
|
||||||
|
:type seconds: int
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def after_sleep(self, task: Task, seconds: Union[int, float]):
|
||||||
|
"""
|
||||||
|
This method is called before after a tasks
|
||||||
|
awakes from sleeping
|
||||||
|
|
||||||
|
:param task: The Task object representing a
|
||||||
|
giambio Task and wrapping a coroutine
|
||||||
|
:type task: class: giambio.objects.Task
|
||||||
|
:param seconds: The amount of seconds the
|
||||||
|
task actually slept
|
||||||
|
:type seconds: int
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def before_io(self, timeout: Union[int, float]):
|
||||||
|
"""
|
||||||
|
This method is called right before
|
||||||
|
the event loop checks for I/O events
|
||||||
|
|
||||||
|
:param timeout: The max. amount of seconds
|
||||||
|
that the loop will hang when using the select()
|
||||||
|
system call
|
||||||
|
:type timeout: int
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def after_io(self, timeout: Union[int, float]):
|
||||||
|
"""
|
||||||
|
This method is called right after
|
||||||
|
the event loop has checked for I/O events
|
||||||
|
|
||||||
|
:param timeout: The max. amount of seconds
|
||||||
|
that the loop has hung when using the select()
|
||||||
|
system call
|
||||||
|
:type timeout: int
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def before_cancel(self, task: Task):
|
||||||
|
"""
|
||||||
|
This method is called right before a task
|
||||||
|
gets cancelled
|
||||||
|
|
||||||
|
:param task: The Task object representing a
|
||||||
|
giambio Task and wrapping a coroutine
|
||||||
|
:type task: class: giambio.objects.Task
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def after_cancel(self, task: Task):
|
||||||
|
"""
|
||||||
|
This method is called right after a task
|
||||||
|
gets cancelled
|
||||||
|
|
||||||
|
:param task: The Task object representing a
|
||||||
|
giambio Task and wrapping a coroutine
|
||||||
|
:type task: class: giambio.objects.Task
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NotImplementedError
|
|
@ -0,0 +1,8 @@
|
||||||
|
home = /usr
|
||||||
|
implementation = CPython
|
||||||
|
version_info = 3.7.3.final.0
|
||||||
|
virtualenv = 20.1.0
|
||||||
|
include-system-site-packages = false
|
||||||
|
base-prefix = /usr
|
||||||
|
base-exec-prefix = /usr
|
||||||
|
base-executable = /usr/bin/python3
|
|
@ -1,42 +0,0 @@
|
||||||
import giambio
|
|
||||||
|
|
||||||
|
|
||||||
# A test for context managers
|
|
||||||
|
|
||||||
|
|
||||||
async def countdown(n: int):
|
|
||||||
print(f"Counting down from {n}!")
|
|
||||||
while n > 0:
|
|
||||||
print(f"Down {n}")
|
|
||||||
n -= 1
|
|
||||||
await giambio.sleep(1)
|
|
||||||
# raise Exception("oh no man") # Uncomment to test propagation
|
|
||||||
print("Countdown over")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
async def countup(stop: int, step: int = 1):
|
|
||||||
print(f"Counting up to {stop}!")
|
|
||||||
x = 0
|
|
||||||
while x < stop:
|
|
||||||
print(f"Up {x}")
|
|
||||||
x += 1
|
|
||||||
await giambio.sleep(step)
|
|
||||||
print("Countup over")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
start = giambio.clock()
|
|
||||||
try:
|
|
||||||
async with giambio.create_pool() as pool:
|
|
||||||
pool.spawn(countdown, 10)
|
|
||||||
pool.spawn(countup, 5, 2)
|
|
||||||
print("Children spawned, awaiting completion")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Got -> {type(e).__name__}: {e}")
|
|
||||||
print(f"Task execution complete in {giambio.clock() - start:.2f} seconds")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
giambio.run(main)
|
|
|
@ -21,7 +21,7 @@ async def serve(address: tuple):
|
||||||
conn, addr = await asock.accept()
|
conn, addr = await asock.accept()
|
||||||
logging.info(f"{addr[0]}:{addr[1]} connected")
|
logging.info(f"{addr[0]}:{addr[1]} connected")
|
||||||
pool.spawn(handler, conn, addr)
|
pool.spawn(handler, conn, addr)
|
||||||
print("oof done")
|
|
||||||
|
|
||||||
async def handler(sock: AsyncSocket, addr: tuple):
|
async def handler(sock: AsyncSocket, addr: tuple):
|
||||||
addr = f"{addr[0]}:{addr[1]}"
|
addr = f"{addr[0]}:{addr[1]}"
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
import giambio
|
||||||
|
|
||||||
|
|
||||||
|
class Debugger(giambio.debug.BaseDebugger):
|
||||||
|
"""
|
||||||
|
A simple debugger for this test
|
||||||
|
"""
|
||||||
|
|
||||||
|
def on_start(self):
|
||||||
|
print("## Started running")
|
||||||
|
|
||||||
|
def on_exit(self):
|
||||||
|
print("## Finished running")
|
||||||
|
|
||||||
|
def on_task_schedule(self, task, delay: int):
|
||||||
|
print(f">> A task named '{task.name}' was scheduled to run in {delay:.2f} seconds")
|
||||||
|
|
||||||
|
def on_task_spawn(self, task):
|
||||||
|
print(f">> A task named '{task.name}' was spawned")
|
||||||
|
|
||||||
|
def on_task_exit(self, task):
|
||||||
|
print(f"<< Task '{task.name}' exited")
|
||||||
|
|
||||||
|
def before_task_step(self, task):
|
||||||
|
print(f"-> About to run a step for '{task.name}'")
|
||||||
|
|
||||||
|
def after_task_step(self, task):
|
||||||
|
print(f"<- Ran a step for '{task.name}'")
|
||||||
|
|
||||||
|
def before_sleep(self, task, seconds):
|
||||||
|
print(f"# About to put '{task.name}' to sleep for {seconds:.2f} seconds")
|
||||||
|
|
||||||
|
def after_sleep(self, task, seconds):
|
||||||
|
print(f"# Task '{task.name}' slept for {seconds:.2f} seconds")
|
||||||
|
|
||||||
|
def before_io(self, timeout):
|
||||||
|
print(f"!! About to check for I/O for up to {timeout:.2f} seconds")
|
||||||
|
|
||||||
|
def after_io(self, timeout):
|
||||||
|
print(f"!! Done I/O check (timeout {timeout:.2f} seconds)")
|
||||||
|
|
||||||
|
def before_cancel(self, task):
|
||||||
|
print(f"// About to cancel '{task.name}'")
|
||||||
|
|
||||||
|
def after_cancel(self, task):
|
||||||
|
print(f"// Cancelled '{task.name}'")
|
||||||
|
|
||||||
|
|
||||||
|
async def child():
|
||||||
|
print("[child] Child spawned!! Sleeping for 2 seconds")
|
||||||
|
await giambio.sleep(2)
|
||||||
|
print("[child] Had a nice nap!")
|
||||||
|
|
||||||
|
async def child1():
|
||||||
|
print("[child 1] Child spawned!! Sleeping for 2 seconds")
|
||||||
|
await giambio.sleep(2)
|
||||||
|
print("[child 1] Had a nice nap!")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
start = giambio.clock()
|
||||||
|
try:
|
||||||
|
async with giambio.create_pool() as pool:
|
||||||
|
pool.spawn(child)
|
||||||
|
pool.spawn(child1)
|
||||||
|
print("[main] Children spawned, awaiting completion")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Got -> {type(e).__name__}: {e}")
|
||||||
|
print(f"[main] Children execution complete in {giambio.clock() - start:.2f} seconds")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
giambio.run(main, debugger=Debugger())
|
Loading…
Reference in New Issue