pth(3)

๐Ÿ’– pth ๐Ÿ’– โ€” GNU Portable Threads ๐Ÿงš

okay so bestie, welcome to the pth(3) manual page, the main character of GNU Portable Threads ๐ŸŽ€ grab a snack, this one's a whole journey โœจ


๐Ÿ“› NAME

pth โ€” GNU Portable Threads ๐ŸŒธ

๐Ÿท๏ธ VERSION

GNU Pth 2.0.7 (08-Jun-2006) โญ

๐Ÿ“œ SYNOPSIS

not me dropping the ENTIRE function roster before we even get to the explanation ๐Ÿ˜ญ it's giving table of contents energy

๐Ÿ—‚๏ธ Global Library Management

pth_init, pth_kill, pth_ctrl, pth_version.

๐ŸŽ€ Thread Attribute Handling

pth_attr_of, pth_attr_new, pth_attr_init, pth_attr_set, pth_attr_get, pth_attr_destroy.

๐Ÿ•น๏ธ Thread Control

pth_spawn, pth_once, pth_self, pth_suspend, pth_resume, pth_yield, pth_nap, pth_wait, pth_cancel, pth_abort, pth_raise, pth_join, pth_exit.

๐Ÿงฐ Utilities

pth_fdmode, pth_time, pth_timeout, pth_sfiodisc.

๐Ÿ›‘ Cancellation Management

pth_cancel_point, pth_cancel_state.

๐ŸŽ† Event Handling

pth_event, pth_event_typeof, pth_event_extract, pth_event_concat, pth_event_isolate, pth_event_walk, pth_event_status, pth_event_free.

๐Ÿ”‘ Key-Based Storage

pth_key_create, pth_key_delete, pth_key_setdata, pth_key_getdata.

๐Ÿ“ฌ Message Port Communication

pth_msgport_create, pth_msgport_destroy, pth_msgport_find, pth_msgport_pending, pth_msgport_put, pth_msgport_get, pth_msgport_reply.

๐Ÿงผ Thread Cleanups

pth_cleanup_push, pth_cleanup_pop.

๐Ÿด Process Forking

pth_atfork_push, pth_atfork_pop, pth_fork.

๐Ÿ”’ Synchronization

pth_mutex_init, pth_mutex_acquire, pth_mutex_release, pth_rwlock_init, pth_rwlock_acquire, pth_rwlock_release, pth_cond_init, pth_cond_await, pth_cond_notify, pth_barrier_init, pth_barrier_reach.

๐Ÿฆ‹ User-Space Context

pth_uctx_create, pth_uctx_make, pth_uctx_switch, pth_uctx_destroy.

๐ŸŒˆ Generalized POSIX Replacement API

pth_sigwait_ev, pth_accept_ev, pth_connect_ev, pth_select_ev, pth_poll_ev, pth_read_ev, pth_readv_ev, pth_write_ev, pth_writev_ev, pth_recv_ev, pth_recvfrom_ev, pth_send_ev, pth_sendto_ev.

๐Ÿ’œ Standard POSIX Replacement API

pth_nanosleep, pth_usleep, pth_sleep, pth_waitpid, pth_system, pth_sigmask, pth_sigwait, pth_accept, pth_connect, pth_select, pth_pselect, pth_poll, pth_read, pth_readv, pth_write, pth_writev, pth_pread, pth_pwrite, pth_recv, pth_recvfrom, pth_send, pth_sendto.


๐Ÿ“– DESCRIPTION

first, the art. look at it. RESPECT it. ๐Ÿซถ

         ____  _   _
        โŽช  _ \โŽช โŽช_โŽช โŽช__
        โŽช โŽช_) โŽช __โŽช '_ \         ``Only those who attempt
        โŽช  __/โŽช โŽช_โŽช โŽช โŽช          the absurd can achieve
        โŽช_โŽช    \__โŽช_โŽช โŽช_โŽช          the impossible.''

Pth is a very portable POSIX/ANSI-C based library for Unix platforms that gives you non-preemptive priority-based scheduling for multiple threads of execution โ€” aka what the girlies call multithreading โ€” but inside event-driven applications ๐Ÿ’…

So like, all threads run in the same address space of the application process, but every single thread gets its own lil private setup: its own program counter, its own run-time stack, its own signal mask, and its own "errno" variable. individual. iconic. ๐ŸŒŸ

The scheduling itself? cooperative, bestie. The threads are managed and dispatched by a priority- and event-driven non-preemptive scheduler. The whole intention is that this way you get both better portability AND better run-time performance than you would with preemptive scheduling ๐Ÿ”ฅ And the event facility lets threads just chill and wait until various types of internal and external events happen โ€” things like pending I/O on file descriptors, asynchronous signals, elapsed timers, pending I/O on message ports, thread and process termination, and even the results of your own customized callback functions ๐Ÿ‘‘

Also! Pth ships an optional emulation API for POSIX.1c threads (Pthreads) that you can use for backward compatibility with existing multithreaded applications. Check Pth's pthread(3) manual page for the deets ๐Ÿ“š


๐Ÿงต Threading Background

okay so context time, and this part is a lil long but it SLAPS, stay with me ๐ŸŒธ

When you're programming event-driven applications โ€” usually servers โ€” you've got lots of regular jobs and one-shot requests that all need to be processed in parallel. To efficiently simulate that parallel processing on uniprocessor machines, we use multitasking โ€” meaning we have the application ask the operating system to spawn multiple instances of itself.

On Unix, the kernel typically implements multitasking in a preemptive and priority-based way through heavy-weight processes spawned with fork(2) ๐Ÿ˜ฎโ€๐Ÿ’จ And these processes usually do not share a common address space. Instead they're clearly separated from each other, and they're created by directly cloning a process address space โ€” although modern kernels use memory segment mapping and copy-on-write semantics to avoid unnecessary copying of physical memory. okay so, modern, fine, but still.

And the drawbacks? obvious, bestie:

  • Sharing data between the processes is complicated, and usually can only be done efficiently through shared memory (which itself is not very portable) ๐Ÿ˜ญ
  • Synchronization is complicated because of the preemptive nature of the Unix scheduler โ€” you have to use atomic locks, etc ๐Ÿ˜ตโ€๐Ÿ’ซ
  • The machine's resources can get exhausted very quickly when the server application has to serve too many long-running requests, because heavy-weight processes cost memory ๐Ÿ’€
  • And when each request spawns a sub-process to handle it, the server performance and responsiveness is horrible โ€” heavy-weight processes cost time to spawn ๐ŸŒ
  • Finally, the server application doesn't scale very well with the load, because of all these resource problems ๐Ÿ“‰

In practice, lots of tricks get used to overcome these problems โ€” everything from pre-forked sub-process pools to semi-serialized processing, etc. ๐ŸŽช

One of the most elegant ways to solve these resource- and data-sharing problems is to have multiple light-weight threads of execution inside a single (heavy-weight) process, i.e., to use multithreading โœจ Those threads usually improve responsiveness and performance of the application, often improve and simplify the internal program structure, and โ€” most important โ€” require less system resources than heavy-weight processes.

real talk though: threads are neither the optimal run-time facility for all types of applications, nor can all applications benefit from them. But at least event-driven server applications usually benefit greatly from using threads ๐Ÿ’–

๐ŸŒ The World of Threading

Even though there are LOTS of documents out there describing and defining the world of threading, to understand Pth you only need basic knowledge about threading. So here are the definitions of thread-related terms, and they should at least help you understand thread programming enough to actually use Pth ๐Ÿซถ

  • process vs. thread โ€” A process on Unix systems consists of at least these fundamental ingredients: virtual memory table, program code, program counter, heap memory, stack memory, stack pointer, file descriptor set, signal table. On every process switch, the kernel saves and restores these ingredients for the individual processes. A thread, on the other hand, consists of only a private program counter, stack memory, stack pointer and signal table. All the other ingredients โ€” in particular the virtual memory โ€” it shares with the other threads of the same process ๐Ÿ‘ฏ
  • kernel-space vs. user-space threading โ€” Threads on a Unix platform traditionally can be implemented either inside kernel-space or user-space. When threads are implemented by the kernel, the thread context switches are performed by the kernel without the application's knowledge. Similarly, when threads are implemented in user-space, the thread context switches are performed by an application library, without the kernel's knowledge. There are also hybrid threading approaches where, typically, a user-space library binds one or more user-space threads to one or more kernel-space threads (usually called light-weight processes โ€” or in short LWPs). User-space threads are usually more portable and can perform faster and cheaper context switches (for instance via swapcontext(2) or setjmp(3)/longjmp(3)) than kernel based threads. Kernel-space threads, on the other hand, can take advantage of multiprocessor machines and don't have any inherent I/O blocking problems. Kernel-space threads are usually scheduled in a preemptive way side-by-side with the underlying processes. User-space threads on the other hand use either preemptive or non-preemptive scheduling ๐Ÿ’…
  • preemptive vs. non-preemptive thread scheduling โ€” In preemptive scheduling, the scheduler lets a thread execute until a blocking situation occurs (usually a function call which would block) or the assigned timeslice elapses. Then it detracts control from the thread without a chance for the thread to object ๐Ÿ˜ค This is usually realized by interrupting the thread through a hardware interrupt signal (for kernel-space threads) or a software interrupt signal (for user-space threads), like "SIGALRM" or "SIGVTALRM". In non-preemptive scheduling, once a thread received control from the scheduler it keeps it until either a blocking situation occurs (again a function call which would block and instead switches back to the scheduler) or the thread explicitly yields control back to the scheduler in a cooperative way ๐Ÿค
  • concurrency vs. parallelism โ€” Concurrency exists when at least two threads are in progress at the same time. Parallelism arises when at least two threads are executing simultaneously. Real parallelism can only be achieved on multiprocessor machines, of course. But one also usually speaks of parallelism or high concurrency in the context of preemptive thread scheduling, and of low concurrency in the context of non-preemptive thread scheduling ๐Ÿง 
  • responsiveness โ€” The responsiveness of a system can be described by the user visible delay until the system responds to an external request. When this delay is small enough and the user doesn't recognize a noticeable delay, the responsiveness of the system is considered good. When the user recognizes or is even annoyed by the delay, the responsiveness of the system is considered bad ๐Ÿ™ˆ
  • reentrant, thread-safe and asynchronous-safe functions โ€” A reentrant function is one that behaves correctly if it is called simultaneously by several threads and then also executes simultaneously. Functions that access global state, such as memory or files, of course, need to be carefully designed in order to be reentrant. Two traditional approaches to solve these problems are caller-supplied states and thread-specific data ๐Ÿ”‘

and that's where part 1 wraps, bestie โ€” more parts are coming, don't go anywhere ๐Ÿฆ‹

Okay so thread-safety is literally just the avoidance of data races โ€” and data races are those situations where data gets set to either the correct or the incorrect value depending on the (unpredictable) order in which multiple threads access and modify the data. So a function is thread-safe when it still behaves semantically correct when called simultaneously by several threads (it is not required that the functions also execute simultaneously). The traditional approach to achieve thread-safety is to wrap a function body with an internal mutual exclusion lock (aka mutex). As you should recognize, reentrant is a stronger attribute than thread-safe, because it is harder to achieve and results especially in no run-time contention between threads. So, a reentrant function is always thread-safe, but not vice versa. Periodt. ๐Ÿ’…

Additionally there is a related attribute for functions named asynchronous-safe, which comes into play in conjunction with signal handlers. This is very related to the problem of reentrant functions. An asynchronous-safe function is one that can be called safe and without side-effects from within a signal handler context. Usually very few functions are of this type, because an application is very restricted in what it can perform from within a signal handler (especially what system functions it is allowed to call). The reason mainly is, because only a few system functions are officially declared by POSIX as guaranteed to be asynchronous-safe. Asynchronous-safe functions usually have to be already reentrant. No cap. ๐Ÿซถ

โœจ User-Space Threads โœจ

User-space threads can be implemented in various way. The two traditional approaches are:

  1. Matrix-based explicit dispatching between small units of execution:

Here the global procedures of the application are split into small execution units (each is required to not run for more than a few milliseconds) and those units are implemented by separate functions. Then a global matrix is defined which describes the execution (and perhaps even dependency) order of these functions. The main server procedure then just dispatches between these units by calling one function after each other controlled by this matrix. The threads are created by more than one jump-trail through this matrix and by switching between these jump-trails controlled by corresponding occurred events.

This approach gives the best possible performance, because one can fine-tune the threads of execution by adjusting the matrix, and the scheduling is done explicitly by the application itself. It is also very portable, because the matrix is just an ordinary data structure, and functions are a standard feature of ANSI C. Iconic. ๐Ÿ‘‘

