Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ add_subdirectory(common)
add_subdirectory(examples)
add_subdirectory(profile_showcase)

add_subdirectory(learning_asio)
add_subdirectory(learning_shared_ptr)
add_subdirectory(learning_deadlocks)
add_subdirectory(learning_move_semantics)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ Registered test counts match each module’s `CMakeLists.txt` (what `ctest` runs
| `learning_stl` | 5 | Containers, iterators, algorithms, invalidation |
| `learning_concurrency` | 5 | Threads, mutexes, lock-free basics, pools |
| `learning_polymorphism` | 6 | Virtual dispatch, virtual destructors, abstract interfaces, multiple inheritance, RTTI, CRTP / variant / concepts |
| `learning_asio` | 4 | Asio async model: `io_context`, timers, strands, handler-lifetime use-after-free (Boost.Asio; targets pinned to **C++11**) |
| `learning_design_patterns` | 4 | Creational, structural, behavioral, modern |
| `learning_templates` | 6 | Templates, SFINAE, variadics, traits |
| `learning_performance` | 6 | Profiling, cache layout, elision, SSO, constexpr, benchmarks |
Expand Down
3 changes: 2 additions & 1 deletion docs/LEARNING_PATH.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Each row is a `learning_*` directory. **Registered tests** are targets listed in
| [learning_stl](../learning_stl/) | 5 | Containers, iterators, algorithms, comparators, invalidation. |
| [learning_concurrency](../learning_concurrency/) | 5 | Requires `Threads::Threads`. |
| [learning_polymorphism](../learning_polymorphism/) | 6 | Virtual dispatch, destructors, abstract interfaces, diamonds, RTTI, CRTP/variant/concepts. |
| [learning_asio](../learning_asio/) | 4 | Asio async execution model: `io_context`, `steady_timer`, strands, and a handler-lifetime use-after-free lab. Uses Boost.Asio; targets pinned to **C++11** (overrides project C++20). Strand suite is `gcc-tsan`-clean; the UAF repro is a `DISABLED_` test surfaced via `gcc-asan`. |
| [learning_design_patterns](../learning_design_patterns/) | 4 | Creational, structural, behavioral, modern idioms. |
| [learning_templates](../learning_templates/) | 6 | Specialization, SFINAE, variadics, traits, etc. |
| [learning_performance](../learning_performance/) | 6 | Profiling, cache layout, elision, SSO, constexpr, benchmarks. |
Expand All @@ -51,7 +52,7 @@ Dependencies are soft; adjust for your goals.
3. **Move semantics** — `learning_move_semantics` (pairs well after shared_ptr).
4. **Modern syntax and STL** — `learning_modern_cpp`, `learning_stl`.
5. **Error handling and templates** — `learning_error_handling`, `learning_templates` (order flexible).
6. **Concurrency** — `learning_concurrency` after you are comfortable with mutex/RAII basics.
6. **Concurrency** — `learning_concurrency` after you are comfortable with mutex/RAII basics. Follow with `learning_asio` to see how an event loop (`io_context`) and strands serialize work without explicit locks.
7. **Deadlocks (advanced lab)** — Work through `learning_deadlocks` scenarios one file at a time; some cases are intentionally disabled until remaining fixes are completed.
8. **Polymorphism** — `learning_polymorphism` after move semantics; lays the dispatch/inheritance groundwork the patterns module assumes.
9. **Patterns, performance, debugging** — `learning_design_patterns`, `learning_performance`, `learning_debugging` in any order that matches your projects.
Expand Down
24 changes: 24 additions & 0 deletions learning_asio/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Asio test suite (Boost.Asio) — authored to a C++11 minimum.
#
# Each target is pinned to C++11 (overriding the project-wide C++20) so the
# learner-facing code stays within classic, callback-based Asio idioms. Boost
# is header-only here: Asio and boost::system need no compiled libraries.

find_package(Boost REQUIRED)

set(asio_tests
test_io_context_basics
test_timers
test_post_dispatch_strand
test_timer_lifetime_uaf
test_multi_timer_ownership
)

foreach(asio_test ${asio_tests})
add_learning_test(${asio_test} tests/${asio_test}.cpp instrumentation Boost::headers Threads::Threads)
set_target_properties(${asio_test} PROPERTIES
CXX_STANDARD 11
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
)
endforeach()
217 changes: 217 additions & 0 deletions learning_asio/tests/test_io_context_basics.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// Test Suite: io_context Basics
// Estimated Time: 2 hours
// Difficulty: Easy
// C++ Standard: C++11 (Boost.Asio, classic callback style)
//
// The io_context is Asio's execution engine: it owns a queue of completion
// handlers and runs them on whatever thread calls run(). Nothing in Asio
// happens "in the background" by magic -- a handler only executes while some
// thread is inside run()/poll()/run_one(). These scenarios make that
// scheduling visible through EventLog.
//
// Library-evolution note: the free functions boost::asio::post/dispatch/defer
// and the io_context type itself arrived in Boost 1.66 (2017). Before that the
// type was boost::asio::io_service and you called ctx.post(handler) as a member
// function. The scheduling semantics below are the same in both eras.

#include "instrumentation.h"

#include <boost/asio.hpp>

#include <gtest/gtest.h>
#include <cstddef>
#include <string>
#include <vector>

class IoContextBasicsTest : public ::testing::Test
{
protected:
void SetUp() override
{
EventLog::instance().clear();
}
};

// ============================================================================
// Scenario 1: post() Queues Work That Only Runs Inside run() (Easy)
// ============================================================================

TEST_F(IoContextBasicsTest, PostQueuesHandlersUntilRunDrainsThemFifo)
{
boost::asio::io_context ctx;

boost::asio::post(ctx, [] { EventLog::instance().record("h1"); });
boost::asio::post(ctx, [] { EventLog::instance().record("h2"); });
boost::asio::post(ctx, [] { EventLog::instance().record("h3"); });

// Q: At this point three handlers have been posted but run() has not been
// called. How many have executed, and what EventLog evidence confirms it?
// A:
// R:
EXPECT_EQ(EventLog::instance().events().size(), 0u);

const std::size_t executed = ctx.run();

// Q: post() defers work to the thread that calls run(). In what order are
// the three handlers executed, and which observable signal proves that
// ordering rather than your assumption about it?
// A:
// R:
const std::vector<std::string> ev = EventLog::instance().events();
ASSERT_EQ(ev.size(), 3u);
EXPECT_EQ(ev[0], "h1");
EXPECT_EQ(ev[1], "h2");
EXPECT_EQ(ev[2], "h3");

// Q: run() returns a std::size_t. What does that number count here?
// A:
// R:
EXPECT_EQ(executed, 3u);
}

// ============================================================================
// Scenario 2: run() Returns the Number of Handlers It Executed (Easy)
// ============================================================================

TEST_F(IoContextBasicsTest, RunReturnsHandlerCountAndZeroWhenIdle)
{
boost::asio::io_context ctx;

// Q: No work has been posted. Why does run() return immediately instead of
// blocking forever waiting for something to do?
// A:
// R:
const std::size_t none = ctx.run();
EXPECT_EQ(none, 0u);

// Nothing executed, so EventLog is still empty.
EXPECT_EQ(EventLog::instance().events().size(), 0u);
}

// ============================================================================
// Scenario 3: A Drained Context Is "stopped" Until restart() (Moderate)
// ============================================================================

TEST_F(IoContextBasicsTest, DrainedContextRequiresRestartBeforeReuse)
{
boost::asio::io_context ctx;

boost::asio::post(ctx, [] { EventLog::instance().record("first"); });
const std::size_t first_run = ctx.run();
EXPECT_EQ(first_run, 1u);
EXPECT_EQ(EventLog::instance().count_events("first"), 1u);

// Q: After run() drains all work, what does ctx.stopped() report, and what
// does that imply for the next call to run()?
// A:
// R:
EXPECT_TRUE(ctx.stopped());

// Post more work, then try to run WITHOUT restarting first.
boost::asio::post(ctx, [] { EventLog::instance().record("second"); });
const std::size_t reused_without_restart = ctx.run();

// Q: "second" was posted before this run(). Why does run() return 0 and why
// does EventLog show "second" did NOT execute yet?
// A:
// R:
EXPECT_EQ(reused_without_restart, 0u);
EXPECT_EQ(EventLog::instance().count_events("second"), 0u);

// restart() clears the stopped state so the context can run again. The work
// posted earlier was never discarded -- it was only waiting for a live run.
ctx.restart();
const std::size_t after_restart = ctx.run();

// Q: run() now returns 1 and "second" finally executes. What single call
// made the difference, and what state did it reset?
// A:
// R:
EXPECT_EQ(after_restart, 1u);
EXPECT_EQ(EventLog::instance().count_events("second"), 1u);
}

// ============================================================================
// Scenario 4: dispatch() May Run Inline; post() Always Defers (Moderate)
// ============================================================================

TEST_F(IoContextBasicsTest, DispatchRunsInlineWhilePostDefers)
{
boost::asio::io_context ctx;

// The outer handler runs while we are inside ctx.run() below. From inside
// it, we both post() and dispatch() additional handlers.
boost::asio::post(ctx, [&ctx] {
EventLog::instance().record("outer-start");
boost::asio::post(ctx, [] { EventLog::instance().record("posted"); });
boost::asio::dispatch(ctx, [] { EventLog::instance().record("dispatched"); });
EventLog::instance().record("outer-end");
});

ctx.run();

// Q: dispatch() is allowed to invoke its handler immediately when the caller
// is already running on the io_context. Given that, where does
// "dispatched" fall relative to "outer-end", and where does "posted"
// fall? Predict the full order before reading the expectations below.
// A:
// R:
const std::vector<std::string> ev = EventLog::instance().events();
ASSERT_EQ(ev.size(), 4u);
EXPECT_EQ(ev[0], "outer-start");
EXPECT_EQ(ev[1], "dispatched");
EXPECT_EQ(ev[2], "outer-end");
EXPECT_EQ(ev[3], "posted");

// Q: What observable difference would you expect if "dispatched" were
// changed to another post()? Which two entries would swap?
// A:
// R:

// TODO (learner): Change the inner boost::asio::dispatch(...) to
// boost::asio::post(...) and re-run. Record the new order in the // A:
// lines above and explain why dispatch's inline shortcut disappeared.
}

// ============================================================================
// Scenario 5: poll() Runs Only Ready Work and Returns Without Blocking (Hard)
// ============================================================================

TEST_F(IoContextBasicsTest, PollRunsReadyHandlersThenReturns)
{
boost::asio::io_context ctx;

// A handler that, when run, posts a *second* handler. The second one is not
// "ready" until the first one has executed and queued it.
boost::asio::post(ctx, [&ctx] {
EventLog::instance().record("stage1");
boost::asio::post(ctx, [] { EventLog::instance().record("stage2"); });
});

// Q: poll() executes all handlers that are ready *right now* and then
// returns. "stage2" is only queued once "stage1" runs. Does a single
// poll() reach "stage2"? Predict the count it returns.
// A:
// R:
const std::size_t first_poll = ctx.poll();

// Both stage1 and stage2 ran in this poll: once stage1 executed, stage2 was
// queued and was still "ready" within the same poll() pass.
EXPECT_EQ(first_poll, 2u);
EXPECT_EQ(EventLog::instance().count_events("stage1"), 1u);
EXPECT_EQ(EventLog::instance().count_events("stage2"), 1u);

// Q: poll() differs from run() in what it does when *no* handler is ready.
// With the queue now empty, what does a second poll() return, and why is
// that the key behavioral contrast with run() in a blocking design?
// A:
// R:
ctx.restart();
const std::size_t second_poll = ctx.poll();
EXPECT_EQ(second_poll, 0u);

// TODO (learner): Replace the first poll() with run() and confirm the
// EventLog counts are identical here. Then describe one situation (hint:
// long-lived asynchronous work such as a timer or socket) where run() and
// poll() would behave very differently. Record your answer in // A: above.
}
Loading
Loading