Skip to content

Commit 0f64468

Browse files
committed
little correction
1 parent fba01ff commit 0f64468

5 files changed

Lines changed: 54 additions & 34 deletions

File tree

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -470,9 +470,19 @@ Common HTTPS errors:
470470

471471
## Thread Safety
472472

473-
- AsyncTCP callbacks run on the lwIP/WiFi task while `loop()` (or the auto-loop task) runs on a different core. Since v2.1 the library guards against use-after-free by holding `RequestContext` in `std::shared_ptr` (captured by transport lambdas) and using an `std::atomic<bool> cancelled` flag that is set before cleanup erases the context.
474-
- On ESP32 with `ASYNC_HTTP_ENABLE_AUTOLOOP`, a recursive mutex protects shared containers (`_activeRequests`, `_pendingQueue`, etc.).
475-
- Callbacks are still executed in the context of the network event loop — keep them lightweight and non-blocking.
473+
The client is internally synchronized on ESP32. The model has three actors:
474+
475+
- **lwIP/WiFi task (`tcpip_thread`)** — where AsyncTCP fires data/disconnect/error callbacks. These handlers do *no* heavy work and never take the client lock: they only copy the payload into a thread-safe `WorkerBuffer` (PSRAM-backed) and return immediately, so the network task is never blocked.
476+
- **Worker task (`AsyncHttpWorker`)** — drains the `WorkerBuffer`, takes the recursive client mutex, runs the actual response parsing (`handleData`/`handleDisconnect`/`handleTransportError`), then releases the lock and dispatches user callbacks.
477+
- **Your task(s)** — the public API (`get`, `post`, `setHeader`, …) takes the same recursive mutex, so configuration and request submission are safe to call from any task.
478+
479+
Key guarantees and details:
480+
481+
- **Single recursive mutex** (`_reqMutex`) protects all shared state (`_activeRequests`, `_pendingQueue`, headers, etc.). It is recursive so a user callback may re-enter the client (start/abort a request, change config) without deadlocking. Ownership is queried via FreeRTOS's native `xSemaphoreGetMutexHolder()` — no manual depth bookkeeping.
482+
- **User callbacks run outside the lock.** Success/error/body-chunk callbacks are queued and dispatched by `dispatchCallbacks()` after the mutex is released, so you can safely call client methods from inside a callback. A callback may run either on the worker task or on the task that submitted work — do not assume a fixed thread, and keep callbacks non-blocking (a blocking callback can stall the worker).
483+
- **Use-after-free protection.** `RequestContext` is held in `std::shared_ptr` captured by the transport lambdas, and an `std::atomic<bool> cancelled` flag (set before cleanup erases the context) makes in-flight callbacks no-op safely.
484+
- **Back-pressure.** If the `WorkerBuffer` reaches its hard ceiling (`ASYNC_HTTP_RING_BUFFER_MAX`, default 64 KiB) the transport is closed, which surfaces as a normal disconnect/error on that request.
485+
- **Clean shutdown.** The destructor does not `vTaskDelete()` the worker (which could be mid-parse holding the lock). It sets an exit flag, wakes the worker, and waits for it to leave its loop and self-delete before destroying the mutex. Note: destroying an `AsyncHttpClient` while requests are still in flight is still best avoided — abort outstanding requests (or let them finish) before destruction, since open transports hold lambdas that reference the client.
476486

477487
## Dependencies
478488

src/AsyncHttpClient.cpp

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ AsyncHttpClient::AsyncHttpClient()
3535
_reqMutex = xSemaphoreCreateRecursiveMutex();
3636
#endif
3737
#ifdef ARDUINO_ARCH_ESP32
38+
// Lets the destructor block until the worker has actually left its loop before we tear down state.
39+
_workerDoneSem = xSemaphoreCreateBinary();
3840
xTaskCreatePinnedToCore(_workerTaskThunk, // entry
3941
"AsyncHttpWorker", // name
4042
8192, // stack words
@@ -58,13 +60,6 @@ AsyncHttpClient::AsyncHttpClient()
5860
}
5961