The disadvantage of this approach is that it is complicated to write large applications with this approach, because in those applications one quickly gets hundreds(!) of execution units and the control flow inside such an application is very hard to understand (because it is interrupted by function borders and one always has to remember the global dispatching matrix to follow it). Additionally, all threads operate on the same execution stack. Although this saves memory, it is often nasty, because one cannot switch between threads in the middle of a function. Thus the scheduling borders are the function borders.

  1. Context-based implicit scheduling between threads of execution:

Here the idea is that one programs the application as with forked processes, i.e., one spawns a thread of execution and this runs from the begin to the end without an interrupted control flow. But the control flow can be still interrupted - even in the middle of a function. Actually in a preemptive way, similar to what the kernel does for the heavy-weight processes, i.e., every few milliseconds the user-space scheduler switches between the threads of execution. But the thread itself doesn't recognize this and usually (except for synchronization issues) doesn't have to care about this.

The advantage of this approach is that it's very easy to program, because the control flow and context of a thread directly follows a procedure without forced interrupts through function borders. Additionally, the programming is very similar to a traditional and well understood fork(2) based approach. Lowkey obsessed with this tbh. ๐Ÿฆ‹

The disadvantage is that although the general performance is increased, compared to using approaches based on heavy-weight processes, it is decreased compared to the matrix-approach above. Because the implicit preemptive scheduling does usually a lot more context switches (every user-space context switch costs some overhead even when it is a lot cheaper than a kernel-level context switch) than the explicit cooperative/non-preemptive scheduling. Finally, there is no really portable POSIX/ANSI-C based way to implement user-space preemptive threading. Either the platform already has threads, or one has to hope that some semi-portable package exists for it. And even those semi-portable packages usually have to deal with assembler code and other nasty internals and are not easy to port to forthcoming platforms.

So, in short: the matrix-dispatching approach is portable and fast, but nasty to program. The thread scheduling approach is easy to program, but suffers from synchronization and portability problems caused by its preemptive nature. It's giving trade-offs, bestie. โš–๏ธ

๐Ÿฆ‹ The Compromise of Pth ๐Ÿฆ‹

But why not combine the good aspects of both approaches while avoiding their bad aspects? That's the goal of Pth. Pth implements easy-to-program threads of execution, but avoids the problems of preemptive scheduling by using non-preemptive scheduling instead. Obsessed. ๐Ÿ’–

This sounds like, and is, a useful approach. Nevertheless, one has to keep the implications of non-preemptive thread scheduling in mind when working with Pth. The following list summarizes a few essential points:

  • Pth provides maximum portability, but NOT the fanciest features.

This is, because it uses a nifty and portable POSIX/ANSI-C approach for thread creation (and this way doesn't require any platform dependent assembler hacks) and schedules the threads in non-preemptive way (which doesn't require unportable facilities like "SIGVTALRM"). On the other hand, this way not all fancy threading features can be implemented. Nevertheless the available facilities are enough to provide a robust and full-featured threading system.

  • Pth increases the responsiveness and concurrency of an event-driven application, but NOT the concurrency of number-crunching applications.

The reason is the non-preemptive scheduling. Number-crunching applications usually require preemptive scheduling to achieve concurrency because of their long CPU bursts. For them, non-preemptive scheduling (even together with explicit yielding) provides only the old concept of coroutines. On the other hand, event driven applications benefit greatly from non-preemptive scheduling. They have only short CPU bursts and lots of events to wait on, and this way run faster under non-preemptive scheduling because no unnecessary context switching occurs, as it is the case for preemptive scheduling. That's why Pth is mainly intended for server type applications, although there is no technical restriction. ๐Ÿ–ฅ๏ธโœจ

  • Pth requires thread-safe functions, but NOT reentrant functions.

This nice fact exists again because of the nature of non-preemptive scheduling, where a function isn't interrupted and this way cannot be reentered before it returned. This is a great portability benefit, because thread-safety can be achieved more easily than reentrance possibility. Especially this means that under Pth more existing third-party libraries can be used without side-effects than it's the case for other threading systems. We love a low-maintenance queen. ๐Ÿ‘‘

  • Pth doesn't require any kernel support, but can NOT benefit from multiprocessor machines.

This means that Pth runs on almost all Unix kernels, because the kernel does not need to be aware of the Pth threads (because they are implemented entirely in user-space). On the other hand, it cannot benefit from the existence of multiprocessors, because for this, kernel support would be needed. In practice, this is no problem, because multiprocessor systems are rare, and portability is almost more important than highest concurrency. ๐ŸŒ

๐ŸŒธ The life cycle of a thread ๐ŸŒธ

To understand the Pth Application Programming Interface (API), it helps to first understand the life cycle of a thread in the Pth threading system. It can be illustrated with the following directed graph:

                    NEW
                     โŽช
                     V
             +---> READY ---+
             โŽช       ^      โŽช
             โŽช       โŽช      V
          WAITING <--+-- RUNNING
                            โŽช
             :              V
          SUSPENDED       DEAD

When a new thread is created, it is moved into the NEW queue of the scheduler. On the next dispatching for this thread, the scheduler picks it up from there and moves it to the READY queue. This is a queue containing all threads which want to perform a CPU burst. There they are queued in priority order. On each dispatching step, the scheduler always removes the thread with the highest priority only. It then increases the priority of all remaining threads by 1, to prevent them from starving. ๐Ÿ’€

The thread which was removed from the READY queue is the new RUNNING thread (there is always just one RUNNING thread, of course). The RUNNING thread is assigned execution control. After this thread yields execution (either explicitly by yielding execution or implicitly by calling a function which would block) there are three possibilities: Either it has terminated, then it is moved to the DEAD queue, or it has events on which it wants to wait, then it is moved into the WAITING queue. Else it is assumed it wants to perform more CPU bursts and immediately enters the READY queue again.

Before the next thread is taken out of the READY queue, the WAITING queue is checked for pending events. If one or more events occurred, the threads that are waiting on them are immediately moved to the READY queue.

The purpose of the NEW queue has to do with the fact that in Pth a thread never directly switches to another thread. A thread always yields execution to the scheduler and the scheduler dispatches to the next thread. So a freshly spawned thread has to be kept somewhere until the scheduler gets a chance to pick it up for scheduling. That is what the NEW queue is for. ๐ŸŽ€

Okay so ๐Ÿ’€ the DEAD queue โ€” yes, it's literally called that, the drama โ€” exists for one reason and one reason only: thread joining. Here's the tea. When a thread gets marked as unjoinable, the system just straight-up kicks it out the moment it terminates, no farewell tour, gone. But when a thread is joinable, it slides into the DEAD queue instead, and it just... waits there. Chilling. Until another thread comes along and joins it. That's it, that's the whole gig.

Then we've got one more โœจspecialโœจ queue, a fully separate one, named SUSPENDED. Threads get manually yeeted into it by the application from the NEW, READY or WAITING queues. Its whole purpose is to temporarily absorb suspended threads until the app decides to resume them again. And the slay part? Suspended threads don't cost scheduling or event handling resources at all, because they're temporarily, completely out of the scheduler's scope. Bye bestie, not your problem right now ๐Ÿซถ. And if a thread gets resumed, it's moved back to the queue it originally came from, and that's how it re-enters the scheduler's scope. Iconic behaviour honestly.


๐ŸŽ€ APPLICATION PROGRAMMING INTERFACE (API) ๐ŸŽ€

Alright bestie, now we get into the good stuff. Below, the Pth Application Programming Interface (API) gets discussed in detail. And with everything you now know from above, it should be genuinely easy to figure out how to program threads with this API. Like, you're ready.

In good Unix tradition ๐Ÿง, Pth functions use special return values to flag an error condition โ€” "NULL" in pointer context, "FALSE" in boolean context, and "-1" in integer context โ€” and they set (or pass through) the "errno" system variable so more details about the error can reach the caller.


๐Ÿ’ซ Global Library Management ๐Ÿ’ซ

These functions act on the library as a whole. They're the ones you use to initialize and shutdown the scheduler, and to fetch info out of it.


int pth_init(void);

This initializes the Pth library. It has to be the first Pth API function call in an application, and it is mandatory โ€” no skipping, no cap. It's usually done at the beginning of the main() function of the application. It implicitly spawns the internal scheduler thread and transforms the single execution unit of the current process into a thread (the main thread). It returns "TRUE" on success and "FALSE" on error.


int pth_kill(void);

This kills the Pth library ๐Ÿ’€. It should be the last Pth API function call in an application, but it's not really required. It's usually done at the end of the main function of the application. At minimum, it has to be called from within the main thread. It implicitly kills all threads and transforms back the calling thread into the single execution unit of the underlying process.

So the usual way to terminate a Pth application is either a simple "pth_exit(0);" in the main thread โ€” which waits for all other threads to terminate, kills the threading system, and then terminates the process โ€” or a "pth_kill(); exit(0)" โ€” which immediately kills the threading system and terminates the process.

And heads up ๐Ÿšจ: pth_kill() returns immediately with a return code of "FALSE" if it is not called from within the main thread. Otherwise it kills the threading system and returns "TRUE".


long pth_ctrl(unsigned long query, ...);

This is the generalized query/control function for the Pth library. The argument query is a bitmask formed out of one or more "PTH_CTRL_"XXXX queries. Currently the following queries are supported:

  • "PTH_CTRL_GETTHREADS" โ€” returns the total number of threads currently in existence. This query is actually formed out of the combination of queries for threads in a particular state, i.e., the "PTH_CTRL_GETTHREADS" query is equal to the OR-combination of all the following specialized queries:
    • "PTH_CTRL_GETTHREADS_NEW" โ€” the number of threads in the new queue (threads created via pth_spawn(3) but still not scheduled once).
    • "PTH_CTRL_GETTHREADS_READY" โ€” the number of threads in the ready queue (threads who want to do CPU bursts).
    • "PTH_CTRL_GETTHREADS_RUNNING" โ€” the number of running threads (always just one thread!).
    • "PTH_CTRL_GETTHREADS_WAITING" โ€” the number of threads in the waiting queue (threads waiting for events).
    • "PTH_CTRL_GETTHREADS_SUSPENDED" โ€” the number of threads in the suspended queue (threads waiting to be resumed).
    • "PTH_CTRL_GETTHREADS_DEAD" โ€” the number of threads in the new queue (terminated threads waiting for a join).
  • "PTH_CTRL_GETAVLOAD" โ€” this one needs a second argument of type "float *" (a pointer to a floating point variable). It stores a floating point value describing the exponential averaged load of the scheduler into that variable. The load is a function of the number of threads in the ready queue of the scheduler's dispatching unit. So a load around 1.0 means there's only one ready thread (the standard situation when the application has no high load), and a higher load value means there are more threads ready who want to do CPU bursts. The average load value updates once per second only. The return value for this query is always 0.
  • "PTH_CTRL_GETPRIO" โ€” this one needs a second argument of type "pth_t" identifying a thread. It returns the priority (ranging from "PTH_PRIO_MIN" to "PTH_PRIO_MAX") of the given thread.
  • "PTH_CTRL_GETNAME" โ€” this one needs a second argument of type "pth_t" identifying a thread. It returns the name of the given thread, i.e., the return value of pth_ctrl(3) should be casted to a "char *".
  • "PTH_CTRL_DUMPSTATE" โ€” this one needs a second argument of type "FILE *" that a summary of the internal Pth library state gets written to. The main information currently written out is the current state of the thread pool.
  • "PTH_CTRL_FAVOURNEW" โ€” this one needs a second argument of type "int" which specifies whether the GNU Pth scheduler favours new threads on startup, i.e., whether they are moved from the new queue to the top (argument is "TRUE") or the middle (argument is "FALSE") of the ready queue. The default is to favour new threads, to make sure they don't starve already at startup, although this slightly violates the strict priority based scheduling.

The function returns "-1" on error.


long pth_version(void);

This function returns a hex-value 0xVRRTLL describing the current Pth library version. V is the version, RR the revisions, LL the level and T the type of the level (alphalevel=0, betalevel=1, patchlevel=2, etc). For instance, Pth version 1.0b1 is encoded as 0x100101. The reason for this unusual mapping is that this way the version number is steadily increasing ๐Ÿ“ˆ. The same value is also available at compile time as "PTH_VERSION".


๐Ÿฆ‹ Thread Attribute Handling ๐Ÿฆ‹

