C语言 锁的使用总结

摘要:
使用此函数可以销毁互斥体,该互斥体可以再次初始化。试图销毁处于锁定状态的互斥锁将导致未知行为。pthread_ mutex _ Trylock函数和pthread_mutex _ Lock,只有当互斥锁被锁定时,pthread_mutex _ Trylock才会直接返回错误代码EBUSY,而不是阻塞进程。pthread_mmutex_timedlock也被锁定,但只阻塞指定的时间。当时间到了且无法获得锁时,将返回错误代码ETIMEDOUT。如果读取状态可以由多个线程拥有,则效率会提高。读/写锁用于读取数据结构的次数远远大于用于写入的次数。初始化和去初始化与互斥体的使用类似。需要初始化和取消初始化。

1. C 互斥锁 mutex
初始化与去初始化

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

pthread_mutex_init 使用指定的attr属性初始化一个互斥锁mutex 。如果 atrr 设为 NULL 或者使用一个默认的 pthread_mutexattr_t 类型都是使用默认属性进行初始化。
重复初始化一个已经初始化过的锁会导致未知行为。
pthread_mutex_destroy 可以销毁一个初始化过的锁。使用此函数销毁一个mutex,可以再次初始化。
如果尝试销毁一个锁定状态的mutex会导致未知行为。

除了使用 pthread_mutex_init 函数对 mutex 进行初始化,还可以使用特定的宏在声明 mutex 的时候直接赋值进行静态初始化。例如:

// 普通mutex
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;

// 可递归mutex
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER;
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;

// 有错误检查的mutex,同一线程重复加锁报错
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER;
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;

上面那个带不带NP后缀取决于系统,我用的Ubuntu18.04对应的宏为PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP。

加锁与解锁

// 普通加锁,重复加锁会阻塞进程
int pthread_mutex_lock (pthread_mutex_t *__mutex);
// 重复加锁不阻塞进程
int pthread_mutex_trylock (pthread_mutex_t *__mutex);
// 带有超时功能加锁
int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abs_timeout);
// 解锁
int pthread_mutex_unlock (pthread_mutex_t *__mutex);

pthread_mutex_lock对一个 mutex 加锁。如果一个线程试图锁定一个已经被另一个线程锁定的互斥锁,那么该线程将被挂起,直到拥有该互斥锁的线程先解锁该互斥锁。
默认的 mutex 在同一个线程里再次被加锁会导致未定义行为,如果定义 mutex 为 PTHREAD_MUTEX_RECURSIVE 类型,即可递归 mutex ,则这个锁可以在同一个线程内重复加锁,每次加锁计数器+1,每次解锁计数器-1,当计数器为0 的时候其他线程才可以获取这个锁。

pthread_mutex_trylock 功能与pthread_mutex_lock,只是当mutex已经是锁定的时候,pthread_mutex_trylock直接返回错误码EBUSY,而不是阻塞进程。

pthread_mutex_timedlock也是加锁,但是只阻塞指定的时间,时间一到还没能获取锁则返回错误码ETIMEDOUT。

pthread_mutex_unlock为解锁。如果互斥锁未被锁定,尝试解锁会导致未定义行为。

示例
让一个数从0加到10,然后再减到0。

#include <pthread.h>
#include <stdio.h>

int gValue=0;
pthread_mutex_t gMutex = PTHREAD_MUTEX_INITIALIZER;

void *add(void*){
pthread_mutex_lock(&gMutex); // 加锁
for (int i = 0; i < 10; ++i) {
printf("[1]%d ", ++gValue);
}
pthread_mutex_unlock(&gMutex); // 解锁
}

void *sub(void*){
pthread_mutex_lock(&gMutex); // 加锁
for (int i = 0; i < 10; ++i) {
printf("[2]%d ", --gValue);
}
pthread_mutex_unlock(&gMutex); // 解锁
}


int main() {
pthread_t p1, p2;

pthread_create(&p1, NULL, add, NULL);
pthread_create(&p2, NULL, sub, NULL);

pthread_join(p1, NULL);
pthread_join(p2, NULL);

return 0;
}


输出:
[1]1 [1]2 [1]3 [1]4 [1]5 [1]6 [1]7 [1]8 [1]9 [1]10 [2]9 [2]8 [2]7 [2]6 [2]5 [2]4 [2]3 [2]2 [2]1 [2]0
不加锁的话输出就比较乱了。

2. C 读写锁 rwlock
前面说过互斥锁要么是lock状态,要么是unlock状态,而且一次只能一个线程对其加锁。也就是说这个锁是排他性的,每次只能一个线程拥有。
读写锁,顾名思义用在读写的地方,读写的地方要求就是如果是写的话只能一个线程拥有,防止写错覆盖新的值。如果是读状态可以多个线程拥有,这样就提高了效率,读写锁用于对数据结构读的次数远大于写的情况。
读写锁可以设置为两种加锁状态,即读锁定和写锁定状态。

当处于写锁定状态时,所有加锁操作都会被阻塞。
当处于读锁定状态时,所有试图设置读锁定都会成功,所有试图设置写锁定都会被阻塞,并且还会阻塞后续所有的读锁定加锁操作,直到所有的读锁定都被解锁。
初始化与去初始化
与互斥锁使用方式类似,都需要初始化和去初始化操作。

#include <pthread.h>

int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

pthread_rwlock_t rwlock=PTHREAD_RWLOCK_INITIALIZER;

