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函数打印出来的值应该是一百万。但是实际上的结果呢?

image.png

每次调用的结果都不相同,并且和结果一百万相差甚远,这是为啥呢?

这就不得不谈到一个并发中的问题了 —— 数据竞争

假如现在有两个线程,一个是线程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)的结果都是一百万

image.png

好了虽然有点水但也就介绍到这里吧,剩下的东西也都是简单明了,感兴趣的鱼油可以自己动手用一用。

以下是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相与表示可以递归的定时锁