Skip to content

Commit 911509e

Browse files
Document try_enqueue implicit producer index limit and add unit test (#418)
try_enqueue() fails around BLOCK_SIZE * IMPLICIT_INITIAL_INDEX_SIZE elements per implicit producer because the block index (not the blocks) must grow, which requires allocation that try_enqueue refuses to do. Pre-allocating blocks via the constructor does not help. Added documentation comments near IMPLICIT_INITIAL_INDEX_SIZE in the default traits and near the try_enqueue method explaining the limit and workarounds. Added implicit_producer_index_limit unit test demonstrating the issue and verifying both workarounds (enqueue() and larger IMPLICIT_INITIAL_INDEX_SIZE). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 42d3d07 commit 911509e

2 files changed

Lines changed: 86 additions & 0 deletions

File tree

concurrentqueue.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,12 @@ struct ConcurrentQueueDefaultTraits
355355

356356
// How many full blocks can be expected for a single implicit producer? This should
357357
// reflect that number's maximum for optimal performance. Must be a power of 2.
358+
// Note: This controls the maximum number of elements that can be enqueued by a
359+
// single implicit producer when using try_enqueue (which does not allocate).
360+
// The limit is BLOCK_SIZE * IMPLICIT_INITIAL_INDEX_SIZE elements; beyond that,
361+
// the block index needs to grow, which requires allocation, causing try_enqueue
362+
// to fail. Increase this value (or use enqueue(), which can allocate) if you need
363+
// more capacity per implicit producer. See also issue #418.
358364
static const size_t IMPLICIT_INITIAL_INDEX_SIZE = 32;
359365

360366
// The initial size of the hash table mapping thread IDs to implicit producers.
@@ -1063,6 +1069,11 @@ class ConcurrentQueue
10631069
// Does not allocate memory. Fails if not enough room to enqueue (or implicit
10641070
// production is disabled because Traits::INITIAL_IMPLICIT_PRODUCER_HASH_SIZE
10651071
// is 0).
1072+
// Note: For implicit producers (no token), the maximum number of elements that
1073+
// can be held is BLOCK_SIZE * IMPLICIT_INITIAL_INDEX_SIZE before the block
1074+
// index must grow (which requires allocation and causes try_enqueue to fail).
1075+
// Pre-allocating blocks via the constructor does not increase the index size.
1076+
// Increase IMPLICIT_INITIAL_INDEX_SIZE in your traits, or use enqueue() instead.
10661077
// Thread-safe.
10671078
inline bool try_enqueue(T const& item)
10681079
{

tests/unittests/unittests.cpp

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,18 @@ struct LargeTraits : public MallocTrackingTraits
136136
static const size_t IMPLICIT_INITIAL_INDEX_SIZE = 128;
137137
};
138138

139+
struct SmallImplicitIndexTraits : public MallocTrackingTraits
140+
{
141+
static const size_t BLOCK_SIZE = 4;
142+
static const size_t IMPLICIT_INITIAL_INDEX_SIZE = 4;
143+
};
144+
145+
struct LargerImplicitIndexTraits : public MallocTrackingTraits
146+
{
147+
static const size_t BLOCK_SIZE = 4;
148+
static const size_t IMPLICIT_INITIAL_INDEX_SIZE = 16;
149+
};
150+
139151
// Note: Not thread safe!
140152
struct Foo
141153
{
@@ -375,6 +387,7 @@ class ConcurrentQueueTests : public TestClass<ConcurrentQueueTests>
375387

376388
REGISTER_TEST(explicit_strings_threaded);
377389
REGISTER_TEST(large_traits);
390+
REGISTER_TEST(implicit_producer_index_limit);
378391
}
379392

380393
bool postTest(bool testSucceeded) override
@@ -5031,6 +5044,68 @@ class ConcurrentQueueTests : public TestClass<ConcurrentQueueTests>
50315044
return true;
50325045
}
50335046

5047+
bool implicit_producer_index_limit()
5048+
{
5049+
// Issue #418: try_enqueue() fails around BLOCK_SIZE * IMPLICIT_INITIAL_INDEX_SIZE
5050+
// elements even when blocks have been pre-allocated, because the block index
5051+
// (not the blocks themselves) needs to grow, which requires allocation that
5052+
// try_enqueue refuses to do.
5053+
5054+
// SmallImplicitIndexTraits: BLOCK_SIZE=4, IMPLICIT_INITIAL_INDEX_SIZE=4, limit=16
5055+
// LargerImplicitIndexTraits: BLOCK_SIZE=4, IMPLICIT_INITIAL_INDEX_SIZE=16, limit=64
5056+
5057+
{
5058+
// Demonstrate the limit: try_enqueue fails at BLOCK_SIZE * IMPLICIT_INITIAL_INDEX_SIZE
5059+
const int limit = (int)(SmallImplicitIndexTraits::BLOCK_SIZE * SmallImplicitIndexTraits::IMPLICIT_INITIAL_INDEX_SIZE);
5060+
ConcurrentQueue<int, SmallImplicitIndexTraits> q(limit + 64); // Pre-allocate plenty of blocks
5061+
5062+
int successCount = 0;
5063+
for (int i = 0; i < limit + 64; ++i) {
5064+
if (!q.try_enqueue(i))
5065+
break;
5066+
++successCount;
5067+
}
5068+
// try_enqueue should stop succeeding at the index limit
5069+
ASSERT_OR_FAIL(successCount == limit);
5070+
}
5071+
5072+
{
5073+
// Fix #1: enqueue() (which can allocate) works past the limit
5074+
const int limit = (int)(SmallImplicitIndexTraits::BLOCK_SIZE * SmallImplicitIndexTraits::IMPLICIT_INITIAL_INDEX_SIZE);
5075+
ConcurrentQueue<int, SmallImplicitIndexTraits> q(limit + 64);
5076+
5077+
for (int i = 0; i < limit + 64; ++i) {
5078+
ASSERT_OR_FAIL(q.enqueue(i));
5079+
}
5080+
// Verify all elements are dequeued correctly
5081+
int item;
5082+
for (int i = 0; i < limit + 64; ++i) {
5083+
ASSERT_OR_FAIL(q.try_dequeue(item));
5084+
ASSERT_OR_FAIL(item == i);
5085+
}
5086+
ASSERT_OR_FAIL(!q.try_dequeue(item));
5087+
}
5088+
5089+
{
5090+
// Fix #2: larger IMPLICIT_INITIAL_INDEX_SIZE allows more try_enqueue calls
5091+
const int small_limit = (int)(SmallImplicitIndexTraits::BLOCK_SIZE * SmallImplicitIndexTraits::IMPLICIT_INITIAL_INDEX_SIZE); // 16
5092+
const int large_limit = (int)(LargerImplicitIndexTraits::BLOCK_SIZE * LargerImplicitIndexTraits::IMPLICIT_INITIAL_INDEX_SIZE); // 64
5093+
ConcurrentQueue<int, LargerImplicitIndexTraits> q(large_limit + 64);
5094+
5095+
int successCount = 0;
5096+
for (int i = 0; i < large_limit + 64; ++i) {
5097+
if (!q.try_enqueue(i))
5098+
break;
5099+
++successCount;
5100+
}
5101+
// Should succeed well past the old small limit
5102+
ASSERT_OR_FAIL(successCount > small_limit);
5103+
ASSERT_OR_FAIL(successCount == large_limit);
5104+
}
5105+
5106+
return true;
5107+
}
5108+
50345109
bool large_traits()
50355110
{
50365111
union Elem { uint32_t x; char dummy[156]; };

0 commit comments

Comments
 (0)