多线程编程中锁的种类与应用举例

    科技2024-12-12  12

           线程之间的锁种类大致有以下几种:互斥锁、条件锁、自旋锁(不推荐使用)、递归锁、读写锁(C++17封装入标准库,开始支持,其余均为C++11标准)。一般而言,锁的功能越强大,性能就会越低。

    一、互斥锁

           互斥锁用于控制多个线程对共享资源互斥访问的一个信号量。可以避免多个线程再某一时刻同时操作一个共享资源。

           在某一时刻,只能有一个线程可以获取互斥锁,在释放互斥锁之前其他线程都不能获取该互斥锁。如果其他线程想要获取这个互斥锁,则线程只能以阻塞方式进行等待。

           互斥锁是是一种sleep-waiting的锁。假设线程T1获取互斥锁并且正在core1上运行时,此时线程T2也想要获取互斥锁(pthread_mutex_lock),但是由于T1正在使用互斥锁使得T2被阻塞。当T2处于阻塞状态时,T2被放入到等待队列中去,处理器core2会去处理其他任务而不必一直等待(忙等)。也就是说处理器不会因为线程阻塞而空闲着,它去处理其他事务去了。

    头文件:<pthread.h>

    类型:std::mutex

    用法:构建std::mutex的实例来创建互斥元,调用成员函数lock()和unlock()来解锁。

    用法举例:

    #include <iostream> #include <thread> #include <mutex> using namespace std;

    std::mutex mutexx;

    void helloguangzhou() {     //mutexx.lock();     int i = 10;     while (i--) {         cout << " hello guangzhou" << endl;     }     //mutexx.unlock(); } void hellowworld() {     //mutexx.lock();     int i = 10;     while (i--)     {         cout << " hello world" << endl;     }     //mutexx.unlock(); } int main() {     std::thread  t1(helloguangzhou);     std::thread  t2(hellowworld);     t1.detach();     t2.detach();

        system("pause");     return 0; }

           C++11的标准库中还提供了std::lock_guard类模板做mutex的RAII。

           采用”资源分配时初始化”(RAII——Resource Acquisition Is Initialization)方法来加锁、解锁,这避免了在临界区中因为抛出异常或return等操作导致没有解锁就退出的问题。

           std::lock_guard类的构造函数禁用拷贝构造,且禁用移动构造。std::lock_guard类除了构造函数和析构函数外没有其它成员函数。在std::lock_guard对象构造时,传入的mutex对象(即它所管理的mutex对象)会被当前线程锁住。在lock_guard对象被析构时,它所管理的mutex对象会自动解锁,不需要程序员手动调用lock和unlock对mutex进行上锁和解锁操作。lock_guard对象并不负责管理mutex对象的生命周期,lock_guard对象只是简化了mutex对象的上锁和解锁操作,方便线程对互斥量上锁,即在某个lock_guard对象的生命周期内,它所管理的锁对象会一直保持上锁状态;而lock_guard的生命周期结束之后,它所管理的锁对象会被解锁。程序员可以非常方便地使用lock_guard,而不用担心异常安全问题。

    用法举例:

    #include <iostream> #include <thread> #include <mutex>

    int counter = 0; std::mutex mtx; // 保护counter

    void increase_proxy(int time, int id) {     for (int i = 0; i < time; i++) {         std::lock_guard<std::mutex> lk(mtx);         //mtx.lock();         // 线程1上锁成功后,抛出异常:未释放锁         if (id == 1) {             throw std::runtime_error("throw excption....");         }         // 当前线程休眠1毫秒         std::this_thread::sleep_for(std::chrono::milliseconds(1));         counter++;         //mtx.unlock();     } }

    void increase(int time, int id) {     try {         increase_proxy(time, id);     }     catch (const std::exception& e){         std::cout << "id:" << id << ", " << e.what() << std::endl;     } }

    int main(int argc, char** argv) {     std::thread t1(increase, 1000, 1);     std::thread t2(increase, 1000, 2);     std::thread t3(increase, 1000, 3);     t1.join();     t2.join();     t3.join();     std::cout << "counter:" << counter << std::endl;

        system("pause");     return 0; }

    二、条件锁

           条件锁就是条件变量,某一个线程因为某个条件为满足时可以使用条件变量使改程序处于阻塞状态。条件满足以后,就通过信号量的方式唤醒被该条件阻塞的线程。

           最常见的是,任务队列为空时,线程池种的线程因为任务队列为空这个条件而处于阻塞状态,任务进入后,就会以信号量的方式来唤醒一个线程来处理任务。

    头文件:<condition_variable>

    类型:std::condition_variable(只和std::mutex一起工作)和std::condition_variable_any(符合类似互斥元的最低标准的任何东西一起工作)。

    用法举例:

    #include <iostream> #include <mutex> #include <thread> #include <condition_variable> std::mutex       g_mutex;   // 用到的全局锁 std::condition_variable g_cond;   // 用到的条件变量 int g_i = 0; bool g_running = true; void ThreadFunc(int n) {       // 线程执行函数     for (int i = 0; i < n; ++i) {         {             std::lock_guard<std::mutex> lock(g_mutex);   // 加锁,离开{}作用域后锁释放             ++g_i;             std::cout << "plus g_i by func thread " << std::this_thread::get_id() << std::endl;         }     }     std::unique_lock<std::mutex> lock(g_mutex);    // 加锁     while (g_running) {         std::cout << "wait for exit" << std::endl;         g_cond.wait(lock);                // wait调用后,会先释放锁,之后进入等待状态;当其它进程调用通知激活后,会再次加锁     }     std::cout << "func thread exit" << std::endl; } int main() {     int     n = 5;     std::thread t1(ThreadFunc, n);    // 创建t1线程(func thread),t1会执行`ThreadFunc`中的指令     for (int i = 0; i < 3*n; ++i) {         {             std::lock_guard<std::mutex> lock(g_mutex);             ++g_i;             std::cout << "plus g_i by main thread " << std::this_thread::get_id() << std::endl;         }     }     {         std::lock_guard<std::mutex> lock(g_mutex);         g_running = false;         g_cond.notify_one();   // 通知其它线程     }     t1.join();     // 等待线程t1结束     std::cout << "g_i = " << g_i << std::endl;

        system("pause"); }

    三、自旋锁

           自旋锁是一种基础的同步原语,用于保障对共享数据的互斥访问。

           与互斥锁的相比,在获取锁失败的时候不会使得线程阻塞而是一直自旋尝试获取锁。当线程等待自旋锁的时候,CPU不能做其他事情,而是一直处于轮询忙等的状态。

           自旋锁主要适用于被持有时间短,线程不希望在重新调度上花过多时间的情况。

           实际上许多其他类型的锁在底层使用了自旋锁实现,例如多数互斥锁在试图获取锁的时候会先自旋一小段时间,然后才会休眠。如果在持锁时间很长的场景下使用自旋锁,则会导致CPU在这个线程的时间片用尽之前一直消耗在无意义的忙等上,造成计算资源的浪费。

    用法举例:

    // 用户空间用 atomic_flag 实现自旋互斥 #include <thread> #include <vector> #include <iostream> #include <atomic> using namespace std;

    std::atomic_flag lock = ATOMIC_FLAG_INIT;

    void f(int n) {     for (int cnt = 0; cnt < 5; ++cnt) {         while (lock.test_and_set(std::memory_order_acquire)) ; // 获得锁// 自旋                       std::cout << "Output from thread " << n << '\n';         lock.clear(std::memory_order_release);               // 释放锁     } }

    int main() {     std::vector<std::thread> v;     for (int n = 0; n < 5; ++n) {         v.emplace_back(f, n);     }     for (auto& t : v) {         t.join();     }

        system("pause"); }

    四、递归锁

           Mutex可以分为递归锁(recursive mutex)和非递归锁(non-recursive mutex)。可递归锁也可称为可重入锁(reentrant mutex),非递归锁又叫不可重入锁(non-reentrant mutex)。

           二者唯一的区别是,同一个线程可以多次获取同一个递归锁,不会产生死锁。而如果一个线程多次获取同一个非递归锁,则会产生死锁。

           recursive_mutex 类是同步原语,能用于保护共享数据免受从个多线程同时访问。

           recursive_mutex 提供排他性递归所有权语义:

    调用方线程在从它成功调用 lock 或 try_lock 开始的时期里占有 recursive_mutex 。此时期间,线程可以进行对 lock 或 try_lock 的附加调用。所有权的时期在线程调用 unlock 匹配次数时结束。线程占有 recursive_mutex 时,若其他所有线程试图要求 recursive_mutex 的所有权,则它们将阻塞(对于调用 lock )或收到 false 返回值(对于调用 try_lock )。可锁定 recursive_mutex 次数的最大值是未指定的,但抵达该数后,对 lock 的调用将抛出 std::system_error 而对 try_lock 的调用将返回 false 。

           若 recursive_mutex 在仍为某线程占有时被销毁,则程序行为未定义。 recursive_mutex 类满足互斥 (Mutex) 和标准布局类型 (StandardLayoutType) 的所有要求。

    用法举例:

    #include <iostream> #include <thread> #include <vector> #include <mutex> #include <chrono> #include <stdexcept> using namespace std;

    //std::mutex mtx; recursive_mutex mtx;

    void func(int id,int counter) {     if (counter >= 0)     {         mtx.lock();         cout <<"进程编号"<<id<<",递归深度"<< counter-- << endl;         func(id,counter);         /*if (counter == 0)         {             return;         }*/         mtx.unlock();     } }

    int main(int argc, char** argv) {     thread t1(func, 1, 20);     thread t2(func, 2, 10);     t1.join();     t2.join();     system("pause");     return 0; }

    五、读写锁

           读写锁分为两种锁,一种为读锁,用于读取数据,一种为写锁,用来修改数据。当某一线程专门来读取共享数据时就使用读锁,当某一新线程要修改共享数据时,便使用写锁。

    读锁(共享锁):

    读锁可以被多个线程同时拿到,当临界区被读锁锁住时,其他使用读锁的线程仍旧可以拿到锁,进入临界区读取临界资源。当临界区被写锁锁住时,读锁去获得锁时,会被阻塞直到写锁解锁

    写锁(独占锁):

    写锁每次只能被一个线程拿到,当一个读者已经拿到写锁,进入临界区时,其他后面到达的不管是读锁还是写锁都会被阻塞到解锁当一个写锁去访问临界资源,如果在临界区中仍有一个或多个读者在读取数据时或者有一个写者在进行写数据,写者将被阻塞到所有的读者访问完,将锁解锁后,再获得锁。

    读写锁的优点:读锁的使用提高了高并发度,可以使多个读者在任意时刻高并发读取数据。也保证了写入者在写入数据时,不会被其他写入者与读取者干扰,保证了数据的正确性与安全性。

    适合场景:当有大量线程频繁的读取数据,只有少量的的线程对数据进行修改时。

    注意事项:读写锁(共享锁)是C++17标准,应用时要保证编译器支持。

    应用举例:

    #include <iostream> #include <mutex>  #include <shared_mutex> #include <thread> #include <chrono> #include <stdexcept>

    std::shared_mutex mutex_; int value_ = 0;

    // 多个线程/读者能同时读计数器的值。 void get() {     mutex_.lock_shared();     /*for (int i = 0; i < 10; i++)     {         std::cout << i << std::endl;         std::this_thread::sleep_for(std::chrono::milliseconds(10));     }*/     std::cout << std::this_thread::get_id() << '\t' << value_ << std::endl;     mutex_.unlock_shared(); }

    // 只有一个线程/写者能增加/写线程的值。 void increment() {     mutex_.lock();     //std::this_thread::sleep_for(std::chrono::milliseconds(1));     value_++;     for (int i = 0; i < 10; i++)     {         std::cout << i << std::endl;         std::this_thread::sleep_for(std::chrono::milliseconds(10));     }     mutex_.unlock(); }

    void increment_and_print() {     for (int i = 0; i < 10000; i++) {         increment();         get();     } };

    int main() {

        std::thread thread1(increment_and_print);     std::thread thread2(increment_and_print);

        thread1.join();     thread2.join();

        std::cout << value_ << std::endl;

        system("pause");     return 0; }

    Processed: 0.026, SQL: 8