Scheduling and Preemption of Goroutines in Go

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Goroutines in Go
  4. Goroutine Scheduling
  5. Preemption in Goroutines
  6. Example: Concurrent Web Scraper
  7. Conclusion


Introduction

Welcome to this tutorial on scheduling and preemption of Goroutines in Go. Go is a powerful programming language known for its efficient concurrency model. Goroutines, lightweight threads supported by the Go runtime, allow concurrent execution of functions. Understanding how Goroutines are scheduled and preempted is essential for writing efficient and responsive concurrent programs.

In this tutorial, we will explore Goroutines, the concept of scheduling, and how preemption works in Go. By the end of this tutorial, you will have a clear understanding of how Goroutines are scheduled, how preemption occurs, and how to write concurrent applications that take advantage of these concepts.

Prerequisites

In order to follow this tutorial, you should have a basic understanding of the Go programming language and its syntax. Familiarity with the concept of concurrency will also be helpful, but not required. Make sure you have Go installed on your machine before proceeding.

Goroutines in Go

Goroutines are an integral part of Go’s concurrency model. They allow us to achieve concurrency by running functions concurrently in a lightweight manner. Goroutines are created using the go keyword followed by a function call.

func main() {
    go myFunction() // Creating a Goroutine
    // ...
}

When a Goroutine is created, it is scheduled by the Go runtime to run concurrently with other Goroutines. The Go runtime manages a pool of threads, known as the Goroutine scheduler, that are responsible for executing Goroutines efficiently.

Goroutine Scheduling

Goroutines are scheduled in a cooperative manner. This means that Goroutines themselves decide when to yield control to other Goroutines. The Goroutine scheduler doesn’t preempt running Goroutines unless they explicitly yield or block.

The Go runtime employs a work-stealing scheduler that dynamically adjusts the number of active Goroutines based on the available CPU resources. It ensures that Goroutines are evenly distributed across multiple threads to utilize the available processing power effectively.

To make a Goroutine yield control, we can use the built-in runtime.Gosched() function. This function yields the processor, allowing other Goroutines to run.

func myFunction() {
    // ...
    runtime.Gosched() // Yield control
    // ...
}

By calling runtime.Gosched(), we explicitly give the scheduler an opportunity to switch to other Goroutines. This is particularly useful when we have long-running tasks to ensure other Goroutines get a chance to execute.

Preemption in Goroutines

Preemption occurs when the Goroutine scheduler forcibly suspends the execution of a running Goroutine to allow other Goroutines to run. This ensures fairness and prevents Goroutines from monopolizing the execution time.

Preemption is triggered by certain events, such as a Goroutine making a system call or blocking on a channel operation. The Go runtime also monitors the duration of a running Goroutine. If a Goroutine exceeds a certain time limit (usually a few milliseconds), preemption is triggered to prevent long-running Goroutines from blocking other Goroutines.

The combination of cooperative scheduling and preemption allows Go to provide efficient and responsive concurrency while still preventing Goroutines from monopolizing system resources.

Example: Concurrent Web Scraper

To illustrate the concepts of Goroutine scheduling and preemption, let’s build a simple concurrent web scraper. We will fetch the HTML content of multiple URLs concurrently using Goroutines.

func main() {
    urls := []string{"https://example.com", "https://google.com", "https://github.com"}

    for _, url := range urls {
        go fetchURL(url)
    }

    // Wait for all Goroutines to complete
    time.Sleep(time.Second)
}

func fetchURL(url string) {
    resp, err := http.Get(url)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Fetched %s: %d bytes\n", url, len(body))
}

In this example, we create a Goroutine for each URL in the urls slice. The fetchURL function performs an HTTP GET request to fetch the HTML content of the given URL. By running the fetchURL function concurrently for each URL, we can fetch their content concurrently.

The time.Sleep(time.Second) statement ensures that the main Goroutine waits for all spawned Goroutines to complete before exiting. This is necessary because otherwise, the main Goroutine may exit before the Goroutines finish executing.

Conclusion

In this tutorial, we explored the scheduling and preemption of Goroutines in Go. We learned that Goroutines are scheduled in a cooperative manner, allowing control to be yielded explicitly. Goroutines can be scheduled across multiple threads by the Goroutine scheduler to leverage available CPU resources efficiently.

We also saw that preemption ensures fairness and prevents long-running Goroutines from monopolizing system resources. The Go runtime triggers preemption in certain situations, such as system calls or excessive Goroutine execution time.

By applying these concepts, we can write efficient and responsive concurrent programs in Go. We illustrated this with an example of a concurrent web scraper that fetches content from multiple URLs concurrently.

Now that you understand the scheduling and preemption of Goroutines in Go, you have a solid foundation for building concurrent applications that make the most of Go’s powerful concurrency features.