Skip to content

Commit 074a493

Browse files
committed
runtime: move scheduler code around
This moves all scheduler code into a separate file that is only compiled when there's a scheduler in use (the tasks or asyncify scheduler, which are both cooperative). The main goal of this change is to make it easier to add a new "scheduler" based on OS threads. It also fixes a few subtle issues with `-gc=none`: - Gosched() panicked. This is now fixed to just return immediately (the only logical thing to do when there's only one goroutine). - Timers aren't supported without a scheduler, but the relevant code was still present and would happily add a timer to the queue. It just never ran. So now it exits with a runtime error, similar to any blocking operation.
1 parent b8420e7 commit 074a493

12 files changed

+313
-277
lines changed

src/internal/task/task.go

+3
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ func getGoroutineStackSize(fn uintptr) uintptr
3333

3434
//go:linkname runtime_alloc runtime.alloc
3535
func runtime_alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer
36+
37+
//go:linkname scheduleTask runtime.scheduleTask
38+
func scheduleTask(*Task)

src/internal/task/task_asyncify.go

+1-4
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ type stackState struct {
5252
func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
5353
t := &Task{}
5454
t.state.initialize(fn, args, stackSize)
55-
runqueuePushBack(t)
55+
scheduleTask(t)
5656
}
5757

5858
//export tinygo_launch
@@ -82,9 +82,6 @@ func (s *state) initialize(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
8282
s.csp = unsafe.Add(stack, stackSize)
8383
}
8484

85-
//go:linkname runqueuePushBack runtime.runqueuePushBack
86-
func runqueuePushBack(*Task)
87-
8885
// currentTask is the current running task, or nil if currently in the scheduler.
8986
var currentTask *Task
9087

src/internal/task/task_stack.go

+1-4
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,12 @@ func swapTask(oldStack uintptr, newStack *uintptr)
101101
//go:extern tinygo_startTask
102102
var startTask [0]uint8
103103

104-
//go:linkname runqueuePushBack runtime.runqueuePushBack
105-
func runqueuePushBack(*Task)
106-
107104
// start creates and starts a new goroutine with the given function and arguments.
108105
// The new goroutine is scheduled to run later.
109106
func start(fn uintptr, args unsafe.Pointer, stackSize uintptr) {
110107
t := &Task{}
111108
t.state.initialize(fn, args, stackSize)
112-
runqueuePushBack(t)
109+
scheduleTask(t)
113110
}
114111

115112
// OnSystemStack returns whether the caller is running on the system stack.

src/runtime/arch_tinygoriscv.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ func procUnpin() {
3636

3737
func waitForEvents() {
3838
mask := riscv.DisableInterrupts()
39-
if !runqueue.Empty() {
39+
runqueue := schedulerRunQueue()
40+
if !(runqueue != nil && runqueue.Empty()) {
4041
riscv.Asm("wfi")
4142
}
4243
riscv.EnableInterrupts(mask)

src/runtime/chan.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,7 @@ func (ch *channel) resumeRX(ok bool) unsafe.Pointer {
183183
b.detach()
184184
}
185185

186-
// push task onto runqueue
187-
runqueue.Push(b.t)
186+
scheduleTask(b.t)
188187

189188
return dst
190189
}
@@ -210,8 +209,7 @@ func (ch *channel) resumeTX() unsafe.Pointer {
210209
b.detach()
211210
}
212211

213-
// push task onto runqueue
214-
runqueue.Push(b.t)
212+
scheduleTask(b.t)
215213

216214
return src
217215
}

src/runtime/cond.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func (c *Cond) Notify() bool {
3434
default:
3535
// Unblock the waiting task.
3636
if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.t)), unsafe.Pointer(t), nil) {
37-
runqueuePushBack(t)
37+
scheduleTask(t)
3838
return true
3939
}
4040
}

