Linux C/C++ 中锁的使用总结

10 篇文章 9 订阅

本文总结C和C++中各种锁以及使用方式,主要是C语言中的互斥锁 mutex 和读写锁 rwlock,以及C++中的互斥锁mutex以互斥锁管理。C++中的各种mutex其实是对C语言中的mutex的面相对象的封装,此外的mutex管理部分的类其实是用RAII的风格对mutex对象进行进一步包装。
此外线程间通信还有信号量,因为它不叫锁就不说了,还有个非常不常用的自旋锁也不说了,还有名字很罕见的闩 latch 与屏障 barrier 也不说了。感兴趣自己看 cppreference.com吧。

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可以同时执行。有问题的话欢迎指出。

3. C++ mutex

C++中的锁 mutex 其实是对C语言 mutex 进行面向对象的封装,根据不同特定封装成不同的mutex类,并添加一些安全性检查之类的特性。可以认为每种mutex类内部都有一个C mutex成员变量。
常见的mutex有如下几种:

C++ mutex功能解释
std::mutex普通mutex
std::timed_mutex带有有时限锁定功能的mutex
std::recursive_mutex可被同一线程递归锁定的mutex
recursive_timed_mutex可被同一线程递归锁定的,且带有时限的mutex
shared_mutex共享mutex

std:mutex

std::mutex 是个普通的C mutex封装,大概是下面这个样子,当然还有一些安全性检查等。

class mutex
{
public:
    void lock() {
        pthread_mutex_lock(&_M_mutex);
    }

    bool try_lock() {
        return !pthread_mutex_trylock(&_M_mutex);
    }

    void unlock() {
        pthread_mutex_unlock(&_M_mutex);
    }

    pthread_mutex_t* native_handle() { return &_M_mutex; }
    pthread_mutex_t _M_mutex = PTHREAD_MUTEX_INITIALIZER; // 我是最普通的那个mutex
};

使用方面和C mutex 差不多,例如

int gValue=0;
std::mutex gMutex;

void add(){
    gMutex.lock();
    for (int i = 0; i < 10; ++i) {
        printf("[1]%d ", ++gValue);
    }
    gMutex.unlock();
}

std::timed_mutex

普通的mutex获取不到锁会一直阻塞,std::timed_mutex多了个可以只阻塞一段时间的加锁函数。

可以理解为给 std::mutex 增加了一个对C mutex int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *abs_timeout);封装的函数,然后起名为std::timed_mutex,这个封装的函数为

template< class Rep, class Period >
bool try_lock_for( const std::chrono::duration<Rep,Period>& timeout_duration );

template< class Clock, class Duration >
bool try_lock_until( const std::chrono::time_point<Clock,Duration>& timeout_time );

try_lock_for 为阻塞指定时长,没有获取到锁就返回false。参数是时间长度。
try_lock_until 为阻塞到指定时间,没有获取到锁就返回false。参数是时间点。

示例:

int gValue=0;
std::timed_mutex gMutex;

void f(){
	// 一段时长,可以使用其他 std::chrono:: 时间单位
    std::chrono::milliseconds timeout(10);	
    if (gMutex.try_lock_for(timeout)) {
		// TODO:获取到了mutex,开始干活
		...
        gMutex.unlock(); // 干完活了,解锁
    } else {
		// 时间到了,也没获取到mutex,干点别的吧
    }
}

