Analysis of C++ programming in the thread

  • 2020-04-02 03:13:28
  • OfStack

Concept of threads

The Text Segment and Data Segment of a thread in C++ are Shared. If a function is defined, it can be called in each thread, and if a global variable is defined, it can be accessed in each thread. In addition, threads share the following process resources and environment:

      File descriptor       The processing of each signal       Current working directory       User id and group id

However, some resources are allocated per thread:

      Thread id       Context, including the values of various registers, program counters, and stack Pointers       Stack space       The errno variable       Signal shielding word       Scheduling priority

The thread library functions we'll be studying are defined by the POSIX standard and are called POSIX threads or pthreads.
The thread of control
Create a thread

The function prototype of creating thread is as follows:


#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

Return value: 0 on success, error number on failure.

After calling pthread_create() in a thread to create a new thread, the current thread returns from pthread_create() to continue execution, and the code executed by the new thread is determined by the function pointer start_routine we passed to pthread_create. The start_routine function receives an argument, passed to it through the arg parameter of pthread_create, of type void*, which is defined by the caller. The return value type of start_routine is also void *, and the meaning of this pointer is also defined by the caller. When start_routine returns, this thread exits, and other threads can call pthread_join to get the return value of start_routine.

When the pthread_create returns successfully, the id of the newly created thread is filled into the memory unit pointed to by the thread parameter. We know that the type of process id is pid_t, the id of each process is unique in the whole system, and the call to getpid will get the id of the current process, which is a positive integer value. The type of the thread id is thread_t, it only guaranteed to be unique, in the current process in different systems thread_t this type has a different implementation, it may be an integer value, may also be a structure, may also be an address, so can't simply as integer use printf print, call pthread_self can get the current thread id.

Let's start with a simple example:


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

pthread_t ntid;

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

  pid = getpid();
  tid = pthread_self();
  printf("%s pid %u tid %u (0x%x)n", s, (unsigned int)pid,
      (unsigned int)tid, (unsigned int)tid);
}

void *thr_fn(void *arg)
{
  printids(arg);
  return NULL;
}

int main(void)
{
  int err;

  err = pthread_create(&ntid, NULL, thr_fn, (void *)"Child Process:");
  if (err != 0) {
    fprintf(stderr, "can't create thread: %sn", strerror(err));
    exit(1);
  }
  printids("main thread:");
  sleep(1);

  return 0;
}


The compilation results are as follows:


g++ thread.cpp -o thread -lpthread
./thread
main thread: pid 21046 tid 3612727104 (0xd755d740)
Child Process: pid 21046 tid 3604444928 (0xd6d77700)

As can be seen from the results, thread_t type is an address value, and multiple threads belonging to the same process can get the same process number by calling getpid, while calling pthread_self can get different thread Numbers.

If any one thread calls the exit or associated with, all the threads are terminated, the entire process from the main function return is equivalent to call exit, in order to prevent the newly created thread has not been performed is terminated, delay of 1 second, before we return the main function of this is only an expedient measure, even if the main thread wait a second, the kernel does not necessarily will dispatch the newly created thread execution, next, we study a better solution.
Termination of the thread

If you need to terminate a thread instead of the entire process, there are three ways to do this:

      Return from the thread function. This method is not suitable for the main thread, and returning from the main function is equivalent to calling exit.       A thread can call pthread_cancel to terminate another thread in the same process.       A thread can terminate itself by calling pthread_exit.

Here we focus on the usage of pthread_exit and pthread_join.


#include <pthread.h>

void pthread_exit(void *value_ptr);

Value_ptr is of type void*, and as with the return value of the thread function, other threads can call pthread_join to get the pointer.
Note that the unit of memory to which a pthread_exit or return pointer refers must be global or malloc allocated and cannot be allocated on the stack of a thread function because the thread function has already exited when another thread gets the return pointer.


#include <pthread.h>

int pthread_join(pthread_t thread, void **value_ptr);

