__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.
Details
- Reviewers
rmaprath jroelofs thakis • Quuxplusone DanielMcIntosh-IBM - Group Reviewers
Restricted Project - Commits
- rGbe8c2df2b15b: [libc++][NFC] Clean up std::__call_once
Diff Detail
- Repository
- rG LLVM Github Monorepo
Unit Tests
Event Timeline
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.
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,
|
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 |
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.
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.
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
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.
This approach looks reasonable to me. However,