【问题标题】:Passing rvalues through std::thread通过 std::thread 传递右值
【发布时间】:2018-08-29 11:25:43
【问题描述】:

这是一个为什么这个代码不能工作的问题。我想知道如何修复命名空间 dj 中的代码,而不是演示程序中的代码。在进一步阅读之前,您可能需要运行该程序。

当我通过std::thread 传递rvalue std::string 时,字符串到达​​并行函数时为空。也许原始值已被 std::moved,但最终出现在错误的位置。但我认为问题可能出在函数timer 上。那里我认为字符串是通过引用捕获的,当右值消失时,引用无效。

// In the demo program, the string is the one element of `...args`.
template<typename F, typename... Args>
void timer(double seconds, F& f, Args&&... args) {
    auto th = std::thread(  
        [&, seconds] { delayed(seconds, f, std::forward<Args>(args)...); });
    th.detach();
}

我不知道如何为 lambda 捕获内容。 & 号让我害怕,但我无法解决它。我在使用 bindfunction 代替 lambda 方面进行了各种尝试。没有喜悦。

...

演示程序... 主线程启动一个暂停给定秒数的线程。然后新线程打印一个字符串并发出哔哔声,直到主线程将原子布尔值设置为 true。要显示它不起作用,请将全局布尔值 demo_the_problem 设置为 true。字符串到达​​报警函数时为空。

#include <thread>
#include <chrono>
#include <iostream>

static bool demo_the_problem = false;

namespace dj {

    inline void std_sleep(long double seconds) noexcept
    {    
        using duration_t = std::chrono::duration<long long, std::nano>;
        const auto duration =  duration_t(static_cast<long long> (seconds * 1e9));
        std::this_thread::sleep_for(duration);
    }

    // Runs a command f after delaying an amount of time
    template<typename F, typename... Args>
    auto delayed(double seconds, F& f, Args&&... args) {
        std_sleep(seconds);
        return f(std::forward<Args>(args)...);
    }

    // Runs a function after a given delay. Returns nothing.
    template<typename F, typename... Args>
    void timer(double seconds, F& f, Args&&... args) {
        auto th = std::thread( // XXX This appears to be where I lose ring_tone. XXX
            [&, seconds] { delayed(seconds, f, std::forward<Args>(args)...); });
        th.detach();
    }

}

using namespace dj;

int main() {

    std::atomic<bool> off_button(false);

    // Test dj::timer, which invokes a void function after a given
    // period of time. In this case, the function is "alarm_clock".
    auto alarm_clock = [&off_button](const std::string ring_tone) {
        char bel = 7;
        std::cout << ring_tone << '\n';
        while (!off_button) {
            std_sleep(0.5);
            std::cout << bel;
        }
        off_button = false;
    };

    auto ring = std::string("BRINNNNGGG!");
    if (demo_the_problem)
        timer(4.0, alarm_clock, std::string("BRINNNNGGG!")); // Not OK - ring arrives as empty string
    else {
        timer(4.0, alarm_clock, ring);  // Ring tone arrives intact.
    }


    // Mess around for a while
    for (int i = 0; i < 12; ++i) {
        if (i == 7) {
            off_button = true; // Hit the button to turn off the alarm
        }
        std::cout << "TICK ";
        std_sleep(0.5);
        std::cout << "tock ";
        std_sleep(0.5);
    }

    // and wait for the button to pop back up.
    while(off_button) std::this_thread::yield();
    std::cout << "Yawn." << std::endl;
    return 0;
}

【问题讨论】:

  • 你确定 temp std::string 作为右值传递吗?将 temp 提升为 lvalue 的讨厌的 MS“功能”可能会有所帮助。
  • @Slava - 我希望不要有任何依赖于 MS“功能”的东西,我尽可能关闭。

标签: c++ multithreading visual-c++ stl c++17


【解决方案1】:
template<typename F, typename... Args>
void timer(double seconds, F& f, Args&&... args) {
  auto th = std::thread(  
    [&, seconds] { delayed(seconds, f, std::forward<Args>(args)...); });
  th.detach();
}

这会导致未定义的行为,因为引用捕获的数据销毁相对于线程中的代码没有排序。

永远不要在生命周期(或它的副本)超过当前范围的 lambda 上使用 &amp;。期间。

您的detach 也是代码气味;没有实用的方法来确定线程是否在main 结束之前完成,并且超过 main 的线程具有未指定的行为。这是 C++,你负责清理你的资源使用。找到解决方案。我暂时忽略它。

template<typename F, typename... Args>
void timer(double seconds, F&& f, Args&&... args) {
  auto th = std::thread(  
    [seconds,
     f=std::forward<F>(f),
     tup=std::make_tuple(std::forward<Args>(args)...)
    ]
    {
// TODO:      delayed(seconds, f, std::forward<Args>(args)...);
    }
  );
  th.detach();
}

现在我们只需要编写// TODO 行。

`中这很容易。

template<typename F, typename... Args>
void timer(double seconds, F&& f, Args&&... args) {
  auto th = std::thread(  
    [seconds,
     f=std::forward<F>(f),
     tup=std::make_tuple(std::forward<Args>(args)...)
    ]() mutable
    {
      std::apply(
        [&](auto&&...args){
          delayed(seconds, f, decltype(args)(args)...);
        },
        std::move(tup)
      );
    }
  );
  th.detach();
}

请注意,此结果复制所有内容到线程中。如果您真的非常想传入一个左值引用,请使用 std::ref,这主要是为了让它对您有用。

中,您最好的解决方案是编写自己的notstd::apply。有很多人写过这个,包括myself here

请注意,我在 lambda 中使用了 [&amp;];该 lambda 不会超过当前范围(实际上它不会超过当前行)。这是你应该使用[&amp;]的唯一情况。

【讨论】:

  • 谢谢。这需要考虑很多。关于气味。是香水。该线程通过原子布尔值显示何时完成。查看演示程序的底部。
  • "请注意,此结果会将所有内容复制到线程中。如果您真的非常想传入左值引用,请使用 std::ref ,这主要是为您工作。"在实现被调用的函数时,用户通过引用捕获原子变量是合理的。您是否暗示用户必须了解要求以及如何使用 std::ref?
  • @jive 在设置该布尔值和线程实际完成之间的线程中可以花费无限的时间。理论上(无 sewuencing)和实践(在设置 bool 后运行破坏捕获数据的代码)。不是香水,是肥料。学会不使用分离。
  • 好的。给我看老师。我想知道如何正确地做。我已经知道怎么做错了。
  • @jive 这是 C++,生命周期和所有权是每个人的责任。隐藏生命周期的依赖关系会让你的程序崩溃,或者它的工作是偶然的。您不会仅仅因为有人传递左值就添加跨线程生命周期依赖项。默认情况下,在线程之间复制东西(或完全传递所有权),尽可能限制共享。沿受控通道(如线程安全队列或期货或事件泵)返回数据。线程是hard。您的方法将导致程序充其量只是偶然运行。
猜你喜欢
  • 2011-06-19
  • 1970-01-01
  • 1970-01-01
  • 2020-12-15
  • 2021-11-27
相关资源
最近更新 更多