Learn about threads

I Thread concept

In some cases, multiple control processes need to be executed simultaneously in one process. For example, to realize the download software of a graphical interface, on the one hand, it needs to interact with the user, wait for and process the user's mouse and keyboard events, on the other hand, it needs to download multiple files at the same time, wait for and process the data from multiple network host methods, These tasks need a "wait - > process" cycle, so how can we carry out multiple tasks at the same time?

1. Thread required: it is the smallest unit that the operating system can schedule operations. It is included in the process and is the actual operation unit in the process. A thread refers to a single sequence of control flow in a process. Multiple threads can be concurrent in a process, and each thread executes different tasks in parallel.

2. Because multiple threads of the same process share the same address space, code blocks and data blocks are shared. If a function is defined, it can be called in each thread. If a global variable is defined, it can be accessed in each thread. In addition, each thread also shares the following process resources and environment:

① File descriptor table

② Processing mode of each signal

③ Current working directory

④ User id and group id

However, some resources are shared by each thread:

① Thread id

② Context, including the values of various registers, program counters and stack pointers

③ Stack space

④ errno variable

⑤ Signal shielding word

⑥ Scheduling priority

For libpthread shared library of thread function on Linux, add - lpthread when compiling

②. Thread control

1. Create thread

Return value: 0 for success and error number for failure. Previously learned system functions return 0 for success and - 1 for failure, and the error number is saved in the global variable errno.

pthread library functions return the error number through the return value. Although each thread also has an errno, it is provided for compatibility with other function interfaces. pthread library itself is not applicable to it. It is better to return the error code through the return value.

2. Get the id of the current thread

Return value: always returns successfully, and returns the ID of the thread calling the function

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

void *thr_fn(void *arg) {
    printf("%s\n", arg);
    return NULL;
}

void printid(char *tip) {
    pid_t pid = getpid();
    pthread_t tid = pthread_self();
    printf("%s pid:%u tid: %u (%p)\n", tip, pid, tid, tid);
    //printf("%s thr_fn=%p\n", tip, thr_fn);
    return;
}

int main(void) {
    pthread_t ntid;
    int ret = pthread_create(&ntid, NULL, thr_fn, "new thread");
    if (ret) {
        printf("create thread err:%s\n", strerror(ret));
        exit(1);
    }
    //sleep(1);
    printid("main thread\n");
    return 0;
}

3. Think: the main thread saves the id of the newly created site in a global variable ntid. If the newly created thread does not call pthread_self, but print the ntid directly. Can you achieve the same effect?

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

pthread_t ntid;
void *thr_fn(void *arg) {
    printid(arg);
    printf("%s ntid=%p\n", arg, ntid);
    return NULL;
}

void printid(char *tip) {
    pid_t pid = getpid();
    pthread_t tid = pthread_self();
    printf("%s pid:%u tid: %u (%p)\n", tip, pid, tid, tid);
    return;
}

int main(void) {  
    int ret = pthread_create(&ntid, NULL, thr_fn, "new thread");
    if (ret) {
        printf("create thread err:%s\n", strerror(ret));
        exit(1);
    }
    sleep(1);
    printid("main thread\n");
    return 0;
}

 

4. If you need to terminate only one thread without terminating the whole process, there are three methods?

① return from the thread function. This method is not applicable to the main thread. Returning from the main function is equivalent to calling exit.

② A thread can call pthread_cancel terminates another thread in the same process.

③ Threads can call pthread_exit terminates itself.

 

value_ptr is of void * type, which is the same as the return value of thread function. Other threads can call pthread_join gets this pointer.

Note: pthread_ The memory unit returned by exit or return must be global or allocated by malloc, and cannot be allocated on the stack of thread function (because when other threads get this return pointer, the thread function has exited)

5. The thread calling this function will hang and wait until the thread with id thread terminates. Thread thread terminates in different ways through pthread_ The termination status obtained by join is different, as follows:

① If the thread returns through return, value_ The unit pointed to by PTR stores the return value of thread function.

② If the thread is called by another thread, pthread_cancel terminates abnormally, value_ The constant pthread is stored in the cell pointed to by PTR_ CANCLED.

③ If the thread calls pthread itself_ Exit terminated, value_ The unit pointed to by PTR stores the information transmitted to pthread_ Parameter of exit.

④ If you are not interested in the termination state of the thread thread, you can pass NULL to value_ptr parameter.

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

void *thr_fn1(void *arg) {
    printf("thread1 returning\n");
    return (void *)1;
}

void *thr_fn2(void *arg) {
    printf("thread2 exiting\n");
    pthread_exit((void *)2);
    return NULL;
}

