This is an archive of the discontinued LLVM Phabricator instance.

[libc++][NFC] Clean up std::__call_once
ClosedPublic

Authored by ldionne on Oct 22 2021, 8:17 AM.

Details

Summary

__call_once is large and cluttered with #ifdef preprocessor guards. This
cleans it up a bit by using an exception guard instead of try-catch.

Diff Detail

Event Timeline

DanielMcIntosh-IBM requested review of this revision.Oct 22 2021, 8:17 AM
DanielMcIntosh-IBM created this revision.
Herald added a project: Restricted Project. · View Herald TranscriptOct 22 2021, 8:17 AM
Herald added a reviewer: Restricted Project. · View Herald Transcript

IMHO the "before" picture is pretty gross, but the "after" picture isn't very much improved. (Of course the diff makes the change look scarier than it probably really is, but still...)
Perhaps consider naming the helpers something like call_once_update_from_zero_to_one(flag), call_once_store_zero(flag), call_once_store_minusone(flag), and maybe adding some comments about the flow: or maybe those comments are already present in <mutex>. The flow here, AIUI, is:

  • flag starts as 0
  • While any one thread is climbing the glass hill, flag is 1
  • When that thread fails, it resets flag back to 0
  • Once any thread has reached the top of the hill, flag becomes -1 and stays that way forever

Interestingly,

std::once_flag f;
int main() {
    std::call_once(f, [&]() { std::call_once(f, []() { puts("hello"); }); });
}

is a deadlock when !defined(_LIBCPP_HAS_NO_THREADS) but finishes with no output when defined(_LIBCPP_HAS_NO_THREADS). (This is probably intentional and a good idea, to keep the no-threads code even simpler than it would be by default, in a case that is presumably undefined behavior according to the Standard. OTOH, libstdc++ somehow manages in this case to throw system_error instead of deadlocking.)

Anyway, FWIW, I see no particular problem with this PR.

DanielMcIntosh-IBM updated this revision to Diff 382144.EditedOct 25 2021, 3:31 PM

Change _State_type to an enum. This hopefully indirectly addresses @Quuxplusone's comment

libcxx/include/mutex
581–583

