Understanding the basics of CPU Affinity
Let's visualize a Linux system with four CPU cores and, for simplicity, one ready-to-run thread. On which CPU core will this thread run? The kernel will decide this; the key thing to realize is that it could run upon any of the four available CPUs!
Can the CPU(s) it could possibly be run upon be specified by the programmer? Yes, indeed; just this feature alone is called CPU affinity. On Linux, it is a per-thread attribute (within the OS). The CPU affinity can be changed on a per-thread basis by changing the thread's CPU affinity mask; this is achieved, of course, via a system call.
I have already written some articles around this area which can help you understand the basics:
Check thread count per process in Linux
CPU, processors, core, threads - Explained in layman's terms
Now that we are clear what is CPU Affinity, let's understand how it can be set using GO code. We will be using Linux environment for all our examples which rely on underlying kernel modules so it may or may not work in other environments.
Can we set CPU Affinity in GO using GOMAXPROCS?
Unfortunately NO, the runtime.GOMAXPROCS(n) function in Golang does not directly set the CPU affinity of a process in Linux. Instead, it sets the maximum number of OS threads that the Go runtime will use to execute Go code concurrently. It does not set the number of cores a process will be limited to.
You can read more at Understanding GOMAXPROCS with Example
Method-1: Set CPU Affinity using syscall.SchedSetaffinity()
In Golang, you can set CPU affinity by using the syscall package and the SchedSetaffinity function. The SchedSetaffinity function takes a process ID (PID) and a bitmask that specifies which CPUs the process is allowed to run on.
package main
import (
"fmt"
"syscall"
"time"
"unsafe"
)
func main() {
var mask uintptr
// Get the current CPU affinity of the process
if _, _, err := syscall.RawSyscall(syscall.SYS_SCHED_GETAFFINITY, 0, uintptr(unsafe.Sizeof(mask)), uintptr(unsafe.Pointer(&mask))); err != 0 {
fmt.Println("Failed to get CPU affinity:", err)
return
}
fmt.Println("Current CPU affinity:", mask)
// Set the new CPU affinity
mask = 3
if _, _, err := syscall.RawSyscall(syscall.SYS_SCHED_SETAFFINITY, 0, uintptr(unsafe.Sizeof(mask)), uintptr(unsafe.Pointer(&mask))); err != 0 {
fmt.Println("Failed to set CPU affinity:", err)
return
}
fmt.Println("New CPU affinity:", mask)
// some code
for {
println("Hello, World!")
time.Sleep(1 * time.Second)
}
}
Output:
]# go run main.go
Current CPU affinity: 15
New CPU affinity: 3
Hello, World!
Hello, World!
Hello, World!
Method-2: Set CPU Affinity using taskset
taskset is used to set or retrieve the CPU affinity of a running process given its PID or to launch a new COMMAND with a given CPU affinity. Here's an example using the os/exec
package to set the CPU affinity for the current process to use CPU 0 and CPU 1:
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
pid := os.Getpid()
// Get the current CPU affinity of the process
cmd := exec.Command("taskset", "-p", fmt.Sprintf("%d", pid))
out, err := cmd.Output()
if err != nil {
fmt.Println(err)
}
// Set the CPU affinity for the current process to use CPU 0 and CPU 1
cmd = exec.Command("taskset", "-p", "0,1", fmt.Sprintf("%d", pid))
out, err = cmd.Output()
if err != nil {
fmt.Println(err)
}
fmt.Printf("%s", out)
}
Output:
# go run main.go pid 871612's current affinity mask: f pid 871612's new affinity mask: 1
This code will get the current process ID using os.Getpid()
and then it creates an instance of the exec.Command
struct, passing the taskset
command, the -p
flag (to change the affinity mask of a process) and a string representing the desired CPU mask (in this case "0,1") and the pid of the process to be modified.
Still failing to set CPU Affinity?
Are you still unable to assign CPU to your process? There can be various reasons for the same:
- The
runtime.GOMAXPROCS()
function sets the maximum number of CPUs that can execute Go code simultaneously, but it does not set the CPU affinity. If you want to set the CPU affinity of a process, you'll need to use a different method, such as thetaskset
command as I mentioned earlier. - It's possible that your system does not support the ability to set the CPU affinity for a process. This is a feature provided by the kernel and not all kernels support it.
- If you're running the code on a multi-node environment like Kubernetes or other container orchestration systems, you will need to set the CPU affinity via the orchestration system.
Summary
There are multiple methods to set CPU Affinity in golang such as using taskset
, syscall
package or you can use C library inside the GO code to use SYS_SCHED_SETAFFINITY API. It is important to note that setting CPU affinity is a feature provided by the operating system and not all operating systems support it. So, you should also check if your OS supports this functionality.
Moreover changing the affinity can also lead to performance issues as when kernel allots a CPU to any process, so it automatically checks the availability of free CPU and then assigns the process to that CPU but when we are forcing a certain CPU to the process then there is no guarantee that the CPU in question is free and can cause issues to the application.
References