在 std::thread 和 std::ref 中使用地址清理调用 std::invoke(std::forward(...)) 时的奇怪行为
Strange behavior when calling std::invoke(std::forward(...)) with address-sanitization in a std::thread with a std::ref
问题
我正在尝试将 lambda 闭包传递给 std::thread
,它使用任意封闭参数调用任意封闭函数。
template< class Function, class... Args >
std::thread timed_thread(Function&& f, Args&&... args) {
// Regarding capturing perfectly-forwarded variables in lambda, see [1]
auto thread_thunk = ([&] {
std::cout << "Start thread timer" << std::endl;
// Regarding std::invoke(_decay_copy(...), ...), see (3) of [2].
// Assume no exception can be thrown from copying.
std::invoke(_decay_copy(std::forward<Function>(f)),
_decay_copy(std::forward<Args>(args)...));
}
}
int main() {
int i = 3;
std::thread t = timed_thread(&print_int_ref, std::ref(i));
t.join()
return 0;
}
/*
[1]:
[2]: https://en.cppreference.com/w/cpp/thread/thread/thread
*/
- 我使用
std::forward
以便右值引用和左值引用得到 转发(正确发送)。
- 因为
std::invoke
和 lambda 创建临时数据结构,调用者必须将引用包装在 std::ref
.
该代码似乎有效,但会导致 stack-use-after-scope
地址清理。这是我的主要困惑。
嫌疑人
我认为这可能与 有关,但我没有看到这种关系,因为我没有返回参考;对 i
的引用应该在 main
的堆栈帧的持续时间内有效,这应该比线程更持久,因为 main
加入了它。引用通过副本 (std::reference_wrapper
) 传递到 thread_thunk
.
我怀疑args...
无法通过引用捕获,那么应该如何捕获呢?
第二个困惑:将 {std::thread t = timed_thread(blah); t.join();}
(强制析构函数的大括号)更改为 timed_thread(blah).join();
不会引起这样的问题,即使对我来说它们看起来是等价的。
最小示例
#include <functional>
#include <iostream>
#include <thread>
template <class T>
std::decay_t<T> _decay_copy(T&& v) { return std::forward<T>(v); }
template< class Function, class... Args >
std::thread timed_thread(Function&& f, Args&&... args) {
// Regarding capturing perfectly-forwarded variables in lambda, see [1]
auto thread_thunk = ([&] {
std::cout << "Start thread timer" << std::endl;
// Regarding std::invoke(_decay_copy(...), ...), see (3) of [2].
// Assume no exception can be thrown from copying.
std::invoke(_decay_copy(std::forward<Function>(f)),
_decay_copy(std::forward<Args>(args)...));
std::cout << "End thread timer" << std::endl;
});
/* The single-threaded version code works perfectly */
// thread_thunk();
// return std::thread{[]{}};
/* multithreaded version appears to work
but triggers "stack-use-after-scope" with ASAN */
return std::thread{thread_thunk};
}
void print_int_ref(int& i) { std::cout << i << std::endl; }
int main() {
int i = 3;
/* This code appears to work
but triggers "stack-use-after-scope" with ASAN */
// {
// std::thread t = timed_thread(&print_int_ref, std::ref(i));
// t.join();
// }
/* This code works perfectly */
timed_thread(&print_int_ref, std::ref(i)).join();
return 0;
}
编译器命令:clang++ -pthread -std=c++17 -Wall -Wextra -fsanitize=address test.cpp && ./a.out
。 Remvoe address
看看它是否有效。
这两个版本似乎都是未定义的行为。未定义的行为是否会被消毒剂捕获是家常便饭。如果程序重新运行足够多的次数,即使是所谓的工作版本也很可能也会触发消毒剂。错误在这里:
std::thread timed_thread(Function&& f, Args&&... args) {
// Regarding capturing perfectly-forwarded variables in lambda, see [1]
auto thread_thunk = ([&] {
闭包使用捕获的args
by reference.
如您所知,timed_thread
的参数超出范围并在 timed_thread
returns 时被销毁。那是他们的范围。这就是 C++ 的工作原理。
但是你不能保证,无论如何,这个闭包会被新的执行线程执行并引用捕获的,参考,所有的args...
,在他们烟消云散之前:
return std::thread{thread_thunk};
除非这个新线程设法执行 thread_hunk
中引用捕获的代码,by reference args...
,它将在此函数 returns 之后结束访问,这会导致未定义的行为。
生命周期结束后使用的对象是 std::ref(i)。按照参考。该函数通过引用获取 std::ref,lambda 通过引用捕获,lambda 被复制到新创建的线程中,该线程将引用复制到 std::ref(i).
工作版本正在工作,因为 std::ref(i) 的生命周期在分号处结束,线程在此之前加入。
问题
我正在尝试将 lambda 闭包传递给 std::thread
,它使用任意封闭参数调用任意封闭函数。
template< class Function, class... Args >
std::thread timed_thread(Function&& f, Args&&... args) {
// Regarding capturing perfectly-forwarded variables in lambda, see [1]
auto thread_thunk = ([&] {
std::cout << "Start thread timer" << std::endl;
// Regarding std::invoke(_decay_copy(...), ...), see (3) of [2].
// Assume no exception can be thrown from copying.
std::invoke(_decay_copy(std::forward<Function>(f)),
_decay_copy(std::forward<Args>(args)...));
}
}
int main() {
int i = 3;
std::thread t = timed_thread(&print_int_ref, std::ref(i));
t.join()
return 0;
}
/*
[1]:
[2]: https://en.cppreference.com/w/cpp/thread/thread/thread
*/
- 我使用
std::forward
以便右值引用和左值引用得到 转发(正确发送)。 - 因为
std::invoke
和 lambda 创建临时数据结构,调用者必须将引用包装在std::ref
.
该代码似乎有效,但会导致 stack-use-after-scope
地址清理。这是我的主要困惑。
嫌疑人
我认为这可能与 i
的引用应该在 main
的堆栈帧的持续时间内有效,这应该比线程更持久,因为 main
加入了它。引用通过副本 (std::reference_wrapper
) 传递到 thread_thunk
.
我怀疑args...
无法通过引用捕获,那么应该如何捕获呢?
第二个困惑:将 {std::thread t = timed_thread(blah); t.join();}
(强制析构函数的大括号)更改为 timed_thread(blah).join();
不会引起这样的问题,即使对我来说它们看起来是等价的。
最小示例
#include <functional>
#include <iostream>
#include <thread>
template <class T>
std::decay_t<T> _decay_copy(T&& v) { return std::forward<T>(v); }
template< class Function, class... Args >
std::thread timed_thread(Function&& f, Args&&... args) {
// Regarding capturing perfectly-forwarded variables in lambda, see [1]
auto thread_thunk = ([&] {
std::cout << "Start thread timer" << std::endl;
// Regarding std::invoke(_decay_copy(...), ...), see (3) of [2].
// Assume no exception can be thrown from copying.
std::invoke(_decay_copy(std::forward<Function>(f)),
_decay_copy(std::forward<Args>(args)...));
std::cout << "End thread timer" << std::endl;
});
/* The single-threaded version code works perfectly */
// thread_thunk();
// return std::thread{[]{}};
/* multithreaded version appears to work
but triggers "stack-use-after-scope" with ASAN */
return std::thread{thread_thunk};
}
void print_int_ref(int& i) { std::cout << i << std::endl; }
int main() {
int i = 3;
/* This code appears to work
but triggers "stack-use-after-scope" with ASAN */
// {
// std::thread t = timed_thread(&print_int_ref, std::ref(i));
// t.join();
// }
/* This code works perfectly */
timed_thread(&print_int_ref, std::ref(i)).join();
return 0;
}
编译器命令:clang++ -pthread -std=c++17 -Wall -Wextra -fsanitize=address test.cpp && ./a.out
。 Remvoe address
看看它是否有效。
这两个版本似乎都是未定义的行为。未定义的行为是否会被消毒剂捕获是家常便饭。如果程序重新运行足够多的次数,即使是所谓的工作版本也很可能也会触发消毒剂。错误在这里:
std::thread timed_thread(Function&& f, Args&&... args) {
// Regarding capturing perfectly-forwarded variables in lambda, see [1]
auto thread_thunk = ([&] {
闭包使用捕获的args
by reference.
如您所知,timed_thread
的参数超出范围并在 timed_thread
returns 时被销毁。那是他们的范围。这就是 C++ 的工作原理。
但是你不能保证,无论如何,这个闭包会被新的执行线程执行并引用捕获的,参考,所有的args...
,在他们烟消云散之前:
return std::thread{thread_thunk};
除非这个新线程设法执行 thread_hunk
中引用捕获的代码,by reference args...
,它将在此函数 returns 之后结束访问,这会导致未定义的行为。
生命周期结束后使用的对象是 std::ref(i)。按照参考。该函数通过引用获取 std::ref,lambda 通过引用捕获,lambda 被复制到新创建的线程中,该线程将引用复制到 std::ref(i).
工作版本正在工作,因为 std::ref(i) 的生命周期在分号处结束,线程在此之前加入。