C++线程锁

1.线程锁

是用于控制多个线程对共享资源的访问,以避免竞态条件和数据不一致性的重要机制

2.std::lock_guard模板类

`std::lock_guard`是C++标准库中的一个类模板,它提供了一种简便的方式来管理互斥锁的锁定和解锁。

**含义**:
`std::lock_guard`是一个互斥锁的封装器,它遵循RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。这种设计哲学确保了在任何情况下,无论是正常执行路径还是由于异常而退出,都能保证资源的正确释放。

**用途**:
`std::lock_guard`的主要用途是在多线程编程中保护共享资源,防止多个线程同时访问同一资源导致的竞态条件。它通过在构造时自动加锁,并在析构时自动解锁,来保证互斥锁的正确使用。

**使用方式**:
`std::lock_guard`的使用非常简单。在需要保护的代码段前,创建一个`std::lock_guard`对象,并将互斥锁作为参数传递给它。当`std::lock_guard`对象被创建时,它会立即锁定传入的互斥锁。当控制流离开`std::lock_guard`对象的作用域时,它的析构函数会被调用,从而解锁互斥锁。这种设计确保了即使在抛出异常或提前返回的情况下,互斥锁也能被正确解锁,从而避免了死锁的风险。

总结来说,`std::lock_guard`是C++11引入的一种简化互斥锁管理的机制,它通过RAII原则确保了互斥锁的安全使用,适用于需要在多线程环境中保护共享资源的场景。

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

std::mutex mtx; // 互斥锁

void process1() {
    std::lock_guard<std::mutex> lock(mtx); // 进程1获取锁
    std::cout << "Process 1 acquired the lock." << std::endl;
    // 执行一些操作...
}

void process2() {
    std::lock_guard<std::mutex> lock(mtx); // 进程2尝试获取锁
    std::cout << "Process 2 acquired the lock." << std::endl;
    // 执行一些操作...
}

int main() {
    std::thread t1(process1); // 创建线程1并运行process1函数
    std::thread t2(process2); // 创建线程2并运行process2函数

    t1.join(); // 等待线程1结束
    t2.join(); // 等待线程2结束

    return 0;
}

有两个进程(实际上是两个线程)同时竞争同一个互斥锁。进程1首先获取了锁,然后输出一条消息表示已经获得了锁。接着,进程2也尝试获取锁,但由于锁已经被进程1持有,进程2将被阻塞,直到进程1释放锁。当进程1完成对共享资源的访问后,它将自动释放锁,此时进程2才能获取到锁并继续执行。

需要注意的是,由于线程的调度是由操作系统控制的,因此进程1和进程2的执行顺序是不确定的。在某些情况下,进程2可能会先于进程1获得锁,导致它们交替执行。但是,由于互斥锁的存在,在任何时刻只有一个进程能够持有锁,从而保证了对共享资源的互斥访问。

3.std::unique_lock模板类

`std::unique_lock`是C++标准库中的一个类模板,它提供了一种灵活的方式来管理互斥锁的锁定和解锁。

**含义**:
`std::unique_lock`是一个互斥锁的封装器,它遵循RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。这种设计哲学确保了在任何情况下,无论是正常执行路径还是由于异常而退出,都能保证资源的正确释放。

**用途**:
`std::unique_lock`的主要用途是在多线程编程中保护共享资源,防止多个线程同时访问同一资源导致的竞态条件。它通过在构造时自动加锁,并在析构时自动解锁,来保证互斥锁的正确使用。与`std::lock_guard`不同,`std::unique_lock`还提供了手动解锁的能力,以及在等待条件变量时自动解锁的功能。

**使用方式**:
`std::unique_lock`的使用非常简单。在需要保护的代码段前,创建一个`std::unique_lock`对象,并将互斥锁作为参数传递给它。当`std::unique_lock`对象被创建时,它会立即锁定传入的互斥锁。当控制流离开`std::unique_lock`对象的作用域时,它的析构函数会被调用,从而解锁互斥锁。这种设计确保了即使在抛出异常或提前返回的情况下,互斥锁也能被正确解锁,从而避免了死锁的风险。

