C++多线程和线程池

join()和detach()

创建的新线程必须被正确的join()和detach()--->

join()后主线程将被阻塞等待新线程执行完毕,不然主线程结束(进程结束,内存释放,释放资源),子线程仍然在后台运行,可能访问非法资源。如果没有join,从属与于主线程的附属线程在主线程退出后,自动退出。线程和std::thread对象还是有关联的。

detach()后,就把新线程理解成一个新的进程就可以了。调用detach()后,std::thread对象不再与实际执行线程关联。

创建一个线程要配套使用join()和 detach()

自旋锁 & 互斥锁

概念:互斥锁是一种独占锁,当线程A加锁成功后,此时互斥锁已经被线程A独占了,只要线程A没有释放手中的锁,线程B就会失败,就会释放掉CPU给其他线程,线程B加锁的代码就会被阻塞。互斥锁加锁失败而阻塞是由操作系统内核实现的,当加锁失败后,内核将线程置为睡眠状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程加锁成功后就可以继续执行。自旋锁通过在用户态使用原子指令(例如 CAS,Compare-And-Swap)来实现加锁和解锁操作,因此在这两个操作中,线程不会主动释放手中的 CPU。这意味着线程会一直尝试获取锁,而不会主动进行上下文切换。

互斥锁的开销(两次线程上下文切换的成本):当线程加锁失败时,内核将线程的状态从【运行】切换到睡眠状态,然后把CPU切换给其他线程运行;当锁被释放时,之前睡眠状态的线程会变成就绪状态,然后内核就会在合适的时间把CPU切换给该线程运行。

分别适用情况:上下切换的耗时大概在几十纳秒到几微秒之间,如果锁住的代码执行时间比较短,可能上下文切换的时间比锁住的代码执行时间还要长。若是能确定被锁住的代码执行时间很短,就不应该使用互斥锁,而应该选择自旋锁。如果是单核CPU且不可抢占式内核,不适用自旋锁,可能会造成空转(毫无意义,因为根本获取不到这把锁)。

std::async和std::future

std::async 是一个简单易用的异步调用函数,它可以创建一个异步的任务(在里面开辟线程或者不开辟),返回一个 std::future 对象。std::future 返回了异步执行任务的结果,可以通过get()函数拿到。

std::async 可以将一个函数作为参数进行异步调用。

std::async 默认使用异步执行策略 std::launch::async,这意味着它会创建一个新线程来执行异步任务,也可以使用 std::launch::deferred 策略,这种策略将延迟执行异步任务,直到 std::future 对象的 get() 方法被调用。所以get相当于功能强大的join()。

std::future对象的一些方法get(),wait(),wait_for(),wait_until()

std::mutex

调用 std::mutex 的 lock() 函数时,会检测 mutex 是否已经上锁,已经上锁则陷入等待,否则进行上锁。std::mutex 的 unlock() 函数,对互斥量进行解锁。无阻塞的 try_lock(),他在上锁失败时不会陷入等待,而是直接返回 false。

std::timed_mutex

如果需要等待,但仅限一段时间,可以用 std::timed_mutex 的 try_lock_for() 函数,参数是等待时长。try_lock_until()时间点前成功上锁返回 true,到了时间点还没有成功上锁返回 false。 

递归锁 

问题背景:除了两个线程会产生死锁之外,单个线程lock之后,再次lock会产生死锁。

解决办法: 使用std:recursive_mutex,他会自动判断是不是同一个线程lock了多次同一个锁,如果是则让计数器加1,之后unlock会让计数器减1,减到0时才真正解锁。但是相比普通的 std::mutex 有一定性能损失。

读写锁

问题背景:除了避免多个线程对一个对象做修改的问题,还有一个问题就是多线程同时读取一个对象。多个线程不允许同时修改一个对象,但多个线程可以同时读取一个对象。其实,保证读取时不会修改数据,修改时不会有人读取即可。

std::shared_mutex mutex;

通过mutex.lock()/unlock()来保证写独占

通过mutex.lock_shared()/unlock_shared()来实现读共享

std::unique_lock

在构建对象的时候可以对mutex上锁,析构对象的时候对mutex解锁(RAII机制),也可以调用lock()和unlock()函数选择手动选择上锁和解锁时机,但是选择手动上锁时候,对构建对象需要指定参数 std::defer_lock。

RAII:资源获取即初始化 , 开发者可以更方便地进行资源管理,避免显式使用new 和delete,从而降低内存泄漏的风险。

std::lock_guard 

std:lock_guard: 工具类,他的构造函数里会调用mtx.lock,超出作用域会调用mtx.unlock()。

condition_variable

std::condition_variable 必须和 std::unique_lock<std::mutex> 一起用

wait(lck)将会让当前线程陷入等待并释放施加在互斥量上的锁,直到其他线程调用。

notify_one()唤醒当前等待线程。wait() 还可以额外指定一个参数wait(lck, expr)其中 expr 是个 lambda 表达式,只有其返回值为 true 时才会真正唤醒。notify_all() 会唤醒阻塞在条件变量上的全部线程。

理解:线程要想访问共享资源必须过两关。第一关对访问共享数据的互斥量进行加锁,第二关等待条件变量满足条件并且之后被其他线程唤醒。唤醒的时候发现失去互斥量所有权,就会使用wait(lck)对互斥量阻塞加锁。这就是所谓的wait()前对mutex互斥量释放锁,wait()后对互斥量加锁。

线程池

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

class ThreadPool {
public:
    ThreadPool(size_t threads = std::thread::hardware_concurrency())
        : stop(false)
    {
        for (size_t i = 0; i < threads; ++i)
            workers.emplace_back([this] {
                for (;;) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lock(queue_mutex);
                        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(queue_mutex);
            stop = true;
        }
        condition.notify_all();
        for (std::thread& worker : workers)
            worker.join();
    }

    template<class F, class... Args>
    void addTask(F&& f, Args&&... args)
    {
        auto task = std::make_shared<std::function<void()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));

        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            tasks.emplace([task]() { (*task)(); });
        }

        condition.notify_one();
    }

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;

    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};

void test_function()
{
    std::cout << "Hello, World!" << std::endl;
}

int main()
{
    ThreadPool pool(4);

    for (int i = 0; i < 8; ++i) {
        pool.addTask(test_function);
    }

    pool.wait();

    std::cout << "All tasks finished" << std::endl;

    return 0;
}

在这个示例中,创建了一个线程池,包含4个工作线程。然后添加了8个测试任务,每个任务都是打印一条消息。最后等待所有任务完成并输出一条完成消息。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值