当在一个带有std::ref的std::线程中调用std::invoke(std::forward(...))并进行地址消毒时,出现奇怪的行为。

问题描述 投票:2回答:1

问题

我正试图将lambda-closure传递到 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]: https://stackoverflow.com/questions/26831382/capturing-perfectly-forwarded-variable-in-lambda
[2]: https://en.cppreference.com/w/cpp/thread/thread/thread
*/
  • 我使用 std::forward 这样,r值引用和l值引用就会得到 转发 (正确派遣)。
  • 因为 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 来看看它的工作。

ASAN回溯

c++ multithreading c++17 move-semantics
1个回答
1
投票

这两个版本似乎都是未定义的行为。未定义的行为是否会被sanitizer捕捉到是大锅饭。如果程序被重新运行的次数足够多的话,即使是所谓的工作版本也很有可能会触发 sanitizer。这个bug就在这里。

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

封闭使用了捕获到的... args 参照.

如你所知,参数为 timed_thread 亡羊补牢 timed_thread 返回。这就是他们的范围。这就是C++的工作原理。

但是你没有保证。任何该闭包被新的执行线程执行并引用捕获的内容。参照,所有 args...趁他们还没在这里消失之前。

return std::thread{thread_thunk};

除非这个新的线程能执行里面的代码... ... thread_hunk的,引用捕获的。参照 args...,它将在这个函数返回后结束访问,这将导致未定义的行为。


0
投票

在其生命期结束后,被使用的对象是std::ref(i)。 按照引用的方式。 函数通过引用获取std::ref,lambda通过引用捕获,lambda被复制到新创建的线程中,该线程将引用复制到std::ref(i)。

工作版本之所以有效,是因为std::ref(i)的寿命在分号处结束,而线程在这之前就已经加入了。

© www.soinside.com 2019 - 2024. All rights reserved.