Attribute objects get used in Pth for two things: first, stand-alone/unbound attribute objects are used to store attributes for threads that are about to be spawned. Bounded attribute objects are used to modify attributes of threads that already exist. The following attribute fields exist in attribute objects:

  • "PTH_ATTR_PRIO" (read-write) ["int"] โ€” thread priority between "PTH_PRIO_MIN" and "PTH_PRIO_MAX". The default is "PTH_PRIO_STD".
  • "PTH_ATTR_NAME" (read-write) ["char *"] โ€” name of thread (up to 40 characters are stored only), mainly for debugging purposes.
  • "PTH_ATTR_DISPATCHES" (read-write) ["int"] โ€” in bounded attribute objects, this field is incremented every time the context is switched to the associated thread.
  • "PTH_ATTR_JOINABLE" (read-write) ["int"] โ€” the thread detachment type. "TRUE" indicates a joinable thread, "FALSE" indicates a detached thread. When a thread is detached, after termination it is immediately kicked out of the system instead of being inserted into the dead queue.
  • "PTH_ATTR_CANCEL_STATE" (read-write) ["unsigned int"] โ€” the thread cancellation state, i.e., a combination of "PTH_CANCEL_ENABLE" or "PTH_CANCEL_DISABLE" and "PTH_CANCEL_DEFERRED" or "PTH_CANCEL_ASYNCHRONOUS".
  • "PTH_ATTR_STACK_SIZE" (read-write) ["unsigned int"] โ€” the thread stack size in bytes. Use lower values than 64 KB with great care!
  • "PTH_ATTR_STACK_ADDR" (read-write) ["char *"] โ€” a pointer to the lower address of a chunk of malloc(3)'ed memory for the stack.
  • "PTH_ATTR_TIME_SPAWN" (read-only) ["pth_time_t"] โ€” the time when the thread was spawned. This can be queried only when the attribute object is bound to a thread.
  • "PTH_ATTR_TIME_LAST" (read-only) ["pth_time_t"] โ€” the time when the thread was last dispatched. This can be queried only when the attribute object is bound to a thread.
  • "PTH_ATTR_TIME_RAN" (read-only) ["pth_time_t"] โ€” the total time the thread was running. This can be queried only when the attribute object is bound to a thread.
  • "PTH_ATTR_START_FUNC" (read-only) ["void *(*)(void *)"] โ€” the thread start function. This can be queried only when the attribute object is bound to a thread.
  • "PTH_ATTR_START_ARG" (read-only) ["void *"] โ€” the thread start argument. This can be queried only when the attribute object is bound to a thread.
  • "PTH_ATTR_STATE" (read-only) ["pth_state_t"] โ€” the scheduling state of the thread, i.e., either "PTH_STATE_NEW", "PTH_STATE_READY", "PTH_STATE_WAITING", or "PTH_STATE_DEAD". This can be queried only when the attribute object is bound to a thread.
  • "PTH_ATTR_EVENTS" (read-only) ["pth_event_t"] โ€” the event ring the thread is waiting for. This can be queried only when the attribute object is bound to a thread.
  • "PTH_ATTR_BOUND" (read-only) ["int"] โ€” whether the attribute object is bound ("TRUE") to a thread or not ("FALSE").

The following API functions can be used to handle the attribute objects:


pth_attr_t pth_attr_of(pth_t tid);

This returns a new attribute object bound to thread tid. Any queries on this object directly fetch attributes from tid, and attribute modifications directly change tid. Use such attribute objects to modify existing threads. ๐Ÿซถ


pth_attr_t pth_attr_new(void);

This returns a new unbound attribute object. An implicit pth_attr_init() is done on it. Any queries on this object just fetch the stored attributes from it, and attribute modifications just change the stored attributes. Use such attribute objects to pre-configure attributes for threads that are about to be spawned โœจ.


๐Ÿงธ int pth_attr_init(pth_attr_t attr);

Bestie, this is where you set up your attribute object attr with all the default values, fresh out the box โœจ. Here's what you get:

  • "PTH_ATTR_PRIO" := "PTH_PRIO_STD"
  • "PTH_ATTR_NAME" := "unknown"
  • "PTH_ATTR_DISPATCHES" := 0
  • "PTH_ATTR_JOINABLE" := "TRUE"
  • "PTH_ATTR_CANCELSTATE" := "PTH_CANCEL_DEFAULT"
  • "PTH_ATTR_STACK_SIZE" := 64*1024
  • "PTH_ATTR_STACK_ADDR" := "NULL"

And every other "PTH_ATTR_*" attribute? Those are read-only, so they don't get default values dropped into attr at all โ€” they literally only exist for bounded attribute objects. No defaults for them, periodt ๐Ÿ’…


๐ŸŽ€ int pth_attr_set(pth_attr_t attr, int field, ...);

This one sets the attribute field field inside attr to whatever value you pass as the extra argument on the variable argument list ๐Ÿซถ. Here are the field/argument pairs you can use:

  • PTH_ATTR_PRIO โ€” int
  • PTH_ATTR_NAME โ€” char *
  • PTH_ATTR_DISPATCHES โ€” int
  • PTH_ATTR_JOINABLE โ€” int
  • PTH_ATTR_CANCEL_STATE โ€” unsigned int
  • PTH_ATTR_STACK_SIZE โ€” unsigned int
  • PTH_ATTR_STACK_ADDR โ€” char *

๐Ÿ” int pth_attr_get(pth_attr_t attr, int field, ...);

Now we're reading instead of writing. This grabs the attribute field field out of attr and stashes its value in the variable you point to with that extra pointer argument ๐ŸŒธ. The field/argument pairs go like this:

  • PTH_ATTR_PRIO โ€” int *
  • PTH_ATTR_NAME โ€” char **
  • PTH_ATTR_DISPATCHES โ€” int *
  • PTH_ATTR_JOINABLE โ€” int *
  • PTH_ATTR_CANCEL_STATE โ€” unsigned int *
  • PTH_ATTR_STACK_SIZE โ€” unsigned int *
  • PTH_ATTR_STACK_ADDR โ€” char **
  • PTH_ATTR_TIME_SPAWN โ€” pth_time_t *
  • PTH_ATTR_TIME_LAST โ€” pth_time_t *
  • PTH_ATTR_TIME_RAN โ€” pth_time_t *
  • PTH_ATTR_START_FUNC โ€” void *(**)(void *)
  • PTH_ATTR_START_ARG โ€” void **
  • PTH_ATTR_STATE โ€” pth_state_t *
  • PTH_ATTR_EVENTS โ€” pth_event_t *
  • PTH_ATTR_BOUND โ€” int *

๐Ÿ’€ int pth_attr_destroy(pth_attr_t attr);

Say bye-bye ๐Ÿ‘‹ โ€” this destroys the attribute object attr. And after this? attr is no longer a valid attribute object. Gone, done, don't touch it, iykyk ๐Ÿ™ˆ


๐Ÿงต Thread Control ๐Ÿงต

These next ones are the whole point of the Pth library, bestie โ€” they control the threading itself and make up the main API of Pth ๐Ÿ”ฅ


๐Ÿ‘‘ pth_t pth_spawn(pth_attr_t attr, void *(*entry)(void *), void *arg);

This spawns a brand-new thread ๐ŸŒฑ with the attributes given in attr โ€” or "PTH_ATTR_DEFAULT" for default attributes, which means thread priority, joinability and cancel state are inherited from the current thread โ€” with its starting point at routine entry.

Heads up though: the dispatch count is not inherited from the current thread when attr isn't specified โ€” nope, it gets initialized to zero ๐Ÿ’…

This entry routine is called as pth_exit(entry(arg)) inside the new thread unit, so basically entry's return value gets fed straight into an implicit pth_exit(3). Which means the thread can totally just exit by returning ๐Ÿซถ. That said, it can also exit explicitly whenever it wants by calling pth_exit(3).

But keep this in mind, bestie: calling the POSIX function exit(3) still terminates the complete process, not just the current thread. Not the vibe you want ๐Ÿ˜ญ

There's no Pth-internal limit on how many threads you can spawn, except the limit implied by the available virtual memory. Pth internally keeps track of threads in dynamic data structures. The function returns "NULL" on error.


โœจ int pth_once(pth_once_t *ctrlvar, void (*func)(void *), void *arg);

A lil convenience function ๐ŸŽ€ โ€” it uses a control variable of type "pth_once_t" to make sure a constructor function func is called only once as func(arg) in the system. In other words: only the first call to pth_once(3) by any thread in the system succeeds. Iconic, honestly.

The variable referenced via ctrlvar should be declared as

"pth_once_t" variable-name = "PTH_ONCE_INIT";

before calling this function.


๐Ÿฆ‹ pth_t pth_self(void);

This just hands you back the unique thread handle of the currently running thread ๐Ÿ’–. That handle itself has to be treated as an opaque entity by the application โ€” no peeking inside, bestie. It's usually used as an argument to other functions that require an argument of type "pth_t".


๐Ÿ˜ด int pth_suspend(pth_t tid);

This suspends a thread tid until it gets manually resumed again via pth_resume(3). To do that, the thread is moved to the SUSPENDED queue, and this way it's completely out of the scheduler's event handling and thread dispatching scope ๐Ÿšซ

Suspending the current thread is not allowed. Don't do it ๐Ÿ™…

The function returns "TRUE" on success and "FALSE" on errors.


๐ŸŒˆ int pth_resume(pth_t tid);

The glow-up comeback ๐Ÿ’ซ โ€” this resumes a previously suspended thread tid, which means tid has to still be sitting on the SUSPENDED queue. The thread gets moved to the NEW, READY or WAITING queue (depending on what its state was when the pth_suspend(3) call was made) and this way it re-enters the event handling and thread dispatching scope of the scheduler ๐ŸŽ‰

The function returns "TRUE" on success and "FALSE" on errors.


๐Ÿ“ฃ int pth_raise(pth_t tid, int sig)

This function raises a signal for delivery to thread tid only ๐Ÿ‘‘. When you just raise a signal via raise(3) or kill(2), it gets delivered to an arbitrary thread that doesn't have this signal blocked. With pth_raise(3) you can send a signal to a thread and it guarantees that only this thread gets the signal delivered ๐ŸŽฏ

But bestie, keep in mind: the signal's action is still configured process-wide. That part doesn't change, fr fr.

When sig is 0, plain thread checking is performed โ€” i.e., "pth_raise(tid, 0)" returns "TRUE" when thread tid still exists in the PTH system, but it doesn't send any signal to it ๐Ÿ’…


๐Ÿ”„ int pth_yield(pth_t tid);

This explicitly hands execution control back to the scheduler thread ๐Ÿค. Usually execution gets transferred back to the scheduler implicitly when a thread waits for an event. But when a thread has to do larger CPU bursts, it can be reasonable to interrupt it explicitly by doing a few pth_yield(3) calls so other threads get a chance to execute too ๐Ÿซถ. This is obviously the cooperating part of Pth.

A thread doesn't have to yield execution, of course. But when you want to program a server application with good response times, the threads should be cooperative โ€” i.e., they should split their CPU bursts into smaller units with this call โญ

Usually you specify tid as "NULL" to tell the scheduler it can freely decide which thread to dispatch next. But if you want to hint to the scheduler that a particular thread should be favored on the next dispatching step, you can specify that thread explicitly. This allows the usage of the old concept of coroutines, where a thread/routine switches to a particular cooperating thread ๐ŸŒธ

If tid is not "NULL" and points to a new or ready thread, it's guaranteed that this thread receives execution control on the next dispatching step. If tid is in a different state (that is, not in "PTH_STATE_NEW" or "PTH_STATE_READY"), an error is reported ๐Ÿšจ

The function usually returns "TRUE" for success, and only "FALSE" (with "errno" set to "EINVAL") if tid specified an invalid or still not new or ready thread.


๐Ÿ’ค int pth_nap(pth_time_t naptime);

This function suspends the execution of the current thread until naptime is elapsed ๐Ÿ˜ด. naptime is of type "pth_time_t", and this way it theoretically has a resolution of one microsecond. In practice you should rely on neither that, nor on the thread being awakened exactly after naptime has elapsed ๐Ÿ™ˆ

It only guarantees that the thread will sleep at least naptime. But because of the non-preemptive nature of Pth, it can last longer (when another thread kept the CPU for a long time ๐Ÿ˜ญ). Plus the resolution depends on the implementation of timers by the operating system, and those usually only have a resolution of 10 microseconds or larger.

But usually this isn't important for an application, unless it tries to use this facility for real time tasks.


โณ int pth_wait(pth_event_t ev);

This is the link between the scheduler and the event facility ๐Ÿ”— (see below for the various pth_event_xxx() functions). It's modeled like select(2), i.e., you hand this function one or more events (in the event ring specified by ev) that the current thread wants to wait on. The scheduler wakes the thread when one or more of them occurred or failed, after tagging them as such โœจ

The ev argument is a pointer to an event ring, which isn't changed except for the tagging. pth_wait(3) returns the number of occurred or failed events, and the application can use pth_event_status(3) to test which events occurred or failed ๐Ÿ”


๐Ÿ›‘ int pth_cancel(pth_t tid);

This cancels a thread tid ๐Ÿ’€. How the cancellation is actually done depends on the cancellation state of tid, which the thread can configure itself:

  • When its state is "PTH_CANCEL_DISABLE", a cancellation request is just made pending.
  • When it is "PTH_CANCEL_ENABLE", it depends on the cancellation type what is performed.
  • When it's "PTH_CANCEL_DEFERRED", again the cancellation request is just made pending.
  • But when it's "PTH_CANCEL_ASYNCHRONOUS", the thread is immediately canceled before pth_cancel(3) returns ๐Ÿ˜ณ

