This package contains a group of different data structures I use to develop my odin apps and games.
The most important is event-loop.
This is my custom implementation of event loop lightly inspired by node.js.
This provides a ready to use base for event-driven applications.
Be aware that event-loop implementation is naive and may not meet more sophisticated requirements.
This implementation contains many shortcuts that make implementation trivial but makes sacrifices in other areas like edge-case performance.
---
title: Event-Loop Architecture
---
graph TD;
classDef Operation fill:blue,color:white
classDef Queue fill:yellow,color:black
classDef Process fill:white,color:black
classDef Finish fill:green,color:black
classDef Executor fill:orange,color:black
classDef TaskResult fill:aqua,color:black
pushTasks["pushTasks()"]:::Operation
popResults["popResults()"]:::Operation
flush["flush()"]:::Operation
task["->task()"]:::Operation
microTask["->microTask()"]:::Operation
result["->result()"]:::Operation
unSchedule["->unSchedule()"]:::Operation
taskExecutor[Task Executor]:::Executor
microTaskExecutor[Micro Task Executor]:::Executor
taskQueue[Task Queue]:::Queue
microTaskQueue[Micro Task Queue]:::Queue
scheduledQueue[Priority Queue]:::Queue
resultQueue[Result Queue]:::Queue
Finish:::Finish
TaskFinish["Finish"]:::Finish
TaskStart["Start"]:::Finish
processScheduledTask["Process Scheduled Task"]:::Process
processTask["Process Task"]:::Process
passResultsToQueue["Pass Results To Queue"]:::Process
passScheduledTasksToQueue["Pass Scheduled Tasks to Queue"]:::Process
scheduleTask["Schedule Task"]:::Process
pushMicroTasks["Push Micro Tasks"]:::Process
nextScheduledTaskPresent@{ shape: diamond, label: "Next scheduled present?" }
isInterval@{ shape: diamond, label: "Is Interval?" }
nextTaskPresent@{ shape: diamond, label: "Next task present?" }
nextMicroTaskPresent@{ shape: diamond, label: "Next micro present?" }
pushTasks --> taskQueue
resultQueue --> popResults
flush --> nextTaskPresent
nextTaskPresent -->|yes| processTask
processTask --> nextTaskPresent
nextTaskPresent -->|no| nextScheduledTaskPresent
nextScheduledTaskPresent -->|yes| isInterval
isInterval -->|yes| scheduleTask
scheduleTask --> processScheduledTask
isInterval -->|no| processScheduledTask
processScheduledTask --> nextScheduledTaskPresent
nextScheduledTaskPresent -->|no| Finish
processScheduledTask -.- ProcessTask
processTask -.- ProcessTask
nextTaskPresent -.- taskQueue
passResultsToQueue -.- resultQueue
passScheduledTasksToQueue -.- scheduledQueue
nextScheduledTaskPresent -.- scheduledQueue
nextMicroTaskPresent -.- microTaskQueue
pushMicroTasks -.- microTaskQueue
scheduleTask -.- scheduledQueue
taskResult:::TaskResult
unSchedule --> scheduledQueue
subgraph TaskResult
task --> taskResult
result --> taskResult
end
subgraph ProcessTask
taskExecutor --> pushMicroTasks
unSchedule --> taskResult
microTask -.- taskExecutor
unSchedule -.- taskExecutor
result -.- taskExecutor
task -.- taskExecutor
microTask -.- microTaskQueue
TaskStart --> taskExecutor
pushMicroTasks --> nextMicroTaskPresent
nextMicroTaskPresent -->|yes| microTaskExecutor
nextMicroTaskPresent -->|no| passResultsToQueue
passResultsToQueue --> passScheduledTasksToQueue
passResultsToQueue -.- taskResult
passScheduledTasksToQueue --> TaskFinish
passScheduledTasksToQueue -.- taskResult
microTaskExecutor --> pushMicroTasks
end
This single loop should always run using single thread BUT:
- you can
pushTasks()using different thread - you can
popResults()using different thread - you MUST use mutex if you want to
pushTasks()from multiple threads - you MUST use mutex if you want to
popResults()from multiple threads
Event loop runs inside a single thread but it can execute multithreading work.
For example using this package allows to schedule jobs to be executed on separate threads, using syntax similar to javascript Promise.all.
EventLoop can be defined with different QueueType.
- SPSC_LOCK_FREE - queue with static capacity, as name suggests lock free, but can return error if capacity was exceeded, you can ignore error but item will not be put there. This is user responsibility when using pushTasks to throttle when queue is fullfilled. This is also user responsibility to popResults on time before queue will be filled.
- SPSC_MUTEX - dynamic queue with mutex, save an reliable but not as fast.
->task()- Scheduling New Tasks- TIMEOUT - single execution delayed by duration
- if duration is equal to 0 - executes task after all current tasks inside current flush
- if duration is greater than 0 - task will not be executed in current flush
- INTERVAL - multiple executions delayed by duration
- duration must be greater than 0 - tasks will be executed until unScheduled
- At the end of task execution scheduled tasks and results are commited to the appriopriate queues.
- TIMEOUT - single execution delayed by duration
->unSchedule()UnSchedule Scheduled Tasks->task()procedure returnsReferenceIdReferenceIdallows to unSchedule scheduled tasks
->microTask()Schedule Micro Task- micro task is added to the end of microtask queue, always executes within the same task execution
->result()Add Result- use this to add results to the result queue, you can then read results using
popResults()function
- use this to add results to the result queue, you can then read results using
- Event loop current time changes once per flush.
- In theory if different thread constantly adds tasks during flush, flush may never end.
Please look at event loop tests, my game architecture looks basically the same except I have more detailed error handling and task execution is splitted through many modules rather than having everything in single procedure.
All contributions, bug reports, pull requests, feature requests etc. are more than welcome!