Semaphores

A semaphore is an object that stores a non-negative integral value that can only be read or written to using atomic operations. Semaphores are shared resources that can be used to control access to other shared resources and synchronize tasks.

If the semaphore is an integer variable, it is called a counting semaphore. If it is a Boolean variable, it is called a mutex (mutual exclusion).

POSIX Semaphores

POSIX includes a set of system calls that provide functions to create, read, update, and delete semaphores in a set of semaphores.

semget() returns a semaphore id for a set of semaphores associated with a key.  All of the processes that share a set of semaphores call semget() with the same key and will receive from semget() the same semaphore id.  The second parameter specifies how many semaphores to create in the set.  The last parameter specifies flags for the operation, e.g. 0666 | IPC_CREATE.

#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/sem.h> 
int semget(key_t key, int nsems, int semflg);

The following example shows how to create a singleton set of semaphores.

char *filepath = "/tmp";  
int tokid = 0;  
key_t key;  
int semid;  

if ((key = ftok(filepath, tokid)) == -1) {      
    print_error("Cannot create token", errno);      
    return -1;  
}  
if ((semid = semget(key, 1, 0666 | IPC_CREAT)) == -1) {          
    print_error("Cannot create semaphore", errno);      
    return -1;  
}  

semctl() performs control operations on a set of semaphores. The first argument holds the semaphore id of the semaphore set.  The second argument holds the index of a particular semaphore.  The third argument specifies the command (e.g. SETVAL, GETVAL, SETALL, GETALL, IPC_RMID).  The last argument, if necessary, is a union. Check the man page for the union structure necessary for your system.

#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/sem.h> 

int semctl(int semid, int semnum, int cmd, ...);

The following examples demonstrates how to set the semaphore at index 0 to the value 0.

union semun { 
    int val; 
    struct semid_ds* buf; 
    ushort* array; 
} arg; 
arg.val = 0; 

if (semctl(semid, 0, SETVAL, arg) == -1) { 
    print_error("Error setting semaphore to 0", errno); 
    return -1; 
}

The following example shows how to get the value of the semaphore at index 0.

int semValue; 

if ((semValue = semctl(semid, 0, GETVAL)) == -1) { 
    print_error("Error getting semaphore value", errno); 
    return -1; 
}

The example below shows how to remove a set of semaphores.

if (semctl(semid, 0, IPC_RMID) == -1) { 
    print_error("Error releasing semaphore set", errno); 
    return -1; 
}

The last function is semop() which performs a set of operations on a set of semaphores.  Either all of the operations are executed atomically or none are.

#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/sem.h> 

int semop(int semid, struct sembuf *sops, unsigned nsops);

The following examples show how to increment and decrement each of the semaphores in the set.

 
struct sembuf op[1];  // array of ops 
op[0].sem_num = 0; 
op[0].sem_op = 1;     
op[0].sem_flg = 0; 

if (semop(semid, op, 1) == -1) {     
print_error("Error incrementing semaphore", errno);     
return -1; 
}
 
struct sembuf op[1]; 
op[0].sem_num = 0; 
op[0].sem_op = -1; 
op[0].sem_flg = 0; 

if (semop(semid, op, 1) == -1) {     
    print_error("Error decrementing semaphore", errno);     
    return -1; 
}

Sockets

For a definitive text on sockets programming see Stevens, Fenner, and Rudoff’s, Unix Network Programming, Volume 1: The Sockets Networking API, 3/E.

The word server can be used to refer to

  1. a host machine that runs one or more services or
  2. a program that runs on a host machine and provides a service

When we refer to server, we’ll be referring to a program that runs on a host machine and provides a service.

TCP/IP

Though sockets can use many different protocols to exchange data between machines, we will consider only TCP/IP, a pair of protocols initially developed by Robert E. Kahn (DARPA) and Vinton Cerf (ARPANET) in 1973.

  • TCP -Transmission Control Protocol ensures integrity of packets and reliability.
  • IP – Inter-networking Protocol is the protocol that specifies how data is exchanged across network boundaries. It is designed to be efficient and fast, but does not ensure integrity or reliability.

Two primary principles of the TCP/IP architecture are:

  1. End-to-end principle:  Most of the maintenance of the state of the connections and overall intelligence is placed at the hosts (TCP).
  2. Robustness principle: A sender must send well-formed datagrams and accept any well-formed datagram it can interpret.

Network Ports

