std::ref
is a start but it's not enough. c++ is only guaranteed to be aware of changes to a variable by another thread if that variable is guarded by either,
a) an atomic, or
b) a memory-fence (mutex, condition_variable, etc)
It is also wise to synchronise the threads before allowing main to finish. Notice that I have a call to fi.get()
which will block the main thread until the future is satisfied by the async thread.
updated code:
#include <cmath>
#include <iostream>
#include <thread>
#include <future>
#include <chrono>
#include <functional>
#include <atomic>
// provide a means of emitting to stdout without a race condition
std::mutex emit_mutex;
template<class...Ts> void emit(Ts&&...ts)
{
auto lock = std::unique_lock<std::mutex>(emit_mutex);
using expand = int[];
void(expand{
0,
((std::cout << ts), 0)...
});
}
// cross-thread communications are UB unless either:
// a. they are through an atomic
// b. there is a memory fence operation in both threads
// (e.g. condition_variable)
int func(std::atomic<bool>& on)
{
while(on) {
auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
emit(ctime(&t), "\n");
std::this_thread::sleep_for(std::chrono::seconds(1));
}
return 6;
}
int main()
{
std::atomic<bool> on { true };
std::future<int> fi = std::async(std::launch::async, func, std::ref(on));
std::this_thread::sleep_for(std::chrono::seconds(5));
on = false;
emit("function returned ", fi.get(), "\n");
return 0;
}
example output:
Wed Jun 22 09:50:58 2016
Wed Jun 22 09:50:59 2016
Wed Jun 22 09:51:00 2016
Wed Jun 22 09:51:01 2016
Wed Jun 22 09:51:02 2016
function returned 6
by request, an explanation of emit<>(...)
template<class...Ts> void emit(Ts&&...ts)
emit
is a function which returns void and takes any number of parameters by x-value reference (i.e. either a const ref, a ref or an r-value ref). I.e. it will accept anything. This means we can call:
emit(foo())
- call with the return value of a function (r-value)
emit(x, y, foo(), bar(), "text")
- call with two references, 2 r-value-references and a string literal
using expand = int[];
defines a type to be an array of integers of indeterminate length. We're going to use this merely to force the evaluation of expressions when we instantiate an object of type expand
. The actual array itself will be discarded by the optimiser - we just want the side-effects of constructing it.
void(expand{ ... });
- forces the compiler to instantiate the array but the void cast tells it that we'll never use the actual array itself.
((std::cout << ts), 0)...
- for every parameter (denoted by ts) expand one term in the construction of the array. Remember that the array is integers. cout << ts
will return an ostream&
, so we use the comma operator to sequentially order the call to ostream<<
before we simply evaluate an expression of 0. The zero could actually be any integer. It wouldn't matter. It is this integer which is conceptually stored in the array (which is going to be discarded anyway).
0,
- The first element of the array is zero. This caters for the case where someone calls emit()
with no arguments. The parameter pack Ts
will be empty. If we didn't have this leading zero, the resulting evaluation of the array would be int [] { }
, which is a zero-length array, which is illegal in c++.
Further notes for beginners:
everything inside the initialiser list of an array is an expression.
An expression is a sequence of 'arithmetic' operations which results in some object. That object may be an actual object (class instance), a pointer, a reference or a fundamental type like an integer.
So in this context, std::cout << x
is an expression computed by calling std::ostream::operator<<(std::cout, x)
(or its free function equivalent, depending on what x is). The return value from this expression is always std::ostream&
.
Putting the expression in brackets does not change its meaning. It simply forces ordering. e.g. a << b + c
means 'a shifted left by (b plus c)' whereas (a << b) + c
means 'a shifted left by b, then add c'.
The comma ',' is an operator too. a(), b()
means 'call function a and then discard the result and then call function b. The value returned shall be the value returned by b'.
So, with some mental gymnastics, you ought to be able to see that ((std::cout << x), 0)
means 'call std::ostream::operator<<(std::cout, x)
, throw away the resulting ostream reference, and then evaluate the value 0). The result of this expression is 0, but the side-effect of streaming x into cout
will happen before we result in 0'.
So when Ts
is (say) an int and a string pointer, Ts...
will be a typelist like this <int, const char*>
and ts...
will be effectively <int(x), const char*("Hello world")>
So the expression would expand to:
void(int[] {
0,
((std::cout << x), 0),
((std::cout << "Hello world"), 0),
});
Which in baby steps means:
- allocate an array of length 3
- array[0] = 0
- call std::cout << x, throw away the result, array[1] = 0
- call std::cout << "Hello world", throw away the result, array[2] = 0
And of course the optimiser sees that this array is never used (because we didn't give it a name) so it removes all the un-necessary bits (because that's what optimisers do), and it becomes equivalent to:
- call std::cout << x, throw away the result
- call std::cout << "Hello world", throw away the result