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

Made sure detail::condition_variable can be safely destroyed #1488

Merged
merged 3 commits into from May 12, 2015
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions hpx/lcos/detail/future_data.hpp
Expand Up @@ -244,6 +244,11 @@ namespace detail

// handle all threads waiting for the block to become full
cond_.notify_all(l, ec);

// Note: cond_.notify_all() may cause this shared state to be
// destroyed. This is, however, safe as the scoped lock above
// will not touch the wrapped mutex if it was unlocked inside
// notify_all().
}

// invoke the callback (continuation) function
Expand Down
2 changes: 2 additions & 0 deletions hpx/lcos/local/counting_semaphore.hpp
Expand Up @@ -129,6 +129,8 @@ namespace hpx { namespace lcos { namespace local
value_ += count;
for (boost::int64_t i = 0; value_ >= 0 && i < count; ++i)
{
// notify_one() returns false if no more threads are
// waiting
if (!cond_.notify_one(l))
break;
}
Expand Down
39 changes: 30 additions & 9 deletions hpx/lcos/local/detail/condition_variable.hpp
Expand Up @@ -74,7 +74,9 @@ namespace hpx { namespace lcos { namespace local { namespace detail
{
if (!queue_.empty())
{
LERR_(fatal) << "~condition_variable: queue is not empty, aborting threads";
LERR_(fatal)
<< "~condition_variable: queue is not empty, "
"aborting threads";

local::no_mutex no_mtx;
abort_all(no_mtx);
Expand All @@ -97,6 +99,9 @@ namespace hpx { namespace lcos { namespace local { namespace detail
return queue_.size();
}

// Return false if no more threads are waiting. If it returns false
// the lock is left unlocked, otherwise it will be relocked before
// returning.
template <typename Lock>
bool notify_one(Lock& lock, error_code& ec = throws)
{
Expand All @@ -115,20 +120,36 @@ namespace hpx { namespace lcos { namespace local { namespace detail
queue_.front().id_ = threads::invalid_thread_id_repr;
queue_.pop_front();

util::scoped_unlock<Lock> unlock(lock);
if (!queue_.empty())
{
util::scoped_unlock<Lock> unlock(lock);

threads::set_thread_state(threads::thread_id_type(
reinterpret_cast<threads::thread_data_base*>(id)),
threads::pending, threads::wait_timeout,
threads::thread_priority_default, ec);
if (!ec) return true;
threads::set_thread_state(threads::thread_id_type(
reinterpret_cast<threads::thread_data_base*>(id)),
threads::pending, threads::wait_timeout,
threads::thread_priority_default, ec);
if (!ec) return true;
}
else
{
// Since this is the last thread waiting on this condition
// variable it could happen that this instance will be
// destroyed before set_thread_state() returns. We have to
// make sure that this instance is not touched anymore.
lock.unlock();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this change mean that we have to go over all uses of notify_one and make sure that the result is the expected one? I believe this would lead to unlock() being called in an already unlocked mutex.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went through all current uses of detail::condition_variable::notify_one and fixed its uses. Also a unlocked lock will not touch the mutex anymore (at least not boost::unique_lock which we usually use as the scoped lock mechanism). All of this is not too satisfying, but I believe it's the best we can currently do.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went through all use cases too and they are fine in a surprising way. This is mostly because they all use nested ::scoped_lock, which are not scoped locks but unique_locks. This is at least fragile.

All uses of this condition variable are implementation details, so this is not a big deal. We should make a note or ticket to review this in a future, in particular once we move away from nested scoped_locks.


threads::set_thread_state(threads::thread_id_type(
reinterpret_cast<threads::thread_data_base*>(id)),
threads::pending, threads::wait_timeout,
threads::thread_priority_default, ec);
}
}

return false;
}

// This leaves the lock unlocked
template <typename Lock>
void notify_all(Lock& lock, error_code& ec = throws) // leaves the lock unlocked
void notify_all(Lock& lock, error_code& ec = throws)
{
HPX_ASSERT_OWNS_LOCK(lock);

Expand Down
4 changes: 2 additions & 2 deletions hpx/lcos/local/promise.hpp
Expand Up @@ -129,10 +129,10 @@ namespace hpx { namespace lcos { namespace local
return;
}

has_result_ = true;

shared_state_->set_result(std::forward<T>(value), ec);
if (ec) return;

has_result_ = true;
}

#ifndef BOOST_NO_CXX11_EXPLICIT_CONVERSION_OPERATORS
Expand Down
8 changes: 6 additions & 2 deletions tests/regressions/lcos/CMakeLists.txt
@@ -1,4 +1,4 @@
# Copyright (c) 2007-2013 Hartmut Kaiser
# Copyright (c) 2007-2015 Hartmut Kaiser
# Copyright (c) 2011-2012 Bryce Adelstein-Lelbach
#
# Distributed under the Boost Software License, Version 1.0. (See accompanying
Expand All @@ -18,6 +18,7 @@ set(tests
lifetime_588
lifetime_588_1
promise_leak_996
safely_destroy_cv_1481
set_hpx_limit_798
shared_stated_leaked_1211
)
Expand All @@ -30,7 +31,7 @@ set(tests ${tests}
dataflow_future_swap
dataflow_791
)
if(HPX_WITH_CXX11_LAMBDAS)
if(HPX_WITH_CXX11_LAMBDAS)
set(tests ${tests}
dataflow_const_functor_773
dataflow_future_swap2
Expand All @@ -56,6 +57,9 @@ if(HPX_WITH_CXX11_LAMBDAS)
future_hang_on_wait_with_callback_629)
endif()

set(safely_destroy_cv_1481_PARAMETERS THREADS_PER_LOCALITY 2)

# Create test cases
foreach(test ${tests})
set(sources
${test}.cpp)
Expand Down
44 changes: 44 additions & 0 deletions tests/regressions/lcos/safely_destroy_cv_1481.cpp
@@ -0,0 +1,44 @@
// Copyright 2015 (c) Hartmut Kaiser
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

// This test case demonstrates the issue described in #1481:
// Sync primitives safe destruction

#include <hpx/hpx.hpp>
#include <hpx/hpx_main.hpp>
#include <hpx/include/local_lcos.hpp>
#include <hpx/util/lightweight_test.hpp>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test is called safely_destroy_cv but actually test for promise/future.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhh, right. It still relies on the behavior of the condition_variable, though. Let's change the name of the test, though.


void test_safe_destruction()
{
hpx::thread t;
hpx::future<void> outer;

{
hpx::lcos::local::promise<void> p;
hpx::shared_future<void> inner = p.get_future().share();

// Delay returning from p.set_value() below to destroy the promise
// before set_value returns.
outer = inner.then(
[](hpx::shared_future<void> &&)
{
hpx::this_thread::sleep_for(boost::chrono::milliseconds(100));
});

// create a thread which will make the inner future ready
t = hpx::thread([&p]() { p.set_value(); });
inner.get();
}

outer.get();
t.join();
}

int main()
{
test_safe_destruction();
return hpx::util::report_errors();
}