在 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
*/

该代码似乎有效,但会导致 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 看看它是否有效。

ASAN backtrace

这两个版本似乎都是未定义的行为。未定义的行为是否会被消毒剂捕获是家常便饭。如果程序重新运行足够多的次数,即使是所谓的工作版本也很可能也会触发消毒剂。错误在这里:

std::thread timed_thread(Function&& f, Args&&... args) {
  // Regarding capturing perfectly-forwarded variables in lambda, see [1]
   auto thread_thunk = ([&] {

闭包使用捕获的argsby 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) 的生命周期在分号处结束,线程在此之前加入。