Multithreaded programming

Keywords: C Multithreading Operating System

1. What is a thread?
Source code - compile and link - > program - load into memory - > process
                                       |                                  |
                                    Documents                             Memory
                                                                        /   \
                                                                  Code < - Execution
                                                                  Data < - processing
                                                                    |            | <- CPU
                                                                  Static       dynamic
                                                                    |            |
                                                                  resources       thread
Thread is the execution process of a process, that is, the control sequence within the process, or a task in the process.
A process can have multiple threads at the same time, that is, multiple execution paths scheduled by the system at the same time, but there must be at least one main thread - main function and other functions called by it.
All threads of a process share the code area, data area, BSS area, heap area, environment variable and command line parameter area, file descriptor table, signal processing function, current working directory, various ID s of users and groups, etc. But the stack area is not shared. Each thread of a process has its own independent stack area.

Thread scheduling:
1) The processing unit in the system kernel that is responsible for thread scheduling is called scheduler;
2) The scheduler arranges all threads in the ready state (not blocked on any system call) into a queue, that is, the so-called ready queue;
3) The scheduler obtains the first thread from the ready queue, allocates a time slice to it, and causes the processor to execute the thread. After a period of time:
A. When the time slice of the thread is exhausted, the scheduler immediately terminates the thread and arranges it to the end of the ready queue, and then obtains the next thread from the head of the queue;
B. The thread's time slice is not exhausted, but it needs to block a system call, such as waiting for I/O or sleep. The scheduler will abort the thread and move it from the ready queue to the waiting queue until the waiting conditions are met, and then it will be moved back to the ready queue;
4) During the execution of low priority threads, high priority threads are ready, and the latter will preempt the time slice of the former;
5) If the ready queue is empty, the system kernel enters the idle state until it is not empty;
6) In a multitask time-sharing system like Linux, the basic scheduling unit is thread;
7) The time slice allocated for threads should not be too long, because too long time slice will lead to too long waiting time for threads without processor, reduce the parallelism of system operation, and users will feel obvious response delay; Time slice should not be too short, because too short time slice will increase the frequency of context switching between threads and reduce the running performance of the system.

2. Basic characteristics of threads
1) Thread is an independent entity in the process. It can have its own resources and can be independently identified - thread ID. at the same time, it is also used as a basic calling unit to participate in the allocation of time slices.
2) Threads have different states, such as create, run, terminate, pause, resume, cancel, etc.
3) Most of the resources that a thread can use still belong to the process, so a thread, as a part of a process, cannot exist independently of the process.
4) A process can execute multiple threads at the same time. These threads can execute the same code and complete the same task, or execute different code and complete different tasks.
5) The cost of creating a thread is much less than the cost of creating a process. Threads are also called lightweight processes. Therefore, when solving problems such as concurrency, multithreading is given priority, followed by multiprocessing.
6) The problem with multithreading is that too many resources are shared, which can easily lead to conflicts. In order to solve conflicts, additional overhead may be required. Therefore, multithreading still has its advantages.
Process, memory barrier, communication.
Thread, memory sharing, synchronization.

3.POSIX thread
#include <pthread.h>
Add - lpthread when linking     - lpthread -> libpthread.so

4. Create thread
Thread process function: a function called by the kernel in a thread. The calling process of this function is the execution process of the thread. Returning from this function means the end of the thread. Therefore, the main function is actually the thread process function of the main thread of a process. All self created threads must have a corresponding thread procedure function.
Void * thread procedure function (void*arg){
    Thread execution
}
arg - thread parameter pointer

int pthread_create(pthread_t* tid, const pthread_attr_t* attr, void* (*start_routine)(void*), void* arg);
0 is returned for success and error code is returned for failure.
tid - output thread ID (TID).
attr - thread attribute, NULL indicates the default attribute.
start_routine - thread procedure function pointer
arg - thread parameter pointer
pthread_create
    -> 1. Create a new thread
        -> 2. Call the thread procedure function (start_routine) and pass in the thread parameter pointer (arg)
