This is an automated email from the ASF dual-hosted git repository.
apitrou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/main by this push:
new c4d17fdbf3 GH-44846: [C++] Fix thread-unsafe access in
ConcurrentQueue::UnsyncFront (#44849)
c4d17fdbf3 is described below
commit c4d17fdbf37448cd361341a40c642f4a918f0f2e
Author: Antoine Pitrou <[email protected]>
AuthorDate: Tue Nov 26 10:36:41 2024 +0100
GH-44846: [C++] Fix thread-unsafe access in ConcurrentQueue::UnsyncFront
(#44849)
### Rationale for this change
The `UnsyncFront` method claims that it can access a `std::deque`'s front
element without synchronizing with another thread that would call
`std::deque::push_back`, but `std::deque::front` can actually be implemented in
terms of `std::deque::begin` while `std::deque::push_back` is specified to
invalidate iterators.
In the end, `UnsyncFront` is concretely thread-unsafe even though it might
ideally be thread-safe. This shows up as occasional Thread Sanitizer failures.
### What changes are included in this PR?
Replace `UnsyncFront` with a thread-safe `Front` method.
### Are these changes tested?
Yes.
### Are there any user-facing changes?
No.
* GitHub Issue: #44846
Authored-by: Antoine Pitrou <[email protected]>
Signed-off-by: Antoine Pitrou <[email protected]>
---
cpp/src/arrow/acero/asof_join_node.cc | 9 ++++-----
cpp/src/arrow/acero/concurrent_queue_internal.h | 20 +++++++++++---------
cpp/src/arrow/acero/sorted_merge_node.cc | 6 +++---
3 files changed, 18 insertions(+), 17 deletions(-)
diff --git a/cpp/src/arrow/acero/asof_join_node.cc
b/cpp/src/arrow/acero/asof_join_node.cc
index 92e404b207..3ab976e671 100644
--- a/cpp/src/arrow/acero/asof_join_node.cc
+++ b/cpp/src/arrow/acero/asof_join_node.cc
@@ -567,7 +567,7 @@ class InputState : public
util::SerialSequencingQueue::Processor {
// Gets latest batch (precondition: must not be empty)
const std::shared_ptr<arrow::RecordBatch>& GetLatestBatch() const {
- return queue_.UnsyncFront();
+ return queue_.Front();
}
#define LATEST_VAL_CASE(id, val) \
@@ -634,15 +634,14 @@ class InputState : public
util::SerialSequencingQueue::Processor {
}
latest_time_ = next_time;
// If we have an active batch
- if (++latest_ref_row_ >= (row_index_t)queue_.UnsyncFront()->num_rows()) {
+ if (++latest_ref_row_ >= (row_index_t)queue_.Front()->num_rows()) {
// hit the end of the batch, need to get the next batch if possible.
++batches_processed_;
latest_ref_row_ = 0;
have_active_batch &= !queue_.TryPop();
if (have_active_batch) {
- DCHECK_GT(queue_.UnsyncFront()->num_rows(), 0); // empty batches
disallowed
- memo_.UpdateTime(GetTime(queue_.UnsyncFront().get(), time_type_id_,
- time_col_index_,
+ DCHECK_GT(queue_.Front()->num_rows(), 0); // empty batches
disallowed
+ memo_.UpdateTime(GetTime(queue_.Front().get(), time_type_id_,
time_col_index_,
0)); // time changed
}
}
diff --git a/cpp/src/arrow/acero/concurrent_queue_internal.h
b/cpp/src/arrow/acero/concurrent_queue_internal.h
index f530394187..20ec2089be 100644
--- a/cpp/src/arrow/acero/concurrent_queue_internal.h
+++ b/cpp/src/arrow/acero/concurrent_queue_internal.h
@@ -65,17 +65,19 @@ class ConcurrentQueue {
return queue_.empty();
}
- // Un-synchronized access to front
- // For this to be "safe":
- // 1) the caller logically guarantees that queue is not empty
- // 2) pop/try_pop cannot be called concurrently with this
- const T& UnsyncFront() const { return queue_.front(); }
-
- size_t UnsyncSize() const { return queue_.size(); }
+ const T& Front() const {
+ // Need to lock the queue because `front()` may be implemented in terms
+ // of `begin()`, which isn't safe with concurrent calls to e.g. `push()`.
+ // (see GH-44846)
+ std::unique_lock<std::mutex> lock(mutex_);
+ return queue_.front();
+ }
protected:
std::mutex& GetMutex() { return mutex_; }
+ size_t SizeUnlocked() const { return queue_.size(); }
+
T PopUnlocked() {
auto item = queue_.front();
queue_.pop();
@@ -111,12 +113,12 @@ class BackpressureConcurrentQueue : public
ConcurrentQueue<T> {
private:
struct DoHandle {
explicit DoHandle(BackpressureConcurrentQueue& queue)
- : queue_(queue), start_size_(queue_.UnsyncSize()) {}
+ : queue_(queue), start_size_(queue_.SizeUnlocked()) {}
~DoHandle() {
// unsynced access is safe since DoHandle is internally only used when
the
// lock is held
- size_t end_size = queue_.UnsyncSize();
+ size_t end_size = queue_.SizeUnlocked();
queue_.handler_.Handle(start_size_, end_size);
}
diff --git a/cpp/src/arrow/acero/sorted_merge_node.cc
b/cpp/src/arrow/acero/sorted_merge_node.cc
index 2845383cee..c49aca17fb 100644
--- a/cpp/src/arrow/acero/sorted_merge_node.cc
+++ b/cpp/src/arrow/acero/sorted_merge_node.cc
@@ -145,7 +145,7 @@ class InputState {
// Gets latest batch (precondition: must not be empty)
const std::shared_ptr<arrow::RecordBatch>& GetLatestBatch() const {
- return queue_.UnsyncFront();
+ return queue_.Front();
}
#define LATEST_VAL_CASE(id, val) \
@@ -178,7 +178,7 @@ class InputState {
row_index_t start = latest_ref_row_;
row_index_t end = latest_ref_row_;
time_unit_t startTime = GetLatestTime();
- std::shared_ptr<arrow::RecordBatch> batch = queue_.UnsyncFront();
+ std::shared_ptr<arrow::RecordBatch> batch = queue_.Front();
auto rows_in_batch = (row_index_t)batch->num_rows();
while (GetLatestTime() == startTime) {
@@ -190,7 +190,7 @@ class InputState {
latest_ref_row_ = 0;
active &= !queue_.TryPop();
if (active) {
- DCHECK_GT(queue_.UnsyncFront()->num_rows(),
+ DCHECK_GT(queue_.Front()->num_rows(),
0); // empty batches disallowed, sanity check
}
break;