mirror of https://github.com/nocturn9x/giambio.git
Small changes, cancellation needs a fix
This commit is contained in:
parent
fbee6c6f96
commit
1676f3149b
|
@ -33,7 +33,7 @@ giambio has been designed with simplicity in mind, so this README won't go deep
|
||||||
|
|
||||||
Just to clarify things, giambio does not avoid the Global Interpreter Lock nor it performs any sort of multithreading or multiprocessing (at least by default). Remember that **concurrency is not parallelism**, concurrent tasks will switch back and forth and proceed with their calculations but won't be running independently like they would do if they were forked off to a process pool. That's why it is called concurrency, because multiple tasks **concur** for the same amount of resources. (Which is basically the same thing that happens inside your CPU at a much lower level, because processors run many more tasks than their actual number of cores)
|
Just to clarify things, giambio does not avoid the Global Interpreter Lock nor it performs any sort of multithreading or multiprocessing (at least by default). Remember that **concurrency is not parallelism**, concurrent tasks will switch back and forth and proceed with their calculations but won't be running independently like they would do if they were forked off to a process pool. That's why it is called concurrency, because multiple tasks **concur** for the same amount of resources. (Which is basically the same thing that happens inside your CPU at a much lower level, because processors run many more tasks than their actual number of cores)
|
||||||
|
|
||||||
If you read carefully, you might now wonder: _"If a coroutine can call other coroutines, but synchronous functions cannot, how do I enter the async context in the first place?"_. This is done trough a special **synchronous function** (the `start()` method of an `EventLoop` object in our case) which can call asynchronous ones, that **must** be called from a synchronous context to avoid a horrible *deadlock*.
|
If you read carefully, you might now wonder: _"If a coroutine can call other coroutines, but synchronous functions cannot, how do I enter the async context in the first place?"_. This is done trough a special **synchronous function** (the `start` method of an `AsyncScheduler` object in our case) which can call asynchronous ones, that **must** be called from a synchronous context to avoid a horrible *deadlock*.
|
||||||
|
|
||||||
## Let's code
|
## Let's code
|
||||||
|
|
||||||
|
@ -80,9 +80,8 @@ async def echo_handler(sock: AsyncSocket, addr: tuple):
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sched.create_task(server(('', 25000)))
|
|
||||||
try:
|
try:
|
||||||
sched.run()
|
sched.start(server(('', 25000)))
|
||||||
except KeyboardInterrupt: # Exceptions propagate!
|
except KeyboardInterrupt: # Exceptions propagate!
|
||||||
print("Exiting...")
|
print("Exiting...")
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
"""
|
"""
|
||||||
Copyright (C) 2020 nocturn9x
|
Copyright (C) 2020 nocturn9x
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Import libraries and internal resources
|
# Import libraries and internal resources
|
||||||
|
@ -68,30 +68,28 @@ class AsyncScheduler:
|
||||||
if not self.paused:
|
if not self.paused:
|
||||||
break
|
break
|
||||||
timeout = 0.0 if self.tasks else None # If there are no tasks ready wait indefinitely
|
timeout = 0.0 if self.tasks else None # If there are no tasks ready wait indefinitely
|
||||||
tasks = self.selector.select(timeout) # Get sockets that are ready and schedule their tasks
|
io_ready = self.selector.select(timeout) # Get sockets that are ready and schedule their tasks
|
||||||
for key, _ in tasks:
|
for key, _ in io_ready:
|
||||||
self.tasks.append(key.data) # Socket ready? Schedule the task
|
self.tasks.append(key.data) # Socket ready? Schedule the task
|
||||||
self.selector.unregister(
|
self.selector.unregister(
|
||||||
key.fileobj) # Once (re)scheduled, the task does not need to perform I/O multiplexing (for now)
|
key.fileobj) # Once (re)scheduled, the task does not need to perform I/O multiplexing (for now)
|
||||||
while self.tasks: # While there are tasks to run
|
while self.tasks: # While there are tasks to run
|
||||||
self.current_task = self.tasks.popleft() # Sets the currently running task
|
self.current_task = self.tasks.popleft() # Sets the currently running task
|
||||||
|
self.current_task.status = "run"
|
||||||
try:
|
try:
|
||||||
method, *args = self.current_task.run() # Run a single step with the calculation
|
method, *args = self.current_task.run() # Run a single step with the calculation
|
||||||
getattr(self, method)(*args) # Sneaky method call, thanks to David Beazley for this ;)
|
getattr(self, method)(*args) # Sneaky method call, thanks to David Beazley for this ;)
|
||||||
except CancelledError as cancelled: # Coroutine was cancelled
|
except CancelledError as cancelled: # Coroutine was cancelled
|
||||||
task = cancelled.args[0]
|
task = cancelled.args[0]
|
||||||
task.cancelled = True
|
task.cancelled = True
|
||||||
self.reschedule_parent()
|
self.tasks.remove(task)
|
||||||
self.tasks.append(self.current_task)
|
|
||||||
except RuntimeError:
|
|
||||||
self.reschedule_parent()
|
|
||||||
except StopIteration as e: # Coroutine ends
|
except StopIteration as e: # Coroutine ends
|
||||||
self.current_task.result = e.args[0] if e.args else None
|
self.current_task.result = e.args[0] if e.args else None
|
||||||
self.current_task.finished = True
|
self.current_task.finished = True
|
||||||
self.reschedule_parent()
|
self.reschedule_parent(self.current_task)
|
||||||
except Exception as error: # Coroutine raised
|
except BaseException as error: # Coroutine raised
|
||||||
self.current_task.exc = error
|
self.current_task.exc = error
|
||||||
self.reschedule_parent()
|
self.reschedule_parent(self.current_task)
|
||||||
raise # Maybe find a better way to propagate errors?
|
raise # Maybe find a better way to propagate errors?
|
||||||
|
|
||||||
|
|
||||||
|
@ -109,12 +107,13 @@ class AsyncScheduler:
|
||||||
self.create_task(coro)
|
self.create_task(coro)
|
||||||
self.run()
|
self.run()
|
||||||
|
|
||||||
def reschedule_parent(self):
|
def reschedule_parent(self, coro):
|
||||||
"""Reschedules the parent task"""
|
"""Reschedules the parent task"""
|
||||||
|
|
||||||
popped = self.joined.pop(self.current_task, None)
|
popped = self.joined.pop(coro, None)
|
||||||
if popped:
|
if popped:
|
||||||
self.tasks.append(popped)
|
self.tasks.append(popped)
|
||||||
|
return popped
|
||||||
|
|
||||||
def want_read(self, sock: socket.socket):
|
def want_read(self, sock: socket.socket):
|
||||||
"""Handler for the 'want_read' event, registers the socket inside the selector to perform I/0 multiplexing"""
|
"""Handler for the 'want_read' event, registers the socket inside the selector to perform I/0 multiplexing"""
|
||||||
|
@ -139,14 +138,14 @@ class AsyncScheduler:
|
||||||
if busy:
|
if busy:
|
||||||
raise ResourceBusy("The given resource is busy!")
|
raise ResourceBusy("The given resource is busy!")
|
||||||
|
|
||||||
def join(self, coro: types.coroutine):
|
def join(self, child: types.coroutine):
|
||||||
"""Handler for the 'join' event, does some magic to tell the scheduler
|
"""Handler for the 'join' event, does some magic to tell the scheduler
|
||||||
to wait until the passed coroutine ends. The result of this call equals whatever the
|
to wait until the passed coroutine ends. The result of this call equals whatever the
|
||||||
coroutine returns or, if an exception gets raised, the exception will get propagated inside the
|
coroutine returns or, if an exception gets raised, the exception will get propagated inside the
|
||||||
parent task"""
|
parent task"""
|
||||||
|
|
||||||
if coro not in self.joined:
|
if child not in self.joined:
|
||||||
self.joined[coro] = self.current_task
|
self.joined[child] = self.current_task
|
||||||
else:
|
else:
|
||||||
raise AlreadyJoinedError("Joining the same task multiple times is not allowed!")
|
raise AlreadyJoinedError("Joining the same task multiple times is not allowed!")
|
||||||
|
|
||||||
|
@ -154,6 +153,7 @@ 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.sequence += 1
|
self.sequence += 1
|
||||||
|
self.current_task.status = "sleep"
|
||||||
heappush(self.paused, (self.clock() + seconds, self.sequence, self.current_task))
|
heappush(self.paused, (self.clock() + seconds, self.sequence, self.current_task))
|
||||||
|
|
||||||
def cancel(self, task):
|
def cancel(self, task):
|
||||||
|
@ -161,6 +161,7 @@ class AsyncScheduler:
|
||||||
in order to stop it from executing. The loop continues to execute as tasks
|
in order to stop it from executing. The loop continues to execute as tasks
|
||||||
are independent"""
|
are independent"""
|
||||||
|
|
||||||
|
self.reschedule_parent(task)
|
||||||
task.throw(CancelledError(task))
|
task.throw(CancelledError(task))
|
||||||
|
|
||||||
def wrap_socket(self, sock):
|
def wrap_socket(self, sock):
|
||||||
|
|
|
@ -28,6 +28,7 @@ class Task:
|
||||||
self.exc = None
|
self.exc = None
|
||||||
self.result = None
|
self.result = None
|
||||||
self.finished = False
|
self.finished = False
|
||||||
|
self.status = "init"
|
||||||
|
|
||||||
def run(self, what=None):
|
def run(self, what=None):
|
||||||
"""Simple abstraction layer over the coroutines ``send`` method"""
|
"""Simple abstraction layer over the coroutines ``send`` method"""
|
||||||
|
@ -52,4 +53,4 @@ class Task:
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""Implements repr(self)"""
|
"""Implements repr(self)"""
|
||||||
|
|
||||||
return f"Task({self.coroutine}, cancelled={self.cancelled}, exc={repr(self.exc)}, result={self.result}, finished={self.finished})"
|
return f"Task({self.coroutine}, cancelled={self.cancelled}, exc={repr(self.exc)}, result={self.result}, finished={self.finished}, status={self.status})"
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
from giambio import AsyncScheduler, sleep
|
import giambio
|
||||||
|
|
||||||
|
|
||||||
async def countdown(n: int):
|
async def countdown(n: int):
|
||||||
while n > 0:
|
while n > 0:
|
||||||
print(f"Down {n}")
|
print(f"Down {n}")
|
||||||
n -= 1
|
n -= 1
|
||||||
await sleep(1)
|
await giambio.sleep(1)
|
||||||
print("Countdown over")
|
print("Countdown over")
|
||||||
|
|
||||||
|
|
||||||
async def countup(stop, step: int or float = 1):
|
async def countup(stop: int, step: int = 1):
|
||||||
x = 0
|
x = 0
|
||||||
while x < stop:
|
while x < stop:
|
||||||
print(f"Up {x}")
|
print(f"Up {x}")
|
||||||
x += 1
|
x += 1
|
||||||
await sleep(step)
|
await giambio.sleep(step)
|
||||||
print("Countup over")
|
print("Countup over")
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,14 +22,14 @@ async def main():
|
||||||
cdown = scheduler.create_task(countdown(10))
|
cdown = scheduler.create_task(countdown(10))
|
||||||
cup = scheduler.create_task(countup(5, 2))
|
cup = scheduler.create_task(countup(5, 2))
|
||||||
print("Counters started, awaiting completion")
|
print("Counters started, awaiting completion")
|
||||||
await sleep(2)
|
await giambio.sleep(2)
|
||||||
print("Slept 1 second, killing countdown")
|
print("Slept 2 seconds, killing countup")
|
||||||
await cdown.cancel()
|
await cup.cancel() ## DOES NOT WORK!!!
|
||||||
await cup.join()
|
await cup.join()
|
||||||
await cdown.join()
|
await cdown.join()
|
||||||
print("Task execution complete")
|
print("Task execution complete")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
scheduler = AsyncScheduler()
|
scheduler = giambio.AsyncScheduler()
|
||||||
scheduler.start(main())
|
scheduler.start(main())
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue