多线程补充

1.线程创建

#include <iostream>
#include <memory>
#include <thread>

int main() {
    // 演示使用 std::make_unique 动态分配线程对象
    {
        // 创建一个线程对象,使用 std::make_unique 动态分配
        auto thread1 = std::make_unique<std::thread>([]() {
            std::cout << "Hello from thread 1!" << std::endl;
        });

        // 确保线程完成执行
        if (thread1->joinable()) {
            thread1->join();
        }
    } // thread1 的生命周期结束,std::unique_ptr 自动释放线程对象

    // 演示手动使用 new 动态分配线程对象
    {
        // 创建一个线程对象,手动使用 new 动态分配
        std::thread* thread2 = new std::thread([]() {
            std::cout << "Hello from thread 2!" << std::endl;
        });

        // 确保线程完成执行
        if (thread2->joinable()) {
            thread2->join();
        }

        // 手动释放动态分配的内存
        delete thread2;
    } // thread2 的生命周期结束,手动释放线程对象

    return 0;
}
  1. 使用 std::make_unique

    • 创建一个 std::unique_ptr<std::thread> 对象 thread1

    • 使用 std::make_unique 动态分配 std::thread 对象,并初始化线程。

    • 调用 join() 确保线程完成执行。

    • thread1 的生命周期结束时(即离开作用域),std::unique_ptr 会自动释放它所管理的 std::thread 对象。

  2. 手动使用 new

    • 创建一个 std::thread* 指针 thread2,并使用 new 动态分配 std::thread 对象。

    • 调用 join() 确保线程完成执行。

    • 使用 delete 手动释放动态分配的内存。

    • thread2 的生命周期结束时(即离开作用域),需要确保手动释放内存,否则会导致内存泄漏。

  3. 代码块的作用域

    • 使用 {} 将两种情况分别放在不同的代码块中,确保变量名不冲突。

    • 每个代码块的作用域结束后,相关资源会被自动释放(对于 std::unique_ptr)或手动释放(对于 new 分配的内存)。

1.1 std::make_unique 内容补充

是的,std::make_unique 的括号中的内容相当于构造函数的参数。std::make_unique 是一个模板函数,它会根据提供的参数构造一个对象,并将其所有权交给一个 std::unique_ptr。这些参数会直接传递给目标对象的构造函数。

语法和原理

std::make_unique 的语法如下:

cpp复制

template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args);
  • T 是目标对象的类型。

  • Args&&... args 是可变参数模板,表示传递给构造函数的参数列表。

  • std::make_unique 会将这些参数转发到 T 的构造函数中,构造一个 T 类型的对象,并将其所有权交给一个 std::unique_ptr<T>

示例

假设有一个类 MyClass,它有一个构造函数:

cpp复制

class MyClass {
public:
    MyClass(int x, double y) {
        std::cout << "Constructor called with x = " << x << " and y = " << y << std::endl;
    }
};

使用 std::make_unique 来创建一个 MyClass 对象:

cpp复制

auto ptr = std::make_unique<MyClass>(42, 3.14);

在这个例子中:

  • std::make_unique<MyClass>(42, 3.14) 会调用 MyClass 的构造函数 MyClass(int x, double y),并将参数 423.14 传递给它。

  • 构造函数被调用后,std::make_unique 会将新创建的 MyClass 对象的地址传递给一个 std::unique_ptr<MyClass>,从而管理该对象的生命周期。

输出

运行上述代码时,输出将是:

Constructor called with x = 42 and y = 3.14

为什么使用 std::make_unique

  • 安全性std::make_unique 确保对象的构造和智能指针的创建是原子操作。如果构造函数抛出异常,std::make_unique 会自动处理异常,避免资源泄漏。

  • 简洁性std::make_unique 的语法比手动使用 newstd::unique_ptr 更简洁。

  • 避免悬挂指针:如果构造函数失败,std::make_unique 会确保智能指针不会指向一个未成功构造的对象。

对比手动使用 new

手动使用 newstd::unique_ptr 的方式如下:

cpp复制

auto ptr = std::unique_ptr<MyClass>(new MyClass(42, 3.14));

这种方式虽然也能达到类似的效果,但存在以下问题:

  • 如果 new MyClass(42, 3.14) 抛出异常,std::unique_ptr 的构造函数将不会被调用,可能导致资源泄漏。

  • 代码更冗长,不够简洁。

总结

std::make_unique 的括号中的内容确实相当于构造函数的参数。它会将这些参数转发给目标对象的构造函数,并将新创建的对象的地址传递给一个 std::unique_ptr,从而安全地管理对象的生命周期。推荐在需要动态分配对象时使用 std::make_unique,以提高代码的安全性和可读性。

2.延迟线程启动任务