src/runtime/gc_blocks.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -430,12 +430,13 @@ func runGC() (freeBytes uintptr) {
430430
// Therefore we need to scan the runqueue separately.
431431
var markedTaskQueue task.Queue
432432
runqueueScan:
433+
runqueue := schedulerRunQueue()
433434
for !runqueue.Empty() {
434435
// Pop the next task off of the runqueue.
435436
t := runqueue.Pop()
436437

437438
// Mark the task if it has not already been marked.
438-
markRoot(uintptr(unsafe.Pointer(&runqueue)), uintptr(unsafe.Pointer(t)))
439+
markRoot(uintptr(unsafe.Pointer(runqueue)), uintptr(unsafe.Pointer(t)))
439440

440441
// Push the task onto our temporary queue.
441442
markedTaskQueue.Push(t)
@@ -450,7 +451,7 @@ func runGC() (freeBytes uintptr) {
450451
interrupt.Restore(i)
451452
goto runqueueScan
452453
}
453-
runqueue = markedTaskQueue
454+
*runqueue = markedTaskQueue
454455
interrupt.Restore(i)
455456
} else {
456457
finishMark()

src/runtime/scheduler.go

+2-219
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,9 @@
11
package runtime
22

3-
// This file implements the TinyGo scheduler. This scheduler is a very simple
4-
// cooperative round robin scheduler, with a runqueue that contains a linked
5-
// list of goroutines (tasks) that should be run next, in order of when they
6-
// were added to the queue (first-in, first-out). It also contains a sleep queue
7-
// with sleeping goroutines in order of when they should be re-activated.
8-
//
9-
// The scheduler is used both for the asyncify based scheduler and for the task
10-
// based scheduler. In both cases, the 'internal/task.Task' type is used to represent one
11-
// goroutine.
12-
13-
import (
14-
"internal/task"
15-
"runtime/interrupt"
16-
)
3+
import "internal/task"
174

185
const schedulerDebug = false
196

20-
// On JavaScript, we can't do a blocking sleep. Instead we have to return and
21-
// queue a new scheduler invocation using setTimeout.
22-
const asyncScheduler = GOOS == "js"
23-
24-
var schedulerDone bool
25-
26-
// Queues used by the scheduler.
27-
var (
28-
runqueue task.Queue
29-
sleepQueue *task.Task
30-
sleepQueueBaseTime timeUnit
31-
timerQueue *timerNode
32-
)
33-
347
// Simple logging, for debugging.
358
func scheduleLog(msg string) {
369
if schedulerDebug {
@@ -52,202 +25,12 @@ func scheduleLogChan(msg string, ch *channel, t *task.Task) {
5225
}
5326
}
5427

55-
// deadlock is called when a goroutine cannot proceed any more, but is in theory
56-
// not exited (so deferred calls won't run). This can happen for example in code
57-
// like this, that blocks forever:
58-
//
59-
// select{}
60-
//
61-
//go:noinline
62-
func deadlock() {
63-
// call yield without requesting a wakeup
64-
task.Pause()
65-
panic("unreachable")
66-
}
67-
6828
// Goexit terminates the currently running goroutine. No other goroutines are affected.
6929
//
7030
// Unlike the main Go implementation, no deferred calls will be run.
7131
//
7232
//go:inline
7333
func Goexit() {
74-
// its really just a deadlock
34+
// TODO: run deferred functions
7535
deadlock()
7636
}
77-
78-
// Add this task to the end of the run queue.
79-
func runqueuePushBack(t *task.Task) {
80-
runqueue.Push(t)
81-
}
82-
83-
// Add this task to the sleep queue, assuming its state is set to sleeping.
84-
func addSleepTask(t *task.Task, duration timeUnit) {
85-
if schedulerDebug {
86-
println(" set sleep:", t, duration)
87-
if t.Next != nil {
88-
panic("runtime: addSleepTask: expected next task to be nil")
89-
}
90-
}
91-
t.Data = uint64(duration)
92-
now := ticks()
93-
if sleepQueue == nil {
94-
scheduleLog(" -> sleep new queue")
95-
96-
// set new base time
97-
sleepQueueBaseTime = now
98-
}
99-
100-
// Add to sleep queue.
101-
q := &sleepQueue
102-
for ; *q != nil; q = &(*q).Next {
103-
if t.Data < (*q).Data {
104-
// this will finish earlier than the next - insert here
105-
break
106-
} else {
107-
// this will finish later - adjust delay
108-
t.Data -= (*q).Data
109-
}
110-
}
111-
if *q != nil {
112-
// cut delay time between this sleep task and the next
113-
(*q).Data -= t.Data
114-
}
115-
t.Next = *q
116-
*q = t
117-
}
118-
119-
// addTimer adds the given timer node to the timer queue. It must not be in the
120-
// queue already.
121-
// This function is very similar to addSleepTask but for timerQueue instead of
122-
// sleepQueue.
123-
func addTimer(tim *timerNode) {
124-
mask := interrupt.Disable()
125-
126-
// Add to timer queue.
127-
q := &timerQueue
128-
for ; *q != nil; q = &(*q).next {
129-
if tim.whenTicks() < (*q).whenTicks() {
130-
// this will finish earlier than the next - insert here
131-
break
132-
}
133-
}
134-
tim.next = *q
135-
*q = tim
136-
interrupt.Restore(mask)
137-
}
138-
139-
// removeTimer is the implementation of time.stopTimer. It removes a timer from
140-
// the timer queue, returning true if the timer is present in the timer queue.
141-
func removeTimer(tim *timer) bool {
142-
removedTimer := false
143-
mask := interrupt.Disable()
144-
for t := &timerQueue; *t != nil; t = &(*t).next {
145-
if (*t).timer == tim {
146-
scheduleLog("removed timer")
147-
*t = (*t).next
148-
removedTimer = true
149-
break
150-
}
151-
}
152-
if !removedTimer {
153-
scheduleLog("did not remove timer")
154-
}
155-
interrupt.Restore(mask)
156-
return removedTimer
157-
}
158-
159-
// Run the scheduler until all tasks have finished.
160-
// There are a few special cases:
161-
// - When returnAtDeadlock is true, it also returns when there are no more
162-
// runnable goroutines.
163-
// - When using the asyncify scheduler, it returns when it has to wait
164-
// (JavaScript uses setTimeout so the scheduler must return to the JS
165-
// environment).
166-
func scheduler(returnAtDeadlock bool) {
167-
// Main scheduler loop.
168-
var now timeUnit
169-
for !schedulerDone {
170-
scheduleLog("")
171-
scheduleLog(" schedule")
172-
if sleepQueue != nil || timerQueue != nil {
173-
now = ticks()
174-
}
175-
176-
// Add tasks that are done sleeping to the end of the runqueue so they
177-
// will be executed soon.
178-
if sleepQueue != nil && now-sleepQueueBaseTime >= timeUnit(sleepQueue.Data) {
179-
t := sleepQueue
180-
scheduleLogTask(" awake:", t)
181-
sleepQueueBaseTime += timeUnit(t.Data)
182-
sleepQueue = t.Next
183-
t.Next = nil
184-
runqueue.Push(t)
185-
}
186-
187-
// Check for expired timers to trigger.
188-
if timerQueue != nil && now >= timerQueue.whenTicks() {
189-
scheduleLog("--- timer awoke")
190-
delay := ticksToNanoseconds(now - timerQueue.whenTicks())
191-
// Pop timer from queue.
192-
tn := timerQueue
193-
timerQueue = tn.next
194-
tn.next = nil
195-
// Run the callback stored in this timer node.
196-
tn.callback(tn, delay)
197-
}
198-
199-
t := runqueue.Pop()
200-
if t == nil {
201-
if sleepQueue == nil && timerQueue == nil {
202-
if returnAtDeadlock {
203-
return
204-
}
205-
if asyncScheduler {
206-
// JavaScript is treated specially, see below.
207-
return
208-
}
209-
waitForEvents()
210-
continue
211-
}
212-
213-
var timeLeft timeUnit
214-
if sleepQueue != nil {
215-
timeLeft = timeUnit(sleepQueue.Data) - (now - sleepQueueBaseTime)
216-
}
217-
if timerQueue != nil {
218-
timeLeftForTimer := timerQueue.whenTicks() - now
219-
if sleepQueue == nil || timeLeftForTimer < timeLeft {
220-
timeLeft = timeLeftForTimer
221-
}
222-
}
223-
224-
if schedulerDebug {
225-
println(" sleeping...", sleepQueue, uint(timeLeft))
226-
for t := sleepQueue; t != nil; t = t.Next {
227-
println(" task sleeping:", t, timeUnit(t.Data))
228-
}
229-
for tim := timerQueue; tim != nil; tim = tim.next {
230-
println("--- timer waiting:", tim, tim.whenTicks())
231-
}
232-
}
233-
sleepTicks(timeLeft)
234-
if asyncScheduler {
235-
// The sleepTicks function above only sets a timeout at which
236-
// point the scheduler will be called again. It does not really
237-
// sleep. So instead of sleeping, we return and expect to be
238-
// called again.
239-
break
240-
}
241-
continue
242-
}
243-
244-
// Run the given task.
245-
scheduleLogTask(" run:", t)
246-
t.Resume()
247-
}
248-
}
249-
250-
func Gosched() {
251-
runqueue.Push(task.Current())
252-
task.Pause()
253-
}

src/runtime/scheduler_any.go

-31
This file was deleted.

0 commit comments

Comments
 (0)