C++所有锁的讲解、使用场景、相应的C++代码示例

一、互斥锁(Mutex)

1. std::mutex

含义: std::mutex 最基本的互斥锁,当一个线程占用锁时,其他线程必须等待该锁被释放。

使用场景: 当需要保护共享资源不被多个线程同时修改时使用。

代码示例:

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

std::mutex mtx;  // 全局互斥锁
int counter = 0; // 共享资源

void attempt_10k_increases() 
{
    for (int i = 0; i < 10000; ++i) 
    {
        mtx.lock();
        ++counter;  // 受保护的操作
        mtx.unlock();
    }
}

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

    for (auto& th : threads) 
    {
    	th.join();
    }
    
    std::cout << "Result of counter: " << counter << std::endl;
    return 0;
}

输出结果: Result of counter: 100000

解释: 这个程序创建了10个线程,每个线程尝试对counter增加10000次。通过使用std::mutex, 我们确保每次只有一个线程可以增加计数器,避免了数据竞争。

2. std::recursive_mutex

含义: 递归互斥锁,允许同一个线程多次获取同一锁。

使用场景: 在递归函数中需要多次获取同一个锁的情况。

代码示例:

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

std::recursive_mutex rec_mtx;
int count = 0;

void recursive_increment(int level) 
{
    if (level > 0) 
    {
        rec_mtx.lock();
        recursive_increment(level - 1);
        rec_mtx.unlock();
    } 
    else 
    {
        ++count;
    }
}

int main() 
{
    std::thread t(recursive_increment, 10);
    t.join();
    std::cout << "Count is: " << count << std::endl;
    return 0;
}

输出结果: Count is: 1

解释: 这段代码在递归函数recursive_increment中使用std::recursive_mutex。每次调用都会尝试加锁,由于使用的是递归互斥锁,同一线程可以多次成功获取锁。

二、定时锁

1. std::timed_mutex

含义: 允许尝试锁定一定时间,如果在指定时间内没有获取到锁,则线程可以执行其他操作或放弃。

使用场景: 当你不希望线程因等待锁而无限期阻塞时使用。

代码示例:

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

std::timed_mutex timed_mtx;

void attempt_lock_for(int id) 
{
    auto now = std::chrono::steady_clock::now();
    if (timed_mtx.try_lock_for(std::chrono::seconds(1))) 
    {
        std::cout << "Thread " << id << " got the lock." << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(2)); // hold the lock for 2 seconds
        timed_mtx.unlock();
    } 
    else 
    {
        std::cout << "Thread " << id << " couldn't get the lock." << std::endl;
    }
}

int main() 
{
    std::thread threads[2];
    for (int i = 0; i < 2; ++i) 
    {
        threads[i] = std::thread(attempt_lock_for, i);
    }
    for (auto& th : threads) 
    {
    	th.join();
    }
    
    return 0;
}

输出结果:

Thread 0 got the lock.
Thread 1 couldn't get the lock.

解释: 这段代码创建了两个线程,每个线程尝试锁定同一个std::timed_mutex。第一个线程获取锁并持有2秒钟,而第二个线程只尝试1秒钟去获取锁,因此它失败了。

2. std::recursive_timed_mutex

含义: std::recursive_timed_mutex结合了std::recursive_mutexstd::timed_mutex的特点,允许同一个线程多次加锁,并提供了尝试加锁的超时功能。

使用场景: 适用于需要递归锁定资源,并且希望能够设置尝试获取锁的超时时间的场景。这在需要防止线程在等待锁时无限阻塞的复杂递归调用中特别有用。

代码示例:

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

std::recursive_timed_mutex rt_mtx;

void recursive_access(int level, int thread_id) 
{
    if (rt_mtx.try_lock_for(std::chrono::milliseconds(100))) 
    {
        std::cout << "Thread " << thread_id << " entered level " << level << std::endl;
        if (level > 0) 
        {
            std::this_thread::sleep_for(std::chrono::milliseconds(50));
            recursive_access(level - 1, thread_id);
        }
        rt_mtx.unlock();
    } 
    else 
    {
        std::cout << "Thread " << thread_id << " could not enter level " << level << std::endl;
    }
}

int main() {
    std::thread t1(recursive_access, 3, 1);
    std::thread t2(recursive_access, 3, 2);

    t1.join();
    t2.join();

    return 0;
}

输出结果: 输出结果可能会变化,因为线程的执行和锁的获取取决于操作系统的线程调度策略。可能的输出包括两个线程交替进入不同的递归级别,或者一个线程完全执行完毕后另一个线程开始执行。

解释: 在这个示例中,每个线程尝试进入一个递归函数recursive_access,该函数使用std::recursive_timed_mutex。每个线程在进入下一个递归级别之前会尝试获取锁,并设置超时时间为100毫秒。如果一个线程在递归的某个级别成功获取了锁,它会打印信息然后在释放锁之前休眠50毫秒。如果获取锁失败(可能由于另一个线程正在持有锁),它将打印未能进入该级别的消息。