The created child thread and the parent thread that created the child thread are parallel, and the scheduling order is unpredictable. Therefore, when the pthread_create function returns, the execution position of the child thread cannot be determined. Its thread process function may not have been called, may be executing, or may even have returned. The parameter object passed to the thread must be no longer used in the thread process function It can only be released if it is used.
Code: create.c

/* create.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void* thread_proc(void* arg) {
    printf("%lu Thread:%s\n", pthread_self(), (char*)arg);
    return NULL;
}
int main(void) {
    pthread_t tid;
    int error = pthread_create(&tid, NULL, thread_proc, "I'm a child thread!");
    if (error) {
        fprintf(stderr, "pthread_create: %s\n", strerror(error));
        return -1;
    }
    printf("%lu Thread: I am the main thread, creating%lu Thread.\n", pthread_self(), tid);
    sleep(1);
    return 0;
}

Use pthread_self() to view the thread's own TID

The main thread and multiple sub threads created through the pthread_create function run "simultaneously" in time. If no synchronization conditions are attached, the sequence of their execution steps is completely unpredictable, which is called free concurrency.
In order to make the implementation of thread procedure function more flexible, specific information can be passed through thread parameters to help thread procedure function perform different tasks.
Code: arg.c

/* arg.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <unistd.h>
#include <pthread.h>
#define PI 3.14159
void* thread_area(void* arg) {
    double r = *(double*)arg;
    *(double*)arg = PI * r * r;
    return NULL;
}
struct Pyth {
    double a, b, c;
};
void* thread_pyth(void* arg) {
    struct Pyth* pyth = (struct Pyth*)arg;
    pyth->c = sqrt((pyth->a * pyth->a + pyth->b * pyth->b));
    return NULL;
}
void* thread_aver(void* arg) {
    double* d = (double*)arg;
    d[2] = (d[0] + d[1]) / 2;
    return NULL;
}
void* thread_show(void* arg) {
    printf("%d\n", *(int*)arg);
    return NULL;
}
int main(void) {
    pthread_t tid;
    double r = 10;
    pthread_create(&tid, NULL, thread_area, &r);
    usleep(100000);
    printf("%g\n", r);
    struct Pyth pyth = {3, 4};
    pthread_create(&tid, NULL, thread_pyth, &pyth);
    usleep(100000);
    printf("%g\n", pyth.c);
    double d[3] = {123, 456};
    pthread_create(&tid, NULL, thread_aver, d);
    usleep(100000);
    printf("%g\n", d[2]);
    int* n = malloc(sizeof(int));
    *n = 1234;
    pthread_create(&tid, NULL, thread_show, n);
    usleep(100000);
    free(n);
    return 0;
}

gcc -o arg arg.c -lpthread -lm

5. Merge thread
        Create point       Conjunction
--------+---------+------->Main thread
             \________/ Child thread
int pthread_join(pthread_t tid, void** retval);
0 is returned for success and error code is returned for failure.
tid - thread ID
retval - thread exit code
When pthread_join function is called:
The tid thread has terminated. Return the thread exit code immediately.
The tid thread has not been terminated, blocking and waiting until it is terminated by the merging thread.
Void * thread procedure function (void * thread parameter pointer){
    Thread execution
    return p;
}
Function: wait for the child thread to terminate, clean up the thread resources, and obtain the return value of the thread process function
Code: join.c

/* join.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <unistd.h>
#include <pthread.h>
#define PI 3.14159
void* thread_area(void* arg) {
    double r = *(double*)arg;
    double* s = malloc(sizeof(double));
    *s = PI * r * r;
    return s;
}
int main(void) {
    pthread_t tid;
    double r = 10;
    pthread_create(&tid, NULL, thread_area, &r);
    double* s;
    pthread_join(tid, (void**)&s);
    printf("%g\n", *s);
    free(s);
    return 0;
}

6. Detach threads
Sometimes, as the creator of the child thread, the parent thread may not care when the child thread terminates, and the parent thread does not need to obtain the return value of the child thread. In this case, child threads can be set to separate threads. Once such threads are terminated, their resources will be automatically recovered by the system without calling pthread_ in their parent thread. Join function.
int pthread_detach(pthread_t tid);
0 is returned for success and error code is returned for failure.
tid - thread ID
Code: detach.c

/* detach.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void* thread_proc(void* arg) {
    for (int i = 0; i < 200; ++i) {
        putchar('-');
        usleep(50000);
    }
    return NULL;
}
int main(void) {
    setbuf(stdout, NULL);
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    pthread_t tid;
    pthread_create(&tid, /*NULL*/&attr, thread_proc, NULL);
    pthread_attr_destroy(&attr);
    //pthread_detach(tid);
    int error = pthread_join(tid, NULL);
    if (error)
        fprintf(stderr, "pthread_join: %s\n", strerror(error));
    for (int i = 0; i < 200; ++i) {
        putchar('+');
        usleep(100000);
    }
    printf("\n");
    return 0;
}

