系列c++开发



前言


一、C++线程中的几类锁?

C++多线程中的锁主要有五类:互斥锁(信号量)、条件锁、自旋锁、读写锁、递归锁。

互斥锁
互斥锁用于控制多个线程对它们之间共享资源互斥访问的一个信号量。也就是说为了避免多个线程在某一时刻同时操作一个共享资源。
在某一时刻只有一个线程可以获得互斥锁,在释放互斥锁之前其它线程都不能获得互斥锁,以阻塞的状态在一个等待队列中等待。

头文件:#include <mutex>
类型:std::mutex、std::lock_guard
用法:在C++中,通过构造std::mutex的实例创建互斥单元,调用成员函数lock()来锁定共享资源,调用unlock()来解锁。不过一般不使用这种解决方案,更多的是使用C++标准库中的std::lock_guard类模板,实现了一个互斥量包装程序,提供了一种方便的RAII风格的机制在作用域块中。

信号量Semaphore
通过信号量也可以实现互斥锁的功能,不过比互斥锁的功能更加强大。在多线程环境下使用,可以用来保证两个或多个关键代码段不被并发调用。当sem_init初始化一个信号量,pshared=0、value=1的时候,即可实现互斥锁的功能。
可以分为两类:
二值信号量:信号量的值只有0和1,这和互斥量很类似,若资源被锁住,信号量的值为0,若资源可用,则信号量的值为1;
计数信号量:信号量的值在0到一个大于1的限制值之间,该计数表示可用的资源的个数。

include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_destroy(sem_t *sem);
pshared:控制信号量的类型,如果其值为0,就表示信号量是当前进程的局部信号量,否则信号量就可以在多个进程间共享。

条件锁
条件锁就是所谓的条件变量,当某一个线程因为某个条件未满足时可以使用条件变量使该程序处于阻塞状态,一旦条件满足则以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。最为常见的就是在线程池中,初始情况下因为没有任务使得任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒该线程来处理这个任务。

特别特别需要注意的是spurious wakeups(虚假唤醒),用于多线程竞争条件下,可能为抢到资源重新wait,此时需要while语句来实现。
条件变量的使用流程
mutex lock->wait->mutex unlock,wait被notify后,重新try lock,可能lock失败,即虚假唤醒的过程,为了防止虚假唤醒,必须使用whle来实现
while (true) {
std::unique_lock std::mutex guard(_mutex);
_condition.wait(guard, [] {return true;});
}

头文件: #include <condition_variable>
std::mutex
std::condition_variable
condition_variable类成员wait 、wait_for 或 wait_until。

void wait(std::unique_lock<std::mutex>& lock);
template<class Predicate>          //Predicate 谓词函数,可以普通函数或者lambda表达式
void wait(std::unique_lock<std::mutex>& lock, Predicate pred); 

template<class Rep, class Period>
std::cv_status wait_for(std::unique_lock<std::mutex>& lock,
                        const std::chrono::duration<Rep, Period>& rel_time);
template<class Rep, class Period, class Predicate>
bool wait_for(std::unique_lock<std::mutex>& lock,
              const std::chrono::duration<Rep, Period>& rel_time,Predicate pred);