在C++中,std::make_unique 是一个模板函数,用于创建一个 std::unique_ptr 并初始化它所管理的对象。当你使用 std::make_unique<std::thread> 时,它会立即构造一个 std::thread 对象,并且线程会立即开始执行。

如果你希望延迟线程的启动,而不是在创建时立即传入线程函数,你可以使用 std::unique_ptr 来管理一个尚未启动的 std::thread 对象,然后在稍后的时间点再启动线程。这可以通过将线程函数存储为一个可调用对象(如 std::function 或 lambda 表达式)来实现。

cpp复制

#include <iostream>
#include <memory>
#include <thread>
#include <chrono>

int main() {
    // 创建一个 std::unique_ptr<std::thread>,初始值为 nullptr
    auto thread1 = std::make_unique<std::thread>();

    // 定义线程函数
    auto threadFunc = []() {
        std::cout << "Hello from thread 1!" << std::endl;
    };

    std::cout << "Thread will sleep for 3 seconds..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(3)); // 当前线程暂停3秒
    std::cout << "Thread woke up!" << std::endl;

    // 在稍后的时间点启动线程
    if (!thread1->joinable()) {
        *thread1 = std::thread(threadFunc);
    }

    // 确保线程完成执行
    if (thread1->joinable()) {
        thread1->join();
    }

    return 0;
}

输出

复制

Thread will sleep for 3 seconds...
// 3秒后
Thread woke up!
Hello from thread 1!

代码解释

  1. 初始化 std::unique_ptr<std::thread>nullptr

    cpp复制

    auto thread1 = std::make_unique<std::thread>();
    

    这里创建了一个 std::unique_ptr<std::thread>,初始值是一个默认构造的 std::thread 对象。由于默认构造的 std::thread 对象是空的,不会占用任何线程资源。

  2. 延迟启动线程

    cpp复制

    if (!thread1->joinable()) {
        *thread1 = std::thread(threadFunc);
    }
    

    在这里,你检查线程是否已经启动。如果没有启动,你通过 *thread1 = std::thread(threadFunc); 创建一个新的线程对象,并将其赋值给 std::unique_ptr 所管理的对象。这会启动线程。

  3. 确保线程完成执行

    cpp复制

    if (thread1->joinable()) {
        thread1->join();
    }
    

    在程序结束前,确保线程完成执行。

3. 利用了RAII(Resource Acquisition Is Initialization)原则来管理线程的生命周期

#include <thread>

#include <utility>

  

class thread_guard {

    std::thread& t;

public:

    explicit thread_guard(std::thread& t_) : t(t_) {}

    ~thread_guard() {

        if (t.joinable()) {

            t.join();

        }

    }

    thread_guard(thread_guard const&) = delete;

    thread_guard& operator=(thread_guard const&) = delete;

};

  

void do_something_in_current_thread() {

    // 模拟一些操作

}

  

int main() {

    int some_local_state = 0;

    auto my_func = [&some_local_state]() {

        // 使用some_local_state进行操作

        };

  

    std::thread t(my_func);

    thread_guard g(t);

    do_something_in_current_thread();

}  // 自动调用thread_guard的析构函数,确保线程正确结束

4.async 自动管理生命周期

理解std::async和如何使用它来简化线程管理是非常有用的。下面我将详细解释std::async,并展示如何用它来替换你提供的代码中的手动线程创建。

什么是 std::async

std::async 是 C++11 引入的一个函数模板,它可以用来启动一个异步任务,并返回一个 std::future 对象。这个对象可以用来获取异步任务的结果或等待任务完成。std::async 自动管理底层线程的生命周期,包括在适当的时候自动 join 线程,因此不需要像手动管理线程那样担心线程资源的释放问题。

使用 std::async 替换你的代码

下面是使用 std::async 来简化你的代码的例子:

#include <future>
#include <iostream>

// 假设这是你要在线程中执行的任务
void my_func(int& some_local_state) {
    // 修改some_local_state 或者做一些其他操作
    std::cout << "Thread is running with state: " << some_local_state << std::endl;
}

void do_something_in_current_thread() {
    // 模拟一些操作
    std::cout << "Doing something in current thread." << std::endl;
}

int main() {
    int some_local_state = 0;

    // 使用 std::async 启动异步任务
    auto future = std::async(std::launch::async, [](int& state) {
        my_func(state);
    }, std::ref(some_local_state));

    // 在主线程中做其他事情
    do_something_in_current_thread();

    // 如果需要等待异步任务完成并获取结果(如果有),可以调用 get 方法
    // 注意:即使不显式调用 get,析构函数也会确保异步任务完成
    future.get(); // 可选,用于等待异步任务结束

    return 0;
}