pthread_attr_t  attr;
pthread_ attr_ init(&attr); // Initialize thread properties with default values
pthread_attr_setdetachstat(&attr,PTHREAD_CREATE_DETACHED);
pthread_create(..., &attr, ...);
pthread_attr_destroy(&attr);

7. Thread ID
pthread_t tid;
pthread_ create(&tid, ...);—> Output TID of child thread
pthread_ self(); -> Returns the TID of the calling thread
if (tid1 == tid2) / / poor compatibility for structures
    ...
int pthread_equal(pthread_t tid1, pthread_t tid2);
If two TID s are equal, it returns non-zero, otherwise it returns 0.
if (pthread_equal(tid1, tid2)) / / good compatibility
Code: equal.c

/* equal.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <syscall.h>
#include <pthread.h>
pthread_t g_main; // TID of main thread
void foo(void) {
    if (pthread_equal(pthread_self(), g_main))
        printf("foo The function was called in the main thread.\n");
    else
        printf("foo The function was called in a child thread.\n");
}
void* thread_proc(void* arg) {
    printf("Sub thread of system kernel TID: %ld\n", syscall(SYS_gettid));
    foo();
    return NULL;
}
int main(void) {
    g_main = pthread_self();
    printf("POSIX Main thread of Library TID: %lu\n", g_main);
    printf("Main thread of system kernel TID: %ld\n", syscall(SYS_gettid));
    printf("Process PID: %d\n", getpid());
    foo();
    pthread_t tid;
    pthread_create(&tid, NULL, thread_proc, NULL);
    pthread_join(tid, NULL);
    return 0;
}

Through PTHREAD_ The thread ID and PTHREAD returned by the self function_ The thread ID output from the create function is the same as the virtual (pseudo) thread ID maintained internally by the PTHREAD library, which can be used for other PTHREAD functions that need to provide thread ID. The real thread ID maintained by the system kernel can be obtained through syscall(SYS_gettid):
#Include < unistd. H > / / declare syscall function
#Include < syscall. H > / / define SYS_gettid macro
In Linux system, the PID of a process is actually the TID of its main thread.

8. Terminate thread (own)
1) When returned from a thread procedure function, the thread executing the procedure function terminates. Its return value can be through pthread_ The second parameter of the join function is output to the caller.
2) Pthread can be called in the thread procedure function and any function called by it_ The exit function terminates the current thread:
void pthread_exit(void* retval);
The parameter retval of this function is equivalent to the return value of the thread procedure function, and can also be pthreaded_ The second parameter of the join is output to the caller.
Code: exit.c

/* exit.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <unistd.h>
#include <pthread.h>
#define PI 3.14159
void calc_area(double r) {
    double* s = malloc(sizeof(double));
    *s = PI * r * r;
    pthread_exit(s);
}
void* thread_area(void* arg) {
    printf("call calc_area function...\n");
    calc_area(*(double*)arg);
    printf("from calc_area Function returns.\n");
    return NULL;
}
int main(void) {
    pthread_t tid;
    double r = 10;
    pthread_create(&tid, NULL, thread_area, &r);
    //pthread_exit(NULL);
    double* s;
    pthread_join(tid, (void**)&s);
    printf("%g\n", *s);
    free(s);
    return 0;
}

Note: call pthread_ in sub thread Exit will only terminate the calling thread itself and have no impact on its other sibling threads and main threads. But if you call pthread_ in the main thread Exit, the whole process and all its threads will be terminated.

9. Cancel (other) threads
int pthread_cancel(pthread_t tid);
0 is returned for success and error code is returned for failure.
TID - the TID of the canceled thread.
This function only sends a cancellation request to a specific thread and does not wait for it to terminate. By default, the thread will not terminate immediately after receiving the cancellation request, but will continue to run until it reaches a cancellation point. At the cancellation point, the thread checks whether it has been cancelled. If so, it terminates immediately. Cancellation points usually appear in some specific system calls.
Set the cancellation status of the calling thread to accept or ignore the cancellation request:
int pthread_setcancelstate(int state, int* oldstate);
state - cancel status, take the following values:
PTHREAD_CANCEL_ENABLE - accept cancel request (default)
PTHREAD_CANCEL_DISABLE - ignore cancel request
old_state - output the original cancellation status, which can be NULL.
Set the cancellation type of the calling thread to delay cancellation or immediate cancellation:
int pthread_setcanceltype(int type, int* oldtype);
Type - cancel type, take the following values:
PTHREAD_CANCEL_DEFERRED - delayed cancellation (default). After receiving a cancellation request, if it is not ignored, it will continue to run for a period of time until the cancellation point is executed.
PTHREAD_CANCEL_ASYNCHRONOUS - cancel immediately. After receiving the cancellation request, if it is not ignored, terminate the operation immediately.
old_type - output the original cancellation type, which can be NULL.
Code: cancel.c

/* cancel.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void elapse(void) {
    for (unsigned int i = 0; i < 800000000; ++i);
}
void* thread_proc(void* arg) {
    /*
    // Cancel request is not allowed
    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
    */
    // Cancel type cancel now
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);
    for (;;) {
        printf("Thread: Zi said in Kawakami that the past is like a man.\n");
        elapse();
    }
    return NULL;
}
int main(void) {
    setbuf(stdout, NULL);
    pthread_t tid;
    pthread_create(&tid, NULL, thread_proc, NULL);
    getchar();
    printf("Send cancel request to thread...\n");
    pthread_cancel(tid);
    printf("Wait for the thread to terminate...\n");
    pthread_join(tid, NULL);
    printf("Thread terminated.\n");
    return 0;
}