The effect of a thread cancellation is equal to implicitly forcing the thread to call "pth_exit(PTH_CANCELED)" at one of its cancellation points. In Pth, threads enter a cancellation point either explicitly via pth_cancel_point(3), or implicitly by waiting for an event ๐Ÿ‘‘


๐Ÿ’ฅ int pth_abort(pth_t tid);

This is the cruel way to cancel a thread tid ๐Ÿ˜ญ. When it's already dead and waiting to be joined, it just joins it (via "pth_join("tid", NULL)") and this way kicks it out of the system ๐Ÿšช

Else, it forces the thread to be not joinable and to allow asynchronous cancellation, and then cancels it via "pth_cancel("tid")".


๐Ÿซ‚ int pth_join(pth_t tid, void **value);

This joins the current thread with the thread specified via tid ๐Ÿซถ. It first suspends the current thread until the tid thread has terminated. Then it's awakened and stores the value of tid's pth_exit(3) call into *value (if value is not "NULL") and returns to the caller โœจ

A thread can be joined only when it has the attribute "PTH_ATTR_JOINABLE" set to "TRUE" (that's the default ๐Ÿ’…). And a thread can only be joined once โ€” i.e., after the pth_join(3) call, the thread tid is completely removed from the system. Gone, bestie, no take-backs ๐Ÿฆ‹

โœจ void pth_exit(void *value); โœจ

okay so this is the "bestie i'm logging off" function โ€” it terminates the current thread. ๐Ÿ’…

Whether it gets yeeted out of the system immediately or gently tucked into the dead queue of the scheduler depends entirely on its join type, which was decided back at spawning time. so:

  • if it has the attribute "PTH_ATTR_JOINABLE" set to "FALSE", it's immediately removed and value is just... ignored. ghosted. ๐Ÿ’€
  • otherwise the thread is inserted into the dead queue and value is remembered for a subsequent pth_join(3) call by another thread. it's giving "wait for me, i have lore to share". ๐Ÿซถ

๐Ÿ› ๏ธ Utilities ๐Ÿ› ๏ธ

Utility functions. that's the whole vibe. ๐Ÿ’–

int pth_fdmode(int fd, int mode);

This switches the non-blocking mode flag on the file descriptor fd. the mode argument can be:

  • "PTH_FDMODE_BLOCK" โ€” switches fd into blocking I/O mode
  • "PTH_FDMODE_NONBLOCK" โ€” switches fd into non-blocking I/O mode
  • "PTH_FDMODE_POLL" โ€” just polls the current mode, no changes, no notes

It hands you back the current mode (either "PTH_FDMODE_BLOCK" or "PTH_FDMODE_NONBLOCK") or "PTH_FDMODE_ERROR" on error. ๐Ÿ˜ญ

Keep in mind that since Pth 1.1 there is no longer a requirement to manually switch a file descriptor into non-blocking mode in order to use it. This is automatically done temporarily inside Pth. Instead, when you now switch a file descriptor explicitly into non-blocking mode, pth_read(3) or pth_write(3) will never block the current thread.

pth_time_t pth_time(long sec, long usec);

A constructor for a "pth_time_t" structure โ€” it's the convenient little shortcut so you never have to build a temporary structure value by hand, no cap. It returns a pth_time_t structure holding the absolute time value specified by sec and usec. โญ

pth_time_t pth_timeout(long sec, long usec);

Also a constructor for a "pth_time_t" structure, also there so you can dodge temporary structure values. It returns a pth_time_t structure holding the absolute time value calculated by adding sec and usec to the current time. โœจ

Sfdisc_t *pth_sfiodisc(void);

This function is always available, but it's only reasonably usable when Pth was built with Sfio support (the "--with-sfio" option) and "PTH_EXT_SFIO" is then defined by "pth.h". ๐Ÿฆ‹

It's useful for applications that want to use the comprehensive Sfio I/O library together with the Pth threading library. then this function is your girl: call it to get an Sfio discipline structure ("Sfdisc_t") which you can push onto Sfio streams ("Sfio_t") so that the stream uses pth_read(3)/pth_write(2) instead of read(2)/write(2). the benefit? I/O on the Sfio stream only blocks the current thread instead of the whole process. main character behaviour honestly. ๐Ÿ‘‘

The application has to free(3) the "Sfdisc_t" structure when it is no longer needed โ€” clean up after yourself, bestie, no leaving dishes in the sink. ๐Ÿงš

The Sfio package can be found at http://www.research.att.com/sw/tools/sfio/.


๐Ÿšซ Cancellation Management ๐Ÿšซ

Pth supports POSIX style thread cancellation via pth_cancel(3) and the following two related functions:

void pth_cancel_state(int newstate, int *oldstate);

This manages the cancellation state of the current thread. ๐ŸŽ€

  • when oldstate is not "NULL", the function stores the old cancellation state under the variable pointed to by oldstate
  • when newstate is not 0, it sets the new cancellation state

and heads up: oldstate is created before newstate is set. order matters, don't come for me. ๐Ÿ’…

A state is a combination of "PTH_CANCEL_ENABLE" or "PTH_CANCEL_DISABLE" and "PTH_CANCEL_DEFERRED" or "PTH_CANCEL_ASYNCHRONOUS".

  • "PTH_CANCEL_ENABLEโŽชPTH_CANCEL_DEFERRED" (or "PTH_CANCEL_DEFAULT") is the default state โ€” cancellation is possible, but only at cancellation points. chill, controlled, scheduled.
  • use "PTH_CANCEL_DISABLE" to completely disable cancellation for a thread.
  • use "PTH_CANCEL_ASYNCHRONOUS" for allowing asynchronous cancellations, i.e., cancellations which can happen at any time. that's the unhinged one, ok? ๐Ÿ˜ญ๐Ÿ”ฅ

void pth_cancel_point(void);

This explicitly enters a cancellation point. When the current cancellation state is "PTH_CANCEL_DISABLE" or no cancellation request is pending, this has no side-effect and returns immediately โ€” like, nothing happened, we move on. ๐Ÿ™ˆ

Else it calls "pth_exit(PTH_CANCELED)". bye bestie! ๐Ÿ’€


๐ŸŽ‰ Event Handling ๐ŸŽ‰

Pth has a very flexible event facility which is linked into the scheduler through the pth_wait(3) function. The following functions provide the handling of event rings. ๐ŸŒˆ

pth_event_t pth_event(unsigned long spec, ...);

This creates a new event ring consisting of a single initial event. The type of the generated event is specified by spec. The following types are available:

  • "PTH_EVENT_FD" โ€” this is a file descriptor event. one or more of "PTH_UNTIL_FD_READABLE", "PTH_UNTIL_FD_WRITEABLE" or "PTH_UNTIL_FD_EXCEPTION" have to be OR-ed into spec to specify on which state of the file descriptor you want to wait. the file descriptor itself has to be given as an additional argument. Example: "pth_event(PTH_EVENT_FDโŽชPTH_UNTIL_FD_READABLE, fd)".
  • "PTH_EVENT_SELECT" โ€” this is a multiple file descriptor event modeled directly after the select(2) call (actually it is also used to implement pth_select(3) internally). it's a convenient way to wait for a large set of file descriptors at once, and at each file descriptor for a different type of state. plus, as a nice side-effect, you receive the number of file descriptors which causes the event to be occurred (using BSD semantics, i.e., when a file descriptor occurred in two sets it's counted twice ๐Ÿ‘€). The arguments correspond directly to the select(2) function arguments except that there is no timeout argument (because timeouts already can be handled via "PTH_EVENT_TIME" events).

Example: "pth_event(PTH_EVENT_SELECT, &rc, nfd, rfds, wfds, efds)" where "rc" has to be of type "int *", "nfd" has to be of type "int" and "rfds", "wfds" and "efds" have to be of type "fd_set *" (see select(2)). The number of occurred file descriptors are stored in "rc".

  • "PTH_EVENT_SIGS" โ€” this is a signal set event. the two additional arguments have to be a pointer to a signal set (type "sigset_t *") and a pointer to a signal number variable (type "int *"). This event waits until one of the signals in the signal set occurred. as a result the occurred signal number is stored in the second additional argument.
Keep in mind that the Pth scheduler doesn't block signals automatically. So when you want to wait for a signal with this event you've to block it via sigprocmask(2) or it will be delivered without your notice. ๐Ÿ˜ณ

Example: "sigemptyset(&set); sigaddset(&set, SIGINT); pth_event(PTH_EVENT_SIG, &set, &sig);".

  • "PTH_EVENT_TIME" โ€” this is a time point event. the additional argument has to be of type "pth_time_t" (usually on-the-fly generated via pth_time(3)). This event waits until the specified time point has elapsed. keep in mind that the value is an absolute time point and not an offset. when you want to wait for a specified amount of time, you've to add the current time to the offset (usually on-the-fly achieved via pth_timeout(3)). Example: "pth_event(PTH_EVENT_TIME, pth_timeout(2,0))".
  • "PTH_EVENT_MSG" โ€” this is a message port event. the additional argument has to be of type "pth_msgport_t". This event waits until one or more messages were received on the specified message port. Example: "pth_event(PTH_EVENT_MSG, mp)". ๐Ÿ“ฌ
  • "PTH_EVENT_TID" โ€” this is a thread event. the additional argument has to be of type "pth_t". One of "PTH_UNTIL_TID_NEW", "PTH_UNTIL_TID_READY", "PTH_UNTIL_TID_WAITING" or "PTH_UNTIL_TID_DEAD" has to be OR-ed into spec to specify on which state of the thread you want to wait. Example: "pth_event(PTH_EVENT_TIDโŽชPTH_UNTIL_TID_DEAD, tid)".
  • "PTH_EVENT_FUNC" โ€” this is a custom callback function event. three additional arguments have to be given with the following types: "int (*)(void *)", "void *" and "pth_time_t". the first is a function pointer to a check function and the second argument is a user-supplied context value which is passed to this function. The scheduler calls this function on a regular basis (on his own scheduler stack, so be very careful! โš ๏ธ) and the thread is kept sleeping while the function returns "FALSE". Once it returned "TRUE" the thread will be awakened. The check interval is defined by the third argument, i.e., the check function is polled again not until this amount of time elapsed. Example: "pth_event(PTH_EVENT_FUNC, func, arg, pth_time(0,500000))".

unsigned long pth_event_typeof(pth_event_t ev);

This returns the type of event ev. It's a combination of the describing "PTH_EVENT_XX" and "PTH_UNTIL_XX" value. This is especially useful to know which arguments have to be supplied to the pth_event_extract(3) function. ๐Ÿ”

int pth_event_extract(pth_event_t ev, ...);

When pth_event(3) is treated like sprintf(3), then this function is sscanf(3) โ€” i.e., it is the inverse operation of pth_event(3). Which means it can be used to extract the ingredients of an event. ๐Ÿงช

The ingredients are stored into variables which are given as pointers on the variable argument list. Which pointers have to be present depends on the event type and has to be determined by the caller before, via pth_event_typeof(3).

To make it clear, when you constructed ev via "ev = pth_event(PTH_EVENT_FD, fd);" you have to extract it via "pth_event_extract(ev, &fd)", etc. For multiple arguments of an event the order of the pointer arguments is the same as for pth_event(3). But always keep in mind that you have to always supply pointers to variables and these variables have to be of the same type as the argument of pth_event(3) required. type match or it's a whole mess, bestie. ๐Ÿ’…

pth_event_t pth_event_concat(pth_event_t ev, ...);

This concatenates one or more additional event rings to the event ring ev and returns ev. The end of the argument list has to be marked with a "NULL" argument. Use this function to create real event rings out of the single-event rings created by pth_event(3). ๐Ÿ”—

pth_event_t pth_event_isolate(pth_event_t ev);

This isolates the event ev from possibly appended events in the event ring.

  • when in ev only one event exists, this returns "NULL".
  • when remaining events exist, they form a new event ring which is returned. โœ‚๏ธ
  • pth_event_t pth_event_walk(pth_event_t ev, int direction); โ€” okay so this one walks around the event ring ev ๐Ÿ’ซ. When direction is "PTH_WALK_NEXT" it steps to the next event, when it's "PTH_WALK_PREV" it just previews the previous one, and either way it hands you back the newly reached event. And because the devs love a little combo move, you can OR "PTH_UNTIL_OCCURRED" into direction to walk to the next/previous event in the ring ev that has actually occurred. ๐Ÿšถโ€โ™€๏ธ
  • pth_status_t pth_event_status(pth_event_t ev); โ€” gives you the status of event ev. And bestie, this is fast โšก because it only peeks at a tag on ev that the scheduler either set or didn't set yet. So likeโ€ฆ it does not go check the event itself, it just reports the scheduler's most recent knowledge of it. The status codes it can return are: "PTH_STATUS_PENDING" (still pending, chill), "PTH_STATUS_OCCURRED" (it happened, slay โœจ), "PTH_STATUS_FAILED" (it failed, rip ๐Ÿ’€).
  • int pth_event_free(pth_event_t ev, int mode); โ€” frees the event ev (when mode is "PTH_FREE_THIS") or all the events appended to the event ring under ev (when mode is "PTH_FREE_ALL"). Cleanup era ๐Ÿงน

