C 语言的并发原语(二)
哈喽哈喽,我来啦~
C11开始C语言有了自己的并发原语不依赖,上一篇帖子C 语言的并发原语(一)已经介绍了C语言自己的线程,已经有了并发那怎么少得了锁呢?所以这一篇帖子将介绍C语言中的互斥锁(C语言标准库中没有读写锁哦)
以防有些基础不扎实的鱼油不清楚为啥需要锁这种东西~我们就先来看一段代码
#include <stdio.h>
#include <threads.h>
long num = 0;
int
thrd_add()
{
for (int i = 0; i < 1e5; i++) {
num += 1;
}
return 0;
}
int
main(int argc, char* argv[])
{
int tn = 10;
thrd_t n[tn];
for (int i = 0; i < tn; i++) {
thrd_create(n + i, thrd_add, NULL);
}
for (int i = 0; i < tn; i++) {
thrd_join(n[i], NULL);
}
printf("%ld\n", num);
}
创建了10个线程,每个线程都对这个num变量进行循环加1加十万次。所以预期中main函数结尾的printf
函数打印出来的值应该是一百万。但是实际上的结果呢?
每次调用的结果都不相同,并且和结果一百万相差甚远,这是为啥呢?
这就不得不谈到一个并发中的问题了 —— 数据竞争
假如现在有两个线程,一个是线程1一个是线程2,有一个公共的变量是n它是0,两个线程同时对这个n进行+1的操作。
理想中两个线程加了两次,线程1访问到n然后得到0,使用0+1再写入n,这个时候n是1,线程2访问n的时候得到2再+1,这个时候n是2
但实际情况中却不是这样,在线程1和线程2同时访问n的时候拿到的都是0, 线程1和线程2都是在对0进行+1所以得到的结果可能会是1。这种情况就是数据竞争。
所以唯一的解决办法就是,不让线程同时对一个数据进行处理,使其排队的对一个数据进行处理,锁就是这个用途。
互斥锁,顾名思义当一个线程拿到锁后其他线程在访问数据就会被排斥,等待拿到锁的线程解锁后,其他线程就会开始抢占锁,哪个线程抢到了,哪个线程就可以继续访数据然后解锁,持续如此直到所有线程访问数据的线程都解锁了为止。(说得比较笼统想深入的鱼油可以直接以互斥锁为关键词进行检索)
为了避免数据竞争需要在对数据进行操作的地方加上锁,这样就不管有多少线程,在对数据进行处理的时候就能保证同一时间只有一个线程在对数据处理,而不是多个线程同时处理保证了数据的准确性~
C语言中的锁也是在<threads.h>
这个头文件中
初始化锁的是mtx_init
这个函数,这是它的函数签名int mtx_init (mtx_t *__mutex, int __type)
int
为它的返回值,初始化成功则返回thrd_success
失败返回thrd_error
mtx_t* __mutex
是需要一个mtx_t
类型的指针来保存mtx_init
返回的互斥锁id
int __type
则是互斥的类型,互斥类型有这些mtx_plain
普通互斥锁,mtx_timed
定时互斥锁,mtx_plain | mtx_recursive
可递归互斥锁,mtx_timed | mtx_recursive
可递归定时锁
上锁则是使用mtx_lock
这个函数,这是它的函数签名int mtx_lock (mtx_t *__mutex)
int
为它的返回值,上锁成功则返回thrd_success
上锁失败则返回thrd_error
mtx_t* __mutex
互斥锁的id,根据id来上锁,如果不是递归锁却发生了递归行为则是未定义行为
解锁则是使用mtx_unlock
这个函数和mtx_lock
的函数签名一样,只不过是解锁,如果解锁已经解锁的id是一个未定义行为
mtx_destroy
函数是释放锁所占用的资源,如果释放了一个已经释放了的锁该操作则是一个未定义行为
#include <stdio.h>
#include <threads.h>
long num = 0;
int
thrd_add(void* key)
{
mtx_lock(key);
for (int i = 0; i < 1e5; i++) {
num++;
}
mtx_unlock(key);
return 0;
}
int
main(int argc, char* argv[])
{
int tn = 10;
thrd_t n[tn];
mtx_t key;
mtx_init(&key, mtx_plain);
for (int i = 0; i < tn; i++) {
thrd_create(n + i, thrd_add, &key);
}
for (int i = 0; i < tn; i++) {
thrd_join(n[i], NULL);
}
mtx_destroy(&key);
printf("%ld\n", num);
}
这是普通互斥锁的应用,这个时候不管运行多少次最后printf("%ld\n", num)
的结果都是一百万
好了虽然有点水但也就介绍到这里吧,剩下的东西也都是简单明了,感兴趣的鱼油可以自己动手用一用。
以下是C11的互斥锁相关信息:
- mtx_t 类型 互斥锁唯一标识也可以叫互斥锁id
- mtx_init 函数 初始化互斥锁
- **mtx_lock **函数 根据互斥锁id进行上锁,并阻塞其他线程直到解锁
- mtx_unlock 函数 根据互斥锁id进行解锁,阻塞的线程开始抢占锁
- mtx_timedlock 函数 根据定时互斥锁id进行上锁,阻塞其他线程直到有线程解锁或者超时,也使用
mtx_unlock
解锁 - mtx_trylock 函数 尝试对互斥锁id上锁,如果互斥锁id并没有被上锁则开始上锁,如果已经上锁了则直接返回不会阻塞,如果上锁成功返回
thrd_success
,已经锁定返回thrd_busy
,上锁失败返回thrd_error
- mtx_destroy 函数 根据互斥锁的id来释放相应互斥锁所占用的资源
- mtx_plain 枚举状态 初始化的时候所用的类型用于表示初始化一个普通的互斥锁
- mtx_timed 枚举状态 初始化的时候所用的类型用于表示初始化一个定时的互斥锁
- mtx_recursive 枚举状态 初始化的时候所用的类型用于表示初始化一个可以递归的互斥锁,通常和其他两个状态相与使用,与
mtx_plain
相与表示可以递归的互斥锁,与mtx_timed
相与表示可以递归的定时锁
这个涉及到比较底层也比较基础,我只能笼统一点的跟你说让你有个大致的了解。
线程是系统抽象出来的,他们的调度都是由系统来决定,系统对待它们的调度方式是抢占式,谁抢到了cpu就给谁执行。
数据竞争呢就是在线程1需要对数据进行修改,首先它要做第就是要获取到数据然后进行修改,但是在修改的途中呢,被线程2抢占到了cpu这个时候数据还没有被线程1修改所以还是原来的状态,那到的数据自然也是原来的状态了,然后两个线程修改得到的数据相当于只有一个修改成功了。
有了数据竞争的概念后,再来说说线程调度的概念,线程其实没有什么“休眠状态”这个所谓的“休眠状态”只是看起来是在休眠,但其实是线程被 操作系统的调度器中断使其让出cpu的占用,并没有什么“休眠”“中断中”这种状态,被中断的线程也还是会继续参与调度。那么什么时候线程会被中断呢?被动的中断是每个线程都会被分配一个时间这个时间就是这个线程所占用cpu的时间,当时间结束的时候则会被中断然后给其他线程抢到cpu执行,被中断的线程则是重新排队重新抢占cpu。主动的中断就比如我上一篇帖子中说的
thrd_yield
这个就是主动的让调度器中断线程使其重新排队重新抢占cpu,但这个函数并不能保证被中断的线程下次获取到cpu的时间,可能是中断后又立马能抢占到cpu也可能是中断后很久才能抢占到cpu,想要中断后指定特定的时间后才能获取到cpu的则是thrd_sleep
这个“休眠”函数,这个函数中断了线程后会在指定的时间后使其占用到cpu也就是所谓的“唤醒”。有了这一个概念就能更好的理解锁是怎么互斥的了,锁id和线程id进行绑定后当除了绑定的线程以外的线程调用了lock函数则都会被中断直到确保与其绑定的线程拿到cpu执行到unlock与锁id进行解绑,这样就完成了同一时间中只有一个线程在操作数据,并不是什么“被更高优先级打断”。
再来说说你说的自旋锁,自旋锁一般只需要两个状态true和false,当锁没有被占用的时候是false,被占用了是true,第一个线程在获取到锁的时候会锁的状态会被改变变成为true,这个时候其他的线程拿到这个锁的时候就是在做自旋的操作而不是和第一个线程竞争数据了,但死循环也就是所谓的自旋并不是在真的在无休止的占用cpu一直在检查状态,死循环会被识别为长时间占用cpu的代码,所以分配给它的时间会比其他代码长一些,在这段时间里自旋锁里的线程都将是一个线程跑了很长一段时间然后被中断切换到下一个线程,如果切换到了第一个线程,第一个线程执行完了代码解锁了后这个自旋锁的状态就会变为false,待cpu占用的时间耗尽其他线程就会开始抢占cpu,抢占到cpu但线程自旋检查状态发现为false,则会像第一个线程那样将自旋锁的状态改变为true,这样周而复始的操作。
好了,看了这些你应该会有个基础概念了,再深入就是怎么优化锁了,这个你就留给你自己思考吧~回复中有一些不严谨的地方这是为了让你更方便理解说得比较笼统,还是建议你去补一下计算机组成原理还有操作系统的知识~
受益匪浅 我理解 在linux中 线程实际上是一种相互竞争的关系 因此需要锁区保证每个线程能够有序、正常的运行。所以我们需要锁去做出保证。互斥锁实际上会有多种情况 最简单的是休眠唤醒。
mutex_lock去获得互斥锁的话。就会导致休眠,所以是不能在中断中使用,因为他无法被更高优先级打断。
而mutex_lock_interruptible,他在进入休眠之前是可以被被打断的。
正常开发中还有点就是自旋(optimistic spinning),在锁被持有的时候,自旋等待,不用直接休眠。当然我也只会抄着用,具体的原理还是不太清楚,大佬有空可以讲讲。
你是真卷