解释

  1. std::async: 我们使用 std::async 来启动 my_func 函数作为异步任务。std::launch::async 策略保证了任务会在新线程上运行。

  2. std::ref: 因为 std::async 的参数是通过值传递的,如果你想让异步任务访问外部变量(如这里的 some_local_state),你需要使用 std::ref 包装这些变量,这样它们就可以通过引用被传递给异步任务。

  3. future.get(): 调用 get() 方法会阻塞当前线程直到异步任务完成。如果任务有返回值,get() 还会返回该值。在这个例子中,由于 my_func 没有返回值,get() 主要用于确保主线程等待异步任务完成。

为什么使用 std::async 更简单?

  • 自动管理std::async 自动处理线程的创建、销毁以及资源回收,减少了手动编写类似 thread_guard 类的需求。
  • 易于使用:通过 std::future,你可以更方便地与异步任务进行交互,比如等待任务完成或者获取任务结果。
  • 减少错误:由于减少了手动管理线程的机会,降低了因忘记 join 线程导致的资源泄露风险。

希望这能帮助你更好地理解 std::async 和其相对于手动线程管理的优势。如果你有任何进一步的问题,请随时提问!

5.避免多线程中的未定义行为:局部变量的安全处理

你提到的问题是多线程编程中常见的一个陷阱,特别是在使用局部变量时。当主线程结束并销毁其局部变量时,如果其他线程仍在访问这些局部变量的引用或指针,就会导致未定义行为。让我们详细解释这个问题,并提供一些解决方案。

问题描述

在你的代码示例中,oops 函数创建了一个 func 对象,并将其传递给一个新的线程。这个 func 对象持有一个对局部变量 some_local_state 的引用。由于 my_thread.detach() 被调用,主线程不会等待新线程完成,而是立即继续执行并最终退出函数,导致 some_local_state 被销毁。然而,新线程可能还在运行,并尝试访问已经被销毁的 some_local_state,从而引发未定义行为。

struct func {
    int& i;
    func(int& i_) : i(i_) {}
    void operator() () {
        for (unsigned j = 0; j < 1000000; ++j) {
            do_something(i); // 潜在访问隐患:空引用
        }
    }
};

void oops() {
    int some_local_state = 0;
    func my_func(some_local_state);
    std::thread my_thread(my_func);
    my_thread.detach(); // 不等待线程结束
} // 新线程可能还在运行

解决方案

为了避免这种未定义行为,有几种常见的方式来处理这种情况:

1. 使用智能指针(如 std::shared_ptr

将局部变量包装在一个智能指针中,并确保该智能指针的生命周期超过新线程的生命周期。

#include <iostream>
#include <thread>
#include <memory>

void do_something(int& i) {
    // 假设这里有一些操作
    std::cout << "Value: " << i << std::endl;
}

struct func {
    std::shared_ptr<int> i;
    func(std::shared_ptr<int> i_) : i(i_) {}
    void operator() () {
        for (unsigned j = 0; j < 1000000; ++j) {
            do_something(*i);
        }
    }
};

void safer() {
    auto some_local_state = std::make_shared<int>(0);
    func my_func(some_local_state);
    std::thread my_thread(my_func);
    my_thread.detach(); // 线程会持有 shared_ptr,确保数据存活
} // some_local_state 在这里被销毁,但线程仍然可以访问它

在这个例子中,std::shared_ptr<int> 确保了 some_local_state 的生命周期至少与新线程一样长。

在多线程编程中,确保共享数据的生命周期至少与使用这些数据的线程一样长是至关重要的。使用 std::shared_ptr 是一种有效的方法来管理这种生命周期,因为它通过引用计数机制自动管理对象的生命周期。

让我们详细解释为什么在这个例子中,std::shared_ptr<int> 能够确保 some_local_state 的生命周期至少与新线程一样长。

代码示例回顾

以下是使用 std::shared_ptr 的代码示例:

#include <iostream>
#include <thread>
#include <memory>

void do_something(int& i) {
    // 假设这里有一些操作
    std::cout << "Value: " << i << std::endl;
}

struct func {
    std::shared_ptr<int> i;
    func(std::shared_ptr<int> i_) : i(i_) {}
    void operator() () {
        for (unsigned j = 0; j < 1000000; ++j) {
            do_something(*i);
        }
    }
};

void safer() {
    auto some_local_state = std::make_shared<int>(0);  // 创建 shared_ptr
    func my_func(some_local_state);
    std::thread my_thread(my_func);
    my_thread.detach(); // 线程会持有 shared_ptr,确保数据存活
} // some_local_state 在这里被销毁,但线程仍然可以访问它
解释 std::shared_ptr 的工作原理
  1. 引用计数

    • std::shared_ptr 使用引用计数来跟踪有多少个 shared_ptr 实例指向同一个对象。每当一个新的 shared_ptr 指向该对象时,引用计数增加;每当一个 shared_ptr 被销毁或重置时,引用计数减少。
    • 当引用计数变为零时,表示没有更多的 shared_ptr 指向该对象,此时对象会被销毁。
  2. 生命周期管理

    • safer 函数中,我们使用 std::make_shared<int>(0) 创建了一个 std::shared_ptr<int> 对象,并将其绑定到局部变量 some_local_state
    • 这个 shared_ptr 被传递给 func 结构体,并存储在其成员变量 i 中。
    • 当创建新的线程并传入 my_func 时,线程内部的操作(即 func::operator())会继续持有这个 shared_ptr
  3. 线程持有 shared_ptr

    • 尽管 safer 函数结束时局部变量 some_local_state 超出作用域并被销毁,但由于 func 对象和线程内部的操作仍然持有对 shared_ptr 的引用,引用计数不会降为零。
    • 因此,shared_ptr 管理的对象(即 some_local_state 的值)不会被销毁,直到所有持有它的 shared_ptr 都被销毁或重置。
示例图解
safer函数开始 -> 创建 shared_ptr<int> some_local_state (引用计数=1)
                 |
                 v
创建 func my_func(some_local_state) (引用计数=2, 因为 my_func.i 也持有了 shared_ptr)
                 |
                 v
创建 std::thread my_thread(my_func) (引用计数=2, 线程内部也持有了 shared_ptr)
                 |
                 v
调用 my_thread.detach() (主线程结束, some_local_state 被销毁, 但引用计数仍为1)
                 |
                 v
线程继续运行, 直到引用计数变为0 (此时 shared_ptr 才会销毁托管对象)
关键点总结
  • 引用计数std::shared_ptr 通过引用计数机制确保只要还有一个 shared_ptr 实例存在,其管理的对象就不会被销毁。
  • 线程持有 shared_ptr:当我们将 shared_ptr 传递给新线程并在新线程中使用它时,新线程内部的操作也会持有对该对象的引用,从而延长了对象的生命周期。
  • 避免悬空引用:通过这种方式,我们可以确保即使在主线程结束之后,新线程仍然能够安全地访问共享数据,而不会导致悬空引用或未定义行为。

因此,使用 std::shared_ptr<int> 可以确保 some_local_state 的生命周期至少与新线程一样长,从而避免了由于局部变量被提前销毁而导致的潜在问题。

2. 使用值传递而非引用

如果你不需要修改原始数据,可以通过值传递来避免引用局部变量的问题。

#include <iostream>
#include <thread>

void do_something(int i) {
    // 假设这里有一些操作
    std::cout << "Value: " << i << std::endl;
}

struct func {
    int i;
    func(int i_) : i(i_) {}
    void operator() () {
        for (unsigned j = 0; j < 1000000; ++j) {
            do_something(i);
        }
    }
};

void safer_by_value() {
    int some_local_state = 0;
    func my_func(some_local_state);
    std::thread my_thread(my_func);
    my_thread.join(); // 或者 detach,取决于需求
}

在这种情况下,func 结构体直接存储了 some_local_state 的副本,因此即使 some_local_state 被销毁,新线程也不会受到影响。

3. 使用 std::async 并传递参数

你可以使用 std::async 来启动异步任务,并通过值传递参数。

#include <iostream>
#include <future>

void do_something(int i) {
    // 假设这里有一些操作
    std::cout << "Value: " << i << std::endl;
}

void async_example() {
    int some_local_state = 0;
    auto future = std::async(std::launch::async, [some_local_state]() {
        for (unsigned j = 0; j < 1000000; ++j) {
            do_something(some_local_state);
        }
    });

    // 主线程可以继续做其他事情
    // 如果需要等待异步任务完成,可以调用 future.get()
    future.get();
}

在这个例子中,捕获列表 [some_local_state] 是按值捕获的,因此 some_local_state 的副本会被传递给异步任务。

总结

  • 引用局部变量:当线程引用局部变量时,必须确保这些变量的生命周期足够长,以覆盖线程的整个生命周期。
  • 智能指针:使用 std::shared_ptr 可以延长对象的生命周期,确保在线程运行期间数据依然有效。
  • 值传递:通过值传递参数,可以避免引用局部变量带来的问题。
  • std::async:使用 std::async 和 lambda 表达式,可以通过值捕获参数,简化线程管理并确保数据安全。

选择合适的策略取决于具体的应用场景和需求。希望这些解释和示例能够帮助你更好地理解和解决这个问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

丁金金_chihiro_修行

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

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

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

打赏作者

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

抵扣说明:

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

余额充值