๐ŸŒธ Key-Based Storage ๐ŸŒธ

These functions give you thread-local storage through unique keys, kinda like the POSIX Pthread API does. Use this for thread-specific global data, iykyk. ๐Ÿ’–

  • int pth_key_create(pth_key_t *key, void (*func)(void *)); โ€” creates a brand new unique key and stores it in key. Bonus: func can name a destructor function that gets called when the current thread terminates with that key. ๐ŸŽ€
  • int pth_key_delete(pth_key_t key); โ€” explicitly destroys a key key. No takebacks. ๐Ÿ’…
  • int pth_key_setdata(pth_key_t key, const void *value); โ€” stores value under key. ๐Ÿ“ฅ
  • void *pth_key_getdata(pth_key_t key); โ€” retrieves the value under key. ๐Ÿ“ค

๐Ÿฆ‹ Message Port Communication ๐Ÿฆ‹

These functions hand you message ports, which are the move for efficient and flexible inter-thread communication. โœจ

  • pth_msgport_t pth_msgport_create(const char *name); โ€” returns a pointer to a fresh new message port. If name is not "NULL", that name can be used by other threads via pth_msgport_find(3) to find the message port โ€” perfect for when they don't directly know the pointer to it. ๐Ÿซถ
  • void pth_msgport_destroy(pth_msgport_t mp); โ€” destroys a message port mp. But first, every pending message on it gets replied to its origin message port. Polite queen behavior ๐Ÿ‘‘
  • pth_msgport_t pth_msgport_find(const char *name); โ€” finds a message port in the system by name and returns the pointer to it. ๐Ÿ”
  • int pth_msgport_pending(pth_msgport_t mp); โ€” returns the number of pending messages sitting on message port mp. ๐Ÿ”ข
  • int pth_msgport_put(pth_msgport_t mp, pth_message_t *m); โ€” puts (or sends) a message m to message port mp. ๐Ÿ“จ
  • pth_message_t *pth_msgport_get(pth_msgport_t mp); โ€” gets (or receives) the top message from message port mp. Incoming messages always live in a queue, so yeah, there can totally be more pending messages, of course. ๐Ÿ“ฌ
  • int pth_msgport_reply(pth_message_t *m); โ€” replies a message m to the message port of the sender. ๐Ÿ’Œ

๐ŸŽ€ Thread Cleanups ๐ŸŽ€

Per-thread cleanup functions, because tidiness is a lifestyle. ๐Ÿงผ

  • int pth_cleanup_push(void (*handler)(void *), void *arg); โ€” pushes the routine handler onto the stack of cleanup routines for the current thread. These routines get called in LIFO order when the thread terminates. Stack energy fr fr ๐Ÿ“š
  • int pth_cleanup_pop(int execute); โ€” pops the top-most routine off the stack of cleanup routines for the current thread. When execute is "TRUE" the routine is additionally called on the way out. ๐Ÿ’…

๐ŸŒˆ Process Forking ๐ŸŒˆ

The following functions provide some special support for process forking situations inside the threading environment. Fork drama, but make it managed. ๐Ÿด

  • int pth_atfork_push(void (*prepare)(void *), void (*)(void *parent), void (*)(void *child), void *arg); โ€” this declares forking handlers to be called before and after pth_fork(3), in the context of the thread that called pth_fork(3). The prepare handler runs before fork(2) processing commences. The parent handler runs after fork(2) processing completes in the parent process. The child handler runs after fork(2) processing completed in the child process. If you don't want handling at one or more of these three points, just pass the corresponding handler as "NULL". Every handler gets called with arg as its argument.

The order of calls to pth_atfork_push(3) is significant. The parent and child handlers are called in the order in which they were established by calls to pth_atfork_push(3), i.e., FIFO. The prepare fork handlers are called in the opposite order, i.e., LIFO. So yes, one of them goes backwards, iconic chaos ๐ŸŒช๏ธ

  • int pth_atfork_pop(void); โ€” removes the top-most handlers on the forking handler stack which were established with the last pth_atfork_push(3) call. It returns "FALSE" when no more handlers couldn't be removed from the stack. ๐Ÿ—‘๏ธ
  • pid_t pth_fork(void); โ€” this is a variant of fork(2) with one big difference: only the current thread gets forked into a separate process. Meaning in the parent process nothing changes, while in the child process all threads are gone except for the scheduler and the calling thread. If you really want to duplicate all threads in the current process you should use fork(2) directly โ€” but honestly that's usually not reasonable. ๐Ÿ’€ Additionally this function takes care of forking handlers as established by pth_fork_push(3). ๐Ÿงฌ

โญ Synchronization โญ

The following functions provide synchronization support via mutual exclusion locks (mutex), read-write locks (rwlock), condition variables (cond) and barriers (barrier). Keep in mind that in a non-preemptive threading system like Pth this might sound unnecessary at first look, because a thread isn't interrupted by the system. And actually, when you have a critical code section which doesn't contain any pth_xxx() functions, you don't need any mutex to protect it, of course. ๐Ÿ˜Œ

But when your critical code section contains any pth_xxx() function, the chance is high that these temporarily switch to the scheduler. And this way other threads can make progress and enter your critical code section, too. This is especially true for critical code sections which implicitly or explicitly use the event mechanism. So likeโ€ฆ lock your stuff bestie ๐Ÿ”

  • int pth_mutex_init(pth_mutex_t *mutex); โ€” dynamically initializes a mutex variable of type "pth_mutex_t". Alternatively you can also use static initialization via "pth_mutex_t mutex = PTH_MUTEX_INIT". โœจ
  • int pth_mutex_acquire(pth_mutex_t *mutex, int try, pth_event_t ev); โ€” acquires a mutex mutex. If the mutex is already locked by another thread, the current thread's execution is suspended until the mutex is unlocked again, or additionally the extra events in ev occurred (when ev is not "NULL"). Recursive locking is explicitly supported, i.e., a thread is allowed to acquire a mutex more than once before it's released. But then it also has to be released the same number of times until the mutex is again lockable by others. ๐Ÿ” When try is "TRUE" this function never suspends execution. Instead it returns "FALSE" with "errno" set to "EBUSY". ๐Ÿ™…โ€โ™€๏ธ
  • int pth_mutex_release(pth_mutex_t *mutex); โ€” decrements the recursion locking count on mutex and when it hits zero it releases the mutex mutex. ๐Ÿ•Š๏ธ
  • int pth_rwlock_init(pth_rwlock_t *rwlock); โ€” dynamically initializes a read-write lock variable of type "pth_rwlock_t". Alternatively you can also use static initialization via "pth_rwlock_t rwlock = PTH_RWLOCK_INIT". โœจ
  • int pth_rwlock_acquire(pth_rwlock_t *rwlock, int op, int try, pth_event_t ev); โ€” acquires a read-only (when op is "PTH_RWLOCK_RD") or a read-write (when op is "PTH_RWLOCK_RW") lock rwlock. When the lock is only locked by other threads in read-only mode, the lock succeeds. But when one thread holds a read-write lock, all locking attempts suspend the current thread until this lock is released again. Additionally in ev you can give events to let the locking timeout, etc. When try is "TRUE" this function never suspends execution. Instead it returns "FALSE" with "errno" set to "EBUSY". ๐Ÿšฆ
  • int pth_rwlock_release(pth_rwlock_t *rwlock); โ€” releases a previously acquired (read-only or read-write) lock. Bye lock ๐Ÿ‘‹
  • int pth_cond_init(pth_cond_t *cond); โ€” dynamically initializes a condition variable variable of type "pth_cond_t". Alternatively you can also use static initialization via "pth_cond_t cond = PTH_COND_INIT". โœจ
  • int pth_cond_await(pth_cond_t *cond, pth_mutex_t *mutex, pth_event_t ev); โ€” awaits a condition situation. The caller has to follow the semantics of the POSIX condition variables: mutex has to be acquired before this function is called. The execution of the current thread is then suspended either until the events in ev occurred (when ev is not "NULL") or cond was notified by another thread via pth_cond_notify(3). While the thread is waiting, mutex is released. And before it returns, mutex is reacquired. ๐ŸŽ
  • int pth_cond_notify(pth_cond_t *cond, int broadcast); โ€” notifies one or all threads which are waiting on cond. When broadcast is "TRUE" all threads are notified, else only a single (unspecified) one. ๐Ÿ“ฃ
  • int pth_barrier_init(pth_barrier_t *barrier, int threshold); โ€” dynamically initializes a barrier variable of type "pth_barrier_t". Alternatively you can also use static initialization via "pth_barrier_t barrier = PTH_BARRIER_INIT("threadhold")". โœจ
  • int pth_barrier_reach(pth_barrier_t *barrier); โ€” reaches a barrier barrier. If this is the last thread (as specified by threshold on init of barrier) all threads are awakened. Else the current thread is suspended until the last thread reached the barrier and this way awakes all threads. ๐ŸŒŠ The function returns (beside "FALSE" on error) the value "TRUE" for any thread which neither reached the barrier as the first nor the last thread; "PTH_BARRIER_HEADLIGHT" for the thread which reached the barrier as the first thread and "PTH_BARRIER_TAILLIGHT" for the thread which reached the barrier as the last thread. ๐Ÿ

๐Ÿ’œ User-Space Context ๐Ÿ’œ

The following functions provide a stand-alone sub-API for user-space context switching. Internally it's based on the same underlying machine context switching mechanism the threads in GNU Pth are based on. Hence these functions you can use for implementing your own simple user-space threads. The "pth_uctx_t" context is somewhat modeled after POSIX ucontext(3). ๐Ÿงตโœจ

okay bestie, quick lil reality check on the ~vibes~ of making a user-space context ๐Ÿ’…

The time required to create (via pth_uctx_make(3)) a user-space context can range from just a few microseconds up to a more dramatical time (depending on the machine context switching method which is available on the platform). On the other hand, the raw performance in switching the user-space contexts is always very good (nearly independent of the used machine context switching method). For instance, on an Intel Pentium-III CPU with 800Mhz running under FreeBSD 4 one usually achieves about 260,000 user-space context switches (via pth_uctx_switch(3)) per second.

so like... making one? could be a lil slow, could be a whole dramatic saga, depends on your platform's machine context switching method. switching them though? iconic. always fast. basically doesn't even care which method you're on. that's the slay. โœจ

int pth_uctx_create(pth_uctx_t *uctx);

bestie this one just... makes a user-space context and stores it into uctx. and that's IT. there's still no underlying user-space context configured. you literally still have to do that yourself with pth_uctx_make(3). no shortcuts, no autopilot, it's giving "you have to finish the job yourself." On success, this function returns "TRUE", else "FALSE". ๐Ÿ’–

int pth_uctx_make(pth_uctx_t uctx, char *sk_addr, size_t sk_size, const
sigset_t *sigmask, void (*start_func)(void *), void *start_arg,
pth_uctx_t uctx_after);

okay THIS one is the main character. โœจ It makes a new user-space context in uctx which will operate on the run-time stack sk_addr (which is of maximum size sk_size), with the signals in sigmask blocked (if sigmask is not "NULL") and starting to execute with the call start_func(start_arg).

If sk_addr is "NULL", a stack is dynamically allocated. the stack size sk_size has to be at least 16384 (16KB). like that's not a suggestion bestie, that's the floor.

If the start function start_func returns and uctx_after is not "NULL", an implicit user-space context switch to this context is performed. Else (if uctx_after is "NULL") the process is terminated with exit(3). so if you leave that last arg as NULL and your function comes back... yeah. the whole process is done. gone. ๐Ÿ’€

This function is somewhat modeled after POSIX makecontext(3). On success, this function returns "TRUE", else "FALSE". ๐Ÿซถ

int pth_uctx_switch(pth_uctx_t uctx_from, pth_uctx_t uctx_to);

the switcheroo. the plot twist. the swap. ๐Ÿ’… This function saves the current user-space context in uctx_from for later restoring by another call to pth_uctx_switch(3) and restores the new user-space context from uctx_to, which previously had to be set with either a previous call to pth_uctx_switch(3) or initially by pth_uctx_make(3).

This function is somewhat modeled after POSIX swapcontext(3). If uctx_from or uctx_to are "NULL" or if uctx_to contains no valid user-space context, "FALSE" is returned instead of "TRUE". These are the only errors possible. so yeah, just those two ways to get a FALSE. that's the whole error menu. ๐Ÿ“