Return value: 0 on success, error number on failure.

The thread calling this function hangs and waits until the thread with id of thread terminates. Threads terminate in different ways, and the termination state obtained by pthread_join is different, which is summarized as follows:

      If a thread returns by return, the cell that value_ptr points to holds the return value of the thread function.       If a thread is aborted by another thread's call to pthread_cancel, the cell that value_ptr points to holds the constant PTHREAD_CANCELED.       If the thread terminates by calling pthread_exit itself, the cell that value_ptr points to holds the parameter passed to pthread_exit.

If you are not interested in the thread's termination state, you can pass NULL to the value_ptr parameter. The reference code is as follows:


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

void* thread_function_1(void *arg)
{
  printf("thread 1 runningn");
  return (void *)1;
}

void* thread_function_2(void *arg)
{
  printf("thread 2 exitingn");
  pthread_exit((void *) 2);
}

void* thread_function_3(void* arg)
{
  while (1) {
    printf("thread 3 writeingn");
    sleep(1);
  }
}


int main(void)
{
  pthread_t tid;
  void *tret;

  pthread_create(&tid, NULL, thread_function_1, NULL);
  pthread_join(tid, &tret);
  printf("thread 1 exit code %dn", *((int*) (&tret)));

  pthread_create(&tid, NULL, thread_function_2, NULL);
  pthread_join(tid, &tret);
  printf("thread 2 exit code %dn", *((int*) (&tret)));

  pthread_create(&tid, NULL, thread_function_3, NULL);
  sleep(3);
  pthread_cancel(tid);
  pthread_join(tid, &tret);
  printf("thread 3 exit code %dn", *((int*) (&tret)));

  return 0;
}

The running result is:


thread 1 running
thread 1 exit code 1
thread 2 exiting
thread 2 exit code 2
thread 3 writeing
thread 3 writeing
thread 3 writeing
thread 3 exit code -1


It can be seen that the value of the constant PTHREAD_CANCELED in the pthread library of Linux is -1. Its definition can be found in the header file pthread.h:


#define PTHREAD_CANCELED ((void *) -1)


Interthread synchronization

There may be conflicts when multiple threads access Shared data at the same time. For example, two threads need to add 1 to a global variable. This operation requires three instructions to complete on a certain platform:

      Reads variable values from memory to registers.       Register value + 1.       Writes the value of the register back into memory.

At this time, it is easy for two processes to manipulate the register variable value at the same time, resulting in incorrect final results.

Solution is to introduce the Mutex (Mutex, Mutual Exclusive Lock), gets the Lock of the thread can complete "read-modify-write" operation, and then release the Lock to other threads, don't get locked thread can only wait and cannot access to Shared data, in this way, "read-modify-write" three-step operation of an atomic operation, otherwise is executed, or does not perform, do not perform in the middle is interrupted, also won't do this operation in other processors in parallel.

Mutex is represented by a variable of type pthread_mutex_t, which can be initialized and destroyed by:


#include <pthread.h>