总结来说,`std::unique_lock`是C++11引入的一种灵活的互斥锁管理机制,它通过RAII原则确保了互斥锁的安全使用,适用于需要在多线程环境中保护共享资源的场景。

条件锁std::condition_variable

在C++中,并没有直接称为“条件锁”的原生同步原语。实际是std::condition_variable,这是一个同步原语,通常与互斥锁(如std::mutex)一起使用,以实现线程之间的条件同步。

std::condition_variable允许线程等待某个条件成立,而另一个线程或线程组则可能在修改条件后通知等待的线程。这提供了一种线程间通信的机制,使得线程可以安全地协调它们的工作。

wait() 方法

  • 功能:等待条件成立。
  • 使用方式:通常与 std::unique_lock 一起使用,当调用 wait() 时,unique_lock 会自动释放互斥锁,允许其他线程获取锁并修改条件。当条件成立时(通常通过另一个线程调用 notify_one() 或 notify_all()),wait() 返回,unique_lock 重新获取锁。
std::mutex mtx;  
std::condition_variable cv;  
bool ready = false;  

void wait_for_condition() {  
    std::unique_lock<std::mutex> lck(mtx);  
    while (!ready) {  // 循环等待条件成立  
        cv.wait(lck); // 释放锁并等待通知  
    }  
    // 条件成立,执行后续操作  
}

wait_for() 和 wait_until() 方法

  • 功能:等待条件成立,但带有超时限制。
  • 使用方式:类似于 wait(),但允许指定一个超时时间。如果超时时间到达而条件仍未成立,则方法返回。
  •  wait_for() 方法在指定的超时时间到达时,如果条件仍未满足,它会返回 std::cv_status::timeout。如果条件在超时前被满足(即另一个线程调用了 notify_one() 或 notify_all()),那么 wait_for() 会返回 std::cv_status::no_timeout
std::chrono::milliseconds timeout(1000); // 1秒超时  
cv.wait_for(lck, timeout, [](){ return ready; });

 

notify_one() 方法

  • 功能:唤醒等待在 condition_variable 上的一个线程。
  • 使用方式:当条件发生变化且可能满足某个或某些线程的等待条件时,调用此方法。
void change_condition_and_notify() {  
    std::lock_guard<std::mutex> lck(mtx);  
    // 修改条件...  
    ready = true;  
    cv.notify_one(); // 唤醒一个等待的线程  
}

 

notify_all() 方法

  • 功能:唤醒等待在 condition_variable 上的所有线程。
  • 使用方式:当条件发生变化且可能满足所有等待线程的等待条件时,调用此方法。
void change_condition_and_notify_all() {  
    std::lock_guard<std::mutex> lck(mtx);  
    // 修改条件...  
    ready = true;  
    cv.notify_all(); // 唤醒所有等待的线程  
}

 在使用 std::condition_variable 时,需要确保对共享数据的访问是线程安全的。通常,这意味着在调用 wait()wait_for()wait_until() 之前和之后都需要持有互斥锁。此外,通知线程(即调用 notify_one() 或 notify_all() 的线程)也需要在修改条件变量和发出通知时持有相同的互斥锁。这样可以避免数据竞争和条件竞争。

最后,请注意,虽然 std::condition_variable 提供了线程间同步的机制,但它并不保证线程执行的顺序。因此,在设计多线程程序时,还需要考虑其他同步原语(如互斥锁、信号量等)以及潜在的竞态条件。

 示例

#include <iostream>
#include <mutex>
#include <thread>
#include <condition_variable>
#include <windows.h>
std::mutex mtx; // 互斥锁
std::condition_variable cv; // 条件变量
bool ready = false; // 共享资源的状态标志
int counter = 0;
void producer() {
	Sleep(6000);
    std::unique_lock<std::mutex> lock(mtx); // 生产者获取锁
    ready = true; // 设置共享资源状态为true
	std::cout << "im producer 1" << std::endl;
    cv.notify_one(); // 通知消费者
	//lock.unlock(); // 手动解锁,t2线程获得锁,两个线程交叉执行
	std::cout << "im producer 2" << std::endl;
	for (int i = 0; i < 500000; ++i) { 
        ++counter;  
    }
	std::cout << "im producer 3" << std::endl;
}