这种类型的锁是非常特定场景下的工具,适用于需要递归锁控制的同时又不希望线程在获取不到锁时无限等待的情况。

三、读写锁(Shared Mutex)

std::shared_mutex

含义: 允许多个线程同时读取资源,但只允许一个线程写入。

使用场景: 适用于读操作远多于写操作的情况。

代码示例:

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

std::shared_mutex shared_mtx;
int data = 0;

void reader_function(int id) 
{
    shared_mtx.lock_shared();
    std::cout << "Reader " << id << " sees data as: " << data << std::endl;
    shared_mtx.unlock_shared();
}

void writer_function(int new_data) 
{
    shared_mtx.lock();
    data = new_data;
    std::cout << "Writer updates data to: " << data << std::endl;
    shared_mtx.unlock();
}

int main() 
{
    std::thread writer(writer_function, 100);
    std::thread readers[10];
    for (int i = 0; i < 10; ++i)
    {
        readers[i] = std::thread(reader_function, i);
    }

    writer.join();
    for (auto& reader : readers)
    {
        reader.join();
    }

    return 0;
}

输出结果: 输出结果可能会有所不同,因为读写顺序由操作系统的线程调度决定。

解释: 本例中,一个写线程在修改数据,多个读线程在同时读数据。通过std::shared_mutex,我们允许多个读操作同时进行,但写操作是独占的。

四、自旋锁

自旋锁在C++标准库中没有直接提供一个专门的类型,但它可以使用原子操作,尤其是std::atomic_flag来实现。自旋锁是一种低级同步机制,适用于锁持有时间非常短的情况。与其他锁不同,当自旋锁无法获取锁时,它将在一个循环中持续检查锁的状态,这意味着它会保持CPU的活跃状态,而不是使线程进入休眠。

含义:自旋锁是一种在等待解锁时使线程保持忙等(busy-wait)的锁,这意味着线程会持续占用CPU时间直到它能获取到锁。

使用场景:自旋锁适用于锁持有时间非常短且线程不希望在操作系统调度中频繁上下文切换的场景。这通常用在低延迟系统中,或者当线程数量不多于CPU核心数量时,确保CPU不会在等待锁时空闲。

自旋锁的代码示例
下面是使用std::atomic_flag实现简单自旋锁的示例:

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

class SpinLock 
{
private:
    std::atomic_flag lock_flag = ATOMIC_FLAG_INIT;

public:
    void lock() 
    {
        while (lock_flag.test_and_set(std::memory_order_acquire)) 
        {
            // 循环等待,直到锁变为可用状态
        }
    }

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

SpinLock spinlock;

void work(int id) 
{
    spinlock.lock();
    std::cout << "Thread " << id << " entered critical section." << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 模拟工作
    std::cout << "Thread " << id << " leaving critical section." << std::endl;
    spinlock.unlock();
}

int main() 
{
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) 
    {
        threads.emplace_back(work, i);
    }

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

    return 0;
}

输出结果

Thread 0 entered critical section.
Thread 0 leaving critical section.
Thread 1 entered critical section.
Thread 1 leaving critical section.
...

解释
在此示例中,SpinLock类使用std::atomic_flag实现。lock方法通过test_and_set在一个循环中尝试设置标志位直到成功,从而实现锁的功能。unlock方法通过clear清除标志位。这保证了在某个时刻只有一个线程可以进入临界区,同时使用了忙等待而不是线程休眠。由于忙等待的性质,自旋锁特别适用于预期锁只被短暂持有的场景。

五、唯一锁(Unique Lock)

std::unique_lock

含义: 比std::lock_guard更灵活的锁,支持延迟锁定、时间锁定、以及在同一作用域中的锁的转移。

使用场景: 需要在复杂控制流中灵活管理锁的情况。

代码示例:

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

std::mutex mtx;

void print_even(int x) 
{
    if (x % 2 == 0) 
    {
        std::unique_lock<std::mutex> lock(mtx);
        std::cout << x << " is even." << std::endl;
    } 
    else 
    {
        std::cout << x << " is odd." << std::endl;
    }
}

int main() 
{
    std::thread threads[10];
    for (int i = 0; i < 10; ++i) 
    {
        threads[i] = std::thread(print_even, i);
    }
    for (auto& th : threads) 
    {
    	th.join();
    }
    
    return 0;
}

输出结果:

0 is even.
1 is odd.
2 is even.
3 is odd.
...

解释: 此示例中,我们仅在打印偶数时获取锁。std::unique_lock允许在需要时才加锁,这提供了比std::lock_guard更大的灵活性。

附加

1、锁保护(Lock Guard)

std::lock_guard
含义: 自动管理锁的生命周期,确保作用域结束时释放锁。

使用场景: 当你需要确保在当前作用域结束时自动释放锁,以避免死锁。

