C++互斥锁原理及实际使用方法是什么

发布时间:2023-05-05 16:58:06 作者:iii
来源:亿速云 阅读:161

C++互斥锁原理及实际使用方法是什么

目录

  1. 引言
  2. 互斥锁的基本概念
  3. C++中的互斥锁
  4. 互斥锁的实现原理
  5. 互斥锁的实际使用方法
  6. 互斥锁的性能考虑
  7. 常见问题与解决方案
  8. 总结

引言

在多线程编程中,多个线程可能会同时访问共享资源,从而导致数据竞争和不一致的问题。为了解决这些问题,C++提供了互斥锁(Mutex)机制。本文将详细介绍C++中互斥锁的原理及实际使用方法,帮助读者更好地理解和使用互斥锁。

互斥锁的基本概念

什么是互斥锁

互斥锁(Mutex,全称Mutual Exclusion)是一种同步机制,用于保护共享资源,防止多个线程同时访问。当一个线程持有互斥锁时,其他线程必须等待该线程释放锁后才能访问共享资源。

为什么需要互斥锁

在多线程环境中,多个线程可能会同时访问和修改共享资源,导致数据竞争(Race Condition)。数据竞争会导致程序行为不可预测,甚至崩溃。互斥锁通过确保同一时间只有一个线程可以访问共享资源,从而避免数据竞争。

C++中的互斥锁

C++标准库提供了多种互斥锁类型,以满足不同的需求。

std::mutex

std::mutex 是最基本的互斥锁类型,提供了 lock()unlock() 方法。lock() 方法用于获取锁,unlock() 方法用于释放锁。

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

std::mutex mtx;

void print_block(int n, char c) {
    mtx.lock();
    for (int i = 0; i < n; ++i) { std::cout << c; }
    std::cout << '\n';
    mtx.unlock();
}

int main() {
    std::thread th1(print_block, 50, '*');
    std::thread th2(print_block, 50, '$');

    th1.join();
    th2.join();

    return 0;
}

std::lock_guard

std::lock_guard 是一个RI(Resource Acquisition Is Initialization)风格的锁管理类,它在构造时自动获取锁,在析构时自动释放锁。使用 std::lock_guard 可以避免忘记释放锁的问题。

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

std::mutex mtx;

void print_block(int n, char c) {
    std::lock_guard<std::mutex> lock(mtx);
    for (int i = 0; i < n; ++i) { std::cout << c; }
    std::cout << '\n';
}

int main() {
    std::thread th1(print_block, 50, '*');
    std::thread th2(print_block, 50, '$');

    th1.join();
    th2.join();

    return 0;
}

std::unique_lock

std::unique_lock 是一个更灵活的锁管理类,它允许手动控制锁的获取和释放。std::unique_lock 还支持延迟锁定、尝试锁定和条件变量等功能。

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

std::mutex mtx;

void print_block(int n, char c) {
    std::unique_lock<std::mutex> lock(mtx);
    for (int i = 0; i < n; ++i) { std::cout << c; }
    std::cout << '\n';
    lock.unlock(); // 手动释放锁
}

int main() {
    std::thread th1(print_block, 50, '*');
    std::thread th2(print_block, 50, '$');

    th1.join();
    th2.join();

    return 0;
}

std::recursive_mutex

std::recursive_mutex 是一种递归互斥锁,允许同一个线程多次获取锁。这在递归函数中非常有用。

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

std::recursive_mutex mtx;

void recursive_function(int n) {
    if (n <= 0) return;
    std::lock_guard<std::recursive_mutex> lock(mtx);
    std::cout << "Thread " << std::this_thread::get_id() << " entered recursive_function with n = " << n << '\n';
    recursive_function(n - 1);
}

int main() {
    std::thread th1(recursive_function, 3);
    std::thread th2(recursive_function, 3);

    th1.join();
    th2.join();

    return 0;
}

std::timed_mutex

std::timed_mutex 是一种带超时功能的互斥锁,允许在指定的时间内尝试获取锁。如果超时仍未获取到锁,则返回 false

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

std::timed_mutex mtx;

void print_block(int n, char c) {
    if (mtx.try_lock_for(std::chrono::milliseconds(100))) {
        for (int i = 0; i < n; ++i) { std::cout << c; }
        std::cout << '\n';
        mtx.unlock();
    } else {
        std::cout << "Could not acquire lock\n";
    }
}

int main() {
    std::thread th1(print_block, 50, '*');
    std::thread th2(print_block, 50, '$');

    th1.join();
    th2.join();

    return 0;
}

互斥锁的实现原理

原子操作

互斥锁的实现依赖于原子操作(Atomic Operation)。原子操作是指在多线程环境中,一个操作要么完全执行,要么完全不执行,不会被其他线程打断。常见的原子操作包括 compare-and-swap(CAS)和 test-and-set(TAS)。

自旋锁

自旋锁(Spinlock)是一种简单的互斥锁实现,它通过忙等待(Busy Waiting)的方式等待锁的释放。自旋锁适用于锁持有时间较短的场景,但在锁持有时间较长的情况下,会导致CPU资源的浪费。

#include <atomic>

class Spinlock {
    std::atomic_flag flag = ATOMIC_FLAG_INIT;

public:
    void lock() {
        while (flag.test_and_set(std::memory_order_acquire)) {
            // 忙等待
        }
    }

    void unlock() {
        flag.clear(std::memory_order_release);
    }
};

操作系统支持

现代操作系统提供了对互斥锁的原生支持,如Linux中的 pthread_mutex_t 和Windows中的 CRITICAL_SECTION。这些操作系统级别的互斥锁实现通常比用户级别的自旋锁更高效,因为它们可以利用操作系统的调度机制来避免忙等待。

