First draft of the task system
This commit is contained in:
commit
8926ae0f5c
25
main.odin
Normal file
25
main.odin
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package rune
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
|
||||||
|
test_task :: proc(subtask: int, user_data: rawptr) {
|
||||||
|
fmt.printfln("test task %v", subtask)
|
||||||
|
}
|
||||||
|
|
||||||
|
test_finished :: proc(user_data: rawptr) {
|
||||||
|
fmt.println("test finished!")
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: proc() {
|
||||||
|
init_scheduler(100, 1000)
|
||||||
|
add_worker({.General, .Streaming, .Physics, .Rendering})
|
||||||
|
add_worker({.General, .Streaming, .Physics, .Rendering})
|
||||||
|
add_worker({.General, .Streaming, .Physics, .Rendering})
|
||||||
|
add_worker({.General, .Streaming, .Physics, .Rendering})
|
||||||
|
|
||||||
|
queue_task(test_task, test_finished, nil, 10)
|
||||||
|
|
||||||
|
for true {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
248
task.odin
Normal file
248
task.odin
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
package rune
|
||||||
|
|
||||||
|
import "core:container/queue"
|
||||||
|
import "core:mem"
|
||||||
|
import "core:sync"
|
||||||
|
import "core:thread"
|
||||||
|
|
||||||
|
Task_Error :: enum {
|
||||||
|
None,
|
||||||
|
Too_Many_Tasks,
|
||||||
|
Allocation_Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
Task_Type :: enum {
|
||||||
|
General,
|
||||||
|
Streaming,
|
||||||
|
Rendering,
|
||||||
|
Physics,
|
||||||
|
Audio,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proc called to execute a subtask.
|
||||||
|
Task_Proc :: proc(subtask_idx: int, user_data: rawptr)
|
||||||
|
|
||||||
|
// Called when a task finished executing.
|
||||||
|
//
|
||||||
|
// The idea is that this is the place where subsequent tasks are enqueued.
|
||||||
|
// This way, the user can control dependencies, but the scheduler does not
|
||||||
|
// need to worry about un-runnable tasks, simplifying it (and hopefully improving performance)
|
||||||
|
Task_Finished_Proc :: proc(user_data: rawptr)
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
Task :: struct {
|
||||||
|
entry: Task_Proc,
|
||||||
|
finished: Task_Finished_Proc,
|
||||||
|
user_data: rawptr,
|
||||||
|
remaining_subtasks: int,
|
||||||
|
type: Task_Type,
|
||||||
|
next_free: int,
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
Sub_Task :: struct {
|
||||||
|
task: int, // index into task list
|
||||||
|
idx: int, // this subtasks index
|
||||||
|
next_free: int,
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
Worker :: struct {
|
||||||
|
run: bool,
|
||||||
|
task_types: bit_set[Task_Type],
|
||||||
|
thread: ^thread.Thread,
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
Scheduler :: struct {
|
||||||
|
// Order of subtasks to execute. Highest priority is at index 0
|
||||||
|
schedule_storage: []int,
|
||||||
|
schedule: queue.Queue(int),
|
||||||
|
|
||||||
|
// List of tasks. We don't move these to avoid expensive
|
||||||
|
// copies when the schedule changes.
|
||||||
|
tasks: []Task,
|
||||||
|
subtasks: []Sub_Task,
|
||||||
|
first_free_task: int,
|
||||||
|
first_free_subtask: int,
|
||||||
|
free_subtasks: int,
|
||||||
|
|
||||||
|
// Used when adding or removing tasks from the list,
|
||||||
|
// otherwise tasks are read-only (except for the remaining subtask counter, which is atomic)
|
||||||
|
task_list_mutex: sync.Mutex,
|
||||||
|
subtask_list_mutex: sync.Mutex,
|
||||||
|
schedule_mutex: sync.Mutex,
|
||||||
|
|
||||||
|
// List of workers. We allow the user to dynamically add or remove workers.
|
||||||
|
workers: [dynamic]^Worker,
|
||||||
|
workers_mutex: sync.Mutex,
|
||||||
|
allocator: mem.Allocator,
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
_scheduler: Scheduler
|
||||||
|
|
||||||
|
init_scheduler :: proc(
|
||||||
|
max_tasks: int,
|
||||||
|
max_subtasks_per_task: int,
|
||||||
|
allocator := context.allocator,
|
||||||
|
) {
|
||||||
|
_scheduler.schedule_storage = make([]int, max_tasks * max_subtasks_per_task, allocator)
|
||||||
|
queue.init_from_slice(&_scheduler.schedule, _scheduler.schedule_storage)
|
||||||
|
_scheduler.tasks = make([]Task, max_tasks, allocator)
|
||||||
|
_scheduler.subtasks = make([]Sub_Task, max_tasks * max_subtasks_per_task, allocator)
|
||||||
|
_scheduler.first_free_task = 0
|
||||||
|
for i := 0; i < max_tasks; i += 1 {
|
||||||
|
_scheduler.tasks[i].next_free = i + 1
|
||||||
|
}
|
||||||
|
_scheduler.first_free_subtask = 0
|
||||||
|
for i := 0; i < max_subtasks_per_task * max_tasks; i += 1 {
|
||||||
|
_scheduler.subtasks[i].next_free = i + 1
|
||||||
|
}
|
||||||
|
_scheduler.free_subtasks = max_subtasks_per_task * max_tasks
|
||||||
|
_scheduler.workers = make([dynamic]^Worker, allocator)
|
||||||
|
_scheduler.allocator = allocator
|
||||||
|
}
|
||||||
|
|
||||||
|
add_worker :: proc(task_types: bit_set[Task_Type]) -> (int, Task_Error) {
|
||||||
|
sync.lock(&_scheduler.workers_mutex)
|
||||||
|
defer sync.unlock(&_scheduler.workers_mutex)
|
||||||
|
if d := new(Worker, _scheduler.allocator); d != nil {
|
||||||
|
d.task_types = task_types
|
||||||
|
d.run = true
|
||||||
|
if d.thread = thread.create(worker_proc); d == nil {
|
||||||
|
free(d)
|
||||||
|
return -1, .Allocation_Failed
|
||||||
|
}
|
||||||
|
d.thread.data = d
|
||||||
|
_, err := append(&_scheduler.workers, d)
|
||||||
|
if err != nil {
|
||||||
|
thread.destroy(d.thread)
|
||||||
|
free(d)
|
||||||
|
return -1, .Allocation_Failed
|
||||||
|
}
|
||||||
|
thread.start(d.thread)
|
||||||
|
return len(_scheduler.workers) - 1, .None
|
||||||
|
}
|
||||||
|
return -1, .Allocation_Failed
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_worker :: proc(idx: int) {
|
||||||
|
sync.lock(&_scheduler.workers_mutex)
|
||||||
|
defer sync.unlock(&_scheduler.workers_mutex)
|
||||||
|
if idx >= len(_scheduler.workers) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_scheduler.workers[idx].run = false
|
||||||
|
thread.destroy(_scheduler.workers[idx].thread)
|
||||||
|
free(_scheduler.workers[idx])
|
||||||
|
unordered_remove(&_scheduler.workers, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
queue_task :: proc(
|
||||||
|
entry: Task_Proc,
|
||||||
|
finished: Task_Finished_Proc,
|
||||||
|
user_data: rawptr,
|
||||||
|
subtask_count: int,
|
||||||
|
type := Task_Type.General,
|
||||||
|
) -> Task_Error {
|
||||||
|
// Use the first free slot to store the task
|
||||||
|
sync.lock(&_scheduler.task_list_mutex)
|
||||||
|
slot := _scheduler.first_free_task
|
||||||
|
if slot >= len(_scheduler.tasks) {
|
||||||
|
sync.unlock(&_scheduler.task_list_mutex)
|
||||||
|
return .Too_Many_Tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
task := &_scheduler.tasks[slot]
|
||||||
|
_scheduler.first_free_task = task.next_free
|
||||||
|
task.entry = entry
|
||||||
|
task.finished = finished
|
||||||
|
task.user_data = user_data
|
||||||
|
task.next_free = -1
|
||||||
|
task.remaining_subtasks = subtask_count
|
||||||
|
task.type = type
|
||||||
|
sync.unlock(&_scheduler.task_list_mutex)
|
||||||
|
|
||||||
|
// Create the subtasks
|
||||||
|
sync.lock(&_scheduler.subtask_list_mutex)
|
||||||
|
if _scheduler.free_subtasks < subtask_count {
|
||||||
|
sync.unlock(&_scheduler.subtask_list_mutex)
|
||||||
|
|
||||||
|
sync.lock(&_scheduler.task_list_mutex)
|
||||||
|
// Nevermind, release the task again
|
||||||
|
task.next_free = _scheduler.first_free_task
|
||||||
|
_scheduler.first_free_task = slot
|
||||||
|
sync.unlock(&_scheduler.task_list_mutex)
|
||||||
|
return .Too_Many_Tasks
|
||||||
|
}
|
||||||
|
_scheduler.free_subtasks -= subtask_count
|
||||||
|
for i := 0; i < subtask_count; i += 1 {
|
||||||
|
subtask_slot := _scheduler.first_free_subtask
|
||||||
|
assert(subtask_slot < len(_scheduler.subtasks))
|
||||||
|
subtask := &_scheduler.subtasks[subtask_slot]
|
||||||
|
_scheduler.first_free_subtask = subtask.next_free
|
||||||
|
subtask.next_free = -1
|
||||||
|
subtask.task = slot
|
||||||
|
subtask.idx = i
|
||||||
|
|
||||||
|
sync.lock(&_scheduler.schedule_mutex)
|
||||||
|
// Add to schedule. This is FIFO. We could be more clever (for example use shortest time to finish)
|
||||||
|
queue.push_back(&_scheduler.schedule, subtask_slot)
|
||||||
|
sync.unlock(&_scheduler.schedule_mutex)
|
||||||
|
}
|
||||||
|
sync.unlock(&_scheduler.subtask_list_mutex)
|
||||||
|
|
||||||
|
|
||||||
|
return .None
|
||||||
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
|
worker_proc :: proc(t: ^thread.Thread) {
|
||||||
|
worker := (^Worker)(t.data)
|
||||||
|
task_types := worker.task_types
|
||||||
|
for worker.run {
|
||||||
|
sync.lock(&_scheduler.schedule_mutex)
|
||||||
|
subtask_idx := -1
|
||||||
|
if queue.len(_scheduler.schedule) > 0 {
|
||||||
|
subtask_idx = queue.pop_front(&_scheduler.schedule)
|
||||||
|
}
|
||||||
|
sync.unlock(&_scheduler.schedule_mutex)
|
||||||
|
if subtask_idx == -1 {
|
||||||
|
// NO tasks available
|
||||||
|
thread.yield()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
subtask := &_scheduler.subtasks[subtask_idx]
|
||||||
|
taskidx := subtask.task
|
||||||
|
task := &_scheduler.tasks[taskidx]
|
||||||
|
|
||||||
|
if task.type in task_types {
|
||||||
|
task.entry(subtask.idx, task.user_data)
|
||||||
|
|
||||||
|
sync.lock(&_scheduler.subtask_list_mutex)
|
||||||
|
subtask.next_free = _scheduler.first_free_subtask
|
||||||
|
_scheduler.first_free_subtask = subtask_idx
|
||||||
|
_scheduler.free_subtasks += 1
|
||||||
|
sync.unlock(&_scheduler.subtask_list_mutex)
|
||||||
|
|
||||||
|
prev_cnt := sync.atomic_sub(&task.remaining_subtasks, 1)
|
||||||
|
if prev_cnt == 1 {
|
||||||
|
// Finished the task,
|
||||||
|
task.finished(task.user_data)
|
||||||
|
|
||||||
|
sync.lock(&_scheduler.task_list_mutex)
|
||||||
|
task.next_free = _scheduler.first_free_task
|
||||||
|
_scheduler.first_free_task = taskidx
|
||||||
|
sync.unlock(&_scheduler.task_list_mutex)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Push back in front, let someone else pick the task up
|
||||||
|
sync.lock(&_scheduler.schedule_mutex)
|
||||||
|
queue.push_front(&_scheduler.schedule, subtask_idx)
|
||||||
|
sync.unlock(&_scheduler.schedule_mutex)
|
||||||
|
thread.yield()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user