19

Per [futures.async]/3 bullet 1 of the C++ Standard, when a function f is passed to std::async with the std::launch::async launch policy, f will run "as if in a new thread of execution".

Given that f can do anything, including loop infinitely and block forever, how can an implementation offer the behavior of f running on its own thread without actually running it on its own thread? That is, how can an implementation take advantage of the "as if" wiggle room the Standard provides?

KnowItAllWannabe
  • 12,972
  • 8
  • 50
  • 91
  • 8
    I suspect the wording may be used to allow implementation to use thread pools. When you use a thread pool, you aren't using a *new* thread, you're reusing a thread that's been around for a while, but the result is essentially the same. – François Andrieux May 07 '18 at 19:23
  • 13
    In other words, the important word isn't "thread." The important word is "new." – Solomon Slow May 07 '18 at 19:24
  • 5
    What if it runs in a cooperatively-scheduled coroutine? – Dai May 07 '18 at 19:24
  • The [intro.progress] section on Forward progress may be relevant. – 1201ProgramAlarm May 07 '18 at 19:39
  • If `f` has a `thread_local` variable then does it have to reset that in case it's executed on a thread from a pool that has previously been used? – M.M May 17 '18 at 21:14
  • 3
    @M.M: Yes, I believe that thread-locals would have to be destructed and reconstructed if an existing thread (e.g., from a thread pool) were to be used instead of a new thread. – KnowItAllWannabe May 18 '18 at 03:52
  • @M.M more than `f` can have `thread_local`s. I would assume an implementation that used a thread pool would have a "reset all `thread_local`s state" routine that it ran before starting `f`. Given that is only a subset of the actions required for starting a thread, it is still potentially an improvement – Caleth May 21 '18 at 13:27
  • @FrançoisAndrieux I have suspicions that your comment would be more helpful if posted as an answer... :) – Drew Dormann May 27 '18 at 18:11

7 Answers7

6

Looking in the C++ refs as appear here and here, it seems that the "as if" goal is to let the library implementer some degree of freedom. For example, it says

as if spawned by std::thread(std::forward(f), std::forward(args)...), except that if the function f returns a value or throws an exception, it is stored in the shared state accessible through the std::future that async returns to the caller.

in the first source, and

as if a thread object is constructed with fn and args as arguments, and accessing the shared state of the returned future joins it

in the second. So it looks like the behavior is similar to that of std::thread but might be implemented in a different way. This is interesting, since the phrase thread of execution you quote here is distinguishable from std::thread. Still, it seems that both sources understand the as-if that way.

Another option might be, as suggested by François Andrieux to allow usage of threadpool, as expressed in the first source:

The template function async runs the function f asynchronously (potentially in a separate thread which may be part of a thread pool)

Mike
  • 1,215
  • 7
  • 12
  • I'm marking this as the answer, even though most of what it contains is just a restatement of the "as if" wiggle room that motivated the original question and doesn't address the _how_ implementers can take advantage of the flexibility in the Standard. If I could mark François Andrieux's comment as the answer, I would, but since I can't, the fact that this answer refers to his comment seems to be the best I can do. – KnowItAllWannabe May 27 '18 at 14:48
5

Some ways I can think of that f could run "as if" on a new thread without actually doing so would be if f does not actually use any state shared with other threads; then the implementation could run it as a separate process (since it doesn't need shared memory space) it could also just run it on the main thread as just another function call (if it can prove that f doesn't block or have observable side effects that would differ when run this way). It could also be scheduled to run on an existing (but idle) thread (thread pool).

And if you want to be silly, I guess you could also consider the notion of not running f at all, since there are no guarantees about when new threads will be scheduled to run by an operating system, so an evil implementation could just say that the OS never schedules any thread except the main thread, thus not running f at all is equivalent to scheduling it on a new thread. Of course this is stupid/silly and no sane implementation would ever do that - but in theory the language allows such a degenerate implementation (I think).

Jesper Juhl
  • 30,449
  • 3
  • 47
  • 70
  • In general, it can't run `f`on the main thread as a function call, because if `f` blocks or loops forever, that would typically behave differently than if `f`ran on a new thread. – KnowItAllWannabe May 18 '18 at 03:52
  • 1
    The key point there is not that it will run it on the main thread, but that the compiler could choose to do so if it will behave "as if in a new thread of execution". For example if the compiler can prove that f doesn't block and doesn't need to be on another thread it may be able to inline it instead. It's not saying a compiler should do this, it is just leaving the door open for various optimisations / implementations. – Michael Anderson May 23 '18 at 07:38
  • 1
    @KnowItAllWannabe Not actually true. The Standard says that implementations "*should* ensure that all unblocked threads make progress", but as an ISO standard [that "should" is to be interpreted as a recommendation, not a requirement](https://www.iso.org/foreword-supplementary-information.html). If you have a thread that loops forever, an implementation which allowed that to block the main thread would technically be compliant. – Sneftel May 24 '18 at 15:55
5

The advantage of this solution is when you have optimized implementation for specific role in multi threading world. As I am involved in software update procedures I will use such example. For optimization reason you can call:

std::thread::hardware_concurrency();

You will receive:

Returns the number of concurrent threads supported by the implementation.

Let's say you have result equal 4. You want to perform update many things in parallel. Main thread is monitoring list of futures(3 left) and checking from time to time is it done or not and if done execute next thing from ToDo list. Profit here is for example if you are updating different type of memory like 1-FileSystem, 2-EEPROM, 3-NOR memory or something like that. There is no profit of checking in loop without delay, so you want to give a task to 4-th thread between checks. You are writing function that is digging bitcoins for 50 miliseconds :) and you trigger it as deferred between checks.

And then as you mentioned we have:

advantage of the "as if" wiggle room the Standard provides

2

AFAIK the C++ runtime is able to manage some internal (or specialized) threads, distinguished from the standard threads.

My guess (I'am still learning the advanced features of C++) is that, with the std::launch::async flag, the std::async() will launch f in a new internal thread.

You could launch f in a new standard thread using std::thread. In this case the exceptions will be handled on the called thread and the main code will have to fetch the return value of f.

With the internal thread, the return value and the eventual exception are stored into std::future, returned by std::async() and shared between the threads.

So "as if" stands for "as if I launch f with std::thread but I don't have to". But for sure, f will run on a new thread.

To answer your question about implementation, once the runtime already has standard threads implemented, it would be just the effort to specialize them for specific uses. See also the execution policy for algorithms that support parallelization.

edixon
  • 991
  • 6
  • 16
2

I see main idea of "f will run "as if in a new thread of execution"" is that f will run asynchronously. Whether should it be new thread or something else is implementation details.

wtom
  • 565
  • 4
  • 12
1

The only solution without Thread, I can think of is the yesterdayer' Time-Slicing which is a form of multi-tasking. The functions have to be made re-entrant. In such scenario, every function will run as if they are on different threads, although practically they are on single thread.

seccpur
  • 4,996
  • 2
  • 13
  • 21
0

In the docs, it stated there,

The template function async runs the function f asynchronously (potentially in a separate thread which may be part of a thread pool) and returns a std::future that will eventually hold the result of that function call.

http://en.cppreference.com/w/cpp/thread/async

Truth
  • 76
  • 1
  • 6