This approach looks reasonable to me. However,

  • Looks like Clang does not support enum E : U { as an extension to C++03, which explains the failing C++03 tests now. Maybe you can keep __state_ as an integer type and just use a C++03 enum to name these values? Or just use static const _State_type static data members.
  • These identifiers will need to be _Uglified because they're in a public header. Moving them into mutex.cpp would be one option; or you could make them _Unset, _Pending, _Complete. (Compare to the private enum types in <filesystem>.)
  • And you'll still have to wait for @ldionne to look at this PR in general.
DanielMcIntosh-IBM marked an inline comment as done.Oct 26 2021, 11:14 AM
libcxx/include/mutex
581–583

I would have put them inside mutex.cpp, except then they couldn't be used here on line 587, 673, 689, and 701

Change indentation to match the rest of the file

Still looks like "big ugly diff but probably harmless" to me. Needs more attention from not-me.
Also, your three new static const should probably be static _LIBCPP_CONSTEXPR const just for consistency with everywhere else we do that.

ldionne requested changes to this revision.Dec 13 2021, 12:56 PM
ldionne added a subscriber: var-const.

I really like the introduction of named values like once_flag::_Unset. IMO, we should introduce them in a separate patch which would be trivial to approve.

Then, I would suggest we use a scope guard variable to refactor the messy _LIBCPP_NO_EXCEPTIONS. For example:

void __call_once(volatile once_flag::_State_type& flag, void* arg, void (*func)(void*))
{
#if defined(_LIBCPP_HAS_NO_THREADS)
    if (flag == once_flag::_Unset)
    {
        __scope_guard guard = [&flag] { flag = once_flag::_Unset; };
        flag = once_flag::_Pending;
        func(arg);
        flag = once_flag::_Complete;
    }

#else // !_LIBCPP_HAS_NO_THREADS

    __libcpp_mutex_lock(&mut);
    while (flag == once_flag::_Pending)
        __libcpp_condvar_wait(&cv, &mut);

    if (flag == once_flag::_Unset)
    {
        __scope_guard guard = [&mut, &flag, &cv] {
            __libcpp_mutex_lock(&mut);
            __libcpp_relaxed_store(&flag, once_flag::_Unset);
            __libcpp_mutex_unlock(&mut);
            __libcpp_condvar_broadcast(&cv);
        };

        __libcpp_relaxed_store(&flag, once_flag::_Pending);
        __libcpp_mutex_unlock(&mut);
        func(arg);
        __libcpp_mutex_lock(&mut);
        __libcpp_atomic_store(&flag, once_flag::_Complete, _AO_Release);
        __libcpp_mutex_unlock(&mut);
        __libcpp_condvar_broadcast(&cv);
    }
    else
    {
        __libcpp_mutex_unlock(&mut);
    }
#endif // !_LIBCPP_HAS_NO_THREADS
}

This is much cleaner IMO since we don't have any #ifdefs related to exceptions anymore, and we don't need to change the structure of the code, which obscures what's happening. Also, we've been meaning to introduce such a scope guard with @var-const to refactor some algorithms that look messy right now, so this would kill two birds with one stone. LMK what you think about this suggestion and I can open a patch for __scope_guard if you agree.

This revision now requires changes to proceed.Dec 13 2021, 12:56 PM

FWIW, when I see the name __scope_guard I assume it'll work like a finally clause, running its code unconditionally on scope exit. (Like https://quuxplusone.github.io/blog/2018/08/11/the-auto-macro/ . :))
But what we want here is a catch clause, running its code only if an exception actually is thrown. No matter what you name the scope-guard, it's going to be a speed bump for the reader, whereas try { func(arg); } catch(...) { some code; throw; } can be understood instantly.
Perhaps Louis's idea, but with the scope-guard class type named struct __catch_and_rethrow_logic? It needs something to catch the reader's attention that it's specifically the catch path, not a general-purpose scope guard.

FWIW, when I see the name __scope_guard I assume it'll work like a finally clause, running its code unconditionally on scope exit. (Like https://quuxplusone.github.io/blog/2018/08/11/the-auto-macro/ . :))
But what we want here is a catch clause, running its code only if an exception actually is thrown. No matter what you name the scope-guard, it's going to be a speed bump for the reader, whereas try { func(arg); } catch(...) { some code; throw; } can be understood instantly.
Perhaps Louis's idea, but with the scope-guard class type named struct __catch_and_rethrow_logic? It needs something to catch the reader's attention that it's specifically the catch path, not a general-purpose scope guard.

Fair point. Let's bikeshed about the name and the specific design of that type in D115730.

With that review, we'd be able to write:

void __call_once(volatile once_flag::_State_type& flag, void* arg, void (*func)(void*))
{
#if defined(_LIBCPP_HAS_NO_THREADS)
    if (flag == once_flag::_Unset)
    {
        __transaction transaction([&flag] { flag = once_flag::_Unset; });
        flag = once_flag::_Pending;
        func(arg);
        flag = once_flag::_Complete;
        transaction.__complete();
    }

#else // !_LIBCPP_HAS_NO_THREADS

    __libcpp_mutex_lock(&mut);
    while (flag == once_flag::_Pending)
        __libcpp_condvar_wait(&cv, &mut);

    if (flag == once_flag::_Unset)
    {
        __transaction transaction([&mut, &flag, &cv] {
            __libcpp_mutex_lock(&mut);
            __libcpp_relaxed_store(&flag, once_flag::_Unset);
            __libcpp_mutex_unlock(&mut);
            __libcpp_condvar_broadcast(&cv);
        });

        __libcpp_relaxed_store(&flag, once_flag::_Pending);
        __libcpp_mutex_unlock(&mut);
        func(arg);
        __libcpp_mutex_lock(&mut);
        __libcpp_atomic_store(&flag, once_flag::_Complete, _AO_Release);
        __libcpp_mutex_unlock(&mut);
        __libcpp_condvar_broadcast(&cv);
        transaction.__complete();
    }
    else
    {
        __libcpp_mutex_unlock(&mut);
    }
#endif // !_LIBCPP_HAS_NO_THREADS
}

I am doing the pre-requisite refactoring here: https://github.com/llvm/llvm-project/pull/66289

Herald added a project: Restricted Project. · View Herald TranscriptSep 13 2023, 1:49 PM
ldionne commandeered this revision.Sep 13 2023, 1:57 PM
ldionne edited reviewers, added: DanielMcIntosh-IBM; removed: ldionne.

[Github PR transition cleanup]

Commandeering to finish.

ldionne updated this revision to Diff 556719.Sep 13 2023, 2:01 PM
ldionne retitled this revision from [NFC][libcxx] Clean up std::__call_once to [libc++][NFC] Clean up std::__call_once.
ldionne edited the summary of this revision. (Show Details)

Rebase on top of https://github.com/llvm/llvm-project/pull/66289 and update with the approach suggested in https://reviews.llvm.org/D112319#3190157.

ldionne updated this revision to Diff 556961.Sep 18 2023, 10:26 AM

Rebase onto main.

ldionne accepted this revision as: Restricted Project.Sep 19 2023, 1:16 PM
This revision was not accepted when it landed; it landed in state Needs Review.Sep 19 2023, 1:17 PM
This revision was automatically updated to reflect the committed changes.