condition_variable类成员notify_one或者notify_all。
void notify_one() noexcept;
void notify_all() noexcept;

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待条件变量的条件成立而挂起;另一个线程使条件成立(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥量结合在一起。

自旋锁
  互斥锁和条件锁都是比较常见的锁,比较容易理解。接下来用互斥锁和自旋锁的原理相互比较,来理解自旋锁。互斥锁的工作原理,互斥锁是一种sleep-waiting的锁。也就是说处理器不会因为线程被阻塞而空闲,它会去处理其它事务。
自旋锁的工作原理,自旋锁是一种busy-waiting的锁。“自旋锁”是比较消耗CPU的。

class spinlock_mutex
{
private:
	std::atomic_flag flag;
public:
	spinlock_mutex():
    flag(ATOMIC_FLAG_INIT){
    }
	void lock(){
		while(flag.test_and_set(std::memory_order_acquire));
	}
	void unlock(){
		flag.clear(std::memory_order_release);
	}
};

读写锁
读写锁(readers-writer lock),又称为多读单写锁(multi-reader single-writer lock,或者MRSW lock),共享互斥锁(shared-exclusive lock),以下简称RW lock。读写锁用来解决读写操作并发的问题。多个线程可以并行读取数据,但只能独占式地写或修改数据。
c++11本身是没有读写锁的,目前c函数,或者c++自己实现一个读写锁

#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);   /* 销毁RW lock */
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);       /* 初始化RW lock */
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;   /* 直接赋值方式初始化RW lock */
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);    /* 取得读锁,进入read-mode */
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); /* 尝试取得读锁,失败立即返回  */
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); /* 取得写锁,进入write-mode */
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);    /* 尝试取得写锁,失败立即返回  */
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);    /* 释放读/写锁 */```c

c++实现读写锁

class readWriteLock {
private:
    std::mutex readMtx;
    std::mutex writeMtx;
    int readCnt; // 已加读锁个数
public:
    readWriteLock() : readCnt(0) {}
    void readLock(){
        readMtx.lock();
        if (++readCnt == 1) {
            writeMtx.lock();  // 存在线程读操作时,写加锁(只加一次)
        }
        readMtx.unlock();
    }
    void readUnlock(){
        readMtx.lock();
        if (--readCnt == 0) { // 没有线程读操作时,释放写锁
            writeMtx.unlock();
        }
        readMtx.unlock();
    }
    void writeLock(){
        writeMtx.lock();
    }
    void writeUnlock(){
        writeMtx.unlock();
    }
};

递归锁
递归锁(Recursive Lock)也称为可重入互斥锁(reentrant mutex),是互斥锁的一种,同一线程对其多次加锁不会产生死锁。递归锁会使用引用计数机制,以便可以从同一线程多次加锁、解锁,当加锁、解锁次数相等时,锁才可以被其他线程获取。

头文件:#include <mutex>
类型:std::recursive_mutex 
std::recursive_mutex 的特性和 std::mutex 大致相同,释放互斥量时需要调用与该锁层次深度相同次数

二、C++ 锁的RAII用法

C++11 标准为我们提供了两种基本的锁类型,分别如下:
std::lock_guard,与 Mutex RAII 相关,方便线程对互斥量上锁。
std::unique_lock,与 Mutex RAII 相关,方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。在构造 std::unique_lock 对象时可以接受额外的参数。

还提供了几个与锁类型相关的 Tag 类
constexpr adopt_lock_t adopt_lock {};
申请锁为该成员函数维护,但不会主动lock(),需要手动lock(),但会自动释放unlock()。
constexpr defer_lock_t defer_lock {};
初始化了一个没有加锁的mutex,需要手动加锁lock(),会自动解锁unlock()。
constexpr try_to_lock_t try_to_lock {};
尝试去锁定,尝试lock()去锁定,但锁定失败会立即返回且不会阻塞。

std::lock_guard与std::unique_lock的区别:
std::lock_guard的特点:
(1) 创建即加锁,作用域结束自动析构并解锁,无需手工解锁
(2) 不能中途解锁,必须等作用域结束才解锁
(3) 不能复制
std::unique_lock的特点:
(1) 创建时可以不锁定(通过指定第二个参数为 std::defer_lock),而在需要时再锁定
(2) 可以随时加锁解锁
(3) 作用域规则同 lock_grard,析构时自动释放锁
(4) 不可复制,可移动
(5) 条件变量需要该类型的锁作为参数(此时必须使用 unique_lock)

常用的成员函数:
lock,上锁操作,调用它所管理的 Mutex 对象的 lock 函数。如果在调用 Mutex 对象的 lock 函数时该 Mutex 对象已被另一线程锁住,则当前线程会被阻塞,直到它获得了锁。

    std::unique_lock<std::mutex> lck (mtx,std::defer_lock);
    // critical section (exclusive access to std::cout signaled by locking lck):
    lck.lock();
    std::cout << "thread # " << id << '\n';
    lck.unlock();

try_lock,上锁操作,调用它所管理的 Mutex 对象的 try_lock 函数,如果上锁成功,则返回 true,否则返回 false。

    std::unique_lock<std::mutex> lck(mtx,std::defer_lock);
    // print '*' if successfully locked, 'x' otherwise: 
    if (lck.try_lock())
        std::cout << '*';
    else                    
        std::cout << 'x';

try_lock_for,上锁操作,调用它所管理的 Mutex 对象的 try_lock_for 函数,如果上锁成功,则返回 true,否则返回 false。

	std::timed_mutex mtx;
    std::unique_lock<std::timed_mutex> lck(mtx,std::defer_lock);
    while (!lck.try_lock_for(std::chrono::milliseconds(200))) {
        std::cout << "-";
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    std::cout << "*\n";

try_lock_until,上锁操作,调用它所管理的 Mutex 对象的 try_lock_for 函数,如果上锁成功,则返回 true,否则返回 false。

	std::timed_mutex cinderella;
    struct tm info;

    info.tm_year = 2022 - 1900;
    info.tm_mon = 10 - 1;
    info.tm_mday = 3;
    info.tm_hour = 0;
    info.tm_min = 0;
    info.tm_sec = 0;
    if (cinderella.try_lock_until(mktime(&info))) {
        std::cout << "ride back home on carriage\n";
        cinderella.unlock();
    } else {
        std::cout << "carriage reverts to pumpkin\n";
    }

unlock,解锁操作,调用它所管理的 Mutex 对象的 unlock 函数。

    std::unique_lock<std::mutex> lck (mtx,std::defer_lock);
    // critical section (exclusive access to std::cout signaled by locking lck):
    lck.lock();
    std::cout << "thread # " << id << '\n';
    lck.unlock();

release,返回指向它所管理的 Mutex 对象的指针,并释放所有权。

std::mutex mtx;
int count = 0;
void print_count_and_unlock (std::mutex* p_mtx) {
    ++count;
    std::cout << "count: " << count << '\n';
    p_mtx->unlock();
}
void task() {
    std::unique_lock<std::mutex> lck(mtx);
    print_count_and_unlock(lck.release());
}

owns_lock,返回当前 std::unique_lock 对象是否获得了锁。

    std::unique_lock<std::mutex> lck(mtx,std::try_to_lock);
    // print '*' if successfully locked, 'x' otherwise: 
    if (lck.owns_lock())
        std::cout << '*';
    else                    
        std::cout << 'x';

mutex,返回当前 std::unique_lock 对象所管理的 Mutex 对象的指针。

    std::unique_lock<std::mutex> lck(mtx,std::try_to_lock);
    // print '*' if successfully locked, 'x' otherwise: 
    if (lck)
        std::cout << '*';
    else                    
        std::cout << 'x';

总结

本文主要讲解了五类多线程锁,互斥锁、条件锁、自旋锁、读写锁、递归锁。一般而言,所得功能与性能成反比。希望你能够对c++多线程编程学习逐渐深入。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