A host computer can have multiple servers (programs) running at the same time and each can receive connections from remote clients.  Each of the servers provide a service and communicate with their clients using a particular protocol.  For example, an Apache web server communicates with its clients using the Hyper-text Transport Protocol (HTTP).  Here we can say that Apache is providing the HTTP service.

In order for the os to direct traffic to the correct server, each service is assigned a port number.  On UNIX operating systems the assignment of port numbers to individual services like HTTP is defined in  /etc/services.   Internet Assigned Numbers Authority (IANA) has the responsibility of assigning port numbers.

You’re probably familiar with the following services.  The numbers in parenthesis are their assigned port numbers.

  • FTP (21)
  • SSH (22)
  • HTTP (80)
  • SFTP (115)

Network IPC: Sockets

A socket is an abstraction of a communication endpoint in a client or server that can be read from or written to.  Sockets are used by processes to communicate with one another using message passing.  But unlike shared memory, sockets can be used by processes residing on different hosts and on different networks.

POSIX defines a set of functions that allow clients and servers to connect to one another and communicate with one another via sockets.  The chart below describes some of these functions and lists them in the order in which they are called in both the server and client.

Server Function Client Function
Get the host machine’s name gethostname()    
Get host information getaddrinfo()    
Create socket file desc (fd) socket()    
Bind the socket fd to the server’s host address and a specific port bind()    
Free resources freeaddrinfo()    
Listen on the fd for connections listen()    
    Get host information getaddrinfo()
    Create socket fd socket()
    Attempt to connect the socket fd to the server, a random unused port on the client is used. connect()
    Free resources freeaddrinfo()
Accept connection from client, get new fd to read and write to. accept()    
Write data to client fd send()    
    Receive data from the server recv()
    Write data to server send()
Receive data from the client fd recv()    
Close client fd – not the socket fd used for listening close() Close socket fd close()

These functions use the following header files.

#include <sys/types.h>
#include <netdb.h>   
    getaddrinfo()
    freeaddrinfo()

#include <sys/socket.h>
    getaddrinfo()
    freeaddrinfo()
    socket()
    bind()
    listen()
    accept()
    send()
    recv()

#include <unistd.h>
    gethostname()    
    close()

Getting the Host’s Name

getaddrinfo() requires a host name as the first parameter.  In the code for the server, we can call gethostname() to get that information.  For the client however, if the server resides on a different machine, the client must obtain the domain name or IP address of the server that it wants to connect to.  It can so by

  • hard coding some well known domain name or IP address
  • requiring the user to enter it on command line argument when starting the client
  • require the user to enter it via some application prompt (as in a web browser)

Below is code that can be used in the server to obtain its own host name.

char* host_name = malloc(HOST_NAME_MAX);  // make sure to free this later!!!
memset(host_name, 0, HOST_NAME_MAX);

if (gethostname(host_name, HOST_NAME_MAX) == -1) {
    print_error("gethostname error");
    exit(1);
}

Getting the Port and Host IP address for a Specific Service

Next, both the client and server need to get a list of IP addresses and port numbers for the specific service they want to provide (server) or connect to (client).  They do this by calling getaddrinfo().  In the example below, the service provided (server) and requested (client) is the File Transport Protocol (FTP) service.

struct addrinfo *host_ai;
struct addrinfo hint;

hint.ai_flags = 0;                     /* customize behavior */
hint.ai_family = 0;                    /* protocol family for socket */
hint.ai_socktype = SOCK_STREAM;        /* socket type */
hint.ai_protocol = 0;                  /* protocol for socket */
hint.ai_canonname = NULL;              /* canonical name for service location */
hint.ai_addr = NULL;                   /* socket-address for socket */
hint.ai_addrlen = 0;                   /* length in bytes of socket address */
hint.ai_next = NULL;                   /* pointer to next addrinfo in list */

if ((getaddrinfo(host_name, "ftp", &hint, &host_ai)) != 0) {
    print_error("getaddrinfo error");
    exit(1);
}

The getaddrinfo() function has the following signature:

int getaddrinfo (const char *hostname, const char *servname, const struct addrinfo *hints, struct addrinfo **res);

As stated in the manual page for getaddrinfo(), “The hostname and servname arguments are either pointers to NUL-terminated strings or the null pointer.  An acceptable value for hostname is either a valid hostname or a numeric host address string consisting of a dotted decimal IPv4 address or an IPv6 address.  The servname is either a decimal port number or a service name listed in services(5).”

