Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FYI – discussion of this proposal on the structured concurrency forum #16

Closed
njsmith opened this issue Feb 28, 2019 · 5 comments

Comments

Projects
None yet
2 participants
@njsmith
Copy link

commented Feb 28, 2019

There's been a lot of discussion happening over the last few years about "structured concurrency", across a number of different languages – we even started a forum recently, with participation from folks designing Python, Kotlin, Java, Swift, etc. I just heard about this proposal, and it's very closely related to the kinds of things we're talking about. So, I figured I'd drop you an invitation to join in, if you like! I also made a post summarizing this proposal for folks there, and comparing it to how other languages have approached this, which you might be interested in:

https://trio.discourse.group/t/structured-concurrency-proposal-for-c-20/125

@njsmith njsmith closed this Feb 28, 2019

@lewissbaker

This comment has been minimized.

Copy link
Contributor

commented Feb 28, 2019

The example shown in that post was:

void myfunc(std::stop_token external_stoken = {}) {
    std::jthread nested_worker_1([] (std::stop_token nested_stoken_1) {
        while (!nested_stoken_1.stop_requested()) {
            sleep(1);  // placeholder for real work
        }
    });

    std::jthread nested_worker_2([] (std::stop_token nested_stoken_2) {
        while (!nested_stoken_2.stop_requested()) {
            sleep(1);  // placeholder for real work
        }
    });

    // Do some cancellable work in the main thread
    while (!external_stoken.stop_requested()) {
        sleep(1);  // placeholder for real work
    }
}

This is using the jthread-created stop_token that is signalled when the jthread destructs.
However, there is nothing that requires you to use that stop_token.
In fact, the jthread won't create a new stop_source/stop_token if the callable does not accept the stop_token as its final argument.

Instead you can pass the other stop_token from the parent function in directly:

void myfunc(std::stop_token external_stoken = {}) {
    std::jthread nested_worker_1([=] () {
        while (!external_stoken.stop_requested()) {
            sleep(1);  // placeholder for real work
        }
    });

    std::jthread nested_worker_2([=] () {
        while (!external_stoken.stop_requested()) {
            sleep(1);  // placeholder for real work
        }
    });

    // Do some cancellable work in the main thread
    while (!external_stoken.stop_requested()) {
        sleep(1);  // placeholder for real work
    }
}

This should give you the required semantics of all threads terminating within 1s rather than each thread being signalled to stop only after the previous thread has finished stopping.

@lewissbaker

This comment has been minimized.

Copy link
Contributor

commented Feb 28, 2019

Regarding the comment:

    // Explicit cast-to-void is required by the draft spec
    // I don't know why
    // It might be an error in the draft?
    [&] { (void)nested_worker_1.request_stop() }

Yes, there was a drafting error that included [[nodiscard]] attribute on jthread::request_stop(). This should be corrected in R9 of the paper.

@njsmith

This comment has been minimized.

Copy link
Author

commented Feb 28, 2019

Instead you can pass the other stop_token from the parent function in directly:

Ah, sure. That trick works if the only way the parent can exit is due to being stopped, which is mostly true in the silly example case. But in a more realistic situation we also want to handle the case where the parent exits spontaneously without the external token being set. For example, if creating the second thread raises an exception for some reason, then your version will deadlock in the nested_worker_1 destructor (at least until someone outside sends in a stop request).

@lewissbaker

This comment has been minimized.

Copy link
Contributor

commented Feb 28, 2019

Ok, so what you would like is the ability to pass in your own stop_source into the constructor of jthread and have it call request_stop() on that object in the destructor instead of on one it creates internally.

void myfunc(std::stop_token external_stoken = {}) {
    std::stop_source src;
    std::stop_callback cb{external_stoken, [&] { src.request_stop(); }};

    std::jthread nested_worker_1(src, [] (std::stop_token nested_stoken_1) {
        while (!nested_stoken_1.stop_requested()) {
            sleep(1);  // placeholder for real work
        }
    });

    std::jthread nested_worker_2(src, [] (std::stop_token nested_stoken_2) {
        while (!nested_stoken_2.stop_requested()) {
            sleep(1);  // placeholder for real work
        }
    });

    // Do some cancellable work in the main thread
    while (!external_stoken.stop_requested()) {
        sleep(1);  // placeholder for real work
    }
}
@njsmith

This comment has been minimized.

Copy link
Author

commented Mar 1, 2019

That would be one possible solution, sure. That's what C# calls a "linked" cancel source.

IMO that way is kinda cumbersome, and it's surprising that exiting the nested_worker_2 scope causes nested_worker_1 to be cancelled. If I saw someone passing a stop token/source to the jthread constructor, my first thought would be that it internally creates a linked stop source, and then uses the linked one for everything – so calling external_stoken.request_stop() would immediately transmit a stop request to all threads, but calling nested_worker_2.request_stop() would only transmit the request to that thread.

But, if you do it that way, then you still have the sequential stop problem on regular exits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.