6062
AsyncHttpClient::~AsyncHttpClient() {
61-
#ifdef ARDUINO_ARCH_ESP32
62-
if (_workerTaskHandle) {
63-
TaskHandle_t h = _workerTaskHandle;
64-
_workerTaskHandle = nullptr;
65-
vTaskDelete(h);
66-
}
67-
#endif
6863
#if !ASYNC_TCP_HAS_TIMEOUT && defined(ARDUINO_ARCH_ESP32) && defined(ASYNC_HTTP_ENABLE_AUTOLOOP)
6964
if (_autoLoopTaskHandle) {
7065
TaskHandle_t h = _autoLoopTaskHandle;
@@ -73,6 +68,20 @@ AsyncHttpClient::~AsyncHttpClient() {
7368
}
7469
#endif
7570
#ifdef ARDUINO_ARCH_ESP32
71+
// Stop the worker cooperatively: it may be holding _reqMutex while in handleData(), so we must
72+
// not vTaskDelete() it from here. Signal exit, wake it, and wait until it has left its loop
73+
// (and self-deleted) before destroying the mutex it could otherwise still be holding.
74+
if (_workerTaskHandle) {
75+
_workerShouldExit.store(true, std::memory_order_release);
76+
_workerBuffer.wake(); // unblock waitForItem()
77+
if (_workerDoneSem)
78+
xSemaphoreTake(_workerDoneSem, portMAX_DELAY);
79+
_workerTaskHandle = nullptr;
80+
}
81+
if (_workerDoneSem) {
82+
vSemaphoreDelete(_workerDoneSem);
83+
_workerDoneSem = nullptr;
84+
}
7685
if (_reqMutex) {
7786
vSemaphoreDelete(_reqMutex);
7887
_reqMutex = nullptr;
@@ -86,29 +95,12 @@ AsyncHttpClient::~AsyncHttpClient() {
8695

8796
#ifdef ARDUINO_ARCH_ESP32
8897
void AsyncHttpClient::lock() const {
89-
if (_reqMutex) {
98+
if (_reqMutex)
9099
xSemaphoreTakeRecursive(_reqMutex, portMAX_DELAY);
91-
TaskHandle_t current = xTaskGetCurrentTaskHandle();
92-
if (_reqMutexOwner.load(std::memory_order_relaxed) == current) {
93-
_reqMutexDepth.fetch_add(1, std::memory_order_relaxed);
94-
} else {
95-
_reqMutexOwner.store(current, std::memory_order_relaxed);
96-
_reqMutexDepth.store(1, std::memory_order_relaxed);
97-
}
98-
}
99100
}
100101
void AsyncHttpClient::unlock() const {
101-
if (_reqMutex) {
102-
TaskHandle_t current = xTaskGetCurrentTaskHandle();
103-
uint16_t depth = _reqMutexDepth.load(std::memory_order_relaxed);
104-
if (_reqMutexOwner.load(std::memory_order_relaxed) == current && depth > 0) {
105-
depth = static_cast<uint16_t>(depth - 1);
106-
_reqMutexDepth.store(depth, std::memory_order_relaxed);
107-
if (depth == 0)
108-
_reqMutexOwner.store(nullptr, std::memory_order_relaxed);
109-
}
102+
if (_reqMutex)
110103
xSemaphoreGiveRecursive(_reqMutex);
111-
}
112104
}
113105
#else
114106
void AsyncHttpClient::lock() const {}
@@ -117,8 +109,8 @@ void AsyncHttpClient::unlock() const {}
117109

118110
bool AsyncHttpClient::isLockHeldByCurrentTask() const {
119111
#ifdef ARDUINO_ARCH_ESP32
120-
return _reqMutexOwner.load(std::memory_order_relaxed) == xTaskGetCurrentTaskHandle() &&
121-
_reqMutexDepth.load(std::memory_order_relaxed) > 0;
112+
// FreeRTOS already records the owning task of a (recursive) mutex; no manual tracking needed.
113+
return _reqMutex && xSemaphoreGetMutexHolder(_reqMutex) == xTaskGetCurrentTaskHandle();
122114
#else
123115
return false;
124116
#endif
@@ -144,6 +136,8 @@ void AsyncHttpClient::_workerTaskThunk(void* param) {
144136
void AsyncHttpClient::_workerLoop() {
145137
while (true) {
146138
_workerBuffer.waitForItem();
139+
if (_workerShouldExit.load(std::memory_order_acquire))
140+
break;
147141
WorkerItem item;
148142
while (_workerBuffer.pop(item)) {
149143
auto ctx = std::static_pointer_cast<RequestContext>(item.ctx);
@@ -173,6 +167,11 @@ void AsyncHttpClient::_workerLoop() {
173167
dispatchCallbacks();
174168
}
175169
}
170+
// Shutdown requested: tell the destructor we are out of the loop, then self-delete so we never
171+
// get vTaskDelete()'d while holding _reqMutex. Any items still queued are freed by ~WorkerBuffer.
172+
if (_workerDoneSem)
173+
xSemaphoreGive(_workerDoneSem);
174+
vTaskDelete(nullptr);
176175
}
177176
#endif // ARDUINO_ARCH_ESP32
178177

src/AsyncHttpClient.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,14 +224,16 @@ class AsyncHttpClient {
224224
#ifdef ARDUINO_ARCH_ESP32
225225
WorkerBuffer _workerBuffer;
226226
TaskHandle_t _workerTaskHandle = nullptr;
227+
std::atomic<bool> _workerShouldExit{false}; // set by destructor to request worker shutdown
228+
SemaphoreHandle_t _workerDoneSem = nullptr; // worker gives this once it has exited its loop
227229
static void _workerTaskThunk(void* param);
228230
void _workerLoop();
229231
#endif
230232

231233
#ifdef ARDUINO_ARCH_ESP32
232-
mutable SemaphoreHandle_t _reqMutex = nullptr; // recursive mutex
233-
mutable std::atomic<TaskHandle_t> _reqMutexOwner{nullptr};
234-
mutable std::atomic<uint16_t> _reqMutexDepth{0};
234+
// Recursive mutex. Ownership/recursion depth are tracked natively by FreeRTOS; query the
235+
// holder with xSemaphoreGetMutexHolder() (see isLockHeldByCurrentTask) — no manual bookkeeping.
236+
mutable SemaphoreHandle_t _reqMutex = nullptr;
235237
#endif
236238

237239
// Internal methods

src/WorkerBuffer.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,4 +109,9 @@ void WorkerBuffer::waitForItem() {
109109
xSemaphoreTake(_semaphore, portMAX_DELAY);
110110
}
111111

112+
void WorkerBuffer::wake() {
113+
if (!_semaphore) return;
114+
xSemaphoreGive(_semaphore);
115+
}
116+
112117
#endif // ARDUINO_ARCH_ESP32

src/WorkerBuffer.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ class WorkerBuffer {
5757
// Blocks worker task until an item is available.
5858
void waitForItem();
5959

60+
// Wakes a worker blocked in waitForItem() without enqueuing anything.
61+
// Used to unblock the worker for cooperative shutdown.
62+
void wake();
63+
6064
private:
6165
void enqueue(WorkerItem&& item);
6266

0 commit comments

Comments
 (0)