On the server, the hostname will be the name returned by gethostname().  On the client, the hostname will often be an IP address or domain name.

Since each host can have multiple NIC cards, thus multiple IP addresses, getaddrinfo() stores in host_ai a pointer to a linked list of struct addrinfo elements, each holding a pointer (ai_addr) to a struct that has a port number and IP address.  The variable hint is used as a filter.

The ai_addr member points to a struct sockaddr of length ai_addrlen.

struct sockaddr {
    sa_family_t sa_family;           /* address family (e.g. IPv4) */
    in_port_t sa_port;               /* port */
    struct in_addr sa_addr;          /* address */
};
struct in_addr {
    in_addr_t s_addr;               /* IPv4 address */
};

Creating a Socket

Both the client and server require sockets.  The socket() function uses the sa_family field in an ai_addr struct and the type of stream (below) to set up a socket.  The socket() function, if successful, returns a file descriptor. The server will listen for activity and accept connections using its file descriptor and the client will make a connection to the server using its file descriptor.

int host_fd;

if ((host_fd = socket(host_ai->ai_addr->sa_family, SOCK_STREAM, 0)) == -1) {
    print_error("unable to create socket");
    exit(1);
}

The socket() function has the following signature:

int socket (int domain, int type, int protocol);

Domains

  • AF_INET – IPv4 internet domain
  • AF_INET6 – IPv6 Internet domain

Type

  • SOCK_STREAM – sequenced, reliable, bidirectional, connection-oriented byte stream
  • SOCK_DGRAM – fixed length, connectionless, unreliable message

The protocol is usually set to 0 to select the default protocol for the given domain and socket type.  The default protocol for SOCK_STREAM in the AF_INET domain is IPPROTO_TCP (Transmission Control Protocol)  and the default protocol of a SOCK_DGRAM socket in the AF_INET domain is IPPROTO_UDP (User Datagram Protocol) .

Note:  When the client or server is finished using the file descriptor it should call close() to free up the file resources.

On the Server: Binding an Address to a Socket

We use the bind() function to associate an address and port with the server’s socket.

if (bind(host_fd, host_ai->ai_addr, host_ai->ai_addrlen) == -1) {
    print_error("unable to bind to socket");
    exit(1);
}

freeaddrinfo(host_ai);

The bind() function has the following signature:

int bind (int sockfd, const struct sockaddr *addr, socklen_t len);

Restrictions

  • address must belong to machine that calls bind
  • port number cannot be less than 1024 unless the process has root privilege

We can use the sockaddr returned from getaddrinfo() for addr.

For an internet domain, if we specify the IP address as INADDR_ANY the socket is bound to all the system’s network interfaces, which means we can receive packets from any of the network interface cards.

On the Server: Listening for Connections

A server announces that it is willing to accept connect request from a client by calling the listen() function.

if (listen(host_fd, QLEN) == -1) {
    print_error("listen failed");
    exit(1);
}

The listen function has the following signature

int listen (int sockfd, int backlog);

The backlog value specifies the maximum length for the queue of pending transactions.

On the Server: Accepting Client Connections

The accept() function attempts to create a session with a client.  If successful, a file descriptor is created that is used by the server to read and write data from and to the client respectively.

A server will often accept multiple incoming connections.  In the future we’ll learn how to service individual requests in individual threads of execution.  Without threads, the connection request must be processed one at a time in a for-loop.

struct sockaddr client_sockaddr;
socklen_t client_sockaddr_len = sizeof(client_sockaddr);

for (;;) {
    printf("waiting for connection ...\n");
    int clfd = accept(host_fd, &client_sockaddr, &client_sockaddr_len);

    if (clfd == -1) {
        print_error("accept error");
        exit(1);
    }

    printf("accepted connection, socket [%d]\n", clfd);

    ...

    // serve the client

}

The accept() function has the following signature:

int accept (int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);

The file descriptor returned by accept() is a socket descriptor that is connected to the client.  The server file descriptor passed to accept() remains open to receive additional connections.

On the Client: Connecting to a Server

The connect() function is used to create a connection between the client’s socket and the server.

printf("attempting Connection\n");

if (connect(sockfd, host_ai->ai_addr, host_ai->ai_addrlen) == -1) {
    printf("can't connect to %s\n", argv[1]);
    print_error("Error connecting to server");
}

