Add Borrowable class.

Define Lender/Borrowable/Borrowed classes for sharing resources safely between threads.

PiperOrigin-RevId: 489361079
This commit is contained in:
Janusz Sobczak
2022-11-17 18:56:41 -08:00
committed by Copybara-Service
parent e58dd0a597
commit 141f021140
4 changed files with 295 additions and 0 deletions
+1
View File
@@ -515,6 +515,7 @@ let package = Package(
"internal/platform/bluetooth_adapter_test.cc",
"internal/platform/byte_utils_test.cc",
"internal/platform/direct_executor_test.cc",
"internal/platform/borrowable_test.cc",
// simulation
"connections/implementation/offline_simulation_user.cc",
"connections/implementation/simulation_user.cc",
+2
View File
@@ -311,6 +311,7 @@ cc_library(
hdrs = [
"atomic_boolean.h",
"atomic_reference.h",
"borrowable.h",
"cancelable.h",
"cancelable_alarm.h",
"cancellable_task.h",
@@ -423,6 +424,7 @@ cc_test(
"bluetooth_adapter_test.cc",
"bluetooth_classic_test.cc",
"bluetooth_connection_info_test.cc",
"borrowable_test.cc",
"cancelable_alarm_test.cc",
"condition_variable_test.cc",
"count_down_latch_test.cc",
+161
View File
@@ -0,0 +1,161 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Defines an interface that allows a resource owner to share a resource with
// tasks running on other thread, and protects against use-after-free errors.
// When a resource is borrowed, the borrower has exclusive access to the
// resource. The framework guarantees that the resource stays alive as long it
// is borrowed. Typical usage:
//
// In resource owner:
// Lender<DataType> lender(DataType{});
// Give clients instances of `lender.GetBorrowable()`
//
// In clients:
// Borrowed<DataType> borrowed = borrowable.Borrow();
// if (borrowed) {
// borrowed->SomeMethod();
// }
//
// When `Lender` goes out of scope, all borrowable instances become invalid and
// attempts to `Borrow()` will fail safely.
#ifndef THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_BORROWABLE_H_
#define THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_BORROWABLE_H_
#include <memory>
#include <utility>
#include "internal/platform/mutex.h"
#include "internal/platform/mutex_lock.h"
namespace location {
namespace nearby {
template <typename T>
struct BorrowableSharedData {
explicit BorrowableSharedData(T resource) : resource(std::move(resource)) {}
Mutex mutex;
T resource ABSL_GUARDED_BY(mutex);
bool released ABSL_GUARDED_BY(mutex) = false;
};
// RAII style accessor to a borrowed object. While the caller holds an instance
// of `Borrowed`, they have exclusive access to the borrowed object. Borrowing
// can fail if the object that we are trying to borrow is already destroyed. The
// caller should check if `Borrowed` is valid before accessing.
template <typename T>
class Borrowed {
public:
Borrowed() = default;
explicit Borrowed(std::shared_ptr<BorrowableSharedData<T>> data)
: data_(data), lock_(std::make_unique<MutexLock>(&data->mutex)) {
if (data_->released) {
// We have been handed a half-destroyed object. The owner has already
// marked it as `released` but the shared_ptr was still valid. Let's
// release our copy right away.
lock_.reset();
data_.reset();
}
}
Borrowed(const Borrowed&) = delete;
Borrowed(Borrowed&&) = delete;
Borrowed& operator=(Borrowed other) = delete;
Borrowed& operator=(Borrowed&& other) = delete;
bool Ok() const { return data_ != nullptr; }
explicit operator bool() const { return Ok(); }
T* operator->() {
CHECK(Ok());
return &data_->resource;
}
T& operator*() {
CHECK(Ok());
return data_->resource;
}
private:
std::shared_ptr<BorrowableSharedData<T>> data_;
std::unique_ptr<MutexLock> lock_;
};
// A weak reference to an object that can be borrowed.
template <typename T>
class Borrowable {
public:
explicit Borrowable(std::weak_ptr<BorrowableSharedData<T>> resource)
: resource_(resource) {}
// Gives the caller exclusive access to the stored object. Borrowing fails if
// the object has already been destroyed. The call will block if another
// thread is already borrowing the object.
Borrowed<T> Borrow() const {
std::shared_ptr<BorrowableSharedData<T>> data = resource_.lock();
if (!data) {
return Borrowed<T>();
}
return Borrowed<T>(data);
}
private:
std::weak_ptr<BorrowableSharedData<T>> resource_;
};
// The owner of a resource that can be borrowed.
template <typename T>
class Lender {
public:
explicit Lender(T resource)
: shared_data_(
std::make_shared<BorrowableSharedData<T>>(std::move(resource))) {}
~Lender() { Release(); }
Borrowable<T> GetBorrowable() { return Borrowable<T>(shared_data_); }
// Destroys the stored resource. If the resource is currently borrowed, the
// call will block until the resource is returned.
// When the resource is released, calls to `Borrowable::Borrow()` will fail.
void Release() {
if (!shared_data_) {
return;
}
{
MutexLock lock(&shared_data_->mutex);
// We can't destroy `shared_data_` while holding the mutex inside it.
// Setting `released` fixes the race condition below.
// Thread 1 | Thread 2
// Lender::Release() | Borrowable::Borrow()
// | grabs shared_ptr<T>
// locks mutex |
// sets `released` |
// unlocks mutex |
// | locks mutex
// resets shared_data_ |
// | `released` is set, so borrow fails - unlocks
// | mutex, deletes shared_ptr and returns invalid
// | `Borrowed` object.
shared_data_->released = true;
}
shared_data_.reset();
}
private:
std::shared_ptr<BorrowableSharedData<T>> shared_data_;
};
} // namespace nearby
} // namespace location
#endif // THIRD_PARTY_NEARBY_INTERNAL_PLATFORM_BORROWABLE_H_
+131
View File
@@ -0,0 +1,131 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "internal/platform/borrowable.h"
#include "gmock/gmock.h"
#include "protobuf-matchers/protocol-buffer-matchers.h"
#include "gtest/gtest.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "internal/platform/count_down_latch.h"
#include "internal/platform/multi_thread_executor.h"
#include "internal/platform/single_thread_executor.h"
namespace location {
namespace nearby {
namespace {
constexpr int kDefaultValue = -1;
class Data {
public:
Data() = default;
Data(const Data &) = delete;
Data(Data &&) = default;
void SetValue(int value) { value_ = value; }
int GetValue() { return value_; }
private:
volatile int value_ = kDefaultValue;
};
TEST(Borrowable, CreateAndBorrow) {
constexpr int kValue = 1;
Lender<int> lender(kValue);
Borrowable<int> borrowable = lender.GetBorrowable();
Borrowed<int> borrowed = borrowable.Borrow();
ASSERT_TRUE(borrowed);
EXPECT_EQ(*borrowed, kValue);
}
TEST(Borrowable, BorrowAccessesOriginalObject) {
constexpr int kValue = 12;
Lender<Data> lender(Data{});
Borrowable<Data> borrowable = lender.GetBorrowable();
{
Borrowed<Data> borrowed = borrowable.Borrow();
ASSERT_TRUE(borrowed);
borrowed->SetValue(kValue);
}
EXPECT_EQ(borrowable.Borrow()->GetValue(), kValue);
}
TEST(Borrowable, BorrowAfterReleaseFails) {
constexpr int kValue = 1;
Lender<int> lender(kValue);
Borrowable<int> borrowable = lender.GetBorrowable();
lender.Release();
EXPECT_FALSE(borrowable.Borrow());
}
TEST(Borrowable, ReleaseWaitsForBorrowToEnd) {
constexpr int kValue = 1;
Data *data = new Data();
CountDownLatch latch(1);
Lender<Data *> lender(data);
Borrowable<Data *> borrowable = lender.GetBorrowable();
SingleThreadExecutor executor;
Borrowed<Data *> borrowed = borrowable.Borrow();
ASSERT_TRUE(borrowed);
executor.Execute([&]() {
latch.CountDown();
// Release() must wait until `borrowed` object goes out of scope.
lender.Release();
data->SetValue(kValue);
delete data;
});
EXPECT_TRUE(latch.Await().Ok());
// If `Release()` didn't wait, then we would access a dead object below. Short
// sleep makes the crash more likely.
absl::SleepFor(absl::Milliseconds(100));
EXPECT_EQ((*borrowed)->GetValue(), kDefaultValue);
}
TEST(Borrowable, BorrowIsExclusive) {
constexpr int kThreads = 15;
constexpr int kTasks = 30;
CountDownLatch latch(kTasks);
Lender<Data> lender(Data{});
MultiThreadExecutor executor(kThreads);
for (int i = 0; i < kTasks; i++) {
executor.Execute([&, value = i, borrowable = lender.GetBorrowable()]() {
latch.CountDown();
Borrowed<Data> borrowed = borrowable.Borrow();
ASSERT_TRUE(borrowed);
// Verify that a task has exclusive access to the borrowed object.
borrowed->SetValue(value);
// Sleep yields to other threads, which increases the chance of a race
// condition.
absl::SleepFor(absl::Milliseconds(1));
EXPECT_EQ(borrowed->GetValue(), value);
});
}
EXPECT_TRUE(latch.Await().Ok());
// Verify that the shared data was modified. We can't know for sure what the
// final value is.
EXPECT_NE(lender.GetBorrowable().Borrow()->GetValue(), kDefaultValue);
}
} // namespace
} // namespace nearby
} // namespace location