void consumer() {
	std::cout << "Consumer in" << std::endl;
    std::unique_lock<std::mutex> lock(mtx); // 消费者获取锁
	std::cout << "Consumer wait" << std::endl;
    cv.wait(lock, []{ return ready; }); // 等待条件变量,直到ready为true
    // 执行一些操作...
	std::cout << "do something" << std::endl;
	for (int i = 0; i < 500000; ++i) {			
        ++counter;  
    }
    std::cout << "Consumer end." << std::endl;
}

int main() {
    std::thread t1(producer); // 创建线程1并运行producer函数
    std::thread t2(consumer); // 创建线程2并运行consumer函数
	
    t1.join(); // 等待线程1结束
    t2.join(); // 等待线程2结束
	std::cout << "counter : " << counter << std::endl;
    std::cin.get(); 
    return 0;
}

 Consumer in
Consumer wait
im producer 1
im producer 2
im producer 3
do something
Consumer end.
counter : 1000000

 说明

一个线程可以主动持有两次锁?

在C++中,一个线程不应该尝试主动持有同一个互斥锁(std::mutex)两次,因为这会导致未定义行为,通常表现为死锁。当一个线程已经持有一个互斥锁时,再次尝试获取同一个锁会导致该线程自己阻塞自己,因为它会等待自己释放锁,而这永远不会发生。

一方持有条件锁的线程,调用notify_one会立刻释放锁,使得wait的线程立刻执行吗?

在C++中,当一方线程持有条件锁(通常是通过std::unique_lockstd::lock_guard等锁类型)并改变了一个条件,使得另一个线程从wait()wait_for(), 或 wait_until()中返回时,调用notify_one()notify_all()并不会使得持有锁的线程立刻释放锁。

notify_one()notify_all()的作用是唤醒一个或多个正在等待该条件变量的线程。但是,这些被唤醒的线程并不会立刻执行,它们需要等待当前持有锁的线程释放锁。

具体来说,当调用notify_one()notify_all()时,持有锁的线程仍然保持着对互斥锁的所有权。只有当持有锁的线程主动释放锁(比如通过std::unique_lock的析构函数或者显式调用unlock()方法),其他线程才有机会获取锁并执行。

因此,即使条件已经改变并且调用了notify_one()notify_all(),被唤醒的线程也只有在获得锁之后才能继续执行其后续的代码。这确保了线程安全,防止了数据竞争和条件竞争。

unique_lock 一个进程获得锁后等待条件变量时,他还持有锁吗?

**当一个进程(线程)使用std::unique_lock获得锁后等待条件变量时,它会释放锁。**

在C++中,`std::condition_variable`通常与`std::unique_lock`一起使用来同步线程的执行。当线程需要等待某个条件满足时,它会调用条件变量的`wait`系列方法,并传递一个`unique_lock`对象作为参数。在这个过程中:

1. **等待前自动解锁**:在线程调用`wait`方法进入等待状态之前,`unique_lock`会自动对关联的互斥量进行解锁操作。
2. **唤醒后重新上锁**:一旦条件变量被通知(可能是由于其他线程调用了`notify_one`或`notify_all`),等待的线程被唤醒,此时`unique_lock`将再次自动上锁,以保持同步性。
3. **确保同步性**:这种设计保证了在条件变量等待过程中不会因为持有锁而导致其他线程无法访问共享资源,同时在线程被唤醒后能够立即检查所等待的条件是否满足,如果满足则继续执行。
4. **避免死锁**:使用`unique_lock`管理互斥量的锁定和解锁可以有效防止因忘记解锁而导致的死锁问题,因为它提供了RAII(Resource Acquisition Is Initialization)机制,即在对象生命周期结束时自动解锁。

因此,在使用`std::unique_lock`等待条件变量时,线程会释放掉持有的锁,直到被唤醒后才重新获取锁继续执行。这样的机制有助于提高多线程程序的效率和安全性。

