On threading.
Add: read/write lock, spinlocks, valgrind stuff, read ntpl source code (implementation details), Pthread and MPI, NUMA
Question: linux kernel version, old LinuxThreads or NPTL compiler, MPIAND threads?
The OpenGroup specs for threads (and other.)
| LinuxThreads -vs- NTPL: |
LinuxThreads:
Some documentation hereNTPL:
There's a manager thread (handles thread creation/destruction, fatal signals, memory management (allocated stack, thread local data, etc...), waiting on dead threads.
Synchronization primitives with signals (spurious wake-ups, pressure to the kernel signal system.) Broken SIGSTOP/SIGCONT implementation (ctrl-Z doesn't work) Limits on number of threads.
Reliance on manager thread imposes performance penalties: because of
serialization of creation/deletion, monopolization of one CPU,increases context switching.
List of all thread is maintained, to implementpthread_key_deletefor instance./procclutter.
Correct implementation POSIX signal handling in the kernel solves fatal signal handling issues.Kernel can do thread memory deallocation. Kernel can reap terminated threads.
Thread specific data and local storage are managed through generation counters.
Synchronizations primitives are implemented with futexes. (which can be placed in shared memory, so that PTHREAD_PROCESS_SHARED can be implemented (mutex shared between threads belonging to separate processes))Thread local storage and thread data structures are merged in one block and placed on the stack. Stack frames can be cached for thread creation/deletion performances (stack frame unmap are costly depending on the architecture)
Required kernel support:
- Arbitrary thread specific data areas support
- Extension of
cloneto optimize thread creation and facilitate termination (no manager thread required.)- POSIX signal handling for multi-threaded processes, fatal signal terminate entire process. Stop/continue affects entire process (ctrl-Z works)
exit_groupsyscall added.- And more...
There's a testing effort ongoing: see results here. It's based on the POSIX test suite.
There are scalability limits that started to get documented
1-on-1 -vs- M-on-N
Kernel threads are used (pure user-level implementation makes multi-processor use impossible.) M-on-N schdules M user threads on N kernel threads: two schedulers at work, need cooperation. Doesn't fit well on Linux (user level context switch often requires copy of register content from kernel space.) O(1) scheduler negates advantage of user level scheduler. Overhead cost. Simplifies signal delivery.
| Message queues: |
Inter process message exchange.
mq_opencreate a message Q. It's identified by a filename path starting with/(no directories allowed,) features R/W/RW priviledges. Attributes (mq_maxmsgandmq_msgsize) can be specified if privlegdes for the given message queue are
granted (permission are resolved on name as for file access.)mq_sendsends a message to a message Q. The priority argument determines where the message is inserted in the Q. When the Q is full, the caller blocks (unless O_NONBLOCK) If several threads a blocking, the highest priority one is awaken to send the message.mq_notifysubscribe to message notification. Astruct sigeventargument specifies how to be notified (and how the poster will be notified of the IO completion ?)mq_receivereceives the oldest message of the highest priority. Same blocking policy than formg_send.- Attributes: mq_maxmsg,
mq_msgsize,mq_curmsgsandmq_flags(O_NONBLOCK)
| Initialization: |
pthread_once: Argumentpthread_once_tis initialized toPTHREAD_ONCE_INITand the function is called with a function pointer to execute. Subsequent call won't do anything as the value inpthread_once_tis modified to mark that the initialization happened.
| Thread Specific Data: |
Can be viewed as a thread private array ofPTHREAD_KEYS_MAXvoid *addressed by keys. Keys are common to all threads, but values are private. TSDs are disposed of when cancelling or exiting a thread.This is the old way of doing things. Nowadays, we let the compiler handle things by using the
pthread_key_createallocates a new key (whose value is returned through a parameter) and set the initial value toNULL. A destructor can be attached to the key for destruction at cancellation or exit. The destructor doesn't run if the associated value isNULL, but associatesNULLto the value it's destroying.pthread_key_deletedeallocate a TSD key, but doesn't run the destructor and doesn't care about the deallocated value.__threadkeyword.
| Synchronization: |
Mutexes
Mutexes shouldn't be called from signal handler (not async signal safe.) Mutex functions aren't cancellation point (don't hold mutexes for long when cancellation is deferred.)
Two states: unlocked (not owned), locked (owned by one thread.) Acquiring an owned thread will make the thread block.
pthread_mutexattr_init: Initialize a mutex with attributes (mutex kind determines what happen forpthread_mutex_lockon already owned mutex:PTHREAD_MUTEX_FAST_NPsuspends calling thread for ever,PTHREAD_MUTEX_RECURSIVE_NPreturns immediately with success code, number of time thread owning the mutex is recorded and must be matched by number ofpthread_mutex_unlock;PTHREAD_MUTEX_ERRORCHECK_NPreturns with error codeEDEADLK.)pthread_mutex_lock: if unlocked, the mutex is set in the lock state and to belong to a thread. If locked, depends on fast, recursive and error check mutex kind.pthread_mutex_trylock: just likepthread_mutex_lockbut returnEBUSYif mutex is owned.pthread_mutex_unlock: fast mutexes are returned to the unlock state, recursive mutexes have their reference count decremented and the mutex unlocked when count reaches 0, error checking mutexes are checked for lock state and owning thread appurtenance (error code may be returned.) Fast and recursive mutexes can be unlocked by non owner. This is not a portable behavior.PTHREAD_MUTEX_INITIALIZER,PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP,PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP: Static initialization macros.
pthread_mutexattr_destroydoes nothing for Linux except check that the mutex is unlocked.
pthread_mutexattr_{set,get}typeset/query mutex kind attribute.
Condition variable (condition)
Additional info here. Conditionals are implemented with FUTEXES (wait queues).
Mutexes control access to data. Condition variables provide synchronization on data value, without resorting to polling. Mutexes are required to work with condition, because of race conditions (to avoid a thread signaling a condition before and other thread can wait on it.)Semaphores
pthread_cond_init: There are no condition attributes supported on Linux implementation. Static initialization throughPTHREAD_COND_INITIALIZERpthread_cond_wait: atomically unlocks the associated mutex, and waits on a condition.pthread_cond_timedwaitadd a timeout (And a may set ETIMEDOUT)
pthread_cond_signal: restart one waiting thread that a condition has been met.pthread_cond_broadcastrestart all waiting threads (thundering herd problem/scheduler trashing prone.) When thread is restarted, associated mutex is automatically and atomically locked again.
pthread_cond_destroy: destroy ressources used by condition. Does nothing on linux except check that no thread waits on cond
Semaphores are counters for ressources shared between threads. They are incremented/decrement atomically.Barrier
sem_initinitializes a semaphore with an initial value. Semaphores can be shared amongst processes (LinuxThreads doesn't support this.)sem_waitsuspend calling thread until values as a non zero count, then decreases the semaphore count. It is signal safe (the only POSIX synchronization function that is, LinuxThread implementation isn't)sem_trywaitis the non blocking variant (EAGAINreturned if count is zero.) This is the P operation.sem_postAtomically increases the count of the semaphore and never blocks (this is the V operation)sem_getvaluegets the current count of a semaphore.sem_destroyreleases ressources allocated for a semaphore. For LinuxThreads, just check that no thread is waiting on the semaphore.consumer:
loop:
P (); ; Mutex initialized to 0, consumer blocks
consume;
goto loop;
producer:
loop:
produce;
V (); ; 0 Initialized mutex brought to 1, consumer awakens
goto loop;
Barriers set the number of threads that must reach a barrier before all of them can be allowed to continue.
pthread_barrier_initInitializes a barrier with a count number.
pthread_barrier_waitThread blocks until the required number of threads have reached the specified barrier.
| Thread management: |
pthread_cancel:
Sends cancellation to thread. Receiving thread can ignore the request, honor it right away or defer until the next cancellation point. Receiving thread exits as ifpthread_exit(PTHREAD_CANCELED)had been called.
pthread_setcancelstatesets eitherTHREAD_CANCEL_{ENABLE,DISABLE}
pthread_setcanceltypesetsPTHREAD_CANCEL_ASYNCHRONOUS(as soon as cancellation request is received),PTHREAD_CANCEL_DEFERRED(next cancellation point.When cancelling: execution of cleanup handlers (reverse order, LIFO), finalization for thread specific data and return
PTHREAD_CANCELED.
pthread_cleanup_{push,pop,push_defer_np,pop_restore_np}:
Manages cleanup handlers:Cancellation Points
pthread_cleanup_push: install a cleanup handler, called when thread terminate (cancel or exit.) LIFO.pthread_cleanup_pop: remove last cleanup handler, with possibility of executing it.
Note: these matching pairs should be in the same block/function (they're macros and introduce a{/}sequence.pthread_cleanup_{push_defer,pop_restore}_npNon portable extension (setPTHREAD_CANCEL_DEFERRED, push cleanup handler. pop cleanup handler (with possible execution) and restore cancellation type.
In general, any function that might suspend the execution of a thread for a long time, should be a cancellation point. In practice: depends on the implementation and how POSIX it is. Cancellation point can be explicit or implicit.
pthread_testcancel: tests for a pending cancellation, effectively establishing a cancellation point.
pthread_cond_{timed}wait,pthread_join,sigwaitandsem_wait.
- All other syscalls that cause a process to block: read, select, wait and whatever in the libC uses them.
| Thread linux special: |
pthread_atfork:
Due to implementation limitation: fork on a threaded application duplicate the currently running thread but not others. Mutexes are duplicated in their current state, this gives us a chance to set things straight.
| Thread attributes: |
detachstate:Default attribute values
PTHREAD_CREATE_JOINABLE: thread termination synchronization possible through pthread_join (termination code available.) Allocated thread ressources reclaimed after join.PTHREAD_CREATE_DETACHED: no join synchronization, ressources reclaimed when thread terminates.pthread_detachcan forcePTHREAD_CREATE_DETACHEDschedpolicy:
SCHED_OTHER,SCHED_RR,SCHED_FIFO(both require super user priviledge.)pthread_setschedparamcan change schedpolicy on running thread.schedparam:
- Set priority value for
SCHED_RR,SCHED_FIFOscheduling policy, withingsched_get_priority_{min,max}range (range depending on the policy), usually 0 or 1 to 99.inheritsched:
PTHREAD_EXPLICIT_SCHED: scheduling policy for newly created thread determined by schedpolicy and schedparam.PTHREAD_INHERIT_SCHED: inherited by parent thread.scope:
PTHREAD_SCOPE_SYSTEM: threads contend for CPU time with all other processes runing on the machine (thread priorities are interpreted relatives to other processes priorities.)
PTHREAD_SCOPE_PROCESS: contention occurs with other threads of the running process. No supported on Linux.
Attribute ValueThreads detachstate PTHREAD_CREATE_JOINABLEschedpolicy SCHED_OTHERschedparam 0inheritsched PTHREAD_EXPLICIT_SCHEDscope PTHREAD_SCOPE_SYSTEMMutexes mutex kind PTHREAD_MUTEX_FAST_NPCondition variables cond_attr Value ignored
| Pitfalls: |
Race conditions
Execution results depends on code execution order (code in different thread.) Circumvent with synchronization.Deadlock
thread1 acquires a lock1, thread2 acquires lock2. thread1 blocks acquiring lock2 and thread2 blocks acquiring lock1. Thread1 can't unlock lock1 as its blocked on lock2, thread2 can't unlock lock2 as its blocked on lock1. Deadlock ensues.Livelock
Two or more threads change they states in response to changes in other thread or threads, without doing any usefull work. Differ from a deadlock in that neither tread is blocked or waiting for anything.Priority inversion
Low priority running thread synchronizes access to a ressource with high priority thread. When high priority thread runs, it can't because low priority hasn't released the ressource. This give a chance for a medium priority tasks to run, preventing low priority tasks from running to release what blocks the high priority tasks. As a result, high priority tasks doesn't run when it should and what it controls doesn't happen.What's Linux specific
Solution is:
- Priority inheritance: lower priority temporarily inherits higher priority of high priority thread that locks on its ressource, givin it a chance to run when medium priority would have ran. Requires OS help.
- Priority ceilings: associates a priority with each resource, transfered to the accessor of the resource + 1.
Document the NP stuff.
Does nothing:pthread_mutexattr_destroy, pthread_mutex_destroy
Error code
ESRCHis used for invalid thread scheduling parameter specification.
<< Home