代码: https://github.com/WHaoL/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
代码: https://gitee.com/liangwenhao/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
1. 概念
1.1、线程同步
当有一个线程在对内存进行操作时,直到该线程完成操作,其他线程都不可以对这个内存地址进行操作。
- 在多个线程操作一块共享数据的时候
按照先后顺序依次访问
- 由原来的 并行 -> 串行
1.2、临界资源
- 线程之间共享的资源都称之为临界资源
- 如:堆区、全局区、代码段、动态库加载区
- 在程序中就是全局变量, 堆变量 …
- 线程同步之后,临界资源一次只允许一个线程使用
1.3、临界区
一个访问共用资源的程序片段,而这些共用资源又无法同时被多个线程访问,这个区域称之为临界区。当有线程进入临界区段时,其他线程必须等待,以确保这些共用资源是被互斥使用的。
临界区对应的代码块越小越好(但是,该包括的一定要包括进来)
2. 互斥锁(互斥量)
2.1、互斥锁类型?
// 互斥锁的类型:pthread_mutex_t
pthread_mutex_t mutex;
2.2、互斥锁特点?
让多个线程, 串行的处理临界区资源(一个代码块)
2.3、互斥锁相关函数
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// 初始化互斥锁, 初始化完毕之后, 互斥锁是打开的
参数:
- mutex: 要初始化的互斥锁的地址
- attr: 互斥锁的属性, 一般使用默认属性不设置, 写NULL
restrict: 关键字,修饰一个指针(地址)
- restrict mutex 是说mutex指针指向的地址只能由mutex这个指针进行操作, 其他指针不行
- 举例:
pthread_mutex_t *restrict ptr = mutex;
// error,通过ptr操作mutex指针指向的内存
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 销毁互斥锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 对互斥锁上锁
// 1. 如果互斥锁状态是打开的, 上锁成功, 锁被锁定
// 2. 如果互斥锁状态是已经被锁定了, 线程被阻塞在这把锁上, 当这把锁被打开之后, 线程解除阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 对互斥锁上锁
// 1. 如果互斥锁状态是打开的, 上锁成功, 锁被锁定
// 2. 如果互斥锁状态是已经被锁定了,线程不会阻塞在这把锁上, 这个函数不阻塞线程
// 3. 通过返回值判断加锁成功了还是失败了
// 0-> 成功, 非0 -> 失败
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 打开互斥锁
3. 死锁
两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁 。
3.1、死锁 的几种场景
3.1.1、忘记释放锁,自己将自己锁住
void funcA()
{
while(1)
{
pthread_mutex_lock(&mutex);
......
.......
// 忘记解锁
}
}
3.1.2、单线程重复申请锁
void funcA()
{
while(1)
{
pthread_mutex_lock(&mutex);
......
.......
pthread_mutex_unlock(&mutex);
}
}
int funcB()
{
......;
......;
pthread_mutex_lock(&mutex);
funcA();
pthread_mutex_unlock(&mutex);
}
3.1.3、多线程多锁申请, 抢占锁资源
线程A有一个锁1,线程B有一个锁2。线程A试图调用lock来获取锁2就得挂起等待线程B释放,线程B也调用lock试图获得锁1。那么这就很尴尬,都在等对方释放,然后获得对方的锁。
3.2、死锁的解决
-
1.加锁的时候使用trylock
pthread_mutex_trylock
-
2.在访问其他资源加锁的时候, 先将自己的锁解开
4. 读写锁
1.读写锁类型? 是几把锁?
类型:pthread_rwlock_t
这是一把锁, 可以做两件事儿
- 对读操作加锁
- 对写操作加锁
2.读写锁的特点?
// 场景
// 1. 线程A加读锁成功, 又来了三个线程, 做读操作, 可以加锁成功
- 读写锁对于读来说是共享的, 可以并行操作
// 2. 线程A加写锁成功, 又来了三个线程, 做读操作, 三个线程阻塞
// 线程A加读锁成功, 又来了三个线程, 做写操作, 三个线程阻塞
- 既有读又有写操作, 不共享, 先来的加锁成功, 后来的阻塞,
// 3. 线程A加读锁成功, 又来了B线程加写锁阻塞, 又来了C线程加读锁阻塞
- 写的优先级比读的优先级高,
当线程A解锁之后, 线程B拿到了锁, 加锁成功, C继续阻塞
- 写的不共享的, 是独占的
3.什么时候使用读写锁?
// 假设共享数据大部分时候线程对其进行读操作 -> 概率: 90%读-> 使用读写锁,效率高
大量的读操作,少量的写操作,加读写锁-->能提高程序的效率
4.操作函数
#include <pthread.h>
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
参数:
参数一:读写锁的地址
参数二:读写锁的属性,默认为NULL
// 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
// 加读锁, 阻塞
// 有其他线程对这把锁加了写锁, 这时候阻塞
// 有其他线程对这把锁加了读锁, 不阻塞, 读锁是共享的
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 尝试加读锁, 不阻塞
int pthread_rwlock_tryrdlock(pthread_rwlock_t *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);
练习/code
# 8个线程操作同一个全局变量
3个线程不定时写同一全局资源,
5个线程不定时读同一全局资源
1.查看不加锁结果
2.查看加锁之后的结果
5. 条件变量 -> condition
5.1.1条件变量是锁吗?
不是锁, 不能同步线程,
但是可以阻塞线程, 需要配合互斥锁一起使用
5.1.2条件变量的两个动作
使用者可以设置一些条件
1.如果条件不满足, 可以让线程阻塞(调用条件变量中对应的函数)
2.当条件满足, 唤醒阻塞的线程(调用条件变量中对应的函数)
5.1.3条件变量类型
pthread_cond_t
5.1.4条件变量/信号量的使用场景
在处理生产者和消费者模型的时候, 使用条件变量/信号量
5.3、条件变量操作函数
#include <pthread.h>
// 初始化条件变量
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
参数:
参数一:条件变量的地址
参数二:条件变量的属性,一般写NULL
// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
// 阻塞线程的函数
// 线程走到这个函数的位置:线程阻塞;这个线程已经成功对互斥锁加锁
// 这个wait函数干两件事:
// 1.(阻塞线程之前)
// 假设这个线程阻塞在条件变量这儿,整个程序有可能死锁
// 因此这个wait函数在阻塞线程之前,会自动将线程对应的互斥锁解锁
// 2.(解除阻塞线程之后)
// 当线程解除阻塞,wait函数会将线程对应的互斥锁再加锁
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
参数:
- cond: 条件变量的地址
- mutex: 互斥锁变量 -> 使用这个互斥锁进行线程同步
// 将线程阻塞一定的时长
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
参数:
- cond: 使用的条件变量的地址
- mutex: 互斥锁变量 -> 使用这个互斥锁进行线程同步
- abstime: 阻塞的时长, 这个时间是从1970.1.1开始计算
需要: time(NULL) + 需要阻塞的时长
struct timespec
{
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
即:abstime.tv_sec=time(NULL)+阻塞的秒数
// 唤醒阻塞的线程, 至少唤醒一个阻塞的线程
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞的线程, 唤醒所有阻塞的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
5.2、生产者和消费者模型
// 几个角色: 生产者, 消费者, 还需要一个容器(存储数据的, 为生产者和消费者服务)
// 容器需要对应一个数据结构: 队列
/*
生产者:
- 生成数据放到消息队列中, 队列满了之后, 生产者阻塞
- 当队列从满 -> 不满, 生产者解除阻塞, 继续工作
消费者:
- 处理任务队列中的消息
- 当前任务队列为空的时候, 消费者阻塞
- 当前任务队列不为空的时候, 消费者解除阻塞
*/
5.4、练习
使用条件量实现:生产线和消费者模型
生产者往链表中添加节点
消费者删除链表节点
规则如下:
生产者:新节点挂在头部
消费者:删除头部节点
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <pthread.h>
//6.1.1、定义条件变量
pthread_cond_t cond;
//4.1、创建互斥锁
pthread_mutex_t mutex;
//2.1、定义链表节点的结构体
struct Node
{
int number;//存储的数据
struct Node* next;//指向下一个节点的指针
};
//2.2、定义头结点指针
//生产者和消费者通过它来操作容器
struct Node* head = NULL;
//3.1、线程处理函数
void* producer(void* arg)
{
while(1)
{
//5.1、互斥锁:加锁
pthread_mutex_lock(&mutex);
//3.1.1、生成节点
struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
//3.1.2、初始化
pnew->number = rand()%1000;
pnew->next = NULL;
//3.1.3、添加到表头
pnew->next = head;
head = pnew;
//5.1、互斥锁:解锁
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
//6.2、唤醒阻塞在条件变量上的线程
pthread_cond_signal(&cond);
}
return NULL;
}
//3.1、线程处理函数
void* customer(void* arg)
{
while(1)
{
//5.1、互斥锁:加锁
pthread_mutex_lock(&mutex);
//3.2.1、判断链表是否为空
// 多线程情况下:多个消费者线程同时阻塞在wait函数这儿
// 当生产者线程同时唤醒好几个阻塞在此处的消费者线程后
// 当一个解除阻塞的消费者线程去执行下面的代码(删除节点)后
// 另外一个解除阻塞的消费者线程应该先判断容器是否为空,
// 因为可能消费了一次后,容器又为空了,不能消费,需继续阻塞线程
while(head == NULL)
{
//链表为空,消费者就不能消费,消费者线程阻塞
//6.1、阻塞消费者线程
// 线程走到这个函数的位置:线程阻塞;这个线程已经成功对互斥锁加锁
// 这个wait函数干两件事:
// 1.(阻塞线程之前)
// 假设这个线程阻塞在条件变量这儿,整个程序有可能死锁
// 因此这个wait函数在阻塞线程之前,会自动将线程对应的互斥锁解锁
// 2.(解除阻塞线程之后)
// 当线程解除阻塞,wait函数会将线程对应的互斥锁再加锁
pthread_cond_wait(&cond,&mutex);
}
//3.2.2、链表不为空,开始消费
struct Node* pNode = head;
head = head->next;
printf("删除节点的number:%d,%ld\n",pNode->number,pthread_self());
free(pNode);//删除节点
//5.1、互斥锁:解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
//4.2、初始化互斥锁
pthread_mutex_init(&mutex,NULL);
//6.1.2、初始化条件变量
pthread_cond_init(&cond,NULL);
//1.1、创建线程--5个读,5个写线程
pthread_t ptid[5];//生产者
pthread_t ctid[5];//消费者
for(int i=0;i<5;++i)
{
pthread_create(&ptid[i],NULL,producer,NULL);
pthread_create(&ctid[i],NULL,customer,NULL);
}
//1.2、回收子线程资源
for(int i=0;i<5;++i)
{
pthread_join(ptid[i],NULL);
pthread_join(ctid[i],NULL);
}
//4.3、销毁互斥锁
pthread_mutex_destroy(&mutex);
//6.1.3、销毁条件变量
pthread_cond_destroy(&cond);
return 0;
}
6. 信号量
概念
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。
信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务 并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。
信号量(信号灯)与互斥锁和条件变量的主要不同在于”灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用。
信号量主要阻塞线程, 不能完全保证线程安全.
如果要保证线程安全, 需要信号量和互斥锁一起使用.
信号量类型
sem_t:理解为在这个变量中有一个整形数 >=0
== 0: 线程阻塞
=> 0: 不阻塞
信号量操作函数
#include <semaphore.h>
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
- 参数一: 信号量变量的地址
- pshared:
0: 线程操作
1: 进程操作
- value: 给信号量初始化一个整形值 >=0, 设置给了第一个参数
value值代表了初试时有几个线程工作
// 销毁信号量
int sem_destroy(sem_t *sem);
// 在某些情况下阻塞线程
int sem_wait(sem_t *sem);
调用sem_init的时候,第三个参数value初始化了一个整形数
1.sem_wait每调用一次,这个整形数 -1
2.当这个整形数为0的时候, 再调用这个函数->线程阻塞
int sem_trywait(sem_t *sem);
1.sem_trywait每调用一次,这个整形数 -1
2.当这个整形数为0的时候, 调用这个函数->不阻塞线程, 函数返回
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
1.sem_timedwait每调用一次,这个整形数 -1
2.当这个整形数为0的时候, 调用这个函数->阻塞线程,
阻塞第二个参数指定的时间长度
3.阻塞时间完成, 解除阻塞
// 让信号量变量sem中的整数 +1
int sem_post(sem_t *sem);
// 获取当前参数sem中的整数值
int sem_getvalue(sem_t *sem, int *sval);
1.获取当前参数sem中的整数现在的值为多少
2.通过第二个参数将得到的数值传出
练习
使用信号量实现:生产线和消费者模型:
生产者往链表中添加节点
消费者删除链表节点
/*
思路:
- 分析有几类线程, 需要定义几个信号量: 两类线程、对应两类信号量
- 生产者线程: sem_t psem;
- 消费者线程: sem_t csem;
信号量初始化:
生产者:sem_init(&psem, 0, 5); // 生产者可以生产5次
消费者:sem_init(&psem, 0, 0); // 消费者默认不能消费 -> 阻塞
sem_init(&psem, 0, 5); // 生产者线程5个
sem_init(&psem, 0, 0); // 消费者线程0个,默认不能消费 -> 阻塞
*/
伪代码:
// 生产者线程-回调函数
void* producer(void* arg)
{
// 通过信号量去尝试阻塞线程
sem_wait(&psem); // psem--
// 创建链表节点, 添加到表头
// 创建完成, 通知消费者消费
sem_post(&csem); // csem++
}
// 消费者线程-回调函数
void* customer(void* arg)
{
// 根据初始化数据, 刚开始所有的消费者线程都会阻塞在这
sem_wait(&csem);
// 监测是否可以消费
// 消费:删除链表节点
// 通知生产者生产
sem_post(&psem);//psem++
}
int main()
{
// 创建生产者线程 -> 5
// 创建消费者线程 -> 5
sem_init(&psem, 0, 5); // 生产者线程5个
sem_init(&psem, 0, 0); // 消费者线程0个,默认不能消费 -> 阻塞
}
sem, 0, 0); // 消费者线程0个,默认不能消费 -> 阻塞
*/
```c
伪代码:
// 生产者线程-回调函数
void* producer(void* arg)
{
// 通过信号量去尝试阻塞线程
sem_wait(&psem); // psem--
// 创建链表节点, 添加到表头
// 创建完成, 通知消费者消费
sem_post(&csem); // csem++
}
// 消费者线程-回调函数
void* customer(void* arg)
{
// 根据初始化数据, 刚开始所有的消费者线程都会阻塞在这
sem_wait(&csem);
// 监测是否可以消费
// 消费:删除链表节点
// 通知生产者生产
sem_post(&psem);//psem++
}
int main()
{
// 创建生产者线程 -> 5
// 创建消费者线程 -> 5
sem_init(&psem, 0, 5); // 生产者线程5个
sem_init(&psem, 0, 0); // 消费者线程0个,默认不能消费 -> 阻塞
}