代码示例:

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

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 t1(print_block, 50, '*');
    std::thread t2(print_block, 50, '$');

    t1.join();
    t2.join();

    return 0;
}

输出结果:

**************************************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$

或者两行输出的顺序可能会反过来,取决于线程的调度。

解释: 这个示例中,我们使用std::lock_guard来确保在打印过程中互斥锁被持有。这样可以避免输出交错。

2、条件变量

条件变量是一种同步原语,它可以阻塞一个或多个线程,直到某个特定条件为真。条件变量总是与互斥锁(std::mutex)一起使用,以避免竞争条件。基本操作包括:

  • 等待(wait):线程阻塞,并释放其持有的互斥锁,直到另一个线程通知(notify)条件变量。
  • 通知(notify_one/notify_all):解除一个或所有等待线程的阻塞状态。

如何使用条件变量
条件变量用于复杂的同步问题,例如当线程需要等待某些条件(如资源可用或任务完成)满足时。它们不仅用于避免死锁,还用于减少不必要的忙等待,使得线程管理更为高效。

代码示例

假设有一个生产者-消费者场景,其中生产者不能在缓冲区满时生产,消费者不能在缓冲区空时消费:

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

std::mutex mtx;
std::condition_variable cv;
std::queue<int> products;

void producer(int id) 
{
    for (int i = 0; i < 5; ++i) 
    {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [] { return products.size() < 5; });
        products.push(i);
        std::cout << "Producer " << id << " produced " << i << std::endl;
        lock.unlock();
        cv.notify_all();
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void consumer(int id) 
{
    while (true) 
    {
        std::unique_lock<std::mutex> lock(mtx);
        if (cv.wait_for(lock, std::chrono::seconds(1), [] { return !products.empty(); })) 
        {
            int product = products.front();
            products.pop();
            std::cout << "Consumer " << id << " consumed " << product << std::endl;
            lock.unlock();
            cv.notify_all();
        } 
        else 
        {
            break; // Assume done if no production for 1 second.
        }
    }
}

int main() 
{
    std::thread p1(producer, 1), p2(producer, 2);
    std::thread c1(consumer, 1), c2(consumer, 2);
    p1.join();
    p2.join();
    c1.join();
    c2.join();
    return 0;
}

在这个例子中,我们使用了条件变量来同步生产者和消费者之间的操作:

  • 生产者在队列未满时生产,并在生产后通知消费者;
  • 消费者在队列非空时消费,并在消费后通知生产者。

使用条件变量的优势在于它能够减少资源的浪费,提高线程间的协作效率,特别是在需要频繁等待特定条件的场景中。

03-08
### C语言中的机制实现与用法 #### 初始化、请求和释放自旋 在C语言中,使用自旋是一种简单的方式来进行同步。通过`DEFINE_SPINLOCK(lock)`可以初始化一个自旋;接着利用`spin_lock(&lock)`来获取该,在此之后进入临界区执行特定的操作;最后再调用`spin_unlock(&lock)`完成的释放[^1]。 ```c // 自旋示例代码 DEFINE_SPINLOCK(my_spinlock); void critical_section(void){ spin_lock(&my_spinlock); // 执行临界区内代码 spin_unlock(&my_spinlock); } ``` #### 用户态下的读写操作 对于更复杂的场景,则有适用于用户空间编程的读写。当不再需要持有读写时,可以通过函数`pthread_rwlock_unlock()`对其进行解处理[^2]。 ```c #include <pthread.h> pthread_rwlock_t rwlock; void unlock_rwlock(){ pthread_rwlock_unlock(&rwlock); } ``` #### 多线程环境下的互斥应用 为了防止多线程环境下对共享资源的竞争条件发生,通常会采用互斥作为解决方案之一。这有助于保持程序状态的一致性以及避免潜在的数据竞争问题[^3]。 ```c #include <pthread.h> #include <stdio.h> pthread_mutex_t mutex; int shared_resource = 0; void* thread_function(void *arg){ pthread_mutex_lock(&mutex); printf("Thread is accessing the resource\n"); ++shared_resource; pthread_mutex_unlock(&mutex); return NULL; } int main() { pthread_t t1, t2; pthread_create(&t1, NULL, &thread_function, NULL); pthread_create(&t2, NULL, &thread_function, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("Final value of shared resource: %d\n", shared_resource); return 0; } ``` #### 文件系统的区域定功能 除了上述提到的各种内存级别的外,操作系统还提供了针对文件系统的定能力——即允许应用程序指定某一部分文件内容被独占或共享地加保护起来。这种情况下需要用到`struct flock`结构体及其关联的方法[^4]。 ```c #include <fcntl.h> /* For O_* constants */ #include <unistd.h> /* For fcntl */ int fd; struct flock lock; fd = open ("file.txt", O_RDWR); /* 设置为阻塞其他进程写的写 */ lock.l_type = F_WRLCK; fcntl (fd, F_SETLKW, &lock); // 对已上部分进行读/写... close(fd); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Warren++

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值