printf("connection made...\n");
freeaddrinfo(host_ai);

The connect function has the following signature:

int connect (int sockfd, const struct sockaddr *addr, socklen_t len);

The address we specify is the address of the server.

The connection might fail for several reasons, for example, the server is not up and running, no room in the server’s pending queue, etc.  If successful, we can read and write to the socket file descriptor in order to communicate with the server.

Reading and Writing to Sockets

On the server we can write to the file descriptor created by accept().  On the client, we can read and write to the file descriptor created by socket().  We read and write using the send() and recv() methods as shown below.

int token = 1;
int len = send(clfd, &token, sizeof(token), 0);

if (len == -1) {
    print_error("error sending data");
    // act acordingly
}

The send function has the following signature:

size_t send (int socket, const void *buffer, size_t length, int flags);

The send() function returns the number of bytes that was sent.  If the message is too long to pass atomically through the underlying protocol, EMSGSIZE is stored in errno.  If sending data in a character array we might do this:

int len = send(clfd, buf, strlen(buf), 0);

To read data from the socket we use recv().

int token;
int len = recv(sockfd, &token, 4, 0);

if (len == -1) {
    print_error("recv error");
    // act accordingly - possibly terminate

} else {
    printf("Token from server [%d]\n", token);
    // act accordingly
}

The recv function has the following signature:

size_t recv (int socket,void *buffer,size_t length, int  flags);
  • The recv() function returns the length of the message on successful completion.
  • If the message is too long for the buffer, excess bytes may be discarded depending on the type of socket.
  • If no message is available at the socket, the receive call waits for a message to arrive unless the socket is non-blocking.
  • If no message is available to be received and the peer has performed an orderly shutdown, the value 0 is returned.
  • -1 is returned on error.

Byte order

Network protocols transfer content on a byte-by-byte basis.  So, if a server sends a client a string “hello”, since each character is represented as a byte, ‘h’ is sent first, followed by the ‘e’, the ‘l”, etc.   The client reads the ‘h’, then the ‘e’, etc.

Integers and other data types that require more than a single byte are stored in memory based on how the processor reads the bytes from memory.

Register [B1 B2 B3 B4] 11110000 00111100 00001111 00000000
               MSB           LSB     0xF0            0x3C          0x0F            0x00
       MSB                                                      LSB
Big-endian (first)  
(Store in memory starting at the MSB)  
Increasing addresses -> increasing addresses ->
[B1][B2][B3][B4]     0xF0            0x3C          0x0F            0x00
MSB is at lower address  
   
Little-endian (first)  
(Store in memory starting at the LSB)  
Increasing addresses -> increasing addresses ->
[B4][B3][B2][B1]      0x00            0x0F          0x3C           0xF0
MSB is at higher address  

Intel Pentium, Core i5 and Core 2 Duo use little-endian, whereas Sun SPARC uses big-endian.

Protocols specify a byte ordering so that heterogeneous computer systems can exchange data without confusing the byte order.   TCP/IP uses big-endian order.  To make our applications machine independent we should convert our data to network byte order.

Four functions are provided to convert between processor byte order and the network byte order for TCP/IP applications.

uint32_t htonl (uint32_t hostint32);

uint16_t htons (uint16_t hostint16);

uint32_t ntohl (uint32_t netint32);

uint16_t ntohs (uint16_t netint16);

Threads

A thread is the smallest unit of instructions that can be scheduled by an operating system. Every process has at least one thread of execution, but may have many.

Many applications are multi-threaded. For example,

  • web browser with multiple tabs
  • IDE with code intelligence
  • web server that can service multiple requests concurrently

Benefits

  1. Scalability – a multi-threaded application can run on multiple cores.
  2. Responsiveness – if a thread is blocked the application can perform other tasks in other threads.
  3. Resource sharing – they share memory and process resources – avoiding the need for other IPC.
  4. Economy – typically a multithreaded application use less resources than multi-process application.
  5. Manageability – it is typically easier to coordinate threads to work concurrently than it is to manage processes.

Resources

Some resources are shared among all threads in a process and some resources are provided to each thread.

Per Process Per Thread
Address space (code section & heap) Program counter
Global variables Registers
Open files State Info
Child processes Stack
Pending alarms
Signals and signal handlers
Accounting Information

Amdahl’s Law

A system is concurrent if it performs context switches between execution units making it appear as if each is running on the CPU at the same time.  This occurs for example on a CPU with a single core.

