I have a few questions about the library fibers proposal, P0876.
* Fibers are bound to the thread that initially created them; they cannot be resumed in any other thread. Is this restriction necessary?
What are the performance costs for allowing them to be used in any thread?
Is that perhaps an aspect that can be decided on at `fiber` creation time?
* Is it possible to add a way to transfer ownership of a suspended fiber to another thread? Obvious that depends on exactly why fibers are bound to their creating thread.
* Is it possible to have a way to ask if a fiber is associated with a given thread?
* Would it be possible to create a thread with a fiber?
I'm not saying that you should be able to create a thread and pass it an existing `std::fiber`. I'm asking if it is reasonable for `std::thread` to have some of the fiber constructors, so that we could pass stack allocators that can control the size of stack they get.
* Would it be reasonable to have an API that can take a terminated fiber and give it a new entry-function? The purpose of this would be to avoid having to deallocate a stack and then immediately reallocate it.
You would be able to keep a number of fibers lying around and pull one out when you want to execute a new task.
* It's not clear to me exactly how `resume_with` works, with regard to the underlying parts of the stack. That is, is there a way to use `resume_with` to execute the given function, then immediately go back and resume the execution of whatever was below it on the fiber's stack?
2018-03-10 21:55 GMT+01:00 Nicol Bolas <jmck...@gmail.com>:I have a few questions about the library fibers proposal, P0876.
* Fibers are bound to the thread that initially created them; they cannot be resumed in any other thread. Is this restriction necessary?This restriction was requested by SG1 at the last C++ meeting (because of the TLS problem, which is not fiber specific).
* Would it be possible to create a thread with a fiber?a thread already runs at least one fiber (== one stack)I'm not saying that you should be able to create a thread and pass it an existing `std::fiber`. I'm asking if it is reasonable for `std::thread` to have some of the fiber constructors, so that we could pass stack allocators that can control the size of stack they get.p0320r0 suggestes std::thread::attributes controling the stack size and some other stuff
* Would it be reasonable to have an API that can take a terminated fiber and give it a new entry-function? The purpose of this would be to avoid having to deallocate a stack and then immediately reallocate it.allocating and reallocating is done by the stack allocator ... if you want to resuse stacks ismply create a stack allocator that caches stacks
When calling `resume()`, it is conventional to replace the newly-invalidated instance – the instance on which `resume()` was called – with the new instance returned by that `resume()` call. This helps to avoid inadvertent calls to `resume()` on the old, invalidated instance.
void resume(std::fiber &&f) {f = std::move(f).resume();}
template<typename Fn>
void resume_with(std::fiber &&f, Fn &&fn) {f = std::move(f).resume_with(std::forward<Fn>(fn));}
> To my knowledge, there is nothing in the Coroutines TS that prevents a coroutine function from being executed on a different thread at different stages of its execution.One reason to argue for the difference would be that the coroutines TS is just a low-level primitive. It tells you nothing beyond a set of function calls and a state machine transformation. We would very much want to enforce this requirement for most coroutine code in practice,
but in the library infrastructure, and we have the opportunity to enforce this requirement in the library we provide in the standard.
You can pass an arbitrary coroutine that completed on an arbitrary thread to another coroutine, and the type system can catch that fact and ensure that awaiting on it does not cause the current coroutine to transition between threads.
Coroutines as defined do not work without the library infrastructure that you have to write.
The risk with this proposal is that you really can just resume it from anywhere and suspend that caller. The fiber alone does magical things to your current stack.There is no safety around the basic primitive. As a result, the feeling in the room was that it is better to start from the conservative position and then relax it later once we fully understand any issues that may come up.
On Saturday, March 10, 2018 at 4:37:54 PM UTC-5, Oliver Kowalke wrote:2018-03-10 21:55 GMT+01:00 Nicol Bolas <jmck...@gmail.com>:* It's not clear to me exactly how `resume_with` works, with regard to the underlying parts of the stack. That is, is there a way to use `resume_with` to execute the given function, then immediately go back and resume the execution of whatever was below it on the fiber's stack?yes, the proposal contains an example describing how resume_with() works
One question about this. I see that the return value from an injected function is passed back to the previously executing call to `resume/resume_with` on the underlying call stack. But... what happens if there is no underlying call stack yet. That is, you have a fresh `std::fiber` which has never been `resume`d, and you call `resume_with` on it.
My guess is that the return value will be passed as the parameter to the fiber's entry function.
But I don't think your current proposal says that. Or at least, the definition of `resume_with` doesn't seem to acknowledge that possibility.
Oh, and one other thing. The proposal says this:When calling `resume()`, it is conventional to replace the newly-invalidated instance – the instance on which `resume()` was called – with the new instance returned by that `resume()` call. This helps to avoid inadvertent calls to `resume()` on the old, invalidated instance.
That sounds like a good convention. So maybe there should be a couple of global helper functions to help people adhere to it:
void resume(std::fiber &&f) {f = std::move(f).resume();}
template<typename Fn>
void resume_with(std::fiber &&f, Fn &&fn) {f = std::move(f).resume_with(std::forward<Fn>(fn));}
These could be member functions (with the names `resume_inplace` and `resume_with_inplace`) instead of free functions.
2018-03-11 2:55 GMT+01:00 Nicol Bolas <jmckesson@gmail.com>:On Saturday, March 10, 2018 at 4:37:54 PM UTC-5, Oliver Kowalke wrote:2018-03-10 21:55 GMT+01:00 Nicol Bolas <jmck...@gmail.com>:* It's not clear to me exactly how `resume_with` works, with regard to the underlying parts of the stack. That is, is there a way to use `resume_with` to execute the given function, then immediately go back and resume the execution of whatever was below it on the fiber's stack?yes, the proposal contains an example describing how resume_with() works
One question about this. I see that the return value from an injected function is passed back to the previously executing call to `resume/resume_with` on the underlying call stack. But... what happens if there is no underlying call stack yet. That is, you have a fresh `std::fiber` which has never been `resume`d, and you call `resume_with` on it.It acts like starting the context the first time, executing the function passed to resume_with() and then enters the function that was given the ctor of fiber (should already be addressed in P0876).My guess is that the return value will be passed as the parameter to the fiber's entry function.correctBut I don't think your current proposal says that. Or at least, the definition of `resume_with` doesn't seem to acknowledge that possibility.API section: notes for the constructor
The entry-function fn is not immediately entered. The stack and any other necessary resources are created on construction, but fn is not entered until resume() or resume_with() is called.
The entry-function fn passed to std::fiber will be passed a synthesized std::fiber instance representing the suspended caller of resume().
The function fn passed to resume_with() will be passed a synthesized std::fiber instance representing the suspended caller of resume_with().
The entry-function fn passed to std::fiber will be passed a synthesized std::fiber instance representing the suspended caller of resume() or the return value of an injected fn from a call to resume_with().
An injected function fn() must accept std::fiber&& and return std::fiber. The fiber instance returned by fn() is, in turn, used as the return value for the suspended function: resume() or resume_with().
An injected function fn() must accept std::fiber&& and return std::fiber. The fiber instance returned by fn() is, in turn, used as the return value for the previously executed suspended function in the fiber: resume() or resume_with(). If there was no previously executed suspend function, then the return value will be passed as the parameter of the entry-function.
Oh, and one other thing. The proposal says this:When calling `resume()`, it is conventional to replace the newly-invalidated instance – the instance on which `resume()` was called – with the new instance returned by that `resume()` call. This helps to avoid inadvertent calls to `resume()` on the old, invalidated instance.
That sounds like a good convention. So maybe there should be a couple of global helper functions to help people adhere to it:
void resume(std::fiber &&f) {f = std::move(f).resume();}
template<typename Fn>
void resume_with(std::fiber &&f, Fn &&fn) {f = std::move(f).resume_with(std::forward<Fn>(fn));}
These could be member functions (with the names `resume_inplace` and `resume_with_inplace`) instead of free functions.yes, sounds reasonable
void resume(std::fiber &f) {f = std::move(f).resume();}
But this function doesn't invalidate f! It is an input/output param here
resume(std::fiber(...));
std::fiber(...).resume();
* Fibers are bound to the thread that initially created them; they cannot be resumed in any other thread. Is this restriction necessary?
Stackless coroutines do not have this problem, since the points where suspension and resumption can happen are marked explicitly and compiler does not caches TLS across suspend points.
the TLS problem is not P0876 specific ... it applies to any C++ code that uses TLS (thread_local vars) and moves functors between threadsmigrating fibers between threads is possible if not TLS is used
2018-03-21 17:01 GMT+01:00 Gor Nishanov <gorni...@gmail.com>:<snip>Stackless coroutines do not have this problem, since the points where suspension and resumption can happen are marked explicitly and compiler does not caches TLS across suspend points.the TLS problem is not P0876 specific ... it applies to any C++ code that uses TLS (thread_local vars) and moves functors between threads
the point is: don't use TLS if you want to migrate fibers to other threads
* Fibers are bound to the thread that initially created them; they cannot be resumed in any other thread. Is this restriction necessary?TLS limitation is due to how TLS codegen is done on RISC CPUs today:
On Wed, Mar 21, 2018 at 12:01 PM, Gor Nishanov <gorni...@gmail.com> wrote:* Fibers are bound to the thread that initially created them; they cannot be resumed in any other thread. Is this restriction necessary?TLS limitation is due to how TLS codegen is done on RISC CPUs today:I am just an interested observer trying to understand this thread (sic). May Iask what the fiber suspension/resumption activity has to do with TLS? I amstruggling to make this connection. Are we saying that code that uses TLSand ends up being executed by a Fiber cannot possibly work correctly if itresumes in another thread?
Raymond Chen over here[1] seems to imply that Fibers can quite happily getthemselves suspended by the thread that created them and simply resume onanother thread.
> the point is: don't use TLS if you want to migrate fibers to other threadsWhich means, do not use <filesystem>, do not define static locals within a function, do not use <system_error>, do not use errno, do not use init_once, do not use networking TS, do not use boost::asio and these are just the ones on top of my head of the things which are popular and using thread_local in their implementation.
Den onsdag 21 mars 2018 kl. 19:24:09 UTC+1 skrev Gor Nishanov:> the point is: don't use TLS if you want to migrate fibers to other threadsWhich means, do not use <filesystem>, do not define static locals within a function, do not use <system_error>, do not use errno, do not use init_once, do not use networking TS, do not use boost::asio and these are just the ones on top of my head of the things which are popular and using thread_local in their implementation.I don't think it is this bad: As long as a function inside for instance std::filesystem does not yield it should be ok to call it from a fiber even if said fiber jumps between threads.
On the other hand we are talking about a new language feature here. Even if a fiber is disguised as a class there has to be some codegen magic to make yielding happen.
Yes, boost manages to do it with some assembly function but I assume a language feature would be handled by the compiler in such a way that it can detect a yield point and abstain from caching a TLS entry pointer in a register over it.
If the language feature is specified in this way it will gain an edge over boost's library only version that may increase the motivation to actually standardize it. This said it is not obvious that current uses of TLS will continue to work if you slap yield points into the code at random but it should at least be possible to ensure that the TLS entry corresponds to the current thread at all times.I could be wrong but I think that there are interesting use cases for fibers that can be served by a thread pool. As it may be impossible to know if a library uses TLS it seems scary to not allow the limited usage I proposed (i.e. still no yielding in callbacks).