void g(){
	auto now=std::chrono::steady_clock::now();
    if (gMutex.try_lock_until(now + std::chrono::seconds(10)) {
		// TODO:获取到了mutex,开始干活
		...
        gMutex.unlock(); // 干完活了,解锁
    } else {
		// 时间到了,也没获取到mutex,干点别的吧
    }
}

std::recursive_mutex

std::recursive_mutex 是带有计数功能的,可以在同一个线程内递归lock的锁。和 C mutex 中的 PTHREAD_RECURSIVE_MUTEX 相同。
std::recursive_mutex 类提供3个成员函数:locktry_lockunlock

std::recursive_timed_mutex

std::recursive_timed_mutex 是带有延时功能的可递归 mutex, 比 std::recursive_mutex 多了两个函数:try_lock_fortry_lock_until

4. C++ mutex 管理类

std::lock_guard

lock_guard 是严格基于作用域的RAII风格的 mutex 所有权包装器。
每次使用 mutex 的时候都先调用lock()再调用unlock()lock_guard在构造函数中加锁,在析构函数中解锁,在栈上申请的内存,超过作用域自动析构就解锁了。使用起来比较方便。
示例:

int gValue=0;
std::mutex gMutex;

void add(){
	// 栈内存实例化lock_guard,构造函数中有lock()
    std::lock_guard<std::mutex> guard(gMutex);	
    for (int i = 0; i < 10; ++i) {
        printf("[1]%d ", ++gValue);
    }
    // 超出作用域lock_guard执行析构,析构函数中调用unlock()
}

等效于

void add(){
    gMutex.lock();
    for (int i = 0; i < 10; ++i) {
        printf("[1]%d ", ++gValue);
    }
    gMutex.unlock();
}

std::unique_lock

std::unique_lock也可以提供自动加锁、解锁功能,比std::lock_guard更加灵活,功能强大。
详细描述找机会再总结。

std::shared_lock

std::scoped_lock

  • 6
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Linux C/C++后台架构开发是一门非常具有前景的技能,在互联网和各种电子设备中都得到广泛应用。因此,这门课程的成长体系很重要,能够立足于现有技术发展趋势,不断完善内容,使学员能够跟随市场需求进行技术升级。 首先,课程应该注重基础知识的讲解,包括Linux操作系统的基础知识、C/C++编程语言的基础知识等,这是后续学习的基础。其次,应该注重实战训练,通过项目的实践来加深对知识的理解,并促进学员的技能提升。同时,要合理结合课程的理论知识和实践操作,培养学员的动手能力和实际应用技能。 除此之外,课程还应该关注行业技术变化的趋势,不断更新课程内容,讲解新技术的应用,使学员能够跟随技术的发展趋势提升自己。同时,要注重培养学员的团队合作能力和创新能力,帮助他们更好地适应团队工作和市场需求。 总之,Linux C/C++后台架构开发成长体系课程需要注重基础知识的讲解、实战训练、行业技术变化的跟进和团队合作与创新能力的培养。通过这些方面的努力,才能使学员掌握实用的技术,具备市场竞争力,并有能力适应未来技术的发展趋势。 ### 回答2: 作为一种开源的操作系统,Linux的应用广泛,尤其在服务器端,被广泛应用于Web服务器、数据库服务器等。因此,当今各大企业都需要专业的Linux后台架构开发人员来维护服务器的安全、稳定和高效运行。 而C/C++作为一种高效、可靠的编程语言,被广泛应用于Linux系统编程,尤其是在高性能、实时应用和底层驱动方面。因此,具备C/C++编程能力的Linux后台架构开发人员具有较高的市场竞争力。 在成长体系课程方面,专业的培训机构可以提供基础和高级的C/C++编程语言学习,以及Linux系统编程相关知识扎实的培训。而在课程设置和教学方式上,应该采取理论与实践相结合的方式,让学员在编程实践中逐渐掌握并理解相关知识。同时,在应用开发的过程中,特别是在搭建后台架构时,需要学合理设计系统架构,选择合适的开发工具和技术,并能有效管理和维护系统。 此外,针对行业发展趋势和技术更新,培训机构应当不断更新课程内容,结合最新的技术趋势,为学员提供更具有竞争力的技术挑战,并开展多种实战项目实践。让学员在实践中提升自己的技能和实践经验,不断提升自身的职业竞争力。 ### 回答3: Linux C/C++后台架构开发是一个广受欢迎的领域,无论是大型互联网企业还是中小型企业都需要有相应的开发团队和技术人才。开发人员需要具备扎实的C/C++编程基础,熟悉Linux操作系统的运行机制和性能优化,了解分布式系统架构和网络通信协议等知识,以及掌握一定的数据库开发和管理经验。 针对此领域的开发人员,成长体系课程可以提供以下培训内容: 一、Linux操作系统原理:Linux系统的运行机制,常用命令和工具的使用方法,文件系统和进程管理等。 二、C/C++编程:C/C++基础语法和编程规范,数据结构和算法,内存管理和机制等。 三、分布式系统架构:分布式系统的概念和架构,通信协议和数据传输方式,分布式存储和计算等。 四、网络通信:TCP/IP协议栈和网络编程,HTTP、Websocket等常用协议的使用和封装。 五、数据库开发和管理:常见数据库的概念和使用方法,SQL语言编写,数据库的设计和优化等。 通过以上培训内容的学习和实践,开发人员可以逐渐掌握Linux C/C++后台架构开发的技能和经验,不断提升能力和水平。同时,课程还将涉及团队协作和项目管理等方面的知识,培养学员的软技能和团队意识。最终,学员能够独立完成复杂的后台开发任务,为企业创造更大的价值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值