A system is parallel if it can perform more than one task simultaneously. Multicore processors provide an environment for parallel processing.

Threads allow a program running in a single process to take advantage of a processor’s parallel computing capabilities.

Amdahl’s Law is a formula that identifies the potential performance gains from running a program on fixed input using additional processing cores.

[latex] A(s,N) \leq \frac{1}{s+\frac{1-s}{N}}[/latex]

where s is the percentage of the application that must be performed serially on a system with N processing cores.

Almost any sequence of code can be parallelized to some degree.  But consider the following example.  Suppose we have a function that prompts the user to enter their name, address and phone number and then reads the data into memory.  Since the user has access to only one keyboard and monitor, it makes no sense to divide this task up into smaller tasks.  Therefore 100% of the code needs to be performed serially.

Using Amdahl’s Law, we see that we gain nothing by adding more cores.

  • Speedup from 2 cores: ?(1,2) ≤ 1
  • Speed up from 4 cores: ?(1,4) ≤ 1
  • Speed up from 8 cores: ?(1,8) ≤ 1

Now consider a function that takes an array of k integers as a parameter and prints each of the numbers to the screen (in any order). Here, each one of the print statements can be performed by a separate thread, so we can say that 0% of the code needs to be computed serially or rather 100% can be computed in parallel.  In this case, adding additional cores increases the speed of the program.

  • Speedup from 2 cores: ?(0,2) ≤ 2
  • Speed up from 4 cores: ?(0,4) ≤ 4
  • Speed up from 8 cores: ?(0,8) ≤ 8

Now, consider a program that runs serially 25% of the time and in parallel the other 75% of the time. On two processing cores the program can potentially see a speed up of

[latex]A(0.25,2) \leq \frac{1}{0.25+\frac{1-0.25}{2}} = \frac{1}{0.25 + 0.375} = 1.6[/latex]

Amdahl’s Law implies there is a limit to the potential performance gains when a portion of the code must be performed serially, regardless of the number of CPU cores.

  • with 4 cores: 2.29
  • with 8 cores: 2.9
  • with 16 cores: 3.39
  • with 32 cores: 3.66
  • with 64 cores: 3.82
  • with 128 cores: 3.91
  • with 256 cores: 3.95
  • with 512 cores: 3.98
  • with 1024 cores: 3.99

This formula converges to [latex]1/S = 1/.25 = 4[/latex].  If a program runs 5% serially, the most we can gain is [latex]1/S = 1/.05 = 20[/latex].

This formula suggests that even the slightest amount of serial processing limits the advantages of multicore processors, whereas if it were running completely in parallel the gains continuously increase with an increase in the number of cores.

Gustafson-Barsis’ Law

Amdahl’s Law is predicated on a fixed input size. Gustafson-Barsis’ Law, however, states that as the size of the input dataset increases and the number of cores increases, the lack of gain by the serial computations are overshadowed by the gains achieved by the parallel computations.

So, parallel computing can have limited gains on fixed input sizes but can have significant gains as the size of the dataset increases.

2 Types of Parallelism

Data parallelism divides a dataset into equal parts and runs the same task on each individual subset.

Task parallelism divides an algorithm into separate parts that can be performed in parallel.

Programming Challenges & Solutions

As the number of cores on a computer increase, we as programmers need to take advantage of them to speed up our programs. But designing a multi-threaded program can be challenging.

Things we have to consider:

  • Should we use data parallelism, task parallelism, both or none?
  • What are the individual data segments or tasks?
  • How do we ensure the workload is balanced?
  • How do we coordinate data sharing?
  • How do we debug?

Challenges When Sharing Resources

  • Global variables are shared by threads. So global variables in code that was not intended to be multi-threaded, but now is, can cause bugs.
  • The current working directory is shared by threads.
    • T1 changes the current working directory and reads a file into memory.
    • T2 changes the current working directory and opens, writes to a file.
    • T1 writes the contents in memory to a new file in the current working directory.
      Where did T1 write the new file?

Reentrant Functions

A reentrant function is a function that can be safely interrupted in the middle of execution, called in another thread, and “re-entered” by the initial thread at a later time.

The following function is not reentrant.

int count = 1;    // global

int foo() {
    count = count + 2;
    return count; 
}

Consider if foo() is called by two different threads at the same time.  Then the first call to foo() can return either 3 or 5, depending on whether or not the thread was preempted during the execution of foo().  This is problematic.

