mirror of https://github.com/nocturn9x/giambio.git
Added a small README and divided the lib into multiple files
This commit is contained in:
parent
92835cf0d7
commit
c6e04a5057
25
README.md
25
README.md
|
@ -1,2 +1,27 @@
|
|||
# giambio
|
||||
Giambio: Asynchronous Python made easy (and friendly)
|
||||
|
||||
|
||||
# What's that?
|
||||
|
||||
giambio is a Python framework that implements the aysnchronous mechanism and allow to perform I/O multiplexing and basically do more than one thing at once.
|
||||
This library implements what is known as a stackless mode of execution, or "green threads", though the latter term is misleading as **no multithreading is involved**.
|
||||
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Right now this is no more than a toy implementation to help me understand how Python and async work, but it will (hopefully) become a production ready library soon
|
||||
|
||||
|
||||
## Let's Code!
|
||||
|
||||
Libraries like giambio shine the most when it comes to performing asyncronous I/O (reading a socket, writing to a file, stuff like that).
|
||||
|
||||
The most common example of this is a network server, which just can't be handle one client at a time only.
|
||||
|
||||
One possible approach to achieve concurrency is to use threads, and despite their bad reputation in Python due to the GIL, they actually might be a good choice when it comes to I/O (it is performed out of control of the GIL so that isn't trotthled like CPU-intensive tasks). Problem is, if you have 10000 concurrent connections, you would need to set a limit to the number of threads that can be spawned (you definitely don't want to spawn 10 thousands threads do you?). On top of that, threads are known to be tricky when it comes to synchronization and coordination, so in general you'll hear anyone yelling at you "Don't use threads!". That's when libraries like giambio come in handy!
|
||||
|
||||
A library like giambio implements some low-level primitives and a main loop (known as an **Event Loop**) that acts like a kernel: If a function needs a "system" call (in this case it's I/O) it will trigger a trap and stop, waiting for the event loop to return control and proceed. In the meantime, the loop catches the trap and uses its associated metadata to perform the requested operation. But since I/O 99% of the times implies waiting for a resource to become available, the loop will also run other tasks while it's waiting for that I/O to happen.
|
||||
|
||||
And that's how it works!
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
__author__ = "Nocturn9x aka Isgiambyy"
|
||||
__version__ = (0, 0, 1)
|
||||
from .core import EventLoop, sleep
|
||||
from .core import EventLoop
|
||||
from .exceptions import GiambioError, AlreadyJoinedError, CancelledError
|
||||
from .traps import sleep, join
|
||||
|
||||
__all__ = ["EventLoop", "sleep", "GiambioError", "AlreadyJoinedError", "CancelledError"]
|
||||
|
||||
__all__ = ["EventLoop", "sleep", "join", "GiambioError", "AlreadyJoinedError", "CancelledError"]
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import types
|
||||
|
||||
|
||||
class Result:
|
||||
"""A wrapper for results of coroutines"""
|
||||
|
||||
def __init__(self, val=None, exc: Exception = None):
|
||||
self.val = val
|
||||
self.exc = exc
|
||||
|
||||
def __repr__(self):
|
||||
return f"giambio.core.Result({self.val}, {self.exc})"
|
||||
|
||||
|
||||
class Task:
|
||||
|
||||
"""A simple wrapper around a coroutine object"""
|
||||
|
||||
def __init__(self, coroutine: types.coroutine, loop):
|
||||
self.coroutine = coroutine
|
||||
self.status = False # Not ran yet
|
||||
self.joined = False
|
||||
self.result = None # Updated when the coroutine execution ends
|
||||
self.loop = loop # The EventLoop object that spawned the task
|
||||
self.cancelled = False
|
||||
|
||||
def run(self):
|
||||
self.status = True
|
||||
return self.coroutine.send(None)
|
||||
|
||||
def __repr__(self):
|
||||
return f"giambio.core.Task({self.coroutine}, {self.status}, {self.joined}, {self.result})"
|
||||
|
||||
def throw(self, exception: Exception):
|
||||
self.result = Result(None, exception)
|
||||
return self.coroutine.throw(exception)
|
||||
|
||||
async def cancel(self):
|
||||
return await cancel(self)
|
||||
|
||||
async def join(self):
|
||||
return await join(self)
|
||||
|
||||
def get_result(self):
|
||||
if self.result:
|
||||
if self.result.exc:
|
||||
raise self.result.exc
|
||||
else:
|
||||
return self.result.val
|
||||
|
||||
|
102
giambio/core.py
102
giambio/core.py
|
@ -7,6 +7,8 @@ from .exceptions import AlreadyJoinedError, CancelledError
|
|||
from timeit import default_timer
|
||||
from time import sleep as wait
|
||||
from .socket import AsyncSocket
|
||||
from .traps import join, sleep, want_read, want_write, cancel
|
||||
from .abstractions import Task, Result
|
||||
|
||||
|
||||
class EventLoop:
|
||||
|
@ -114,6 +116,8 @@ class EventLoop:
|
|||
await want_read(sock)
|
||||
return sock.accept()
|
||||
|
||||
|
||||
|
||||
async def sock_sendall(self, sock: socket.socket, data: bytes):
|
||||
"""Sends all the passed data, as bytes, trough the socket asynchronously"""
|
||||
|
||||
|
@ -153,99 +157,9 @@ class EventLoop:
|
|||
|
||||
|
||||
async def connect_sock(self, sock: socket.socket, addr: tuple):
|
||||
await want_write(sock)
|
||||
return sock.connect(addr)
|
||||
try:
|
||||
sock.connect(addr)
|
||||
except BlockingIOError:
|
||||
await want_write(sock)
|
||||
|
||||
|
||||
class Result:
|
||||
"""A wrapper for results of coroutines"""
|
||||
|
||||
def __init__(self, val=None, exc: Exception = None):
|
||||
self.val = val
|
||||
self.exc = exc
|
||||
|
||||
def __repr__(self):
|
||||
return f"giambio.core.Result({self.val}, {self.exc})"
|
||||
|
||||
|
||||
class Task:
|
||||
|
||||
"""A simple wrapper around a coroutine object"""
|
||||
|
||||
def __init__(self, coroutine: types.coroutine, loop: EventLoop):
|
||||
self.coroutine = coroutine
|
||||
self.status = False # Not ran yet
|
||||
self.joined = False
|
||||
self.result = None # Updated when the coroutine execution ends
|
||||
self.loop = loop # The EventLoop object that spawned the task
|
||||
self.cancelled = False
|
||||
|
||||
def run(self):
|
||||
self.status = True
|
||||
return self.coroutine.send(None)
|
||||
|
||||
def __repr__(self):
|
||||
return f"giambio.core.Task({self.coroutine}, {self.status}, {self.joined}, {self.result})"
|
||||
|
||||
def throw(self, exception: Exception):
|
||||
self.result = Result(None, exception)
|
||||
return self.coroutine.throw(exception)
|
||||
|
||||
async def cancel(self):
|
||||
return await cancel(self)
|
||||
|
||||
async def join(self):
|
||||
return await join(self)
|
||||
|
||||
def get_result(self):
|
||||
if self.result:
|
||||
if self.result.exc:
|
||||
raise self.result.exc
|
||||
else:
|
||||
return self.result.val
|
||||
|
||||
|
||||
@types.coroutine
|
||||
def sleep(seconds: int):
|
||||
"""Pause the execution of a coroutine for the passed amount of seconds,
|
||||
without blocking the entire event loop, which keeps watching for other events
|
||||
|
||||
This function is also useful as a sort of checkpoint, because it returns the execution
|
||||
control to the scheduler, which can then switch to another task. If a coroutine does not have
|
||||
enough calls to async methods (or 'checkpoints'), e.g one that needs the 'await' keyword before it, this might
|
||||
affect performance as it would prevent the scheduler from switching tasks properly. If you feel
|
||||
like this happens in your code, try adding a call to giambio.sleep(0); this will act as a checkpoint without
|
||||
actually pausing the execution of your coroutine"""
|
||||
|
||||
yield "want_sleep", seconds
|
||||
|
||||
|
||||
@types.coroutine
|
||||
def want_read(sock: socket.socket):
|
||||
"""'Tells' the event loop that there is some coroutine that wants to read from the passed socket"""
|
||||
|
||||
yield "want_read", sock
|
||||
|
||||
|
||||
@types.coroutine
|
||||
def want_write(sock: socket.socket):
|
||||
"""'Tells' the event loop that there is some coroutine that wants to write into the passed socket"""
|
||||
|
||||
yield "want_write", sock
|
||||
|
||||
|
||||
@types.coroutine
|
||||
def join(task: Task):
|
||||
"""'Tells' the scheduler that the desired task MUST be awaited for completion"""
|
||||
|
||||
if not task.cancelled:
|
||||
task.joined = True
|
||||
yield "want_join", task
|
||||
return task.get_result()
|
||||
|
||||
|
||||
@types.coroutine
|
||||
def cancel(task: Task):
|
||||
"""'Tells' the scheduler that the passed task must be cancelled"""
|
||||
|
||||
yield "want_cancel", task
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
"""Helper methods to interact with the event loop"""
|
||||
|
||||
import types
|
||||
import socket
|
||||
from .abstractions import Task
|
||||
|
||||
|
||||
@types.coroutine
|
||||
def sleep(seconds: int):
|
||||
"""Pause the execution of a coroutine for the passed amount of seconds,
|
||||
without blocking the entire event loop, which keeps watching for other events
|
||||
|
||||
This function is also useful as a sort of checkpoint, because it returns the execution
|
||||
control to the scheduler, which can then switch to another task. If a coroutine does not have
|
||||
enough calls to async methods (or 'checkpoints'), e.g one that needs the 'await' keyword before it, this might
|
||||
affect performance as it would prevent the scheduler from switching tasks properly. If you feel
|
||||
like this happens in your code, try adding a call to giambio.sleep(0); this will act as a checkpoint without
|
||||
actually pausing the execution of your coroutine"""
|
||||
|
||||
yield "want_sleep", seconds
|
||||
|
||||
|
||||
@types.coroutine
|
||||
def want_read(sock: socket.socket):
|
||||
"""'Tells' the event loop that there is some coroutine that wants to read from the passed socket"""
|
||||
|
||||
yield "want_read", sock
|
||||
|
||||
|
||||
@types.coroutine
|
||||
def want_write(sock: socket.socket):
|
||||
"""'Tells' the event loop that there is some coroutine that wants to write into the passed socket"""
|
||||
|
||||
yield "want_write", sock
|
||||
|
||||
|
||||
@types.coroutine
|
||||
def join(task: Task):
|
||||
"""'Tells' the scheduler that the desired task MUST be awaited for completion"""
|
||||
|
||||
task.joined = True
|
||||
yield "want_join", task
|
||||
return task.get_result() # This raises an exception if the child task errored
|
||||
|
||||
|
||||
@types.coroutine
|
||||
def cancel(task: Task):
|
||||
"""'Tells' the scheduler that the passed task must be cancelled"""
|
||||
|
||||
yield "want_cancel", task
|
||||
|
Loading…
Reference in New Issue