10. Thread conflict
g = 0
Thread 1                                   Thread 2
Read the value in memory (g=0) into register (eax=0)
Add 1 to the value in the register (eax = 0 - > 1)
Store the value in register (eax=1) into memory (g=1)
                                  Read the value in memory (g=1) into register (eax=1)
                                  Add 1 to the value in the register (eax = 1 - > 2)
                                  Store the value in register (eax=2) into memory (g=2)
Thread 1                                   Thread 2
Read the value in memory (g=0) into register (eax=0)
                                  Read the value in memory (g=0) into register (eax=0)
Add 1 to the value in the register (eax = 0 - > 1)
                                  Add 1 to the value in the register (eax = 0 - > 1)
Store the value in register (eax=1) into memory (g=1)
                                  Store the value in register (eax=1) into memory (g=1)
When two or two threads access the same object in a non atomic way at the same time, it is very likely to lead to the instability of the final state of the object, which is the so-called thread conflict. The basic principle of conflict resolution is the atomization of sensitive operations, that is, after one thread completes this group of sensitive operations, another thread is allowed to perform similar operations. It is located in the operation code related to shared resources, and only one thread is allowed to execute at any time.
Code: vie.c

/* vie.c */
#include <stdio.h>
#include <string.h>
#include <pthread.h>
unsigned int g = 0;
void* thread_proc(void* arg) {
    for (unsigned int i = 0; i < 100000000; ++i)
        ++g;
    return NULL;
}
int main(void) {
    pthread_t t1, t2;
    pthread_create(&t1, NULL, thread_proc, NULL);
    pthread_create(&t2, NULL, thread_proc, NULL);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    printf("g = %u\n", g);
    return 0;
}

Posted by rubik on Sat, 25 Sep 2021 21:13:16 -0700