void *thr_fn3(void *arg) {
    while (1) {
        printf("thread3 is sleeping\n");
        sleep(1);
    }
    return NULL;
}

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

    pthread_create(&tid, NULL, thr_fn1, NULL);
    pthread_join(tid, &sts);
    printf("thread1 exit code: %ld\n", (long)sts);

    pthread_create(&tid, NULL, thr_fn2, NULL);
    pthread_join(tid, &sts);
    printf("thread2 exit code: %ld\n", (long)sts);

    pthread_create(&tid, NULL, thr_fn3, NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid, &sts);
    printf("thread3 exit code: %ld\n", (long)sts);

    return 0;
}

 

III Inter thread synchronization

1. Multiple threads may conflict when accessing shared data at the same time, which is the same problem as the reentry mentioned in the previous signal. For example, two threads need to increase a global variable by 1. This slave operation requires three instructions on a platform:

Read the variable value from memory to register -- > the value of register plus 1 -- > and write the value of register back to memory

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

int cnt = 0;

void *cntadd(void *arg) {
    for (int i = 0; i < 5000; i++) {
        int val = cnt;
        printf("%p: %d\n", pthread_self(), val);
        cnt = val+1;
    }
    return NULL;
}

int main(void) {
    pthread_t tida, tidb;

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

    pthread_join(tida, NULL);
    pthread_join(tidb, NULL);
    return 0;
}

2. For multithreaded programs, the problem of access conflict is very common. The solution is to introduce Mutex(Mutual Exclusive Lock). The thread that obtains the lock can complete the "read modify write" operation, and then release the lock to other threads. The thread that does not obtain the lock can only wait and cannot access the shared data. In this way, the three-step operation of "read modify write" forms an atomic operation, Either all or none of them will be executed. They will not be interrupted in the middle, nor will they do this operation in parallel on other processors.

pthread_mutex_init function initializes mutex, and the parameter attr sets the mutex attribute. If attr is NULL, it indicates the default attribute.

With pthread_ mutex_ The mutex initialized by init function can be pthread_ mutex_ Destroy.

If mutex variable is statically allocated (global variable or static variable), pthread can also be defined with macro_ MUTEX_ Initializer to initialize, which is equivalent to pthread_mutex_init is initialized and the attr parameter is NULL.

3. The following functions can be used to lock and unlock mutex:

Return value: 0 for success and error number for failure.

A thread can call pthread_mutex_lock obtains mutex if another thread has called pthread_mutex_lock obtains the mutex, then the current thread needs to hang and wait until another thread calls pthread_mutex_unlock releases the mutex, and the current thread is awakened to obtain the mutex and continue execution.

If a thread wants to obtain a lock and does not want to suspend waiting, pthread can be called_ mutex_ Trylock, if mutex has been obtained by another thread, this function will fail and return EBUSY without suspending the thread.

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

int cnt = 0;
pthread_mutex_t add_lock = PTHREAD_MUTEX_INITIALIZER;

void *cntadd(void *arg) {
    for (int i = 0; i < 5000; i++) {
        pthread_mutex_lock(&add_lock);
        int val = cnt;
        printf("%p: %d\n", pthread_self(), val);
        cnt = val+1;
        pthread_mutex_unlock(&add_lock);
    }
    return NULL;
}

int main(void) {
    pthread_t tida, tidb;

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

    pthread_join(tida, NULL);
    pthread_join(tidb, NULL);
    return 0;
}

4. How to implement the operations of suspending waiting and waking up waiting threads?

Each mutex has a waiting queue. If a thread wants to suspend waiting on mutex, first add itself to the waiting queue, then set the thread state to sleep, and then call the scheduler function to switch to other threads. To wake up other threads in the waiting queue, a thread only needs to take an item from the waiting queue, change its state from sleep to ready, and join the ready queue. Then it is possible to switch to the awakened thread the next time the scheduler function executes.

If the same thread calls lock twice, in the second call, because the lock has been occupied, the thread will hang and wait for other threads to release the lock. However, if the lock is occupied by itself, the thread will hang and have no chance to release the lock. Therefore, it will always be in the state of hanging and waiting, which is called deadlock.

Another deadlock situation: thread A obtains lock 1 and thread B obtains lock 2. At this time, thread A calls lock to try to obtain lock 2. As A result, thread B needs to suspend and wait for thread B to release lock 2. At this time, thread B also calls lock to try to obtain lock 1. As A result, thread A needs to suspend and wait for thread A to release lock 1. Therefore, both threads A and B are always suspended. If more threads and locks are involved, the possibility of deadlock will become complex and difficult to judge.

When writing programs, try to avoid obtaining multiple locks at the same time. If it is necessary to do so, one principle:

If all threads acquire locks in the same order when multiple locks are required, there will be no deadlock. For example, a program uses lock 1, lock 2 and lock 3. Their corresponding mutex variables are lock 1 - > lock 2 - > lock 3. When all threads need to obtain two or three locks at the same time, they should obtain them in the order of lock 1, lock 2 and lock 3.