int pth_uctx_destroy(pth_uctx_t uctx);

the clean up era. ๐ŸŒธ This function destroys the user-space context in uctx. The run-time stack associated with the user-space context is deallocated only if it was not given by the application (see sk_addr of pth_uctx_create(3)). read that twice bestie โ€” if YOU handed it a stack, it's not gonna free it for you. you built it, you break it down.

If uctx is "NULL", "FALSE" is returned instead of "TRUE". This is the only error possible. ๐Ÿ˜Œ


โœจ Generalized POSIX Replacement API โœจ

The following functions are generalized replacements functions for the POSIX API, i.e., they are similar to the functions under Standard POSIX Replacement API but all have an additional event argument which can be used for timeouts, etc.

basically: same energy as the POSIX ones but with a bonus ev argument so you can do timeouts and stuff. ๐Ÿ’–
int pth_sigwait_ev(const sigset_t *set, int *sig, pth_event_t ev);

This is equal to pth_sigwait(3) (see below), but has an additional event argument ev. When pth_sigwait(3) suspends the current threads execution it usually only uses the signal event on set to awake. With this function any number of extra events can be used to awake the current thread (remember that ev actually is an event ring). ๐Ÿ’ซ

int pth_connect_ev(int s, const struct sockaddr *addr, socklen_t
addrlen, pth_event_t ev);

This is equal to pth_connect(3) (see below), but has an additional event argument ev. When pth_connect(3) suspends the current threads execution it usually only uses the I/O event on s to awake. With this function any number of extra events can be used to awake the current thread (remember that ev actually is an event ring). ๐Ÿฆ‹

int pth_accept_ev(int s, struct sockaddr *addr, socklen_t *addrlen,
pth_event_t ev);

This is equal to pth_accept(3) (see below), but has an additional event argument ev. When pth_accept(3) suspends the current threads execution it usually only uses the I/O event on s to awake. With this function any number of extra events can be used to awake the current thread (remember that ev actually is an event ring). โœจ

int pth_select_ev(int nfd, fd_set *rfds, fd_set *wfds, fd_set *efds,
struct timeval *timeout, pth_event_t ev);

This is equal to pth_select(3) (see below), but has an additional event argument ev. When pth_select(3) suspends the current threads execution it usually only uses the I/O event on rfds, wfds and efds to awake. With this function any number of extra events can be used to awake the current thread (remember that ev actually is an event ring). ๐Ÿ’œ

int pth_poll_ev(struct pollfd *fds, unsigned int nfd, int timeout,
pth_event_t ev);

This is equal to pth_poll(3) (see below), but has an additional event argument ev. When pth_poll(3) suspends the current threads execution it usually only uses the I/O event on fds to awake. With this function any number of extra events can be used to awake the current thread (remember that ev actually is an event ring). ๐ŸŒˆ

ssize_t pth_read_ev(int fd, void *buf, size_t nbytes, pth_event_t ev);

This is equal to pth_read(3) (see below), but has an additional event argument ev. When pth_read(3) suspends the current threads execution it usually only uses the I/O event on fd to awake. With this function any number of extra events can be used to awake the current thread (remember that ev actually is an event ring). ๐ŸŽ€

ssize_t pth_readv_ev(int fd, const struct iovec *iovec, int iovcnt,
pth_event_t ev);

This is equal to pth_readv(3) (see below), but has an additional event argument ev. When pth_readv(3) suspends the current threads execution it usually only uses the I/O event on fd to awake. With this function any number of extra events can be used to awake the current thread (remember that ev actually is an event ring). ๐Ÿ’–

ssize_t pth_write_ev(int fd, const void *buf, size_t nbytes, pth_event_t
ev);

This is equal to pth_write(3) (see below), but has an additional event argument ev. When pth_write(3) suspends the current threads execution it usually only uses the I/O event on fd to awake. With this function any number of extra events can be used to awake the current thread (remember that ev actually is an event ring). ๐Ÿ”ฅ

ssize_t pth_writev_ev(int fd, const struct iovec *iovec, int iovcnt,
pth_event_t ev);

This is equal to pth_writev(3) (see below), but has an additional event argument ev. When pth_writev(3) suspends the current threads execution it usually only uses the I/O event on fd to awake. With this function any number of extra events can be used to awake the current thread (remember that ev actually is an event ring). ๐Ÿ’…

ssize_t pth_recv_ev(int fd, void *buf, size_t nbytes, int flags,
pth_event_t ev);

This is equal to pth_recv(3) (see below), but has an additional event argument ev. When pth_recv(3) suspends the current threads execution it usually only uses the I/O event on fd to awake. With this function any number of extra events can be used to awake the current thread (remember that ev actually is an event ring). ๐Ÿงš

ssize_t pth_recvfrom_ev(int fd, void *buf, size_t nbytes, int flags,
struct sockaddr *from, socklen_t *fromlen, pth_event_t ev);

This is equal to pth_recvfrom(3) (see below), but has an additional event argument ev. When pth_recvfrom(3) suspends the current threads execution it usually only uses the I/O event on fd to awake. With this function any number of extra events can be used to awake the current thread (remember that ev actually is an event ring). ๐ŸŒธ

ssize_t pth_send_ev(int fd, const void *buf, size_t nbytes, int flags,
pth_event_t ev);

This is equal to pth_send(3) (see below), but has an additional event argument ev. When pth_send(3) suspends the current threads execution it usually only uses the I/O event on fd to awake. With this function any number of extra events can be used to awake the current thread (remember that ev actually is an event ring). ๐Ÿ’Œ

ssize_t pth_sendto_ev(int fd, const void *buf, size_t nbytes, int flags,
const struct sockaddr *to, socklen_t tolen, pth_event_t ev);

This is equal to pth_sendto(3) (see below), but has an additional event argument ev. When pth_sendto(3) suspends the current threads execution it usually only uses the I/O event on fd to awake. With this function any number of extra events can be used to awake the current thread (remember that ev actually is an event ring). โญ


โœจ Standard POSIX Replacement API โœจ

The following functions are standard replacements functions for the POSIX API. The difference is mainly that they suspend the current thread only instead of the whole process in case the file descriptors will block.

the glow-up here: instead of the ENTIRE process going to sleep, it's just the one thread. your other threads keep vibing. main character energy for real. ๐Ÿ‘‘
int pth_nanosleep(const struct timespec *rqtp, struct timespec *rmtp);

This is a variant of the POSIX nanosleep(3) function. It suspends the current threads execution until the amount of time in rqtp elapsed. The thread is guaranteed to not wake up before this time, but because of the non-preemptive scheduling nature of Pth, it can be awakened later, of course. so: never early, sometimes late. that's just how Pth is built. ngl. If rmtp is not "NULL", the "timespec" structure it references is updated to contain the unslept amount (the request time minus the time actually slept time). The difference between nanosleep(3) and pth_nanosleep(3) is that that pth_nanosleep(3) suspends only the execution of the current thread and not the whole process. ๐Ÿ˜ด

int pth_usleep(unsigned int usec);

This is a variant of the 4.3BSD usleep(3) function. It suspends the current threads execution until usec microseconds (= usec*1/1000000 sec) elapsed. The thread is guaranteed to not wake up before this time, but because of the non-preemptive scheduling nature of Pth, it can be awakened later, of course. The difference between usleep(3) and pth_usleep(3) is that that pth_usleep(3) suspends only the execution of the current thread and not the whole process. ๐Ÿ’ค

unsigned int pth_sleep(unsigned int sec);

This is a variant of the POSIX sleep(3) function. It suspends the current threads execution until sec seconds elapsed. The thread is guaranteed to not wake up before this time, but because of the non-preemptive scheduling nature of Pth, it can be awakened later, of course. The difference between sleep(3) and pth_sleep(3) is that pth_sleep(3) suspends only the execution of the current thread and not the whole process. ๐ŸŒ™

pid_t pth_waitpid(pid_t pid, int *status, int options);

okay so this one is the Pth glow-up of POSIX waitpid(2). It puts the current thread's execution on pause until status info shows up for a terminated child process, pid. The whole point, the entire vibe, is that pth_waitpid(3) only suspends the execution of the current thread โ€” not the whole entire process like waitpid(2) does. bestie that's the difference and it's huge. For all the deets on arguments and return code semantics, go look at waitpid(2). ๐Ÿซถ

int pth_system(const char *cmd);

this is the POSIX system(3) variant. It runs the shell command cmd using Bourne Shell ("sh") and suspends the current thread's execution until that command terminates. Same deal as above: the difference between system(3) and pth_system(3) is that pth_system(3) suspends only the current thread, not the whole process. ๐ŸŽ€ Arguments and return code semantics? system(3). Go read it.

int pth_sigmask(int how, const sigset_t *set, sigset_t *oset)

the Pth thread-related equivalent of POSIX sigprocmask(2), aka pthread_sigmask(3). The args how, set and oset relate directly to sigprocmask(2), because lowkey Pth just uses sigprocmask(2) internally right here. So yes, you could technically just call sigprocmask(2) yourself โ€” but for consistency reasons you should use pth_sigmask(3). do it for the culture. ๐Ÿ’…

int pth_sigwait(const sigset_t *set, int *sig);

a variant of the POSIX.1c sigwait(3) function. It suspends the current thread's execution until a signal in set happens, then stores the signal number in sig. Here's the iconic part: the signal is not delivered to a signal handler. Instead it gets caught by the scheduler, purely so it can awake the pth_sigwait() call. The trick โ€” and the noticeable point โ€” is that this way you get an asynchronous-aware application that's written completely synchronously. When you think about the whole problem of async-safe functions, you should recognize that this is a great benefit. no cap. โœจ

int pth_connect(int s, const struct sockaddr *addr, socklen_t addrlen);

a variant of the 4.2BSD connect(2) function. It establishes a connection on a socket s to the target specified in addr and addrlen. You already know the drill: the difference between connect(2) and pth_connect(3) is that pth_connect(3) suspends only the execution of the current thread and not the whole process. Arguments + return code semantics โ†’ connect(2). ๐Ÿ”ฅ

int pth_accept(int s, struct sockaddr *addr, socklen_t *addrlen);

a variant of the 4.2BSD accept(2) function. It accepts a connection on a socket by extracting the first connection request off the queue of pending connections, creates a new socket with the same properties as s, and allocates a new file descriptor for that socket (which is what gets returned). And yep โ€” the difference between accept(2) and pth_accept(3) is that pth_accept(3) suspends only the execution of the current thread, not the whole process. Arguments + return code semantics โ†’ accept(2). ๐Ÿฆ‹

int pth_select(int nfd, fd_set *rfds, fd_set *wfds, fd_set *efds, struct timeval *timeout);

a variant of the 4.2BSD select(2) function. It examines the I/O descriptor sets whose addresses are passed in rfds, wfds, and efds to see if some of their descriptors are ready for reading, ready for writing, or have an exceptional condition pending โ€” respectively, in that order. For more details about arguments and return code semantics, see select(2). โญ

int pth_pselect(int nfd, fd_set *rfds, fd_set *wfds, fd_set *efds, const struct timespec *timeout, const sigset_t *sigmask);

a variant of the POSIX pselect(2) function, which is itself a stronger variant of 4.2BSD select(2). The differences: the higher-resolution "struct timespec" gets passed instead of the lower-resolution "struct timeval", and a signal mask is specified which is temporarily set while waiting for input. Details on arguments and return code semantics โ†’ pselect(2) and select(2). ๐Ÿ‘‘

int pth_poll(struct pollfd *fds, unsigned int nfd, int timeout);

a variant of the SysV poll(2) function. It examines the I/O descriptors passed in the array fds to see if some of them are ready for reading, ready for writing, or have an exceptional condition pending โ€” respectively. Arguments + return code semantics โ†’ poll(2). ๐ŸŒธ

ssize_t pth_read(int fd, void *buf, size_t nbytes);

a variant of the POSIX read(2) function. It reads up to nbytes bytes into buf from file descriptor fd. The difference between read(2) and pth_read(2) is that pth_read(2) suspends execution of the current thread until the file descriptor is ready for reading. Arguments + return code semantics โ†’ read(2). ๐Ÿ“

ssize_t pth_readv(int fd, const struct iovec *iovec, int iovcnt);

a variant of the POSIX readv(2) function. It reads data from file descriptor fd into the first iovcnt rows of the iov vector. The difference between readv(2) and pth_readv(2) is that pth_readv(2) suspends execution of the current thread until the file descriptor is ready for reading. Arguments + return code semantics โ†’ readv(2). ๐Ÿ’œ

ssize_t pth_write(int fd, const void *buf, size_t nbytes);

