Multithreaded programming can be said to be the basic skills of every programmer, but also one of the difficulties in development, this article takes Linux C as an example, describes the creation of threads and common ways of synchronization of several threads, and finally summarizes and thinks about multithreaded programming and gives code examples.
Create a thread
The first step in multithreaded programming is to create a thread. Creating a thread is actually adding a control flow that allows multiple control flows to execute concurrently or in parallel in the same process.
Thread-created functions, other functions are not listed here, see pthread.h.
#include<pthread.h> int pthread_create(pthread_t * RESTRICT thread, /* thread id*/ const pthread_attr_t * RESTRICT attr, Void *(*start_routine)(void*), /* restrict arg /* Restrict arg /);Copy the code
Code examples:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
char* thread_func1(void* arg) {
pid_t pid = getpid();
pthread_t tid = pthread_self();
printf("%s pid: %u, tid: %u (0x%x)\n", (char*)arg, (unsigned int)pid, (unsigned int)tid, (unsigned int)tid);
char* msg = "thread_func1";
return msg;
}
void* thread_func2(void* arg) {
pid_t pid = getpid();
pthread_t tid = pthread_self();
printf("%s pid: %u, tid: %u (0x%x)\n", (char*)arg, (unsigned int)pid, (unsigned int)tid, (unsigned int)tid);
char* msg = "thread_func2 ";
while(1) {
printf("%s running\n", msg);
sleep(1);
}
return NULL;
}
int main(a) {
pthread_t tid1, tid2;
if (pthread_create(&tid1, NULL, (void*)thread_func1, "new thread:") != 0) {
printf("pthread_create error.");
exit(EXIT_FAILURE);
}
if (pthread_create(&tid2, NULL, (void*)thread_func2, "new thread:") != 0) {
printf("pthread_create error.");
exit(EXIT_FAILURE);
}
pthread_detach(tid2);
char* rev = NULL;
pthread_join(tid1, (void *)&rev);
printf("%s return.\n", rev);
pthread_cancel(tid2);
printf("main thread end.\n");
return 0;
}
Copy the code
Second, thread synchronization
Sometimes we need multiple threads to work together to execute, and inter-thread synchronization is required. Common ways to synchronize between threads are:
- The mutex
- A semaphore
- Condition variables,
Let’s start with an example without thread synchronization:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#define LEN 100000
int num = 0;
void* thread_func(void* arg) {
for (int i = 0; i< LEN; ++i) {
num += 1;
}
return NULL;
}
int main(a) {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, (void*)thread_func, NULL);
pthread_create(&tid2, NULL, (void*)thread_func, NULL);
char* rev = NULL;
pthread_join(tid1, (void *)&rev);
pthread_join(tid2, (void *)&rev);
printf("correct result=%d, wrong result=%d.\n".2*LEN, num);
return 0;
}
Copy the code
Correct result=200000, wrong result=106860.
[1] Mutually exclusive
This one is the easiest to understand. When accessing a critical resource, by mutual exclusion, a maximum of one thread can access a critical resource at a time.
Actually mutually exclusive logic is that if there is no other access to the street resource discovery thread lock, lock, access to critical resources, if the other thread during execution to the mutex found that has been locked, thread hang wait for to unlock, the current thread after access to critical resources, unlock and wake up the other being the thread mutex, and waiting for the scheduler once again.
How are the “suspend wait” and “wake up wait thread” operations implemented? Each Mutex has a wait queue. A thread suspends its wait on the Mutex by first putting itself on the wait queue, then putting the thread to sleep, and then calling the scheduler function to switch to another thread. To wake up other threads in the wait queue, a thread simply takes an item out of the wait queue, changes its state from sleep to ready, and joins the ready queue. Then the next time the scheduler function executes, it is possible to switch to the awakened thread.
The main functions are as follows:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr); /* Initialize the mutex */
int pthread_mutex_destroy(pthread_mutex_t *mutex); /* Destroy the mutex */
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
Copy the code
Use mutex to resolve the above calculation error as shown in the following example:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#define LEN 100000
int num = 0;
void* thread_func(void* arg) {
pthread_mutex_t* p_mutex = (pthread_mutex_t*)arg;
for (int i = 0; i< LEN; ++i) {
pthread_mutex_lock(p_mutex);
num += 1;
pthread_mutex_unlock(p_mutex);
}
return NULL;
}
int main(a) {
pthread_mutex_t m_mutex;
pthread_mutex_init(&m_mutex, NULL);
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, (void*)thread_func, (void*)&m_mutex);
pthread_create(&tid2, NULL, (void*)thread_func, (void*)&m_mutex);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_mutex_destroy(&m_mutex);
printf("correct result=%d, result=%d.\n".2*LEN, num);
return 0;
}
Copy the code
Result: correct result=200000, result=200000.
If you have other mutex code nested within the mutex, you need to be aware of deadlocks.
Deadlock can occur in two ways:
- A situation is: if the same thread call lock, twice in the second call, because the lock has been occupied, the thread will hang wait for another thread to release the lock, lock it is occupied by himself, however, the thread and suspended but didn’t have a chance to release the lock, so is always in a state of hang wait for, deadlocks.
- Another typical deadlock situation is: thread A lock 1, won 2 lock thread B, then thread A call lock to try to get 2 lock, the result is the need to hang waiting thread lock is released 2 B, then thread B also call lock to lock 1, the result is the need to hang wait for thread A lock is released 1, then thread A and B are always in A pending state.
How to avoid deadlocks:
- No mutex (this is often difficult)
- You should write programs to avoid acquiring multiple locks at the same time.
- If this is absolutely necessary, there is a rule that if all threads acquire locks in the same order when they need multiple locks (typically by the address order of Mutex variables), no deadlocks occur. (For example, if a program uses locks 1, 2, and 3 and their corresponding Mutex variables have the address of lock 1< lock 2< lock 3, all threads that need to acquire two or three locks at the same time should acquire locks 1, 2, and 3 in the same order. If it is difficult to determine a sequence for all locks, use it as often as possible
pthread_mutex_trylock
Calls to replacepthread_mutex_lock
Call to avoid deadlocks.)
[2] Conditional variables
A condition variable can be summarized as: a thread needs to wait for a condition to be true (and this condition is determined by another thread) before it can continue to execute. If this condition is not true, the thread will block and wait until the condition is true by another thread during execution. The thread will wake up to continue executing.
The relevant functions are as follows:
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
Copy the code
For an example of the most easy to understand condition variables, “producers – consumers” mode, the producer thread to send data to the queue, consumer thread fetching data from the queue, when the consumer thread processing speed is greater than the producer thread, can produce that there are no data in the queue, a processing method is to wait for a period of time “polling” again, but this kind of treatment is not very good, You don’t know how long you have to wait, and condition variables are a good way to solve the problem. Here’s the code:
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>
#define LIMIT 1000
struct data {
int n;
struct data* next;
};
pthread_cond_t condv = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER;
struct data* phead = NULL;
void producer(void* arg) {
printf("producer thread running.\n");
int count = 0;
for (;;) {
int n = rand() % 100;
struct data* nd = (struct data*)malloc(sizeof(struct data));
nd->n = n;
pthread_mutex_lock(&mlock);
struct data* tmp = phead;
phead = nd;
nd->next = tmp;
pthread_mutex_unlock(&mlock);
pthread_cond_signal(&condv);
count += n;
if(count > LIMIT) {
break;
}
sleep(rand()%5);
}
printf("producer count=%d\n", count);
}
void consumer(void* arg) {
printf("consumer thread running.\n");
int count = 0;
for(;;) {
pthread_mutex_lock(&mlock);
if (NULL == phead) {
pthread_cond_wait(&condv, &mlock);
} else {
while(phead ! =NULL) {
count += phead->n;
struct data* tmp = phead;
phead = phead->next;
free(tmp);
}
}
pthread_mutex_unlock(&mlock);
if (count > LIMIT)
break;
}
printf("consumer count=%d\n", count);
}
int main(a) {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, (void*)producer, NULL);
pthread_create(&tid2, NULL, (void*)consumer, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
Copy the code
Execution logic in condition variables:
The key is to understand what happens when int pthread_cond_WAIT (pthread_cond_T * RESTRICT CONd, pthread_mutex_T * RESTRICT mutex) is performed, and the rest is easier to understand. Before executing this function, obtain the mutex and check whether the conditions are met. If the conditions are met, continue to execute the mutex and release the lock. If the execution condition is not met, the lock is released, and the thread blocks here, and waits until another thread notifies the execution condition is met, wakes up the thread, locks again, and then releases the lock as it goes down. (In short: release lock –> block wait –> wake up lock return)
Implementation details can be found in the source code pthread_cond_wait.c and pthread_cond_signal.c
While the above example may be a bit verbose, the following code example is even more concise:
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>
#define NUM 3
pthread_cond_t condv = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER;
void producer(void* arg) {
int n = NUM;
while(n--) {
sleep(1);
pthread_cond_signal(&condv);
printf("producer thread send notify signal. %d\t", NUM-n); }}void consumer(void* arg) {
int n = 0;
while (1) {
pthread_cond_wait(&condv, &mlock);
printf("recv producer thread notify signal. %d\n", ++n);
if (NUM == n) {
break; }}}int main(a) {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, (void*)producer, NULL);
pthread_create(&tid2, NULL, (void*)consumer, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
Copy the code
Running results:
producer thread send notify signal. 1 recv producer thread notify signal. 1
producer thread send notify signal. 2 recv producer thread notify signal. 2
producer thread send notify signal. 3 recv producer thread notify signal. 3
Copy the code
[3] Semaphore
Semaphores are used to control a shared resource that supports only a limited number of users. A count value used to hold between 0 and the specified maximum value. When the thread completes a wait for the Semaphore object, the count is reduced by one; When the thread completes a release of the Semaphore object, the count is incremented by one. When the count is 0, the thread suspends and waits until the count exceeds 0.
The main functions are as follows:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t * sem);
int sem_destroy(sem_t * sem);
Copy the code
A code example is as follows:
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>
#include<semaphore.h>
#define NUM 5
int queue[NUM];
sem_t psem, csem;
void producer(void* arg) {
int pos = 0;
int num, count = 0;
for (int i=0; i<12; ++i) {
num = rand() % 100;
count += num;
sem_wait(&psem);
queue[pos] = num;
sem_post(&csem);
printf("producer: %d\n", num);
pos = (pos+1) % NUM;
sleep(rand()%2);
}
printf("producer count=%d\n", count);
}
void consumer(void* arg){
int pos = 0;
int num, count = 0;
for (int i=0; i<12; ++i) {
sem_wait(&csem);
num = queue[pos];
sem_post(&psem);
printf("consumer: %d\n", num);
count += num;
pos = (pos+1) % NUM;
sleep(rand()%3);
}
printf("consumer count=%d\n", count);
}
int main(a) {
sem_init(&psem, 0, NUM);
sem_init(&csem, 0.0);
pthread_t tid[2];
pthread_create(&tid[0].NULL, (void*)producer, NULL);
pthread_create(&tid[1].NULL, (void*)consumer, NULL);
pthread_join(tid[0].NULL);
pthread_join(tid[1].NULL);
sem_destroy(&psem);
sem_destroy(&csem);
return 0;
}
Copy the code
Semaphore execution logic:
If the value is greater than 0, the semaphore is reduced by 1 to access the shared resource. After the access, the value is increased by 1. If the thread suspended by the semaphore is found, one of the threads is woken up. If the semaphore is 0, suspend and wait.
Sem_post.c
Third, multithreaded programming summary and thinking
Finally, we summarize and think about multithreaded programming.
- The first point is to be careful about synchronization when you do multithreaded programming, because most of the time you create multiple threads to make them work together, and if you don’t synchronize, you can have problems.
- Second, deadlocks. When multiple threads access multiple critical resources, a deadlock can occur due to improper handling. If the compile passes and the runtime is stuck, perhaps because of a deadlock, it is easier to find the problem by thinking about which threads are accessing multiple critical resources.
- Third, the processing of critical resources, multi-threaded problems, big reason is that the problem with multiple threads access to critical resources, a way of handling is to access to critical resources and processing all in one thread, use this thread to service the request of the other threads, so that only one thread access critical resources can solve many problems.
- Fourth, thread pool, when dealing with a large number of short tasks, we can first create a good thread pool, threads in the thread pool constantly from the task queue to execute, so that there is no need to create a large number of threads and destroy threads, not detailed here.
Reference documentation: pThread. h-threads
Welcome to pay attention to personal wechat official account, Let’s go!