Skip to content

Commit 53e5b05

Browse files
authored
Ensure cancellation::none implementation does not allocate (#56)
1 parent 7c6be8a commit 53e5b05

File tree

3 files changed

+78
-12
lines changed

3 files changed

+78
-12
lines changed

CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,20 @@ if(ARCANA_TESTS)
172172
set_property(TARGET gmock_main PROPERTY FOLDER Dependencies)
173173
set_property(TARGET gtest PROPERTY FOLDER Dependencies)
174174
set_property(TARGET gtest_main PROPERTY FOLDER Dependencies)
175+
176+
if(WIN32)
177+
# Standalone executable that uses _CrtDumpMemoryLeaks to verify
178+
# cancellation::none() does not leak due to the no_destroy pattern.
179+
add_executable(cancellation_leak_test
180+
"Source/Windows.Test/Threading/CancellationMemoryLeakTest.cpp")
181+
182+
target_include_directories(cancellation_leak_test
183+
PRIVATE "Source/Shared"
184+
PRIVATE "Source/Windows")
185+
186+
target_link_libraries(cancellation_leak_test
187+
PRIVATE arcana)
188+
189+
add_test(NAME CancellationMemoryLeakTest COMMAND cancellation_leak_test)
190+
endif()
175191
endif()

Source/Shared/arcana/threading/cancellation.h

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
#include "arcana/containers/ticketed_collection.h"
44

55
#include <algorithm>
6-
#include <cassert>
76
#include <atomic>
8-
#include <memory>
9-
#include <vector>
10-
117
#include <functional>
8+
#include <cassert>
9+
#include <memory>
10+
#include <optional>
1211
#include <vector>
1312

1413
namespace arcana
@@ -25,7 +24,7 @@ namespace arcana
2524

2625
virtual bool cancelled() const = 0;
2726

28-
void throw_if_cancellation_requested()
27+
void throw_if_cancellation_requested() const
2928
{
3029
if (cancelled())
3130
{
@@ -60,22 +59,27 @@ namespace arcana
6059

6160
virtual ~cancellation()
6261
{
63-
assert(m_listeners.empty() && "you're destroying the listener collection and you still have listeners");
62+
assert((!m_listeners.has_value() || m_listeners->empty()) && "you're destroying the listener collection and you still have listeners");
6463
}
6564

6665
template<typename CallableT>
6766
ticket internal_add_listener(CallableT&& callback, std::function<void()>& copied)
6867
{
6968
std::lock_guard<std::mutex> guard{ m_mutex };
7069

70+
if (!m_listeners.has_value())
71+
{
72+
m_listeners.emplace();
73+
}
74+
7175
if (m_signaled)
7276
{
7377
copied = std::forward<CallableT>(callback);
74-
return m_listeners.insert(copied, m_mutex);
78+
return m_listeners->insert(copied, m_mutex);
7579
}
7680
else
7781
{
78-
return m_listeners.insert(std::forward<CallableT>(callback), m_mutex);
82+
return m_listeners->insert(std::forward<CallableT>(callback), m_mutex);
7983
}
8084
}
8185

@@ -86,8 +90,11 @@ namespace arcana
8690
{
8791
std::lock_guard<std::mutex> guard{ m_mutex };
8892

89-
listeners.reserve(m_listeners.size());
90-
std::copy(m_listeners.begin(), m_listeners.end(), std::back_inserter(listeners));
93+
if (m_listeners.has_value())
94+
{
95+
listeners.reserve(m_listeners->size());
96+
std::copy(m_listeners->begin(), m_listeners->end(), std::back_inserter(listeners));
97+
}
9198

9299
m_signaled = true;
93100
}
@@ -97,13 +104,19 @@ namespace arcana
97104
// then a child function does the same, the child
98105
// cancellation runs first. This avoids ownership
99106
// semantic issues.
100-
for(auto itr = listeners.rbegin(); itr != listeners.rend(); ++itr)
107+
for (auto itr = listeners.rbegin(); itr != listeners.rend(); ++itr)
108+
{
101109
(*itr)();
110+
}
102111
}
103112

104113
private:
105114
mutable std::mutex m_mutex;
106-
ticketed_collection<std::function<void()>> m_listeners;
115+
// std::optional is used here because the none() singleton is held in a no_destroy wrapper whose destructor
116+
// never runs. The underlying std::vector inside ticketed_collection allocates memory in debug builds on some
117+
// platforms, and that allocation would be a leak since no_destroy never frees it. Using std::optional will
118+
// delay the allocation until it's actually needed, which is never for the none() singleton.
119+
std::optional<ticketed_collection<std::function<void()>>> m_listeners;
107120
bool m_signaled = false;
108121
};
109122

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// Copyright (C) Microsoft Corporation. All rights reserved.
3+
//
4+
5+
// Standalone executable that verifies cancellation::none() does not cause memory
6+
// leaks. cancellation::none() is backed by a no_destroy wrapper whose destructor
7+
// never runs. If the cancellation_source constructor allocates heap memory (e.g.
8+
// an eagerly-constructed std::vector inside ticketed_collection), that memory can
9+
// never be freed and will be reported by _CrtDumpMemoryLeaks.
10+
//
11+
// Including cancellation.h brings the no_destroy inline variable definition into
12+
// this translation unit, so it is constructed during static initialization before
13+
// main runs. We call _CrtDumpMemoryLeaks explicitly and return a non-zero exit
14+
// code so that CTest treats the test as failed when leaks are detected.
15+
16+
#include <crtdbg.h>
17+
#include <cstdlib>
18+
#include <iostream>
19+
20+
#include <arcana/threading/cancellation.h>
21+
22+
int main()
23+
{
24+
#ifdef _DEBUG
25+
// _CrtDumpMemoryLeaks reports all CRT debug-heap blocks still allocated.
26+
if (_CrtDumpMemoryLeaks())
27+
{
28+
std::cerr << "Memory leaks detected!" << std::endl;
29+
return EXIT_FAILURE;
30+
}
31+
32+
return EXIT_SUCCESS;
33+
#else
34+
std::cerr << "Skipping: _CrtDumpMemoryLeaks is a no-op in Release builds." << std::endl;
35+
return EXIT_SUCCESS;
36+
#endif
37+
}

0 commit comments

Comments
 (0)