int pthread_mutex_destory(pthread_mutex_t *mutex);
int pthread_mutex_int(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
pthread_mutex_t mutex = PTHEAD_MUTEX_INITIALIZER;


Return value: 0 on success, error number on failure.

A Mutex initialized with the pthread_mutex_init function can be destroyed with pthread_mutex_destroy. If the Mutex variable is statically assigned (global or static), it can also be initialized with a macro definition of PTHREAD_MUTEX_INITIALIZER, which is equivalent to initializing with pthread_mutex_init and the attr parameter is NULL. The locking and unlocking operations of Mutex can be performed using the following functions:


#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);


Return value: 0 on success, error number on failure.

A thread can call pthread_mutex_lock to obtain the Mutex. If another thread has already called pthread_mutex_lock to obtain the Mutex, the current thread needs to suspend and wait until another thread calls pthread_mutex_unlock to release the Mutex and the current thread is awakened to obtain the Mutex and continue execution.

We use Mutex to solve the problem mentioned above that two threads with global variable +1 can cause chaos:


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

#define NLOOP 5000

int counter;
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

void *do_add_process(void *vptr)
{
  int i, val;

  for (i = 0; i < NLOOP; i ++) {
    pthread_mutex_lock(&counter_mutex);
    val = counter;
    printf("%x:%dn", (unsigned int)pthread_self(), val + 1);
    counter = val + 1;
    pthread_mutex_unlock(&counter_mutex);
  }

  return NULL;
}

int main()
{
  pthread_t tida, tidb;

  pthread_create(&tida, NULL, do_add_process, NULL);
  pthread_create(&tidb, NULL, do_add_process, NULL);

  pthread_join(tida, NULL);
  pthread_join(tidb, NULL);

  return 0;
}

In this way, each run can display 10000. If you remove the lock mechanism, you might have a problem. This mechanism is similar to Java's synchronized block mechanism.
Condition Variable

There is also A case of synchronization between threads: thread A needs to wait for something to be true to continue executing. If this condition is not true, thread A will block and wait. While thread B makes this condition true during execution, it will wake thread A to continue executing. The Conditiion Variable is used in the pthread library to block threads waiting for a condition or to wake up threads waiting for it. Condition Variable is represented by a Variable of type pthread_cond_t, which can be initialized and destroyed by:


#include <pthread.h>

int pthread_cond_destory(pthread_cond_t *cond);
int pthread_cond_init(pthead_cond_t *cond, const pthread_condattr_t *attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

Return value: 0 on success, error number on failure.

Similar to the initialization and destruction of Mutex, the pthread_cond_init function initializes a Condition Variable, the attr parameter is NULL for the default attribute, and the pthread_cond_destroy function destroys a Condition Variable. If the Condition Variable is statically assigned, you can also initialize the PTHEAD_COND_INITIALIZER with a macro, which is equivalent to initializing it with the pthread_cond_init function with an attr parameter of NULL. The operation of Condition Variable can be performed using the following functions:


#include <pthread.h>

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);


As you can see, a Condition Variable is always used with a Mutex. A thread can call pthread_cond_wait to block a wait on a Condition Variable, which does the following three things:

      Release the Mutex.       Block wait.       When awakened, regain the Mutex and return.

The pthread_cond_timedwait function also has an additional parameter that sets the wait timeout and returns ETIMEDOUT if no other thread wakes up the current thread at the time specified by the abstime. A thread can call pthread_cond_signal to wake up another thread waiting on a Condition Variable, or pthread_cond_broadcast to wake up all threads waiting on that Condition Variable.

The following program illustrates a producer-consumer example, where the producer produces a structure that is strung over the header of a linked list, and the consumer takes the structure from the header.


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

struct msg {
  struct msg *next;
  int num;
};

struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void* consumer(void *p)
{
  struct msg *mp;

  for(;;) {
    pthread_mutex_lock(&lock);
    while (head == NULL) {
      pthread_cond_wait(&has_product, &lock);
    }
    mp = head;
    head = mp->next;
    pthread_mutex_unlock(&lock);
    printf("Consume %dn", mp->num);
    free(mp);
    sleep(rand() % 5);
  }
}

void* producer(void *p)
{
  struct msg *mp;

  for(;;) {
    mp = (struct msg *)malloc(sizeof(*mp));
    pthread_mutex_lock(&lock);
    mp->next = head;
    mp->num = rand() % 1000;
    head = mp;
    printf("Product %dn", mp->num);
    pthread_mutex_unlock(&lock);
    pthread_cond_signal(&has_product);
    sleep(rand() % 5);
  }
}

int main()
{
  pthread_t pid, cid;
  srand(time(NULL));

  pthread_create(&pid, NULL, producer, NULL);
  pthread_create(&cid, NULL, consumer, NULL);

  pthread_join(pid, NULL);
  pthread_join(cid, NULL);

  return 0;
}


Related articles: