简体   繁体   English

在带有 std::ref 的 std::thread 中使用地址清理调用 std::invoke(std::forward(...)) 时的奇怪行为

[英]Strange behavior when calling std::invoke(std::forward(…)) with address-sanitization in a std::thread with a std::ref

Problem问题

I am trying to pass a lambda-closure to std::thread that calls arbitrary closed-over function with arbitrary closed-over arguments.我正在尝试将 lambda-closure 传递给std::thread ,该线程调用任意封闭 function 和任意封闭 arguments。

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
*/
  • I use std::forward so that r-value references and l-value references get forwarded (dispatched correctly).我使用std::forward以便转发右值引用和左值引用(正确调度)。
  • Because std::invoke and the lambda create temporary data-structures, the caller must wrap references in std::ref .因为std::invoke和 lambda 创建临时数据结构,调用者必须将引用包装在std::ref中。

The code appears to work, but causes stack-use-after-scope with address sanitization.该代码似乎可以工作,但会导致stack-use-after-scope并进行地址清理。 This is my primary confusion.这是我的主要困惑。

Suspects嫌疑人

I think this may be related to this error , but I do not see the relation since I am not returning a reference;我认为这可能与这个错误有关,但我没有看到这种关系,因为我没有返回参考; The reference to i should be valid for the duration of main 's stack-frame which should outlast the thread because main joins on it.i的引用应该在main的堆栈帧的持续时间内有效,该堆栈帧应该比线程长,因为main加入了它。 The reference is passed by copies ( std::reference_wrapper ) into the thread_thunk .引用通过副本( std::reference_wrapper )传递到thread_thunk

I suspect args... cannot be captured by reference, but then how should it be captured?我怀疑args...不能通过引用捕获,但是应该如何捕获呢?

A secondary confusion: changing {std::thread t = timed_thread(blah); t.join();}次要混淆:更改{std::thread t = timed_thread(blah); t.join();} {std::thread t = timed_thread(blah); t.join();} (braces to force destructor) to timed_thread(blah).join(); {std::thread t = timed_thread(blah); t.join();} (强制析构函数的大括号)到timed_thread(blah).join(); incurs no such problem, even though to me they appear equivalent.不会产生这样的问题,即使对我来说它们看起来是等价的。

Minimal example最小的例子

#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;
}

Compiler command: clang++ -pthread -std=c++17 -Wall -Wextra -fsanitize=address test.cpp &&./a.out .编译器命令: clang++ -pthread -std=c++17 -Wall -Wextra -fsanitize=address test.cpp &&./a.out Remvoe address to see it work.删除address以查看它的工作。

ASAN backtrace ASAN 回溯

Both versions appear to be undefined behavior.两个版本似乎都是未定义的行为。 It is potluck whether the undefined behavior will be caught by the sanitizer.未定义的行为是否会被消毒剂捕获是便饭。 It is fairly likely that even the allegedly working version will also trip the sanitizer, if the program is rerun sufficient amount of times.如果程序重新运行足够多的次数,即使是所谓的工作版本也很可能会触发消毒剂。 The bug is here:错误在这里:

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

The closure uses the captured args by reference .闭包通过引用使用捕获的args

As you know, the parameters to timed_thread go out of scope and get destroyed when timed_thread returns.如您所知, timed_thread go 的参数来自 scope 并在timed_thread返回时被销毁。 That's their scope.那是他们的 scope。 That's how C++ works.这就是 C++ 的工作原理。

But you have no guarantees, whatsoever , that this closure gets executed by the new execution thread and references the captured, by reference , all the args... , before they vanish in a puff of smoke here:但是你不能保证,这个包会被新的执行线程执行并引用捕获的,通过引用,所有的args... ,然后它们在这里消失得无影无踪:

return std::thread{thread_thunk};

Unless this new thread manages to execute the code inside thread_hunk , that references the captured, by reference args... , it will end up accessing after this function returns, and this results in undefined behavior.除非这个新线程设法执行thread_hunk内的代码,该代码引用捕获的,通过引用args... ,否则它将在此 function 返回后结束访问,这将导致未定义的行为。

The object being used after its lifetime is the std::ref(i).在其生命周期之后使用的 object 是 std::ref(i)。 Follow the references.按照参考。 The function takes the std::ref by reference, the lambda captures by reference, the lambda is copied into the newly created thread which copies the reference to the std::ref(i). function 通过引用获取 std::ref,lambda 通过引用捕获,lambda 被复制到新创建的线程中。复制引用到 std::refi)

The working version is working because the lifetime of std::ref(i) ends at the semicolon, and the thread is joined before then.工作版本正在工作,因为 std::ref(i) 的生命周期以分号结束,并且线程在此之前加入。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM