2

This is a why-isn't-this-code-working question. I want to know how to fix the code in namespace dj, not in the demo program. You might want to run the program before reading further.

When I pass an rvalue std::string via std::thread, the string arrives in the parallel function empty. Perhaps the original value has been std::moved, but ended up in the wrong place. But I think the problem is probably in function timer. There I think the string is captured by reference and when the rvalue disappears, the reference is invalid.

// 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();
}

I can't figure out how to capture things for the lambda. The ampersand scares me, but I have not been able to work around it. I have made various attempts at using bind or function instead of a lambda. No joy.

...

Demo program... The main thread starts a tread that pauses for a given number of seconds. The new thread then prints a string and beeps until the main thread sets an atomic bool to true. To show it not working, set the global bool demo_the_problem to true. The string arrives in the alarm function empty.

#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;
}
Jive Dadson
  • 16,680
  • 9
  • 52
  • 65

1 Answers1

4
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();
}

this results in undefined behavior as the reference captured data destruction is not sequenced relative to the code in the thread.

Never, ever use & on a lambda whose lifetime (or copies of it) outlive the current scope. Period.

Your detach is also code smell; there is no practical way to determine if the thread finishes before the end of main, and threads that outlive main have unspecified behavior. This is C++, you are responsible for cleaning up your resource usage. Find a solution for that. I'll ignore it for now.

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();
}

Now we just need to write the // TODO line.

In ` this is easy.

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();
}

Note that this result copies everything into the thread. If you really, really want to pass an lvalue reference in, use std::ref which mostly makes it work for you.

In or your best solution is to write your own notstd::apply. There are many people who have written this, including myself here.

Notice I used [&] in a lambda; that lambda does not outlive the current scope (in fact it doesn't outlive the current line). That is the only situation you should ever use [&].

Jive Dadson
  • 16,680
  • 9
  • 52
  • 65
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Thanks. That's a lot to think about. Regarding the smell. It's perfume. The thread shows when it's finished via the atomic boolean. Look at the bottom of the demo program. – Jive Dadson Mar 20 '18 at 15:21
  • "Note that this result copies everything into the thread. If you really, really want to pass an lvalue reference in, use std::ref which mostly makes it work for you." It would be reasonable for a user to capture an atomic variable by reference when implementing the function that gets invoked. Are you implying that user would have to know about the requirement and how to use std::ref? – Jive Dadson Mar 20 '18 at 15:26
  • @jive an unbounded amount of time can be spent in the thread between setting that bool and the thread actually finishing. Both in theory (no sewuencing) and practice (code destroying captured data runs after the bool is set). It isn't perfume, it is manure. Learn to not use detach. – Yakk - Adam Nevraumont Mar 20 '18 at 15:26
  • Okay. Show me teacher. I want to know how to do it right. I already know how to do it wrong. – Jive Dadson Mar 20 '18 at 15:30
  • @jive This is C++, lifetime and ownership are everyone's responsibility. Hiding lifetime dependencies is going to make your program break, or if it works work by accident. You don't add cross-thread lifetime dependencies just because someone passes an lvalue. By default copy things between threads (or pass ownership totally), limit sharing as much as possible. Deliver data back along controlled channels, like thread safe queues or futures or event pumps. Threading is *hard*. Your approach will result in programs that work by accident at best. – Yakk - Adam Nevraumont Mar 20 '18 at 15:31
  • If I thought my approach was right, I would not have asked the question. – Jive Dadson Mar 20 '18 at 15:33
  • @JiveDadson There was a missing `;`? [live example](http://coliru.stacked-crooked.com/a/9061962e5385bd7a). – Yakk - Adam Nevraumont Mar 20 '18 at 15:51
  • In C++20 even easier! `[seconds, f=forward(f), ...args=forward(args)]() mutable { delayed(seconds, f, forward(args)...); }` – Barry Mar 20 '18 at 16:01
  • @barry Your second `forward` is wrong, it should be `std::move` I think? – Yakk - Adam Nevraumont Mar 20 '18 at 16:01
  • @Yakk No I didn't O:-) – Barry Mar 20 '18 at 16:02
  • @Barry Didn't what? – Yakk - Adam Nevraumont Mar 20 '18 at 16:03
  • @Yakk Hahaha. This'll be so confusing to future readers. Anyway, I think `forward` is right in case they're originally lvalues. – Barry Mar 20 '18 at 16:04
  • @Barry doesn't `...args` capture by value, not by reference? You should treat them as the value category they are, not what they came from. – Yakk - Adam Nevraumont Mar 20 '18 at 16:05
  • @Yakk Yes, you're right. Especially since we need to take ownership. – Barry Mar 20 '18 at 16:09
  • Thanks again, Yakk. Much better. I am still confused about the thread running on after main exits. I would think that if it is not sufficient that the alarm clears the atomic and the main thread waits to see it cleared, then any use of `detach` would cause UB. If so, why does it exist? That's a question for another Question. – Jive Dadson Mar 20 '18 at 17:04
  • @JiveDadson [`set_value_at_thread_exit`](http://en.cppreference.com/w/cpp/thread/promise/set_value_at_thread_exit) and similar can avoid it. But yes, 99.9% of use of detach is people ignoring the UB problem. – Yakk - Adam Nevraumont Mar 20 '18 at 17:06