线程同步
互斥锁
多线程共享一个互斥量,然后线程之间去竞争。得到锁的线程可以进入临界区执行代码。
使用互斥锁的步骤:定义互斥锁–>初始化互斥锁–>加锁–>解锁–>销毁锁
//初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
//解锁,解锁之后代码不再排他访问
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
使用互斥锁最重要的是注意死锁问题,死锁成立的四个条件:
- 互斥条件:一个资源被一个线程占有
- 不剥夺条件:对已经获得的资源在未使用完之前保持占有
- 请求与保持条件:未获得资源导致阻塞不会放弃自己占有的资源
- 循环等待条件:构成一种循环等待资源的关系
破坏任一条件会破坏死锁。
信号量
linux sem 信号量是一种特殊的变量,访问具有原子性, 用于解决进程或线程间共享资源引发的同步问题。
Linux中使用信号量机制控制:定义信号量–>初始化信号量–>信号量PV操作–>销毁信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
//pshared为0代表线程要使用的信号量,为1代表进程要使用的信号量
int sem_post(sem_t * sem);//信号量增加1
int sem_wait(sem_t * sem);//信号量减少1
int sem_destroy(sem_t * sem);//销毁信号量
int sem_getvalue(sem_t * sem, int * sval);//获取当前信号量的值
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
void *th1(void *arg);
void *th2(void *arg);
sem_t sem_H, sem_W;
int main(int argc, char *argv[])
{
pthread_t tid1, tid2;
sem_init(&sem_H, 0, 1); ‣sem: &sem_H ‣pshared: 0 ‣value: 1
sem_init(&sem_W, 0, 0); ‣sem: &sem_W ‣pshared: 0 ‣value: 0
pthread_create(&tid1, NULL, th1, NULL); ‣newthread: &tid1 ‣attr: NULL ‣start_routine: th1 ‣arg: NULL
pthread_create(&tid2, NULL, th2, NULL); ‣newthread: &tid2 ‣attr: NULL ‣start_routine: th2 ‣arg: NULL
pthread_join(tid1, NULL); ‣th: tid1 ‣thread_return: NULL
pthread_join(tid2, NULL); ‣th: tid2 ‣thread_return: NULL
sem_destroy(&sem_H); ‣sem: &sem_H
sem_destroy(&sem_W); ‣sem: &sem_W
printf("Main end!\n"); ‣format: "Main end!\n"
return 0;
}
//start H = 1, W = 0;
//2
//World \n
void *th1(void *arg)
{
int i = 5;
while (i > 0) {
sem_wait(&sem_W); ‣sem: &sem_W
printf("World!\n"); ‣format: "World!\n"
sem_post(&sem_H); ‣sem: &sem_H
sleep(1); ‣seconds: 1
i--;
}
pthread_exit(NULL); ‣retval: NULL
return NULL;
}
//Hello
void *th2(void *arg)
{
int i = 5;
while (i > 0) {
sem_wait(&sem_H); ‣sem: &sem_H
printf("Hello "); ‣format: "Hello "
sem_post(&sem_W); ‣sem: &sem_W
sleep(1); ‣seconds: 1
i--;
}
pthread_exit(NULL); ‣retval: NULL
return NULL;
}
条件变量
条件变量本质也是一个全局变量,它的功能是阻塞线程,直至接收到“条件成立”的信号后,被阻塞的线程才能继续执行。
一个条件变量可以阻塞多个线程,这些线程会组成一个等待队列。当条件成立时,条件变量可以解除线程的“被阻塞状态”。也就是说,条件变量可以完成以下两项操作:
- 阻塞线程,直至接收到“条件成立”的信号;
- 向等待队列中的一个或所有线程发送“条件成立”的信号,解除它们的“被阻塞”状态。
为了避免多线程之间发生“抢夺资源”的问题,条件变量在使用过程中必须和一个互斥锁搭配使用。
#include <pthread.h>
pthread_cond_t myCond;
//初始化
int pthread_cond_init(pthread_cond_t * cond, const pthread_condattr_t * attr);
//等待条件变量处理,调用两个函数之前,我们必须先创建好一个互斥锁并完成“加锁”操作,然后才能作为实参传递给 mutex 参数
int pthread_cond_wait(pthread_cond_t* cond, pthread_mutex_t* mutex);
int pthread_cond_timedwait(pthread_cond_t* cond, pthread_mutex_t* mutex, const struct timespec* abstime);
//发送条件成立的消耗,解除阻塞
int pthread_cond_signal(pthread_cond_t* cond);
int pthread_cond_broadcast(pthread_cond_t* cond);
//销毁
int pthread_cond_destroy(pthread_cond_t *cond);
示例:
void *setA(void *arg)
{
while (TRUE) {
a++;
printf("---setA--- a = %d \n", a);
sleep(1);
pthread_cond_signal(&myCond[0]);
}
return NULL;
}
void *setB(void *arg)
{
while (TRUE) {
b++;
printf("---setB--- b = %d \n", b);
sleep(5);
pthread_cond_signal(&myCond[0]);
}
return NULL;
}
void *printNum(void *arg)
{
while (TRUE) {
pthread_cond_wait(&myCond[0], &condMutex[0]);
printf("---printNum--- a = %d b = %d\n", a, b);
}
return NULL;
}
线程屏障
屏障(barrier)是用户协调多个线程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都达到某一点,然后从该点继续执行。pthread_join函数就是一种屏障,允许一个线程等待,直到另一个线程退出。
但是屏障对象的概念更广,它们允许任意数量的线程等待,直到所有的线程完成处理工作,而线程不需要退出。所有线程达到屏障后可以接着工作。
#include <pthread.h>
//初始化
int pthread_barrier_init(pthread_barrier_t *restrict barrier, const pthread_barrierattr_t *restrict attr, unsigned int count);
//使用
int pthread_barrier_wait(pthread_barrier_t *barrier);
//销毁
int pthread_barrier_destroy(pthread_barrier_t *barrier);
示例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/timeb.h>
#include <time.h>
static int num1 = 0;
static int num2 = 0;
static int count1 = 10000000;
static int count2 = 20000000;
static pthread_barrier_t barrier;
void* fun2(void *arg)
{
pthread_t thread_id = pthread_self();
printf("the thread2 id is %ld\n", (long)thread_id);
for (size_t i = 0; i<count2; ++i) {
num2 += 1;
}
printf("The thread2 num2 is %d\n", num2);
pthread_barrier_wait(&barrier); //等待两个线程都到达 pthread_barrier_wait处
return NULL;
}
int main (int argc, char *argv[])
{
int err;
pthread_t thread1;
pthread_t thread2;
const int thread_num = 2;
thread1 = pthread_self();
printf("the thread1 id is %ld\n", (long)thread1);
//初始化
pthread_barrier_init(&barrier, NULL, thread_num);
// Create thread
err = pthread_create(&thread2, NULL, fun2, NULL);
if (err != 0) {
perror("can't create thread2\n");
}
pthread_detach(thread2);
for (size_t i = 0; i<count1; ++i) {
num1 += 1;
}
printf("The thread1 num1 is %d\n", num1);
pthread_barrier_wait(&barrier); //等待两个线程都到达 pthread_barrier_wait处
//运行到该处时,num1和num2分别为10000000和20000000
printf("the thread1 get SERIAL, num1+num2=%d\n", num1+num2)
pthread_barrier_destroy(&barrier);
return 0;
}
生产者消费者
在生产者-消费者模式中,通常有两类线程,一类是生产者线程一类是消费者线程。生产者线程负责提交用户请求,消费者线程则负责处理生产者提交的任务。
最简单粗暴的做法就是生产者每提交一个任务,消费者就立即处理,直到所有任务处理完。但是这样直接通信很容易出现性能上的问题,消费者必须等待它的生产者提交到任务才能执行,就不能达到真正的并行。同时生产者和消费者之间存在依赖关系,在设计上耦合度非常高,这是不可取的。那么最好的做法就是加一个中间层作为通信桥梁。
生产者和消费者之间通过共享内存缓存区进行通信。多个生产者线程将任务提交给共享内存缓存区,消费者线程并不直接与生产者线程通信,而在共享内存缓冲区获取任务,并行地处理。其中内存缓冲区的主要功能是数据再多线程间的共享,同时还可以通过该缓存区,缓解生产者和消费者间的性能差。它是生产者消费者模式的核心组件,既能作为通信的桥梁,又能避免两者直接通信,从而将生产者和消费者进行解耦。生产者不需要消费者的存在,消费者也不需要知道生产者的存在。
由于内存缓冲区的存在,允许生产者和消费者在执行速度上有差异,无论哪一方速度超过另一方,缓冲区都可以得到缓解,确保系统正常运行。
在实现上可以使用互斥锁和两个信号量来实现。当生产者生产速度大于消费者时,缓冲区先满,生产者不在工作,等待消费者取出商品,缓冲区为非满的时候生产者才可以继续生产。
(之前复制的,不知道来源,侵删)
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
sem_t empty,full; //定义全局同步信号量empty,full
pthread_mutex_t mutex; //定义一个全局互斥量,在不同函数中
int buffer_count=0; //定义一个全局变量,表示管道内得产品数目
void *producer(void *arg ); //生产者线程
void *consumer(void *arg ); //消费者线程
int main(int argc , char *argv[])
{
pthread_t thrd_prod, thrd_cons;
pthread_mutex_init(&mutex , NULL); //初始化互斥量
sem_init(&empty, 0, 5); //初始化empty信号量
sem_init(&full, 0, 0); //初始化full信号量
//创建生产者和消费者线程
if( pthread_create( &thrd_prod, NULL, producer, NULL) != 0 )
printf( "thread create failed." );
if( pthread_create( &thrd_cons , NULL, consumer ,NULL ) != 0 )
printf( "thread create failed." );
//等待线程结束
if( pthread_join( thrd_prod , NULL ) != 0 )
printf(" wait thread failed.");
if( pthread_join( thrd_cons , NULL ) != 0 )
printf(" wait thread failed.");
sem_destroy (&full); //释放同步量 ‣sem: &full
sem_destroy(&empty); //释放同步量 ‣sem: &empty
pthread_mutex_destroy(&mutex); //关闭互斥量 ‣mutex: &mutex
return 0;
}
void *producer(void *arg){
while(1){
sem_wait(&empty); //empty-1
pthread_mutex_lock(&mutex ); //加锁
//成功占有互斥量,接下来可以对缓冲区(仓库)进行生产
//操作
printf( " producer put a product to buffer.");
buffer_count++;
printf("the buffer_count is %d\n",buffer_count) ;
pthread_mutex_unlock(&mutex); //解锁
sem_post(&full); //full+1
}
}
void *consumer(void *arg){
while(1)
{
sem_wait(&full); //full-1
pthread_mutex_lock(&mutex); //加锁
//成功占有互斥量,接下来可以对缓冲区(仓库)进行取出
//操作
printf( "consumer get a product from buffer.");
buffer_count--;
printf("the buffer_count is %d\n",buffer_count);
pthread_mutex_unlock( &mutex ); //解锁
sem_post(&empty); //empty-1
sleep(1);
}
}
可以看出生产者生产速度更快,缓冲区满后生产者停止工作,等待消费者从缓冲区中取出商品后缓冲区非满,生产者继续工作。
线程池
这部分是跟着一个大佬的文章敲的,对线程池稍有一些理解。相当于是上面生产者-消费者的升级版,多了消费者线程对队列进行动态管理,当满足一定条件时添加线程或者销毁线程,贴一下大佬文章的链接,有兴趣去看下。
手写线程池 - C 语言版