4.互斥锁 std::mutex 类

  • 特点:用于确保同一时刻只有一个线程可以访问某个共享资源。当一个线程获得互斥锁时,其他线程必须等待直到锁被释放。
  • 使用:在访问共享资源前,线程需要尝试获取互斥锁。如果锁已被其他线程持有,则该线程将被阻塞,直到锁被释放。

 C++中的互斥锁(Mutex,全称Mutual Exclusion)是一种同步原语,用于保护共享资源,防止多个线程同时访问导致的数据不一致问题。在C++标准库中,std::mutex是最基本的互斥锁实现。

#include <iostream>  
#include <thread>  
#include <mutex>  
  
std::mutex mtx;  // 全局互斥锁  
int counter = 0; // 共享资源  
  
void increment() {  
    for (int i = 0; i < 100000; ++i) {  
        std::lock_guard<std::mutex> lock(mtx);  // 使用锁保护区域  
        ++counter;  
    }  
}  
  
int main() {  
    std::thread t1(increment);  
    std::thread t2(increment);  
  
    t1.join();  
    t2.join();  
  
    std::cout << "Final counter value is " << counter << std::endl;  
    std::cin.get();   
    return 0;  
}  

有两个线程t1t2,它们都试图增加全局变量counter的值。如果没有互斥锁的保护,这两个线程可能会同时访问counter,导致数据不一致。通过使用std::mutex,我们确保了在任何给定时间只有一个线程可以修改counter

注意:在多线程编程中,互斥锁的使用需要谨慎,以避免死锁和其他同步问题。尽量保持锁定的区域尽可能小,以减少线程间的竞争。同时,避免在持有锁时执行耗时的操作,因为这可能会阻塞其他等待锁的线程。

4.超时锁std::timed_mutex 类

`std::timed_mutex`是C++标准库中的一个类模板,它提供了一种带有超时机制的互斥量。与普通的互斥量不同,`std::timed_mutex`允许在尝试锁定时设置一个超时时间,如果在指定的时间内无法获得锁,它将返回一个错误或失败的状态,而不是无限期地等待。

下面是使用`std::timed_mutex`的一些关键步骤和特征:

1. **初始化**:首先需要创建一个`std::timed_mutex`对象。可以使用默认构造函数进行初始化,例如:

std::timed_mutex mutex;

 2. **锁定互斥量**:要锁定互斥量,可以使用`lock()`成员函数。这个函数会阻塞当前线程直到互斥量可用,或者达到指定的超时时间。如果成功获取到锁,则返回`true`;否则返回`false`。

 bool success = mutex.try_lock_for(std::chrono::seconds(5)); // 尝试在5秒内获取锁

3. **解锁互斥量**:当不再需要访问共享资源时,可以使用`unlock()`成员函数来释放互斥量。这允许其他线程能够获取锁并继续执行。

mutex.unlock();

4. **条件变量操作**:`std::timed_mutex`还可以与条件变量一起使用,以实现更复杂的同步操作。可以使用`std::condition_variable`的`wait_for()`或`wait_until()`方法来等待特定条件,并在超时时间内等待。
 

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

std::timed_mutex mutex;

void worker() {
    // 尝试在5秒内获取锁
    if (mutex.try_lock_for(std::chrono::seconds(5))) {
        std::cout << "Worker acquired the lock." << std::endl;
        // 模拟一些工作
        std::this_thread::sleep_for(std::chrono::seconds(2));
        mutex.unlock();
        std::cout << "Worker released the lock." << std::endl;
    } else {
        std::cout << "Worker failed to acquire the lock within the timeout." << std::endl;
    }
}

int main() {
    std::thread t1(worker);
    std::thread t2(worker);

    t1.join();
    t2.join();
	std::cin.get();
    return 0;
}

在这个示例中,我们创建了两个线程,每个线程都尝试在5秒内获取互斥量的锁。如果其中一个线程在超时时间内成功获取到锁,它会打印出相应的消息,然后模拟一些工作(通过休眠2秒),最后释放锁。如果另一个线程在超时时间内未能获取到锁,它会打印出失败的消息。

需要注意的是,`std::timed_mutex`并不保证公平性,即不保证等待时间最长的线程优先获得锁。因此,在使用`std::timed_mutex`时,应该谨慎考虑其适用场景,以避免潜在的竞争条件和死锁问题。

  • 28
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值