初始化的时候同样可以使用常量PTHREAD_RWLOCK_INITIALIZER来定义个默认的读写锁。

加锁与解锁

// 加 读 状态的锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
// 不阻塞版本,成功则返回0
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

// 加 写 状态的锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
// 不阻塞版本,成功则返回0
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

pthread_rwlock_rdlock 是读模式下锁,pthread_rwlock_wrlock 是写模式下锁定,这两种锁定模式都使用同一个函数pthread_rwlock_unlock进行解锁。

示例
写了个非常傻瓜式的小程序来验证这个读写锁的功能。有两个函数一个是往数组里面写字符,一个是读字符,里面都加了sleep模拟耗时的操作。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

char str[10];
size_t pos = 0;

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

// 每次写一个字符
void *writeData(void *name)
{
pthread_rwlock_wrlock(&rwlock); // 写 加锁
sleep(1);
str[pos] = 'a' + pos;
pos++;
printf("%s %ld write\n", (char *)name, time(NULL));
pthread_rwlock_unlock(&rwlock); // 通用解锁函数
}

// 读数组中字符串
void *readData(void *name)
{
pthread_rwlock_rdlock(&rwlock); // 读 加锁
sleep(1);
printf("%s %ld read: str = %s\n", (char *)name, time(NULL), str);
pthread_rwlock_unlock(&rwlock); // 通用解锁函数
}

int main()
{
// 搞了6个线程干起来
pthread_t p[6];
pthread_create(&p[0], NULL, writeData, (void *)"p1"); // 读
pthread_create(&p[1], NULL, readData, (void *)"p2"); // 写
pthread_create(&p[2], NULL, writeData, (void *)"p3"); // 读
pthread_create(&p[3], NULL, readData, (void *)"p4"); // 写
pthread_create(&p[4], NULL, writeData, (void *)"p5"); // 读
pthread_create(&p[5], NULL, readData, (void *)"p6"); // 写

for (int i = 0; i < 6; ++i)
{
pthread_join(p[i], NULL);
}
return 0;
}

如果没有锁的话,这几个操作应该都是随机的。如果读和写函数是用的互斥锁,那么这几个函数的输出也应该是随机的。
但是输出结果是这样的。

p1 1594130585 write
p4 1594130586 read: str = a
p6 1594130586 read: str = a
p2 1594130586 read: str = a
p3 1594130587 write
p5 1594130588 write

每次输出read的几个线程都是几乎同时输出的,因为当有人锁定write锁的时候,没人可以获取锁。当有人锁定read锁的时候,其他write的会阻塞,但是其他read不会被阻塞,所以read可以同时执行。

参考博客:https://blog.csdn.net/shaosunrise/article/details/107620885

免责声明:文章转载自《C语言 锁的使用总结》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇IIS 设置文件传输大小限制[转载]为什么有些MP4文件在Chrome浏览器上播放不了?下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Camera Service

上面这张图比较清楚的表现了camera provider进程在camera架构中位置,作为承上启下的部分,和cameraserver进程和底层的驱动交互,camera provider进程非常重要,camera HAL层几乎全部运行在camera provider进程中完成。 首先看下camera provider所在源码中的位置:hardware/in...

c++对象初始化中 ZeroMemory、memset、直接赋0的区别

首先是ZeroMemory和memset的区别: 1、ZeroMemory是微软的SDK提供的,memset属于C Run-time Library提供的。因此ZeroMemory只能用于Windows系统,而memset还可用于其他系统。 2、ZeroMemory是一个宏,只是用于把一段内存的内容置零,内部其实是用 memset实现的,而memset除...

Linux段管理,BSS段,data段,.rodata段,text段

         近期在解决一个编译问题时,一直在考虑一个问题,那就是Linux下可执行程序执行时内存是什么状态,是依照什么方式分配内存并执行的。查看了一下资料。就此总结一下,众所周知。linux下内存管理是通过虚存管理的,在分配内存是并不是在物理内存开辟了一段空间,而是在使用时才分配的。并且是通过段页式管理。 以上比較废话,開始看看程序执行时内存会是什...

Arm设计思想与高效C语言编程联系

一.RISC设计思想 ARM内核采用RISC体系结构。RISC是一种设计思想,其目标是设计出一套能在高时钟频率下单周期执行,简单而有效的指令集。RISC的设计重点在于由硬件执行的指令的复杂度,这是因为软件比硬件容易提供更大的灵活性和更高的智能。因此,RISC设计对编译器有更高的要求;相反,传统的复杂指令集的计算机(CISC)则更侧重于硬件执行指令的功能性,...

MySQL5.6 与 MySQL5.7 的区别

目录 编译安装区别 初始化的区别 其他区别 编译安装区别 # 5.7在编译安装的时候多了一个 boost 库 [root@db02 mysql-5.7.20]# yum install -y gcc gcc-c++ automake autoconf make cmake bison-devel ncurses-devel libaio-dev...

c语言 GPS nmealib学习笔记

0.nmealib简介 nmealib是一个基于C语言的用于nmea协议的开源库。虽然nmea体积小巧,但是却具备了不少功能。 分析NMEA语句并把结果保存在合适的C语言结构体中。 除了解析NMEA语句之外,还可以产生NMEA语句。 支持多种NMEA语句,包括GPGGA, GPGSA, GPGSV, GPRMC, G...