Regarding processes, we know that every process has a unique PID which is not used by any other process while the machine is running. I was wondering whether threads have such a system as well, or their ID is unique to the process they are running in itself and not the entire system.

Thread ids depend on the threading system used. I will describe POSIX and its implementation on Linux here.

First, the POSIX standard asks that each thread is assigned an identifier of type pthread_t. That's what's returned by pthread_create() (as the first OUT argument) and it is used, for instance, in pthread_join() and pthread_detach(). It is also returned by pthread_self().

Interestingly, POSIX intentionally keeps pthread_t opaque, allowing it to be implemented as either an (integer) identifier or as a struct. For that reason, a comparator pthread_equals() exist to compare 2 thread ids. It should be noted that POSIX thread ids are guaranteed to be unique only within one process - the same POSIX thread id could refer to different threads in different processes.

Applications that are intended to run on all POSIX compliant system should use only pthread_t to refer to threads.

Second, in Linux specifically, a kernel-level thread is created for each POSIX thread. This thread is assigned a kernel-level thread id that comes from the same namespace as process ids. In fact, the thread id of the main thread (the thread that's created when a process is started) is equal to its process id.

This thread id can be obtained using the (hidden) system call gettid(). Shown below is code that uses the generic system call wrapper syscall(2) to invoke this system call. Using this system call, it is possible to implement a (nonportable) function is_lock_held that determines the owner of a lock, which can come in helpful during debugging.

#include <sys/types.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdbool.h>

// return current thread id
static pid_t gettid() { 
    return syscall(SYS_gettid); 
}

static bool is_lock_held(pthread_mutex_t * lock) __attribute__((__unused__));
static bool is_lock_held(pthread_mutex_t * lock) {
    return lock->__data.__owner == gettid();
}

int
main()
{
    printf("%d %d\n", getpid(), gettid());
}

During debugging with gdb, gdb will show both the pthread ID and the task id (as LWP id) in its output.