Reentrant functions are considered thread-safe.  A list of POSIX functions that are not thread-safe can be found on the pthreads man page. Some examples are:

• getdate() • readdir() • system() • strtok() • setenv() • rand()

POSIX Threads (pthreads)

Threads are implemented in a program using a thread library.

IEEE has defined a standard for threads. The package is called pthreads. There are over 60 functions to control threads. Some are as follows:

Thread call Description
pthread_attr_init Create and initialize a thread’s attribute structure
pthread_create Create a new thread of execution, returns thread id
pthread_equal Determine if two threads are equal, returns nonzero if equal
pthread_self Return thread id of the calling thread
pthread_join Wait for a specific thread to exit
pthread_exit Terminate the calling thread, release stack
pthread_attr_destroy Remove a thread’s attribute structure

Programs using pthreads should be compiled using gcc –pthread.

#include <pthread.h>

int pthread_create(
    pthread_t *restrict tidp,
    const pthread_attr_t * restrict attr, 
    void *(*start_routine)(void *), 
    void * arg);

pthread_create() takes a pointer to a pthread_t object and sets its value to the new thread id.

The second argument is a pointer to a thread attribute structure. If NULL is passed, a thread with default settings is created. If the programmer wants to create a thread with customized attributes the programmer can initialize an attribute structure using pthread_att_init() and then call additional functions to set the values of the specific attributes.

The newly created threads start running at the address of the start_routine function. The function takes a single argument, arg, which is a void pointer.

#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h> 
#include <pthread.h>

#define MAX_THREADS 16

void * thread_do(void *arg) { 
    int i = *(int *) arg; 
    printf("thread %d\n", i); 
    fflush(NULL);

    char str[16];
    sprintf(str, "new thread: %d", i); 
    printids(str);

    return ((void *)0); 
}

void printids(const char *s) {
    pid_t pid;
    pthread_t tid;
    pid = getpid();
    tid = pthread_self();

    printf("%s: pid %u tid (0x%x) \n", 
        s, (unsigned int)pid, (unsigned int)tid);
    fflush(NULL); 
}

int main(int argc, char*argv[]) { 
    int err;
    pthread_t tid;
    int num_threads = atoi(argv[1]);

    if (num_threads > MAX_THREADS) {
        printf("usage: %s num_threads (< %d)\n", argv[0], MAX_THREADS);
        exit(0); 
    }

    int thread_args[MAX_THREADS]; 

    int i;
    for (i = 0; i < num_threads; i++)
        thread_args[i] = i;

        for (i = 0; i < atoi(argv[1]); i++) {
            err = pthread_create(&tid, NULL, thread_do, &thread_args[i]);

            if (err != 0) {
                perror("Error creating thread");
            } 
        }
        printids("main thread");
        sleep(2); // wait for the threads to terminate
        exit(0); 
    }
}

Thread Termination

If any thread calls an exit() function, the entire process is terminated along with all other threads.

There are 3 ways that a thread can terminate.

  1. It can return from the start routine.
  2. It can be canceled by another thread in the same process.
  3. The thread can call void pthread_exit()

pthread_exit() passes the pointer passed into the parameter to threads that have successfully joined the calling thread.

#include <pthread.h>
void pthread_exit(void *value_ptr);

Waiting for a Thread to Terminate

#include <pthread.h>
int pthread_join(pthread_t id, void **rval_ptr);

pthread_join() pauses until the thread with the specified id terminates. If the terminating thread calls pthread_exit(), the data passed into pthread_exit() is available for to the waiting thread. PTHREAD_CANCEL is stored in rval_ptr if the terminating thread does not pass data in pthread_exit().

Requesting Another Thread to be Canceled

A thread can request that another thread be canceled by calling pthread_cancel()

#include <pthread.h>
int pthread_cancel(pthread_t id)

By default all threads are cancelable. A thread can change the cancelability state by calling pthread_setcancelstate().

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate)

The state is either PTHREAD_CANCEL_ENABLE or PTHREAD_CANCEL_DISABLE.

If a thread is cancelable and canceled, it will continue until it reaches a cancelation point. A cancelation point is a point where the thread checks to see if it has been canceled and exits. POSIX specifies 55 functions that are cancelation points and 32 functions that can be implemented as cancelation points. When a thread has been canceled, is cancelable and calls one of the cancelation point functions it is terminated.