If it is difficult to prioritize all locks, try to use pthread_mutex_trylock call instead of pthread_mutex_lock call to avoid deadlock.

5. There is another case of synchronization between threads: thread A needs to wait for A certain condition to be established before continuing to execute. If this condition is not established, thread A will block and wait, while thread B will wake up thread A to continue to execute when this condition is established during execution. In the pthread library, the condition variable is used to block the thread waiting for A condition or wake up the thread waiting for the condition. Pthread for conditional variables_ cond_ It can be initialized and destroyed as follows:

Return value: 0 for success and error number for failure.

6. The following functions can be used for the operation of conditional variables:

Return value: 0 for success and error number for failure.

pthread_ cond_ The timedwait function also has an additional parameter to set the waiting timeout. If there is still no other thread to wake up the current thread at the time specified by abstime, timeout will be returned, and a thread can call pthread_cond_signal wakes up and waits for another thread on a condition variable, or pthread can be called_ cond_ Broadcast wakes up all threads waiting on this condition variable.

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

typedef struct Goods
{
    int data;
    struct Goods *next;
} Goods;

Goods *head = NULL;
pthread_mutex_t headlock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t hasGoods = PTHREAD_COND_INITIALIZER;

void *producer(void *arg) {
    Goods *ng;
    while (1) {
        ng = (Goods *)malloc(sizeof(Goods));
        ng->data = rand() % 100;
        pthread_mutex_lock(&headlock);
        ng->next = head;
        head = ng;
        pthread_mutex_unlock(&headlock);
        pthread_cond_signal(&hasGoods);
        printf("produce %d\n", ng->data);
        sleep(rand() % 3);
    }
}

void *consumer(void *arg) {
    Goods *k;
    while (1) {
        pthread_mutex_lock(&headlock);
        if (!head) {
            pthread_cond_wait(&hasGoods, &headlock);
        }
        k = head;
        head = head->next;
        pthread_mutex_unlock(&headlock);
        printf("consume %d\n", k->data);
        free(k);
        sleep(rand() % 3);
    }
}

int main(void) {
    srand(time(NULL));

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

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

    return 0;
}

7. If the mutex variable is not 0, i.e. 1, it is regarded as an available quantity of resources. When initializing, mutex is 1, indicating that there is an available resource. When locking, obtain the resource. Reduce mutex to 0, indicating that there is no more available resource. When unlocking, release the resource and add mutex back to 1, indicating that there is another available resource.

8. Semaphore semaphore is similar to mutex, which indicates the number of available resources. The difference between semaphore and mutex is that this number can be greater than 1. This semaphore can be used not only to agree the synchronization between threads of processes, but also for the synchronization between different processes.

The semaphore variable type is sem_t

sem_init() initializes a semaphore variable. The value parameter indicates the number of available resources, and the pshared parameter is 0, indicating that the semaphore is used for inter thread synchronization of the same process.

SEM should be called after using up the semaphore variable_ Destroy() releases resources related to semaphore.

Call sem_wait() can get the resource. Use the semaphore value minus 1. If SEM is called_ If the value of semaphore is already 0 when waiting(), the wait is suspended. If you don't want to wait, you can call sem_trywait().

Call sem_post() can release resources, increase the semaphore value by 1, and wake up the pending thread at the same time.

 

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

#define NUM 5

int q[NUM];

sem_t blank_number, goods_number;

int head, tail;

void *producer(void *arg) {
    while (1) {
        sem_wait(&blank_number);
        q[tail] = rand() % 100 + 1;
        printf("produce %d\n", q[tail]);
        sem_post(&goods_number);
        tail = (tail + 1) % NUM;
        sleep(rand() % 3);
    }
}

void *consumer(void *arg)  {
    while (1) {
        sem_wait(&goods_number);
        printf("consume %d\n", q[head]);
        q[head] = 0;
        sem_post(&blank_number);
        head = (head + 1) % NUM;
        sleep(rand() % 3);
    }
}

int main() {
    srand(time(NULL));

    sem_init(&blank_number, 0, NUM);
    sem_init(&goods_number, 0, 0);

    pthread_t pid, cid1, cid2, cid3;
    pthread_create(&pid, NULL, producer, NULL);
    pthread_create(&cid1, NULL, consumer, NULL);
    pthread_create(&cid2, NULL, consumer, NULL);
    pthread_create(&cid3, NULL, consumer, NULL);

    pthread_join(pid, NULL);
    pthread_join(cid1, NULL);
    pthread_join(cid2, NULL);
    pthread_join(cid3, NULL);
    return 0;
}

Keywords: C C++

Added by pck76 on Tue, 01 Feb 2022 20:21:05 +0200