文章目录
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;
}
-
使用
std::make_unique
:-
创建一个
std::unique_ptr<std::thread>
对象thread1
。 -
使用
std::make_unique
动态分配std::thread
对象,并初始化线程。 -
调用
join()
确保线程完成执行。 -
当
thread1
的生命周期结束时(即离开作用域),std::unique_ptr
会自动释放它所管理的std::thread
对象。
-
-
手动使用
new
:-
创建一个
std::thread*
指针thread2
,并使用new
动态分配std::thread
对象。 -
调用
join()
确保线程完成执行。 -
使用
delete
手动释放动态分配的内存。 -
当
thread2
的生命周期结束时(即离开作用域),需要确保手动释放内存,否则会导致内存泄漏。
-
-
代码块的作用域:
-
使用
{}
将两种情况分别放在不同的代码块中,确保变量名不冲突。 -
每个代码块的作用域结束后,相关资源会被自动释放(对于
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)
,并将参数42
和3.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
的语法比手动使用new
和std::unique_ptr
更简洁。 -
避免悬挂指针:如果构造函数失败,
std::make_unique
会确保智能指针不会指向一个未成功构造的对象。
对比手动使用 new
手动使用 new
和 std::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!
代码解释
-
初始化
std::unique_ptr<std::thread>
为nullptr
:cpp复制
auto thread1 = std::make_unique<std::thread>();
这里创建了一个
std::unique_ptr<std::thread>
,初始值是一个默认构造的std::thread
对象。由于默认构造的std::thread
对象是空的,不会占用任何线程资源。 -
延迟启动线程:
cpp复制
if (!thread1->joinable()) { *thread1 = std::thread(threadFunc); }
在这里,你检查线程是否已经启动。如果没有启动,你通过
*thread1 = std::thread(threadFunc);
创建一个新的线程对象,并将其赋值给std::unique_ptr
所管理的对象。这会启动线程。 -
确保线程完成执行:
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;
}
解释
-
std::async
: 我们使用std::async
来启动my_func
函数作为异步任务。std::launch::async
策略保证了任务会在新线程上运行。 -
std::ref
: 因为std::async
的参数是通过值传递的,如果你想让异步任务访问外部变量(如这里的some_local_state
),你需要使用std::ref
包装这些变量,这样它们就可以通过引用被传递给异步任务。 -
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
的工作原理
-
引用计数:
std::shared_ptr
使用引用计数来跟踪有多少个shared_ptr
实例指向同一个对象。每当一个新的shared_ptr
指向该对象时,引用计数增加;每当一个shared_ptr
被销毁或重置时,引用计数减少。- 当引用计数变为零时,表示没有更多的
shared_ptr
指向该对象,此时对象会被销毁。
-
生命周期管理:
- 在
safer
函数中,我们使用std::make_shared<int>(0)
创建了一个std::shared_ptr<int>
对象,并将其绑定到局部变量some_local_state
。 - 这个
shared_ptr
被传递给func
结构体,并存储在其成员变量i
中。 - 当创建新的线程并传入
my_func
时,线程内部的操作(即func::operator()
)会继续持有这个shared_ptr
。
- 在
-
线程持有
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 表达式,可以通过值捕获参数,简化线程管理并确保数据安全。
选择合适的策略取决于具体的应用场景和需求。希望这些解释和示例能够帮助你更好地理解和解决这个问题。