What is Workerpool in Golang?

Learn via video courses
Topics Covered

Overview

Golang supports multiple concurrency models. For instance, the actor-based concurrency model in Erlang and the Akka toolkit, thread-based concurrency in Java, and futures and promise-based concurrency in functional programming languages such as Closure. But this article mainly focuses on the basic worker pool using Goroutines and Channels and examines the improvements in throughput.

What are Workerpools in Golang?

Workerpool in Golang which is also known as thread pool is the pattern used to achieve concurrency in golang. The idea behind a worker pool is to have a fixed number of goroutines running in the background, waiting for work to be assigned to them. When work is assigned to a worker, it is executed in the background, allowing the main goroutine to continue executing other code.

Problem it solves:

Assume you don't have a limitless resource on your machine; the minimum size of a goroutine object is 2 KB; creating too many goroutines will soon exhaust your machine's memory, and the CPU will continue executing the job until it hits the limit. We can minimize the burst of CPU and memory by utilizing a restricted pool of workers and keeping the job in the queue since the task will wait in the queue until the worker pulls it. In this scenario, a workerpool is very beneficial to use as it only limits task execution, not the number of tasks queued. Also, it never blocks the submitting tasks.

Working of the Worker Pool in Golang?

The worker pool is responsible for managing the lifecycle of the workerpool goroutines and distributing the tasks among them.

The basic structure of a workerpool in Golang consists of three main components:

  • task queue,
  • worker pool,
  • manager goroutine

The task queue is a channel that is used to store incoming tasks. The workerpool is a group of worker goroutines that are responsible for handling the tasks from the task queue. The manager goroutine is responsible for managing the worker pool, creating new worker goroutines as needed, and distributing the tasks among them.

When a task is added to the task queue, the manager goroutine checks if there is an available worker goroutine to handle it. If there is, the task is immediately passed to the worker goroutine. If there are no available worker goroutines, the manager creates a new worker goroutine to handle the task. This process continues until all the tasks in the task queue have been processed.

One of the key advantages of using a worker pool in Golang is that it allows for better control over the number of worker goroutines that are created. This is important because creating too many worker goroutines can lead to performance issues and resource contention. By using a worker pool, the number of worker goroutines can be limited, ensuring that the system remains stable and efficient.

Illustration/Diagram of Workflow

Steps to Implement Worker Pool in Golang with Code

  • Define the worker function:
    The worker function should have a channel for receiving jobs and a channel for sending results. The worker function should listen for jobs on the jobs channel, perform the desired operations on each job, and send the output to the results channel.
  • Create the channels:
    In the main function, create the channels for jobs and results. The jobs channel should be used to send jobs to the worker pool, and the results channel should be used to receive output from the worker pool.
  • Start the goroutines:
    Use the go keyword to start a specified number of goroutines, each running the worker function with the jobs and results channels as arguments.
  • Send jobs to the jobs channel:
    Use a loop to send a specified number of jobs to the jobs channel.
  • Close the jobs channel:
    Once all the jobs have been sent, close the jobs channel to signal to the worker goroutines that there are no more jobs to be processed.
  • Receive outputs from the results channel:
    Use a loop to receive the outputs from the results channel.
  • Wait for the goroutines to finish:
    Use the sync package's WaitGroup to wait for the goroutines to finish(this is optional we can work without waitgroup also).
  • Close the results channel:
    Once all the outputs are received, close the results channel.

Code Example:

Output for the above code:

Run the code:

Explanation of the code:

  • In the above code, we imported time which helps in pausing the program execution.
  • Next, the worker function is defined, which takes in 3 arguments: an id integer, a jobs channel for receiving integers, and a results channel for sending integers.
  • Within the worker function, a for loop is used to iterate over the jobs channel. For each integer received on the jobs channel (j), the worker prints that it has started the job and sleeps for 1 second. After the sleep, the worker prints that it has finished the job and sends the result (j2j * 2) to the results channel.
  • In the main function, to use our pool of workers we need to send their work and collect their results. We make 2 channels for this.
  • This starts up 3 workers, initially blocked because there are no jobs yet. Here we send 5 jobs.
  • The jobs channel is closed to signal to the workers that all jobs have been sent.
  • A final loop is used to receive numJobs integers from the results channel, where we collect all the results of the work. This also ensures that the worker's goroutines have finished. An alternative way to wait for multiple goroutines is to use a WaitGroup.
  • The program will run with the 3 worker goroutines processing the 5 jobs concurrently and sending the results to the results channel. The output will show when each worker starts and finishes each job.

In simple words, this code uses Go's concurrency features to create worker processes that concurrently process jobs and send results using channels. The program creates 3 worker goroutines, sends 5 jobs to the workers, and receives the results from the workers. The output shows the start and finish times of each worker for each job.

What is the Difference between Using the Goroutine Directly and Workerpool Implementation?

When we make use of this pattern, we will accomplish concurrent job execution by utilizing our system to be more performant and consistent across job executions.

Also, by applying the worker pool pattern (16 concurrent workers) it can reduce the processing time for the same batch size from 17.25387443 Seconds to 1.494497171 Seconds which is very beneficial for our CPU utilization.

  • Goroutine is a lightweight thread of execution, while a worker pool is a way to manage a set of goroutines.
  • Goroutine is managed by the Go runtime, while a worker pool is managed by the developer.
  • Goroutines are a way to run concurrent tasks within a single process, while a worker pool controls the number of concurrent tasks that are running at a given time.
  • Goroutines are used to execute a single task at a time, while a worker pool uses a queue to hold tasks that are waiting to be executed.
  • Goroutines can lead to overloading the system if too many goroutines are created. While a worker pool can help to avoid this issue by controlling the concurrency.
  • Goroutine is simple and easy to use but lacks control over concurrency, while worker pool has control over concurrency but requires more code and complexity.

Conclusion

  • In this article, we learned about why we should consider WorkPool in Golang.
  • The Worker Pool in Golang is an important pattern that can be used to achieve concurrency in the language. It allows for better control over the number of worker goroutines that are created, and better task distribution.
  • By using a worker pool, developers can ensure that their systems remain stable and efficient while processing tasks concurrently.