互斥锁的实际使用方法

基本用法

互斥锁的基本用法已经在前面介绍过,这里再总结一下:

  1. 使用 std::mutex 保护共享资源。
  2. 使用 std::lock_guardstd::unique_lock 自动管理锁的生命周期。
  3. 避免手动调用 lock()unlock(),以防止忘记释放锁。

避免死锁

死锁(Deadlock)是指多个线程互相等待对方释放锁,导致所有线程都无法继续执行。为了避免死锁,可以遵循以下原则:

  1. 按固定顺序获取锁。
  2. 使用 std::lock 一次性获取多个锁。
  3. 避免在持有锁的情况下调用可能获取其他锁的函数。
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx1;
std::mutex mtx2;

void thread1() {
    std::lock(mtx1, mtx2);
    std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
    std::cout << "Thread 1\n";
}

void thread2() {
    std::lock(mtx1, mtx2);
    std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
    std::cout << "Thread 2\n";
}

int main() {
    std::thread th1(thread1);
    std::thread th2(thread2);

    th1.join();
    th2.join();

    return 0;
}

条件变量与互斥锁

条件变量(Condition Variable)用于在多线程环境中实现线程间的同步。条件变量通常与互斥锁一起使用,以实现复杂的同步逻辑。

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

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id(int id) {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{ return ready; });
    std::cout << "Thread " << id << '\n';
}

void go() {
    std::unique_lock<std::mutex> lock(mtx);
    ready = true;
    cv.notify_all();
}

int main() {
    std::thread threads[10];
    for (int i = 0; i < 10; ++i) {
        threads[i] = std::thread(print_id, i);
    }

    std::cout << "10 threads ready to race...\n";
    go();

    for (auto& th : threads) {
        th.join();
    }

    return 0;
}

读写锁

读写锁(Read-Write Lock)允许多个读线程同时访问共享资源,但只允许一个写线程访问共享资源。C++17 引入了 std::shared_mutexstd::shared_lock 来实现读写锁。

#include <iostream>
#include <thread>
#include <shared_mutex>

std::shared_mutex smtx;
int shared_data = 0;

void reader(int id) {
    std::shared_lock<std::shared_mutex> lock(smtx);
    std::cout << "Reader " << id << " reads " << shared_data << '\n';
}

void writer(int id) {
    std::unique_lock<std::shared_mutex> lock(smtx);
    ++shared_data;
    std::cout << "Writer " << id << " writes " << shared_data << '\n';
}

int main() {
    std::thread readers[5];
    std::thread writers[2];

    for (int i = 0; i < 5; ++i) {
        readers[i] = std::thread(reader, i);
    }

    for (int i = 0; i < 2; ++i) {
        writers[i] = std::thread(writer, i);
    }

    for (int i = 0; i < 5; ++i) {
        readers[i].join();
    }

    for (int i = 0; i < 2; ++i) {
        writers[i].join();
    }

    return 0;
}

互斥锁的性能考虑

锁的粒度

锁的粒度(Granularity)是指锁保护的资源范围。锁的粒度过大会导致锁竞争加剧,降低并发性能;锁的粒度过小会增加锁的开销。因此,在设计多线程程序时,需要根据实际情况选择合适的锁粒度。

锁的竞争

锁的竞争(Lock Contention)是指多个线程同时竞争同一个锁,导致线程等待时间增加。为了减少锁的竞争,可以采用以下策略:

  1. 使用读写锁替代互斥锁。
  2. 减少锁的持有时间。
  3. 使用无锁数据结构。

无锁编程

无锁编程(Lock-Free Programming)是一种不使用互斥锁的并发编程技术,通过原子操作和内存屏障来实现线程安全。无锁编程可以避免锁竞争和死锁问题,但实现复杂度较高。

#include <atomic>
#include <iostream>
#include <thread>

std::atomic<int> counter(0);

void increment() {
    for (int i = 0; i < 100000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::thread th1(increment);
    std::thread th2(increment);

    th1.join();
    th2.join();

    std::cout << "Counter = " << counter << '\n';

    return 0;
}

常见问题与解决方案

死锁

死锁是指多个线程互相等待对方释放锁,导致所有线程都无法继续执行。为了避免死锁,可以遵循以下原则:

  1. 按固定顺序获取锁。
  2. 使用 std::lock 一次性获取多个锁。
  3. 避免在持有锁的情况下调用可能获取其他锁的函数。

活锁

活锁(Livelock)是指多个线程不断尝试获取锁,但由于某种原因(如优先级反转)导致无法成功获取锁。活锁的解决方案与死锁类似,主要是避免循环等待。

饥饿

饥饿(Starvation)是指某些线程由于优先级低或竞争激烈,长时间无法获取锁。为了避免饥饿,可以采用公平锁(Fair Lock)或调整线程优先级。

总结

互斥锁是多线程编程中非常重要的同步机制,用于保护共享资源,避免数据竞争。C++标准库提供了多种互斥锁类型,如 std::mutexstd::lock_guardstd::unique_lock 等,以满足不同的需求。在实际使用中,需要注意避免死锁、活锁和饥饿等问题,并根据实际情况选择合适的锁粒度和锁类型。通过合理使用互斥锁,可以编写出高效、安全的多线程程序。

推荐阅读:
  1. 通过OCILIB连接oracle执行存储过程
  2. c++通过ADO对数据库操作

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

c++

上一篇:基于element-ui中el-select下拉框选项过多怎么优化

下一篇:python排序算法之希尔排序怎么实现

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》