Appearance
线程同步
需要的原因
编程中、通信中所说的同步与生活中大家印象中的同步概念略有差异。“同”字应是指协同、协助、互相配合。主旨在协同步调,按预定的先后次序运行。
线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其它线程为保证数据一致性,不能调用该功能。
这一个主要是防止线程使用公共资源的时候, 比如说公共变量的时候发生同时访问(比如读取一个寄存器的时候有读-改-写三步, 读取以后有一个中断把这一个寄存器改变了以后返回, 这时候进行改的变量里面没有更新, 实际写入的数据会进行覆盖中断里面的设置), 以及一个设备同时被多个进程使用
- 资源共享(独享资源则不会)
- 调度随机(意味着数据访问会出现竞争)
- 线程间缺乏必要的同步机制。
以上3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。只要存在竞争关系,数据就很容易出现混乱。
所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。
互斥量mutex
Linux中提供一把互斥锁mutex(也称之为互斥量)。这一个一般有优先级继承, 防止优先级翻转, 不可以在中断里面使用的特点, 这一个锁在初始化的时候可以设置是不是可以重复上锁
每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。
资源还是共享的,线程间也还是竞争的,
但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。
应注意:同一时刻,只能有一个线程持有该锁。
当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。
所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。
因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。
主要应用函数:
pthread_mutex_init函数
pthread_mutex_destroy函数
pthread_mutex_lock函数
pthread_mutex_trylock函数
pthread_mutex_unlock函数
以上5个函数的返回值都是:成功返回0, 失败返回错误号。
pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。
pthread_mutex_t mutex; 变量mutex只有两种取值1、0。
pthread_mutex_init
初始化一个互斥锁(互斥量) ---> 初值可看作1
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
restrict: 这一个指针指向的变量只能被这一个指针调用
参1:传出参数,调用时应传 &mutex
restrict关键字:只用于限制指针,告诉编译器,所有修改该指针指向内存中内容的操作,只能通过本指针完成。不能通过除本指针以外的其他变量或指针修改
参2:互斥量属性。是一个传入参数,通常传NULL,选用默认属性(线程间共享)。 参APUE.12.4同步属性
- 静态初始化:如果互斥锁 mutex 是静态分配的(定义在全局,或加了static关键字修饰),可以直接使用宏进行初始化
cpthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;//一个不可以重入 pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;//可以重入 pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
- 动态初始化:局部变量应采用动态初始化。e.g. pthread_mutex_init(&mutex, NULL)
可以使用函数pthread_mutexattr_init设置它的参数
pthread_mutex_destroy
销毁一个互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_lock
加锁。可理解为将mutex--(或 -1),操作后mutex的值为0。
int pthread_mutex_lock(pthread_mutex_t *mutex);
尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止。
pthread_mutex_unlock
解锁。可理解为将mutex ++(或 +1),操作后mutex的值为1。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
在使用这锁的时候使用结束最好立即解锁
主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级、调度。默认:先阻塞、先唤醒。
phread_mutex_trylock
尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
如果是一个fast锁, 已经上锁会返回EBUSY
示例
c
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
pthread_mutex_t mutex;
void *tfn(void *arg){
while(1){
printf("HELLO ");
sleep(rand()%3);
printf("WORLD\n");
sleep(rand()%3);
}
return NULL;
}
int main(void){
pthread_t tid;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if(ret != 0){
printf("pthread_create error: %s", strerror(ret));
exit(1);
}
while(1){
printf("hello ");
sleep(rand()%3);
printf("word\n");
sleep(rand()%3);
}
return 0;
}
不使用互斥锁的时候
c
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
pthread_mutex_t mutex;
void *tfn(void *arg){
while(1){
pthread_mutex_lock(&mutex);
printf("HELLO ");
sleep(rand()%3);
printf("WORLD\n");
pthread_mutex_unlock(&mutex);
sleep(rand()%3);
}
return NULL;
}
int main(void){
pthread_mutex_init(&mutex, NULL);//初始化
pthread_t tid;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if(ret != 0){
printf("pthread_create error: %s", strerror(ret));
exit(1);
}
while(1){
pthread_mutex_lock(&mutex);//上锁
printf("hello ");
sleep(rand()%3);
printf("word\n");
pthread_mutex_unlock(&mutex);//解锁
sleep(rand()%3);
}
return 0;
}
死锁
- 线程试图对同一个互斥量A加锁两次。
- 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁
读写锁
与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享。写的优先级比较高
读写锁状态:
特别强调:读写锁只有一把,但其具备两种状态:
- 读模式下加锁状态 (读锁)
- 写模式下加锁状态 (写锁)
读写锁特性:
- 读写锁是“写模式加锁”时, 解锁前,所有对该锁加锁的线程都会被阻塞。
- 读写锁是“读模式加锁”时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
- 读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高
读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。
读写锁非常适合于对数据结构读的次数远大于写的情况。
这一个在读线程比较多的时候效率提升比较大
主要应用函数:
pthread_rwlock_init函数
pthread_rwlock_destroy函数
pthread_rwlock_rdlock函数
pthread_rwlock_wrlock函数
pthread_rwlock_tryrdlock函数
pthread_rwlock_trywrlock函数
pthread_rwlock_unlock函数
以上7 个函数的返回值都是:成功返回0, 失败直接返回错误号。
pthread_rwlock_t类型 用于定义一个读写锁变量。
pthread_rwlock_t rwlock;
pthread_rwlock_init
初始化一把读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
参2:attr表读写锁属性,通常使用默认属性,传NULL即可。
pthread_rwlock_destroy
销毁一把读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
pthread_rwlock_rdlock
以读方式请求读写锁。(常简称为:请求读锁)
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_wrlock
以写方式请求读写锁。(常简称为:请求写锁)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
pthread_rwlock_unlock
解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
pthread_rwlock_tryrdlock
非阻塞以读方式请求读写锁(非阻塞请求读锁)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
pthread_rwlock_trywrlock
非阻塞以写方式请求读写锁(非阻塞请求写锁)
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
示例
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
int counter;//全局变量
pthread_rwlock_t rwlock;
void *reader(void* arg){
int t;
int i = (int) arg;
while(1){
pthread_rwlock_rdlock(&rwlock);
t = counter;//获取数据
printf("read ==== %d: %lu, counter: %d\n",i,pthread_self(),t);
pthread_rwlock_unlock(&rwlock);
usleep(10000);
}
return NULL;
}
void *writer(void* arg){
int i = (int)arg;
while(1){
pthread_rwlock_wrlock(&rwlock);
counter++;//写一下数据
usleep(1000);
printf("write ==== %d: %lu, counter: %d\n",i,pthread_self(),counter);
pthread_rwlock_unlock(&rwlock);
usleep(20000);
}
return NULL;
}
int main(void){
int i;
pthread_t tid[8];
pthread_rwlock_init(&rwlock,NULL);
for(i=0;i<3;i++){
pthread_create(&tid[i],NULL,writer,(void *)i);
}
for(i=0;i<5;i++){
pthread_create(&tid[i+3],NULL,reader,(void *)i);
}
for(i=0;i<8;i++){
pthread_join(tid[i],NULL);
}
pthread_rwlock_destroy(&rwlock);
return 0;
}
每一次读取都是写以后的数据
条件变量:
条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。
主要应用函数:
pthread_cond_init函数
pthread_cond_destroy函数
pthread_cond_wait函数
pthread_cond_timedwait函数
pthread_cond_signal函数
pthread_cond_broadcast函数
以上6 个函数的返回值都是:成功返回0, 失败直接返回错误号。
pthread_cond_t类型 用于定义条件变量
pthread_cond_t cond;
pthread_cond_init
初始化一个条件变量
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
参2:attr表条件变量属性,通常为默认值,传NULL即可
也可以使用静态初始化的方法,初始化条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_destroy
销毁一个条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_wait
阻塞等待一个条件变量
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
函数作用:
- 阻塞等待条件变量cond(参1)满足
- 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);, 这一个互斥量需要是一个已经加锁的
1.2.两步为一个原子操作。
- 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);
也就是说在等待的时候是可以使用这一个互斥量对变量进行操作, 没有等待的时候这一个互斥锁是不可以使用的, 一旦调用会进入阻塞
下面是GPT的解释
pthread_cond_wait函数使用一个mutex参数的原因是为了确保在等待条件变量的时候,线程能够安全地释放对共享资源的访问。 具体来说,当一个线程调用pthread_cond_wait函数时,它会先释放之前持有的mutex,并且将自己加入到条件变量的等待队列中等待条件的满足。此时,其他线程可以获得该mutex,继续访问共享资源。当满足某个条件时,调用pthread_cond_signal或pthread_cond_broadcast函数唤醒等待队列中的线程。被唤醒的线程会重新获取mutex,并且继续执行。 使用mutex参数的目的是为了确保线程在等待和唤醒的过程中能够正确地同步对共享资源的访问。如果没有mutex参数,线程就无法安全地释放和重新获取mutex,可能会导致竞争条件和数据不一致的问题。因此,mutex与条件变量配合使用,可以实现线程之间的同步与互斥。
pthread_cond_timedwait
限时等待一个条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
参3: 参看man sem_timedwait函数,查看struct timespec结构体。
c
struct timespec {
time_t tv_sec; /* seconds */ 秒
long tv_nsec; /* nanosecondes*/ 纳秒
}
形参abstime:绝对时间。
如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。
c
struct timespec t = {1, 0};
pthread_cond_timedwait (&cond, &mutex, &t); 只能定时到 1970年1月1日 00:00:01秒(早已经过去)
正确用法:
c
time_t cur = time(NULL); 获取当前时间。
struct timespec t; 定义timespec 结构体变量t
t.tv_sec = cur+1; 定时1秒
pthread_cond_timedwait (&cond, &mutex, &t); 传参 参APUE.11.6线程同步条件变量小节
在使用的时候应该注意, 这一个time返回的时间是一个秒为单位的, 设置不到一秒的等待的时候, 可能会出现设置的时间没有当前时间大, 导致这一个等待无效
c
struct timeval {
time_t tv_sec; /* seconds */ 秒
suseconds_t tv_usec; /* microseconds */ 微秒
};
cstruct timeval tv; gettimeofday(&tv, NULL);
可以使用这一个获取微秒级时间
pthread_cond_signal
唤醒至少一个阻塞在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_broadcast
唤醒全部阻塞在条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
实际使用
- 等待方
- 创建一个锁
- 初始化锁 pthread_mutex_init
- 加锁 pthread_mutex_lock
- 等待条件满足 pthread_cond_wait
- 访问数据
- 解锁, (释放条件变量, 释放锁)
- 生产方
- 生成数据
- 加锁
- 把数据放到公共区域
- 解锁
- 发信号
示例
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
//一个信息链表
struct msg {
struct msg *next;
int num;
};
struct msg* head;
//初始化一个条件变量和一个锁
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer(void *p){
struct msg *mp;
for(;;){
pthread_mutex_lock(&lock);
while(head==NULL){
pthread_cond_wait(&has_product, &lock);//没有数据的时候需要等待
}
//有数据直接处理
mp = head;
head = mp->next;
pthread_mutex_unlock(&lock);
printf("----Consume---%d\n", mp->num);
free(mp);
sleep(rand() % 5);
}
}
void *producer(void *p){
struct msg *mp;
for(;;){
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1;//随机一个产品
printf("-Produce---%d\n", mp->num);
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;//把这一个信息加到链表头部
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product);//发送信号
sleep(rand()%5);
}
}
int main(void){
pthread_t pid, cid;
srand(time(NULL));
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
信号量
相比于互斥量, 这一个的初始化值可以为N, 可以用于管理有多个资源, N是可以同时访问的线程数量
RT-Thread线程间同步(信号量, 互斥量, 事件集)--01API函数使用-CSDN博客
RT-Thread线程间同步(信号量, 互斥量, 事件集)--02源码分析-CSDN博客
可以通过RTOS了解一下简单的原理
主要应用函数:
sem_init函数
sem_destroy函数
sem_wait函数
sem_trywait函数
sem_timedwait函数
sem_post函数
以上6 个函数的返回值都是:成功返回0, 失败返回-1,同时设置errno。(注意,它们没有pthread前缀)
sem_t类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。
sem_t sem; 规定信号量sem不能 < 0。头文件 <semaphore.h>
每一次获取的时候相当于一次--, 可以使用的数量为0的时候这一个线程会进入阻塞
这一个可以应用于线程以及进程之间
信号量基本操作:
sem_wait:
- 信号量大于0,则信号量-- (类比pthread_mutex_lock)
- 信号量等于0,造成线程阻塞
对应
sem_post:
将信号量++,同时唤醒阻塞在信号量上的线程 (类比pthread_mutex_unlock)
但,由于sem_t的实现对用户隐藏,所以所谓的++、--操作只能通过函数来实现,而不能直接++、--符号。
信号量的初值,决定了占用信号量的线程的个数。
sem_init
初始化一个信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
参1:sem信号量
参2:pshared取0用于线程间;取非0(一般为1)用于进程间
The pshared argument indicates whether this semaphore is to be shared between the threads of a process, or between processes.
If pshared has the value 0, then the semaphore is shared between the threads of a process, and should be located at some address that is visible to all threads (e.g., a global variable, or a variable allocated dynami‐ cally on the heap).
If pshared is nonzero, then the semaphore is shared between processes, and should be located in a region of shared memory (see shm_open(3), mmap(2), and shmget(2)). (Since a child created by fork(2) inherits its parent's memory mappings, it can also access the semaphore.) Any process that can access the shared memory region can operate on the semaphore using sem_post(3), sem_wait(3), and so on.
参3:value指定信号量初值
sem_destroy
销毁一个信号量
int sem_destroy(sem_t *sem);
sem_wait
给信号量加锁 --
int sem_wait(sem_t *sem);
sem_post
给信号量解锁 ++
int sem_post(sem_t *sem);
sem_trywait
尝试对信号量加锁 -- (与sem_wait的区别类比lock和trylock)
int sem_trywait(sem_t *sem);
sem_timedwait
限时尝试对信号量加锁 --
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
这一个时间看pthread_cond_timewait的时间, 这一个时间也是一个绝对时间