目录
1. 概念
2. 互斥锁
3. 死锁
4. 读写锁
5. 条件变量
5.1 生产者和消费者模型
6. 信号量
1. 概念
线程同步:> 当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作。
> - 在多个线程操作一块共享数据的时候
> - 按照先后顺序依次访问
> - 有原来的 并行 -> 串行
临界资源:一次只允许一个线程使用的资源。原子操作:
> 原子操作,就是说像原子一样不可再细分不可被中途打断。
> 一个操作是原子操作,意思就是说这个操作是以原子的方式被执行,要一口气执行完,执行过程不能够被OS的其他行为打断,是一个整体的过程,在其执行过程中,OS的其它行为是插不进来的。
2. 互斥锁
互斥锁类型:// pthread_mutex_t 互斥锁的类型pthread_mutex_t mutex;
互斥锁特点:让多个线程, 串行的处理临界区资源(一个代码块)互斥锁相关函数:
#include <pthread.h>// 初始化互斥锁int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);参数: - mutex: 互斥锁的地址- attr: 互相锁的属性, 使用默认属性, 赋值为NULL就可以// 释放互斥锁资源int pthread_mutex_destroy(pthread_mutex_t *mutex);// 将参数指定的互斥锁上锁// 比如: 3个线程, 第一个线程抢到了锁, 对互斥锁加锁 -> 加锁成功, 进入了临界区// 第二,三个个线程也对这把锁加锁, 因为已经被线程1锁定了, 线程2,3阻塞在了这把锁上 -> 不能进入临界区,// 当这把锁被打开, 线程2,3解除阻塞, 线程2,3开始抢锁, 谁抢到谁加锁进入临界区, 另一个继续阻塞在锁上int pthread_mutex_lock(pthread_mutex_t *mutex);// 尝试加锁, 如果这把锁已经被锁定了, 加锁失败, 函数直接返回, 不会阻塞在锁上int pthread_mutex_trylock(pthread_mutex_t *mutex);// 解锁函数int pthread_mutex_unlock(pthread_mutex_t *mutex);
其中:
restrict: 修饰符, 被修饰过的变量特点: 不能被其他指针引用- mutex变量对应一块内存- 举例: pthread_mutex_t* ptr; ptr = &mutex; // error- 即便做了赋值, 使用ptr指针操作mutex对应的内存也是不允许的
3. 死锁
两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁 。
死锁几种场景:
忘记释放锁,自己将自己锁住单线程重复申请锁多线程多锁申请, 抢占锁资源(线程A有一个锁1,线程B有一个锁2。线程A试图调用lock来获取锁2就得挂起等待线程B释放,线程B也调用lock试图获得锁1。都在等对方释放,然后获得对方的锁。)
4. 读写锁
读写锁类型? 是几把锁?读写锁的特点1. 读写锁是一把锁
2. 锁定读操作, 锁定写操作
3. 类型: pthread_rwlock_t
什么时候使用读写锁?/*
1. 读操作可以并进行, 多个线程
2. 写的时候独占资源的
3. 写的优先级高于读的优先级
*/
场景:
// 1. 线程A加读锁成功, 又来了三个线程, 做读操作, 可以加锁成功----读操作是共享的, 三个新来的线程可以加读锁成功 // 2. 线程A加写锁成功, 又来了三个线程, 做读操作, 三个线程阻塞-------加读锁失败, 会阻塞在读锁上, 写完了 // 3. 线程A加读锁成功, 又来了B线程加写锁阻塞, 又来了C线程加读锁阻塞------写的独占的, 写的优先级高
操作函数:互斥锁: 数据所有的读写都是串行的
读写锁:
- 读: 并行
- 写: 串行
读的频率 > 写的频率
#include <pthread.h>// 初始化读写锁int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);参数:- rwlock: 读写锁地址- attr: 读写锁属性, 使用默认属性, 设置为: NULL// 释放读写锁资源int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);// 加读锁// rwlock被加了写锁, 这时候阻塞// rwlock被加了读锁, 不阻塞, 可以加锁成功 -> 读共享int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);// 尝试加读锁int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);// 加写锁// rwlock -> 加了读锁, 加了写锁 多会阻塞 -> 写独占int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);// 尝试加写锁int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);// 读写锁解锁int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
练习例子: 8个线程操作同一个全局变量,其中3个线程不定时写同一全局资源,其中5个线程不定时读同一全局资源
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <errno.h>#include <pthread.h>int number = 1;pthread_rwlock_t rwlock;void* writeNum(void* arg){while(1){pthread_rwlock_wrlock(&rwlock);number++;usleep(100);printf("+++ write, tid: %ld, number: %d\n", pthread_self(), number);pthread_rwlock_unlock(&rwlock);usleep(100);}return NULL;}void* readNum(void* arg){while(1){pthread_rwlock_rdlock(&rwlock);printf("=== read, tid: %ld, number: %d\n", pthread_self(), number);pthread_rwlock_unlock(&rwlock);usleep(100);}return NULL;}int main(int argc, char *argv[]){pthread_t wtid[3], rtid[5];//初始化锁pthread_rwlock_init(&rwlock, NULL);//创建写进程for (int i=0; i<3; ++i){pthread_create(&wtid[i],NULL, writeNum, NULL);}//创建读进程for (int i=0; i<5; ++i){pthread_create(&rtid[i], NULL, readNum, NULL);}//回收进程for (int i=0; i<3; ++i){pthread_join(wtid[i], NULL);}for (int i=0; i<5; ++i){pthread_join(rtid[i], NULL);}//销毁锁pthread_rwlock_destroy(&rwlock);return 0;}
5. 条件变量
条件变量不是锁条件变量两个动作:条件变量类型条件变量能引起某个线程的阻塞具体来说就是:
- 某个条件满足之后, 阻塞线程 - 某个条件满足, 线程解除阻塞
如果使用了条件变量进行线程同步, 多个线程操作了共享数据, 不能解决数据混乱问题,解决该问题,需要配合使用互斥锁
pthread_cond_t
条件变量操作函数
#include <pthread.h>// 初始化条件变量int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);参数: - cond: 条件变量的地址- attr: 使用默认属性, 这个值设置为NULL// 释放资源int pthread_cond_destroy(pthread_cond_t *cond);// 线程调用该函数之后, 阻塞int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);参数:- cond: 条件变量- mutex: 互斥锁struct timespec {time_t tv_sec;/* Seconds */long tv_nsec;/* Nanoseconds [0 .. 999999999] */};// 在指定的时间之后解除阻塞int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);参数:- cond: 条件变量- mutex: 互斥锁- abstime: 阻塞的时间- 当前时间 + 要阻塞的时长struct timeval val;可以使用函数:gettimeofday(&val, NULL);// 唤醒一个或多个阻塞在 pthread_cond_wait / pthread_cond_timedwait 函数上的线程int pthread_cond_signal(pthread_cond_t *cond);// 唤醒所有的阻塞在 pthread_cond_wait / pthread_cond_timedwait 函数上的线程int pthread_cond_broadcast(pthread_cond_t *cond);
5.1 生产者和消费者模型
角色分析:
- 生产者
- 消费者
- 容器
栗子:使用条件量实现 生产线和消费者模型: 生产者往链表中添加节点, 消费者删除链表节点
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <errno.h>#include <pthread.h>pthread_cond_t cond; //条件变量pthread_mutex_t mutex; //互斥锁//连表节点struct Node{int number;struct Node* next;};//指向链表第一个节点的指针struct Node* head = NULL;// 生产者函数、void* producer(void* arg){while(1){//创建新的链表节点pthread_mutex_lock(&mutex);struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));pnew->next = head;head = pnew;pnew->number = rand() % 1000;printf("add+++ node, number: %d, tid = %ld\n", pnew->number, pthread_self());pthread_mutex_unlock(&mutex);//生产者生产了东西,通知消费者消费pthread_cond_signal(&cond);}return NULL;}//消费者函数void* customer(void* arg){while(1){pthread_mutex_lock(&mutex);while (head == NULL){//链表为空,阻塞pthread_cond_wait(&cond, &mutex);}struct Node* pnode = head;head = head->next;printf("del--- node, number: %d, tid = %ld\n", pnode->number, pthread_self());free(pnode);pthread_mutex_unlock(&mutex);}return NULL;}int main(int argc, char *argv[]){pthread_t ptid[5], ctid[5];pthread_cond_init(&cond,NULL);pthread_mutex_init(&mutex,NULL);for (int i=0; i<5; ++i){pthread_create(&ptid[i], NULL, producer, NULL);pthread_create(&ctid[i], NULL, customer, NULL);}for (int i=0; i<5; ++i){pthread_join(ptid[i], NULL);pthread_join(ctid[i], NULL);}pthread_cond_destroy(&cond);pthread_mutex_destroy(&mutex);return 0;}
6. 信号量
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务 并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。信号量(信号灯)与互斥锁和条件变量的主要不同在于”灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用信号量主要阻塞线程, 不能完全保证线程安全.如果要保证线程安全,需要信号量和互斥锁一起使用.- 信号量类型:
sem_t
在这个变量中记录了一个整形数, 如果这个数据 是5, 允许有五个线程访问数据
o o o o o
如果有一线程访问了共享资源, 这个整形数 -1, 后边又有4个线程访问了共享数据 0,
这时候, 再有线程访问共享数据, 这些线程阻塞
- 信号量操作函数:
#include <semaphore.h>// 初始化信号量int sem_init(sem_t *sem, int pshared, unsigned int value);参数: - sem: 信号量的地址- pshared: 0-> 处理线程, 1-> 处理进程- value: sem_t中整形数初始化// 释放资源int sem_destroy(sem_t *sem);// 有可能引起阻塞// 调用一次这个函数 sem 中整形数 --// 当 sem_wait 并且 sem中的整形数为0 , 阻塞了int sem_wait(sem_t *sem);// 当 sem_trywait 并且 sem中的整形数为0 , 返回, 不阻塞int sem_trywait(sem_t *sem);// 当 sem_timedwait 并且 sem中的整形数为0 , 阻塞一定的时长, 时间到达, 返回int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);// 当 sem_post sem 中的整形数 ++int sem_post(sem_t *sem);// 查看 sem中的整形数的值, 通过第二个参数返回int sem_getvalue(sem_t *sem, int *sval);