github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/src/runtime/scheduler.go (about)

     1  package runtime
     2  
     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  )
    17  
    18  const schedulerDebug = false
    19  
    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  
    34  // Simple logging, for debugging.
    35  func scheduleLog(msg string) {
    36  	if schedulerDebug {
    37  		println("---", msg)
    38  	}
    39  }
    40  
    41  // Simple logging with a task pointer, for debugging.
    42  func scheduleLogTask(msg string, t *task.Task) {
    43  	if schedulerDebug {
    44  		println("---", msg, t)
    45  	}
    46  }
    47  
    48  // Simple logging with a channel and task pointer.
    49  func scheduleLogChan(msg string, ch *channel, t *task.Task) {
    50  	if schedulerDebug {
    51  		println("---", msg, ch, t)
    52  	}
    53  }
    54  
    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  
    68  // Goexit terminates the currently running goroutine. No other goroutines are affected.
    69  //
    70  // Unlike the main Go implementation, no deffered calls will be run.
    71  //
    72  //go:inline
    73  func Goexit() {
    74  	// its really just a deadlock
    75  	deadlock()
    76  }
    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  func scheduler() {
   161  	// Main scheduler loop.
   162  	var now timeUnit
   163  	for !schedulerDone {
   164  		scheduleLog("")
   165  		scheduleLog("  schedule")
   166  		if sleepQueue != nil || timerQueue != nil {
   167  			now = ticks()
   168  		}
   169  
   170  		// Add tasks that are done sleeping to the end of the runqueue so they
   171  		// will be executed soon.
   172  		if sleepQueue != nil && now-sleepQueueBaseTime >= timeUnit(sleepQueue.Data) {
   173  			t := sleepQueue
   174  			scheduleLogTask("  awake:", t)
   175  			sleepQueueBaseTime += timeUnit(t.Data)
   176  			sleepQueue = t.Next
   177  			t.Next = nil
   178  			runqueue.Push(t)
   179  		}
   180  
   181  		// Check for expired timers to trigger.
   182  		if timerQueue != nil && now >= timerQueue.whenTicks() {
   183  			scheduleLog("--- timer awoke")
   184  			// Pop timer from queue.
   185  			tn := timerQueue
   186  			timerQueue = tn.next
   187  			tn.next = nil
   188  			// Run the callback stored in this timer node.
   189  			tn.callback(tn)
   190  		}
   191  
   192  		t := runqueue.Pop()
   193  		if t == nil {
   194  			if sleepQueue == nil && timerQueue == nil {
   195  				if asyncScheduler {
   196  					// JavaScript is treated specially, see below.
   197  					return
   198  				}
   199  				waitForEvents()
   200  				continue
   201  			}
   202  
   203  			var timeLeft timeUnit
   204  			if sleepQueue != nil {
   205  				timeLeft = timeUnit(sleepQueue.Data) - (now - sleepQueueBaseTime)
   206  			}
   207  			if timerQueue != nil {
   208  				timeLeftForTimer := timerQueue.whenTicks() - now
   209  				if sleepQueue == nil || timeLeftForTimer < timeLeft {
   210  					timeLeft = timeLeftForTimer
   211  				}
   212  			}
   213  
   214  			if schedulerDebug {
   215  				println("  sleeping...", sleepQueue, uint(timeLeft))
   216  				for t := sleepQueue; t != nil; t = t.Next {
   217  					println("    task sleeping:", t, timeUnit(t.Data))
   218  				}
   219  				for tim := timerQueue; tim != nil; tim = tim.next {
   220  					println("---   timer waiting:", tim, tim.whenTicks())
   221  				}
   222  			}
   223  			sleepTicks(timeLeft)
   224  			if asyncScheduler {
   225  				// The sleepTicks function above only sets a timeout at which
   226  				// point the scheduler will be called again. It does not really
   227  				// sleep. So instead of sleeping, we return and expect to be
   228  				// called again.
   229  				break
   230  			}
   231  			continue
   232  		}
   233  
   234  		// Run the given task.
   235  		scheduleLogTask("  run:", t)
   236  		t.Resume()
   237  	}
   238  }
   239  
   240  // This horrible hack exists to make WASM work properly.
   241  // When a WASM program calls into JS which calls back into WASM, the event with which we called back in needs to be handled before returning.
   242  // Thus there are two copies of the scheduler running at once.
   243  // This is a reduced version of the scheduler which does not deal with the timer queue (that is a problem for the outer scheduler).
   244  func minSched() {
   245  	scheduleLog("start nested scheduler")
   246  	for !schedulerDone {
   247  		t := runqueue.Pop()
   248  		if t == nil {
   249  			break
   250  		}
   251  
   252  		scheduleLogTask("  run:", t)
   253  		t.Resume()
   254  	}
   255  	scheduleLog("stop nested scheduler")
   256  }
   257  
   258  func Gosched() {
   259  	runqueue.Push(task.Current())
   260  	task.Pause()
   261  }