C++11 同步与互斥

C++11 同步与互斥

0. C++11的线程

#include <thread>

  • 面向对象的接口
  • RAII(资源获取即初始化)机制,当线程对象被销毁时,会自动执行析构函数,如果线程仍然在运行,会自动调用std::terminate()来终止程序;
  • 使用std::mutexstd::condition_variable进行同步:C++11线程库引入了互斥量(std::mutex)和条件变量(std::condition_variable),用于线程间的同步和通信。
  • 可移植性:C++11线程库提供了跨平台的线程抽象,使得编写可移植的多线程代码更加容易。开发者不再需要处理底层的线程创建和管理细节,而是直接使用标准线程库提供的接口。

1. 互斥

1.1 锁是实现互斥的方法,在std中实现了多种基本锁如下:

  1. std::mutex:最基本的互斥锁,只能在同一线程中进行加锁和解锁操作。

  2. std::recursive_mutex:递归互斥锁,允许同一线程多次加锁,但必须在同一线程中解锁相同次数。

  3. std::timed_mutex:定时互斥锁,允许线程尝试加锁一段时间,如果超时则放弃锁。

  4. std::recursive_timed_mutex:递归定时互斥锁,结合了递归和定时互斥锁的特性。

  5. std::shared_mutex:共享互斥锁,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。当一个线程持有写入权限时,其他所有线程都无法获取读取或写入权限,直到该线程释放写入权限。注意这是一个读写分离锁,即多个线程可以同时读取共享数据,但只有一个线程可以写入共享数据。当一个线程正在写入共享数据时,其他线程必须等待该线程完成写入操作后才能进行读取或写入操作。

  6. std::shared_timed_mutex:定时共享互斥锁,结合了共享和定时互斥锁的特性。

    这些锁类型都是线程安全的,可以在多线程环境下使用。互斥量的锁定和解锁操作是原子的,即它们在并发环境中是不可分割的操作,确保了线程之间的同步和互斥。这意味着在任何给定时间点,只有一个线程可以持有互斥量的锁,从而保证了共享资源的独占访问。

1.2 锁的封装

为了更方便的管理互斥锁的生命周期,具有RAII特性,即在对象构造时获取资源,在对象析构时释放资源,从而保证资源的正确获取和释放。他利用此原理在析构函数中释放了锁,即可以达到自动释放锁的效果。

  • std::unique_lock<std::mutex> lock(mutex_)

    可以在构造函数中传入一个互斥锁对象,当std::unique_lock对象被销毁时(出了作用域会自动销毁),它会自动释放互斥锁。同时,std::unique_lock还提供了lock()unlock()方法,可以手动控制互斥锁的加锁和解锁。

    /*unique_lock简单实现*/
    template<class Mutex>
    class unique_lock { 
    public:
    	explicit unique_lock(Mutex& m) : mutex_(m) {
        	mutex_.lock();    
        }   
        ~unique_lock() {  
        	mutex_.unlock();    
        } 
    private: 
    	Mutex& mutex_; 
    };
    
  • std::lock_guard<std::mutex> lock(mutex_).当std::lock_guard对象被销毁时,互斥锁会自动解锁。这样可以确保在std::lock_guard对象的生命周期内,互斥锁一直处于锁定状态,从而避免了忘记解锁的问题。

  • std::scoped_lock也是一个模板类,用于管理多个互斥锁。在创建std::scoped_lock对象时,需要传入多个互斥锁对象,该对象会在std::scoped_lock的构造函数中被锁定,当std::scoped_lock对象被销毁时,所有互斥锁会自动解锁。这样可以确保在std::scoped_lock对象的生命周期内,所有互斥锁都处于锁定状态,从而避免了死锁的问题。需要注意的是,std::scoped_lock可以同时锁定多个互斥锁,但是不能锁定同一个互斥锁多次,否则会导致死锁。

2.同步机制

在linux中,可以用于实现同步的机制有条件变量,信号量,互斥锁,在C++11中的同步机制主要是通过条件变量来实现的,需要引入头文件condition_variable,其主要方法有

  • wait(lock, predicate):lambda函数重载等待条件
  • notify_one():至少唤醒一个线程
  • notify_all():唤醒所有线程(如果可用资源不只有1个,竞争互斥锁失败的线程重新等待)
void WorkerThread()
{
    std::cout << "WorkerThread: Started" << std::endl;

    // 等待主线程发出信号
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return isReady; });

    // 收到信号后执行任务
    std::cout << "WorkerThread: Resumed" << std::endl;
    std::cout << "WorkerThread: Performing task..." << std::endl;
    // 执行任务的代码

    std::cout << "WorkerThread: Completed" << std::endl;
}
int main()
{
    // 创建工作线程
    std::thread worker(WorkerThread);
    // 发送信号给工作线程
    {
        std::lock_guard<std::mutex> lock(mtx);
        isReady = true;
        cv.notify_one();
    }
    // 等待工作线程完成
    worker.join();
    return 0;
}

可以使用条件变量与互斥锁实现信号量:

#include <mutex>
#include <condition_variable>

class Semaphore {
public:
    Semaphore(int count = 0) : count_(count) {}

    void Wait() {
        std::unique_lock<std::mutex> lock(mutex_);
        while (count_ == 0) {
            condition_.wait(lock);
        }
        count_--;
    }

    void Signal() {
        std::unique_lock<std::mutex> lock(mutex_);
        count_++;
        condition_.notify_one();
    }

private:
    int count_;
    std::mutex mutex_;
    std::condition_variable condition_;
};

3. 线程池

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

class ThreadPool {
public:
    ThreadPool(size_t numThreads) : stop(false) {
        for (size_t i = 0; i < numThreads; ++i) {
            threads.emplace_back([this]() {
                while (true) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(queueMutex);
                        condition.wait(lock, [this]() {
                            return stop || !tasks.empty();
                        });
                        if (stop && tasks.empty()) {
                            return;
                        }
                        task = std::move(tasks.front());
                        tasks.pop();
                    }
                    task();
                }
            });
        }
    }

    ~ThreadPool() {
        {
            std::unique_lock<std::mutex> lock(queueMutex);
            stop = true;
        }
        condition.notify_all();
        for (std::thread& thread : threads) {
            thread.join();
        }
    }

    template<class F, class... Args>
    void enqueue(F&& f, Args&&... args) {
        {
            std::unique_lock<std::mutex> lock(queueMutex);
            tasks.emplace([=]() {
                std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
            });
        }
        condition.notify_one();
    }

private:
    std::vector<std::thread> threads;
    std::queue<std::function<void()>> tasks;
    std::mutex queueMutex;
    std::condition_variable condition;
    bool stop;
};

// 示例任务函数
void taskFunction(int id) {
    std::cout << "Task " << id << " is running." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Task " << id << " is done." << std::endl;
}

int main() {
    ThreadPool threadPool(4);

    // 添加任务到线程池
    for (int i = 0; i < 8; ++i) {
        threadPool.enqueue(taskFunction, i);
    }

    // 等待任务执行完毕
    std::this_thread::sleep_for(std::chrono::seconds(5));

    return 0;
}

这个地方可能会有一些拓展问题:

  • 如何安排线程优先级?
  • 线程异常时该怎么做?
posted @ 2023-08-04 12:01  石中火本火  阅读(24)  评论(0编辑  收藏  举报