a variant of the POSIX write(2) function. It writes nbytes bytes from buf to file descriptor fd. The difference between write(2) and pth_write(2) is that pth_write(2) suspends execution of the current thread until the file descriptor is ready for writing. Arguments + return code semantics โ†’ write(2). โœจ

ssize_t pth_writev(int fd, const struct iovec *iovec, int iovcnt);

a variant of the POSIX writev(2) function. It writes data to file descriptor fd from the first iovcnt rows of the iov vector. The difference between writev(2) and pth_writev(2) is that pth_writev(2) suspends execution of the current thread until the file descriptor is ready for reading. (yes bestie, that's literally what it says โ€” reading, on the write function. copying the lore verbatim, not my call ๐Ÿ™ˆ) For more details about arguments and return code semantics, see writev(2).

ssize_t pth_pread(int fd, void *buf, size_t nbytes, off_t offset);

a variant of the POSIX pread(3) function. It performs the same action as a regular read(2), except that it reads from a given position in the file without changing the file pointer. The first three arguments are the same as for pth_read(3), with the addition of a fourth argument offset for the desired position inside the file. ๐ŸŽ€

ssize_t pth_pwrite(int fd, const void *buf, size_t nbytes, off_t offset);

a variant of the POSIX pwrite(3) function. It performs the same action as a regular write(2), except that it writes to a given position in the file without changing the file pointer. The first three arguments are the same as for pth_write(3), with the addition of a fourth argument offset for the desired position inside the file. ๐Ÿซถ

ssize_t pth_recv(int fd, void *buf, size_t nbytes, int flags);

a variant of the SUSv2 recv(2) function, and it's equal to:

pth_recvfrom(fd, buf, nbytes, flags, NULL, 0)

that's the whole thing. it's giving "convenience wrapper" and we love that for it. ๐ŸŒˆ

ssize_t pth_recvfrom(int fd, void *buf, size_t nbytes, int flags, struct sockaddr *from, socklen_t *fromlen);

a variant of the SUSv2 recvfrom(2) function. It reads up to nbytes bytes into buf from file descriptor fd while using flags and from/fromlen. The difference between recvfrom(2) and pth_recvfrom(2) is that pth_recvfrom(2) suspends execution of the current thread until the file descriptor is ready for reading. Arguments + return code semantics โ†’ recvfrom(2). ๐Ÿฆ‹

ssize_t pth_send(int fd, const void *buf, size_t nbytes, int flags);

a variant of the SUSv2 send(2) function, and it's equal to:

pth_sendto(fd, buf, nbytes, flags, NULL, 0)

periodt. ๐Ÿ’…

ssize_t pth_sendto(int fd, const void *buf, size_t nbytes, int flags, const struct sockaddr *to, socklen_t tolen);

a variant of the SUSv2 sendto(2) function. It writes nbytes bytes from buf to file descriptor fd while using flags and to/tolen. The difference between sendto(2) and pth_sendto(2) is that pth_sendto(2) suspends execution of the current thread until the file descriptor is ready for writing. Arguments + return code semantics โ†’ sendto(2). ๐Ÿ‘‘

๐ŸŽ€ EXAMPLE ๐ŸŽ€

Okay bestie, here's a lil demo so you can feel the vibe of Pth, fr fr. This example is a completely useless server โ€” all it does is sit there listening on TCP port 12345 and dumping the current time into the socket the moment somebody connects. Not useful! But cute! ๐Ÿ’…

Every incoming connection gets its own freshly spawned thread. And because we wanted to flex the multithreading a bit more, there's also a totally pointless ticker thread running alongside, which spits the current time out to "stderr" every 5 seconds. The example has no error checking at all and it exists purely to show you the look and feel of Pth. Don't ship it. ๐Ÿซถ

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <netdb.h>
#include <unistd.h>
#include "pth.h"

#define PORT 12345

/* the socket connection handler thread */
static void *handler(void *_arg)
{
    int fd = (int)_arg;
    time_t now;
    char *ct;

    now = time(NULL);
    ct = ctime(&now);
    pth_write(fd, ct, strlen(ct));
    close(fd);
    return NULL;
}

/* the stderr time ticker thread */
static void *ticker(void *_arg)
{
    time_t now;
    char *ct;
    float load;

    for (;;) {
        pth_sleep(5);
        now = time(NULL);
        ct = ctime(&now);
        ct[strlen(ct)-1] = '\0';
        pth_ctrl(PTH_CTRL_GETAVLOAD, &load);
        printf("ticker: time: %s, average load: %.2f\n", ct, load);
    }
}

/* the main thread/procedure */
int main(int argc, char *argv[])
{
    pth_attr_t attr;
    struct sockaddr_in sar;
    struct protoent *pe;
    struct sockaddr_in peer_addr;
    int peer_len;
    int sa, sw;
    int port;

    pth_init();
    signal(SIGPIPE, SIG_IGN);

    attr = pth_attr_new();
    pth_attr_set(attr, PTH_ATTR_NAME, "ticker");
    pth_attr_set(attr, PTH_ATTR_STACK_SIZE, 64*1024);
    pth_attr_set(attr, PTH_ATTR_JOINABLE, FALSE);
    pth_spawn(attr, ticker, NULL);

    pe = getprotobyname("tcp");
    sa = socket(AF_INET, SOCK_STREAM, pe->p_proto);
    sar.sin_family = AF_INET;
    sar.sin_addr.s_addr = INADDR_ANY;
    sar.sin_port = htons(PORT);
    bind(sa, (struct sockaddr *)&sar, sizeof(struct sockaddr_in));
    listen(sa, 10);

    pth_attr_set(attr, PTH_ATTR_NAME, "handler");
    for (;;) {
        peer_len = sizeof(peer_addr);
        sw = pth_accept(sa, (struct sockaddr *)&peer_addr, &peer_len);
        pth_spawn(attr, handler, (void *)sw);
    }
}

๐Ÿ—๏ธ BUILD ENVIRONMENTS ๐Ÿ—๏ธ

So now we're gonna talk about the canonical ways to set up a build environment for a Pth-based program. And bestie, the range here is iconic โ€” it goes all the way from "literally three lines in a Makefile" to "okay this is a whole autoconf situation". Pick your fighter. โœจ

Manual Build Environment (Novice)

First up, super simple. Say we've got that test program from above chillin' in a source file called "foo.c". Then we can build a whole build environment by just adding this "Makefile":

$ vi Makefile
CC      = cc
CFLAGS  = `pth-config --cflags`
LDFLAGS = `pth-config --ldflags`
LIBS    = `pth-config --libs`

all: foo
foo: foo.o
    $(CC) $(LDFLAGS) -o foo foo.o $(LIBS)
foo.o: foo.c
    $(CC) $(CFLAGS) -c foo.c
clean:
    rm -f foo foo.o

See what's happening? We're importing the compiler and linker flags on-the-fly from the Pth installation, courtesy of its "pth-config" program. Straightforward, no notes, works great for small projects. ๐ŸŽ€

Autoconf Build Environment (Advanced)

Okay so that last one? Simple, yes. Flexible? Not really. Let's count the reasons, bestie:

  • First, to speed up building, it would be nice to not expand the compiler and linker flags every single time the compiler is started.
  • Second, it would be useful to also be able to build against an uninstalled Pth โ€” like, against a Pth source tree that was just configured and built but never installed.
  • Third, it would be useful to check the Pth version and make sure it's at least some minimum required version.
  • And finally, it would be great to make sure Pth actually works correctly by first doing some sanity compile and run-time checks.

All of that is doable if we use GNU autoconf and the "AC_CHECK_PTH" macro that Pth provides. So for this we set up the following three files. ๐ŸŒธ

First, we need the "Makefile" again โ€” but this time it's got autoconf placeholders and extra cleanup targets. And we name it "Makefile.in", because it's now an input file for autoconf:

$ vi Makefile.in
CC      = @CC@
CFLAGS  = @CFLAGS@
LDFLAGS = @LDFLAGS@
LIBS    = @LIBS@

all: foo
foo: foo.o
    $(CC) $(LDFLAGS) -o foo foo.o $(LIBS)
foo.o: foo.c
    $(CC) $(CFLAGS) -c foo.c
clean:
    rm -f foo foo.o
distclean:
    rm -f foo foo.o
    rm -f config.log config.status config.cache
    rm -f Makefile

Because autoconf generates extra files, we added a canonical "distclean" target to clean all that up. ๐Ÿ’–

Secondly, we wrote "configure.ac", a (minimal) autoconf script specification:

$ vi configure.ac
AC_INIT(Makefile.in)
AC_CHECK_PTH(1.3.0)
AC_OUTPUT(Makefile)

Then we let autoconf's "aclocal" program generate an "aclocal.m4" file for us, containing Pth's "AC_CHECK_PTH" macro. Then we generate the final "configure" script out of that "aclocal.m4" file and the "configure.ac" file:

$ aclocal --acdir=`pth-config --acdir`
$ autoconf

After all that, your working directory should look something like this:

$ ls -l
-rw-r--r--  1 rse  users    176 Nov  3 11:11 Makefile.in
-rw-r--r--  1 rse  users  15314 Nov  3 11:16 aclocal.m4
-rwxr-xr-x  1 rse  users  52045 Nov  3 11:16 configure
-rw-r--r--  1 rse  users     63 Nov  3 11:11 configure.ac
-rw-r--r--  1 rse  users   4227 Nov  3 11:11 foo.c

And if we run "configure" now, we get a correct "Makefile" that we can immediately use to build "foo" โ€” assuming Pth is already installed somewhere, so that "pth-config" is in $PATH. ๐Ÿฆ‹

$ ./configure
creating cache ./config.cache
checking for gcc... gcc
checking whether the C compiler (gcc   ) works... yes
checking whether the C compiler (gcc   ) is a cross-compiler... no
checking whether we are using GNU C... yes
checking whether gcc accepts -g... yes
checking how to run the C preprocessor... gcc -E
checking for GNU Pth... version 1.3.0, installed under /usr/local
updating cache ./config.cache
creating ./config.status
creating Makefile
rse@en1:/e/gnu/pth/ac
$ make
gcc -g -O2 -I/usr/local/include -c foo.c
gcc -L/usr/local/lib -o foo foo.o -lpth

Now, if Pth is installed in non-standard locations, or if "pth-config" just isn't in $PATH, you only have to drop the "configure" script a lil note about where to look โ€” run "configure" with the option --with-pth=dir, where dir is the argument that was used with the "--prefix" option back when Pth was installed. Easy. โœจ

Autoconf Build Environment with Local Copy of Pth (Expert)

Alright, final boss level. ๐Ÿงš Let's say our "foo" program lives under either a GPL or LGPL distribution license, and we want to make it a stand-alone package so it's easier to distribute and install. Meaning: we do not want to force the end-user to go install Pth just so our "foo" package will compile. Rude of us, honestly.

For this, it's convenient practice to just bundle the required libraries (here: Pth) into the source tree of the package (here: "foo"). And Pth ships with all the support needed to make this super easy. Say we want Pth in a subdirectory named "pth/", seamlessly integrated into the configuration and build process of "foo". ๐Ÿ‘‘

First we start with "Makefile.in" again, but this time it's the advanced version that supports subdirectory movement:

$ vi Makefile.in
CC      = @CC@
CFLAGS  = @CFLAGS@
LDFLAGS = @LDFLAGS@
LIBS    = @LIBS@

SUBDIRS = pth

all: subdirs_all foo

subdirs_all:
    @$(MAKE) $(MFLAGS) subdirs TARGET=all
subdirs_clean:
    @$(MAKE) $(MFLAGS) subdirs TARGET=clean
subdirs_distclean:
    @$(MAKE) $(MFLAGS) subdirs TARGET=distclean
subdirs:
    @for subdir in $(SUBDIRS); do \
        echo "===> $$subdir ($(TARGET))"; \
        (cd $$subdir; $(MAKE) $(MFLAGS) $(TARGET) || exit 1) || exit 1; \
        echo "<=== $$subdir"; \
    done

foo: foo.o
    $(CC) $(LDFLAGS) -o foo foo.o $(LIBS)
foo.o: foo.c
    $(CC) $(CFLAGS) -c foo.c

clean: subdirs_clean
    rm -f foo foo.o
distclean: subdirs_distclean
    rm -f foo foo.o
    rm -f config.log config.status config.cache
    rm -f Makefile

Then we create a slightly different autoconf script, "configure.ac":

$ vi configure.ac
AC_INIT(Makefile.in)
AC_CONFIG_AUX_DIR(pth)
AC_CHECK_PTH(1.3.0, subdir:pth --disable-tests)
AC_CONFIG_SUBDIRS(pth)
AC_OUTPUT(Makefile)

So what did we do here? We gave a default value for "foo"'s "--with-pth" option as the second argument to "AC_CHECK_PTH", which says that Pth can be found in the subdirectory named "pth/". We also specified that the "--disable-tests" option of Pth should get passed down to the "pth/" subdirectory, because we only need to build the Pth library itself. And we added an "AC_CONFIG_SUBDIR" call, which tells autoconf it should configure the "pth/" subdirectory too.

The "AC_CONFIG_AUX_DIR" directive was added purely to make autoconf happy, because it wants to find an "install.sh" or "shtool" script whenever "AC_CONFIG_SUBDIRS" is used. ๐Ÿ™ˆ

Now we let autoconf's "aclocal" program generate an "aclocal.m4" file for us again, with the contents of Pth's "AC_CHECK_PTH" macro. Then we generate the "configure" script out of that "aclocal.m4" file and the "configure.ac" file.

$ aclocal --acdir=`pth-config --acdir`
$ autoconf

Now we've gotta create the "pth/" subdirectory itself. For that, we extract the Pth distribution into the "foo" source tree and just rename it to "pth/":

$ gunzip <pth-X.Y.Z.tar.gz | tar xvf -
$ mv pth-X.Y.Z pth

And optionally, to reduce the size of the "pth/" subdirectory, we can strip the Pth sources down to a minimum with the striptease feature:

$ cd pth
$ ./configure
$ make striptease
$ cd ..

After all that, the source tree of "foo" should look similar to this:

$ ls -l
-rw-r--r--  1 rse  users    709 Nov  3 11:51 Makefile.in
-rw-r--r--  1 rse  users  16431 Nov  3 12:20 aclocal.m4
-rwxr-xr-x  1 rse  users  57403 Nov  3 12:21 configure
-rw-r--r--  1 rse  users    129 Nov  3 12:21 configure.ac
-rw-r--r--  1 rse  users   4227 Nov  3 11:11 foo.c
drwxr-xr-x  2 rse  users   3584 Nov  3 12:36 pth
$ ls -l pth/
-rw-rw-r--  1 rse  users   26344 Nov  1 20:12 COPYING
-rw-rw-r--  1 rse  users    2042 Nov  3 12:36 Makefile.in
-rw-rw-r--  1 rse  users    3967 Nov  1 19:48 README
-rw-rw-r--  1 rse  users     340 Nov  3 12:36 README.1st
-rw-rw-r--  1 rse  users   28719 Oct 31 17:06 config.guess
-rw-rw-r--  1 rse  users   24274 Aug 18 13:31 config.sub
-rwxrwxr-x  1 rse  users  155141 Nov  3 12:36 configure
-rw-rw-r--  1 rse  users  162021 Nov  3 12:36 pth.c
-rw-rw-r--  1 rse  users   18687 Nov  2 15:19 pth.h.in
-rw-rw-r--  1 rse  users    5251 Oct 31 12:46 pth_acdef.h.in
-rw-rw-r--  1 rse  users    2120 Nov  1 11:27 pth_acmac.h.in
-rw-rw-r--  1 rse  users    2323 Nov  1 11:27 pth_p.h.in
-rw-rw-r--  1 rse  users     946 Nov  1 11:27 pth_vers.c
-rw-rw-r--  1 rse  users   26848 Nov  1 11:27 pthread.c
-rw-rw-r--  1 rse  users   18772 Nov  1 11:27 pthread.h.in
-rwxrwxr-x  1 rse  users   26188 Nov  3 12:36 shtool

okay so that's the layout bestie, and now when we configure and build the "foo" package it looks a lil something like this:

$ ./configure
creating cache ./config.cache
checking for gcc... gcc
checking whether the C compiler (gcc   ) works... yes
checking whether the C compiler (gcc   ) is a cross-compiler... no
checking whether we are using GNU C... yes
checking whether gcc accepts -g... yes
checking how to run the C preprocessor... gcc -E
checking for GNU Pth... version 1.3.0, local under pth
updating cache ./config.cache
creating ./config.status
creating Makefile
configuring in pth
running /bin/sh ./configure  --enable-subdir --enable-batch
--disable-tests --cache-file=.././config.cache --srcdir=.
loading cache .././config.cache
checking for gcc... (cached) gcc
checking whether the C compiler (gcc   ) works... yes
checking whether the C compiler (gcc   ) is a cross-compiler... no
[...]
$ make
===> pth (all)
./shtool scpp -o pth_p.h -t pth_p.h.in -Dcpp -Cintern -M '==#==' pth.c
pth_vers.c
gcc -c -I. -O2 -pipe pth.c
gcc -c -I. -O2 -pipe pth_vers.c
ar rc libpth.a pth.o pth_vers.o
ranlib libpth.a
<=== pth
gcc -g -O2 -Ipth -c foo.c
gcc -Lpth -o foo foo.o -lpth

as you can see, autoconf now automatically configures the local (stripped down) copy of Pth in the subdirectory "pth/" and the "Makefile" automatically builds the subdirectory, too. slay ๐Ÿ’…


โœจ SYSTEM CALL WRAPPER FACILITY โœจ

so here's the tea โ˜• โ€” Pth per default uses an explicit API, including the system calls. So for instance you've got to explicitly use pth_read(3) when you need a thread-aware read(3), and you cannot expect that by just calling read(3) only the current thread is blocked. nope! Instead, with the standard read(3) call the whole process will be blocked. ๐Ÿ˜ญ

But because for some applications (mainly those consisting of lots of third-party stuff) this can be inconvenient โ€” like, you want a call to read(3) to 'magically' mean pth_read(3). The problem here is that such magic Pth cannot provide per default because it's not really portable. Nevertheless Pth provides a two step approach to solve this problem:

๐Ÿงธ Soft System Call Mapping

This variant is available on all platforms and can always be enabled by building Pth with "--enable-syscall-soft". This then triggers some "#define"'s in the "pth.h" header which map for instance read(3) to pth_read(3), etc. Currently the following functions are mapped: fork(2), nanosleep(3), usleep(3), sleep(3), sigwait(3), waitpid(2), system(3), select(2), poll(2), connect(2), accept(2), read(2), write(2), recv(2), send(2), recvfrom(2), sendto(2).

The drawback of this approach is just that really all source files of the application where these function calls occur have to include "pth.h", of course. And this also means that existing libraries, including the vendor's stdio, usually will still block the whole process if one of its I/O functions block. ๐Ÿ™ˆ

๐Ÿงจ Hard System Call Mapping

This variant is available only on those platforms where the syscall(2) function exists and there it can be enabled by building Pth with "--enable-syscall-hard". This then builds wrapper functions (for instance read(3)) into the Pth library which internally call the real Pth replacement functions (pth_read(3)). Currently the following functions are mapped: fork(2), nanosleep(3), usleep(3), sleep(3), waitpid(2), system(3), select(2), poll(2), connect(2), accept(2), read(2), write(2).

The drawback of this approach is that it depends on the syscall(2) interface and prototype conflicts can occur while building the wrapper functions due to different function signatures in the vendor C header files. But the advantage of this mapping variant is that the source files of the application where these function calls occur have not to include "pth.h" and that existing libraries, including the vendor's stdio, magically become thread-aware (and then block only the current thread). โœจ


๐Ÿ’œ IMPLEMENTATION NOTES ๐Ÿ’œ

Pth is very portable because it has only one part which perhaps has to be ported to new platforms (the machine context initialization). But it is written in a way which works on mostly all Unix platforms which support makecontext(2) or at least sigstack(2) or sigaltstack(2) [see "pth_mctx.c" for details]. Any other Pth code is POSIX and ANSI C based only. ๐Ÿ‘‘

The context switching is done via either SUSv2 makecontext(2) or POSIX make[sig]setjmp(3) and [sig]longjmp(3). Here all CPU registers, the program counter and the stack pointer are switched. Additionally the Pth dispatcher switches also the global Unix "errno" variable [see "pth_mctx.c" for details] and the signal mask (either implicitly via sigsetjmp(3) or in an emulated way via explicit setprocmask(2) calls).

The Pth event manager is mainly select(2) and gettimeofday(2) based, i.e., the current time is fetched via gettimeofday(2) once per context switch for time calculations and all I/O events are implemented via a single central select(2) call [see "pth_sched.c" for details].

The thread control block management is done via virtual priority queues without any additional data structure overhead. For this, the queue linkage attributes are part of the thread control blocks and the queues are actually implemented as rings with a selected element as the entry point [see "pth_tcb.h" and "pth_pqueue.c" for details].

Most time critical code sections (especially the dispatcher and event manager) are speeded up by inline functions (implemented as ANSI C pre-processor macros). Additionally any debugging code is completely removed from the source when not built with -DPTH_DEBUG (see Autoconf "--enable-debug" option), i.e., not only stub functions remain [see "pth_debug.c" for details]. ๐Ÿ’€


๐Ÿšซ RESTRICTIONS ๐Ÿšซ

Pth (intentionally) provides no replacements for non-thread-safe functions (like strtok(3) which uses a static internal buffer) or synchronous system functions (like gethostbyname(3) which doesn't provide an asynchronous mode where it doesn't block). When you want to use those functions in your server application together with threads, you've to either link the application against special third-party libraries (or for thread-safe/reentrant functions possibly against an existing "libc_r" of the platform vendor). For an asynchronous DNS resolver library use the GNU adns package from Ian Jackson ( see http://www.gnu.org/software/adns/adns.html ). ๐Ÿฆ‹


๐Ÿ“œ HISTORY ๐Ÿ“œ

The Pth library was designed and implemented between February and July 1999 by Ralf S. Engelschall after evaluating numerous (mostly preemptive) thread libraries and after intensive discussions with Peter Simons, Martin Kraemer, Lars Eilebrecht and Ralph Babel related to an experimental (matrix based) non-preemptive C++ scheduler class written by Peter Simons.

Pth was then implemented in order to combine the non-preemptive approach of multithreading (which provides better portability and performance) with an API similar to the popular one found in Pthread libraries (which provides easy programming).

So the essential idea of the non-preemptive approach was taken over from Peter Simons scheduler. The priority based scheduling algorithm was suggested by Martin Kraemer. Some code inspiration also came from an experimental threading library (rsthreads) written by Robert S. Thau for an ancient internal test version of the Apache webserver. The concept and API of message ports was borrowed from AmigaOS' Exec subsystem. The concept and idea for the flexible event mechanism came from Paul Vixie's eventlib (which can be found as a part of BIND v8). iconic lore honestly ๐ŸŽ€


๐Ÿซถ BUG REPORTS AND SUPPORT ๐Ÿซถ

If you think you have found a bug in Pth, you should send a report as complete as possible to bug-pth@gnu.org. If you can, please try to fix the problem and include a patch, made with '"diff -u3"', in your report. Always, at least, include a reasonable amount of description in your report to allow the author to deterministically reproduce the bug. ๐Ÿ™

For further support you additionally can subscribe to the pth-users@gnu.org mailing list by sending an Email to pth-users-request@gnu.org with "subscribe pth-users" (or "subscribe pth-users" address if you want to subscribe from a particular Email address) in the body. Then you can discuss your issues with other Pth users by sending messages to pth-users@gnu.org. Currently (as of August 2000) you can reach about 110 Pth users on this mailing list. Old postings you can find at http://www.mail-archive.com/pth-users@gnu.org/. ๐Ÿ’–

๐ŸŒˆ SEE ALSO ๐ŸŒˆ

okay bestie, here's where you go when you wanna keep reading, because I KNOW you're gonna wanna keep reading ๐Ÿ’…

like actual physical books, in your HANDS, the vintage aesthetic of it all ๐ŸŽ€

  • B. Nichols, D. Buttlar, J.P. Farrel: Pthreads Programming - A POSIX Standard for Better Multiprocessing, O'Reilly 1996; ISBN 1-56592-115-1
  • B. Lewis, D. J. Berg: Multithreaded Programming with Pthreads, Sun Microsystems Press, Prentice Hall 1998; ISBN 0-13-680729-1
  • B. Lewis, D. J. Berg: Threads Primer - A Guide To Multithreaded Programming, Prentice Hall 1996; ISBN 0-13-443698-9
  • S. J. Norton, M. D. Dipasquale: Thread Time - The Multithreaded Programming Guide, Prentice Hall 1997; ISBN 0-13-190067-6
  • D. R. Butenhof: Programming with POSIX Threads, Addison Wesley 1997; ISBN 0-201-63392-2

the whole little manpage friend group, no cap:


๐Ÿ‘‘ AUTHOR ๐Ÿ‘‘

the icon, the legend, the person who wrote all of this so you and me could have threads at home ๐Ÿซถ

Ralf S. Engelschall
rse@engelschall.com
www.engelschall.com

and that's the whole page bestie, you made it to the end, I'm SO proud of you ๐Ÿ˜ญ๐Ÿ’– go forth and thread responsibly. this little masterpiece is GNU Pth 2.0.7, dated 08-Jun-2006 โ€” yes that's old, yes it still ate, periodt. ๐Ÿ’…โœจ๐ŸŒธ

๐Ÿ“ฆ where this page comes from