How to Create a Thread and Execute It on Specific CPU Core
What is a Thread?
A thread is an independent path of execution within a process. A process can contain multiple threads, all sharing the same address space, file descriptors, and heap — but each with its own stack and register state.
Examples: a browser runs one thread per tab; a text editor runs one thread for rendering and another for handling keystrokes. On Linux, threads and processes are both represented as tasks — the kernel makes no fundamental distinction between them.
Linux does not distinguish between processes and threads — it uses the generic term task.
clone()withCLONE_THREADis whatpthread_createcalls under the hood.
Thread Lifecycle — Joinable vs Detached
Every POSIX thread is created in one of two modes:
| Mode | How to set | What happens when thread exits |
|---|---|---|
| Joinable (default) | pthread_join() | Resources held until another thread calls pthread_join() |
| Detached | pthread_detach() or attr | Resources released immediately on exit |
pthread_join() on a joinable thread and never detach it, the thread's stack and metadata are never freed — even after the thread function returns.
#include <pthread.h>
#include <stdio.h>
void *worker(void *arg) {
int id = *(int *)arg;
printf("Thread %d running\n", id);
return NULL;
}
int main(void) {
pthread_t tid;
int id = 42;
/* Create joinable thread (default) */
if (pthread_create(&tid, NULL, worker, &id) != 0) {
perror("pthread_create");
return 1;
}
/* Block until thread exits — also frees its resources */
pthread_join(tid, NULL);
return 0;
}
Compile with: gcc thread.c -o thread -lpthread
pthread_create — Full Signature
int pthread_create(
pthread_t *thread, /* OUT: handle to new thread */
const pthread_attr_t *attr, /* thread attributes (NULL = default)*/
void *(*start_routine)(void*), /* function to run */
void *arg /* argument passed to function */
);
/* Returns 0 on success, errno value on failure */
Pass NULL as attr to accept all defaults: scheduling policy SCHED_OTHER, priority 0, all CPU cores allowed, joinable mode.
Setting Thread Attributes
To control scheduling policy, priority, and CPU affinity you need a pthread_attr_t. Always initialise and destroy it:
pthread_attr_t attr; pthread_attr_init(&attr); /* initialise with defaults */ /* ... set fields ... */ pthread_create(&tid, &attr, fn, arg); pthread_attr_destroy(&attr); /* free internal resources */
Scheduling Policy
Linux provides three scheduling policies relevant to real-time and embedded work:
| Policy | Type | Priority range | Use case |
|---|---|---|---|
SCHED_OTHER | Normal (CFS) | 0 (fixed) | Default — general-purpose tasks |
SCHED_FIFO | Real-time | 1–99 | Hard RT — runs until it blocks or yields, no time-slicing |
SCHED_RR | Real-time | 1–99 | Soft RT — like FIFO but with a round-robin timeslice between equal-priority threads |
SCHED_FIFO for hard real-time tasks (sensor interrupt handler, PWM generator). Use SCHED_RR when multiple RT threads share a core and must share time fairly. Use SCHED_OTHER for everything else.
SCHED_FIFO or SCHED_RR requires CAP_SYS_NICE — run as root or grant the capability with setcap cap_sys_nice+ep ./binary.
/* Set SCHED_FIFO on the attribute */ pthread_attr_setschedpolicy(&attr, SCHED_FIFO); /* Must also set this flag — otherwise the attr policy is ignored */ pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
The PTHREAD_EXPLICIT_SCHED flag is critical — without it, the thread inherits the creating thread's policy regardless of what you set in the attribute.
Thread Priority
Real-time threads use priority 1 (lowest RT) to 99 (highest RT). A higher-priority RT thread preempts any lower-priority thread immediately.
struct sched_param param; /* Query the maximum allowed priority for this policy */ param.sched_priority = sched_get_priority_max(SCHED_FIFO); pthread_attr_setschedparam(&attr, ¶m);
CPU Affinity — Pinning to a Core
cpu_set_t is a bitmask — one bit per logical CPU. You manipulate it with four macros:
cpu_set_t cpuset; CPU_ZERO(&cpuset); /* clear all bits */ CPU_SET(2, &cpuset); /* allow core 2 */ CPU_CLR(2, &cpuset); /* remove core 2 */ CPU_ISSET(2, &cpuset); /* check if core 2 is set */
Apply the mask to the attribute before creating the thread:
pthread_attr_setaffinity_np(&attr, sizeof(cpu_set_t), &cpuset);
To verify after creation — read back the actual affinity the thread is running with:
cpu_set_t actual;
pthread_getaffinity_np(tid, sizeof(cpu_set_t), &actual);
for (int c = 0; c < CPU_SETSIZE; c++) {
if (CPU_ISSET(c, &actual))
printf("Thread running on core %d\n", c);
}
Why Pin a Thread to a Core?
Three reasons this matters in embedded and real-time systems:
1. Cache locality — a thread pinned to one core keeps its working set in that core's L1/L2 cache. Migrating between cores causes cold cache misses, increasing latency unpredictably.
2. Interrupt affinity — on multi-core SoCs, IRQs are often distributed across cores. Pin your RT processing thread to the same core that handles the hardware interrupt to eliminate cross-core wakeup latency.
3. Determinism — in hard RT systems, you need to reason about worst-case execution time (WCET). An unbound thread can be migrated at any scheduler tick, making WCET analysis impossible. A pinned thread on a dedicated core eliminates that variable.
Complete Working Example
Creates a SCHED_FIFO RT thread pinned to core 1, verifies its affinity, then joins it:
#define _GNU_SOURCE
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
void *rt_worker(void *arg) {
/* Confirm which core we're on */
cpu_set_t actual;
pthread_getaffinity_np(pthread_self(), sizeof(cpu_set_t), &actual);
for (int c = 0; c < CPU_SETSIZE; c++) {
if (CPU_ISSET(c, &actual))
printf("[worker] running on core %d\n", c);
}
/* Real work goes here */
printf("[worker] doing real-time work...\n");
return NULL;
}
int main(void) {
pthread_t tid;
pthread_attr_t attr;
struct sched_param param;
cpu_set_t cpuset;
int ret;
/* 1. Initialise attribute */
pthread_attr_init(&attr);
/* 2. Set SCHED_FIFO + explicit scheduling */
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
/* 3. Set priority to 80 (leave room above for panic handlers) */
param.sched_priority = 80;
pthread_attr_setschedparam(&attr, ¶m);
/* 4. Pin to core 1 */
CPU_ZERO(&cpuset);
CPU_SET(1, &cpuset);
pthread_attr_setaffinity_np(&attr, sizeof(cpu_set_t), &cpuset);
/* 5. Create thread */
ret = pthread_create(&tid, &attr, rt_worker, NULL);
if (ret != 0) {
fprintf(stderr, "pthread_create: %s\n", strerror(ret));
return 1;
}
pthread_attr_destroy(&attr);
/* 6. Join — wait for thread to finish and free its resources */
pthread_join(tid, NULL);
printf("[main] thread done\n");
return 0;
}
/* Build: gcc -O2 rt_thread.c -o rt_thread -lpthread
Run: sudo ./rt_thread (SCHED_FIFO needs root) */
Common Pitfalls
| Mistake | Effect | Fix |
|---|---|---|
Forgetting -lpthread | Linker error: undefined reference to pthread_create | Add -lpthread to gcc command |
Missing PTHREAD_EXPLICIT_SCHED | Policy/priority in attr silently ignored | Always set pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED) |
| Not joining or detaching | Thread stack never freed — memory leak | Always pthread_join() or pthread_detach() |
| Using priority 99 | Preempts kernel IRQ threads — system may hang | Use 70–90 for application RT; reserve 99 for watchdog only |
| SCHED_FIFO without root | pthread_create returns EPERM | Run as root or use setcap cap_sys_nice+ep |
| Passing stack variable as thread arg | Thread reads freed stack memory | Pass heap-allocated arg or ensure lifetime outlasts thread |
For the full Linux scheduler reference see sched(7) — Linux man pages.