3-Hour Debugging: How `time.sleep` in Async Functions Killed Our asyncio Concurrency
BAOFUFAN
·
2026-05-01
·
via DEV Community
<p>Here’s the situation: we have a data collection service that fetches data from a dozen upstream APIs. The synchronous version took a painful 30 minutes per run. I thought, “This is exactly the kind of problem asyncio was built for!” I spent an afternoon replacing <code>requests</code> with <code>aiohttp</code>, decorating every function with <code>async</code>/<code>await</code>, and ran the code — same 30 minutes. Not a single second faster. I was floored.</p> <p>Eventually, I tracked the culprit to a stray <code>time.sleep(0.5)</code> buried deep inside a nested function. That half-second sleep was enough to freeze the entire event loop inside that coroutine, turning our glorious “async concurrency” back into plain old serial execution.</p> <p>The takeaway? Some of asyncio’s most counterintuitive landmines are impossible to fully appreciate until you step on one yourself. Here’s the full post-mortem: the debugging journey, the root cause, and how to avoid this trap for good.</p> <h2> Why a Single <code>sleep</code> Can Destroy Concurrency </h2> <p>Let’s recap how asyncio works: the <strong>event loop</strong> is essentially a single-threaded scheduler that manages a queue of tasks. Coroutines defined with <code>async def</code> yield control back to the event loop whenever they <code>await</code>, allowing the loop to switch to other ready coroutines and keep making progress.</p> <p>But yielding must be <strong>explicit</strong>. <code>await asyncio.sleep(n)</code> registers a timer with the event loop and immediately hands back control — other tasks get their turn. In contrast, <code>time.sleep(n)</code> is a <strong>synchronous blocking call</strong>. It puts the entire thread to sleep, the event loop gets zero control, and every coroutine you wrote simply waits in line, no matter how many tasks you’ve created.</p> <p>In plain terms:</p> <ul> <li> <code>await asyncio.sleep()</code>: “I’ll set a timer with the event loop and kindly step aside so others can work.”</li> <li> <code>time.sleep()</code>: “I’m going to take a nap, and no one — not even the event loop — can do anything until I wake up.”</li> </ul> <h2> Bad Practice vs. The Right Way </h2> <p><strong>Bad code</strong> (looks async but blocks the single thread):<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight python"><code><span class="kn">import</span> <span class="n">asyncio</span> <span class="kn">import</span> <span class="n">time</span> <span class="k">async</span> <span class="k">def</span> <span class="nf">fetch_data</span><span class="p">(</span><span class="n">url</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span> <span class="c1"># 模拟请求前处理 </span> <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">开始请求 </span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span> <span class="n">time</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)</span> <span class="c1"># ❌ 同步阻塞,整个事件循环停滞 </span> <span class="c1"># 这里还会去发起 aiohttp 请求等等 </span> <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">完成请求 </span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span> <span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span> <span class="n">urls</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="sh">"</span><span class="s">https://api.example.com/data/</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="sh">"</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">10</span><span class="p">)]</span> <span class="c1"># 看似并发启动 </span> <span class="n">tasks</span> <span class="o">=</span> <span class="p">[</span><span class="n">asyncio</span><span class="p">.</span><span class="nf">create_task</span><span class="p">(</span><span class="nf">fetch_data</span><span class="p">(</span><span class="n">url</span><span class="p">))</span> <span class="k">for</span> <span class="n">url</span> <span class="ow">in</span> <span class="n">urls</span><span class="p">]</span> <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="nf">gather</span><span class="p">(</span><span class="o">*</span><span class="n">tasks</span><span class="p">)</span> <span class="n">asyncio</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span><span class="nf">main</span><span class="p">())</span> </code></pre> </div> <p>When you run this, you’ll see the prints appear one by one in order. Ten tasks take over 5 seconds, and concurrency is a complete illusion.</p> <p><strong>Good code</strong> (using <code>asyncio.sleep</code> to yield control):<br> </p> <div class="highlight js-code-highlight"> <pre class="highlight python"><code><span class="kn">import</span> <span class="n">asyncio</span> <span class="kn">import</span> <span class="n">aiohttp</span> <span class="k">async</span> <span class="k">def</span> <span class="nf">fetch_data</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">aiohttp</span><span class="p">.</span><span class="n">ClientSession</span><span class="p">,</span> <span class="n">url</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="nb">dict</span><span class="p">:</span> <span class="sh">"""</span><span class="s"> 真正的异步请求函数:IO 全部交给事件循环调度 </span><span class="sh">"""</span> <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">开始请求 </span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span> <span class="c1"># 模拟速率限制等待,使用 asyncio.sleep,不阻塞其他协程 </span> <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mf">0.5</span><span class="p">)</span> <span class="k">async</span> <span class="k">with</span> <span class="n">session</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span> <span class="k">as</span> <span class="n">resp</span><span class="p">:</span> <span class="n">data</span> <span class="o">=</span> <span class="k">await</span> <span class="n">resp</span><span class="p">.</span><span class="nf">json</span><span class="p">()</span> <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">完成请求 </span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="s">, 状态码 </span><span class="si">{</span><span class="n">resp</span><span class="p">.</span><span class="n">status</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span> <span class="k">return</span> <span class="n">data</span> <span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span> <span class="n">urls</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="sh">"</span><span class="s">https://api.example.com/data/</span><span class="si">{</span><span class="n">i</span><span class="si">}</span><span class="sh">"</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">10</span><span class="p">)]</span> <span class="c1"># 使用连接池复用 TCP 连接,减少开销 </span> <span class="n">connector</span> <span class="o">=</span> <span class="n">aiohttp</span><span class="p">.</span><span class="nc">TCPConnector</span><span class="p">(</span><span class="n">limit</span><span class="o">=</span><span class="mi">20</span><span class="p">)</span> <span class="c1"># 最大并发连接数 </span> <span class="k">async</span> <span class="k">with</span> <span class="n">aiohttp</span><span class="p">.</span><span class="nc">ClientSession</span><span class="p">(</span><span class="n">connector</span><span class="o">=</span><span class="n">connector</span><span class="p">)</span> <span class="k">as</span> <span class="n">session</span><span class="p">:</span> <span class="n">tasks</span> <span class="o">=</span> <span class="p">[</span><span class="nf">fetch_data</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">url</span><span class="p">)</span> <span class="k">for</span> <span class="n">url</span> <span class="ow">in</span> <span class="n">urls</span><span class="p">]</span> <span class="n">results</span> <span class="o">=</span> <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="nf">gather</span><span class="p">(</span><span class="o">*</span><span class="n">tasks</span><span class="p">,</span> <span class="n">return_exceptions</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="c1"># 简单错误处理 </span> <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">result</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">results</span><span class="p">):</span> <span class="k">if</span> <span class="nf">isinstance</span><span class="p">(</span><span class="n">result</span><span class="p">,</span> <span class="nb">Exception</span><span class="p">):</span> <span class="nf">print</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">请求 </span><span class="si">{</span><span class="n">urls</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="si">}</span><span class="s"> 失败: </span><span class="si">{</span><span class="n">result</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span> <span class="n">asyncio</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span><span class="nf">main</span><span class="p">())</span> </code></pre> </div> <p>With this fix, the <code>asyncio.sleep(0.5)</code> calls happen concurrently across all 10 tasks, compressing the total waiting time to roughly 0.5 seconds (plus the actual request time). Paired with aiohttp’s connection pooling, the efficiency gain is night and day.</p> <h2> Lessons Learned: Ignore These and You’ll Trip Again </h2> <ol> <li><p><strong>Check if your dependencies are truly async</strong><br><br> Simply sprinkling <code>async</code>/<code>await</code> into your code isn’t enough. Synchronous libraries like <code>time.sleep</code>, <code>requests</code>, or <code>pymongo</code> will immediately choke your event loop if used inside a coroutine. Always switch to their async equivalents: <code>aiohttp</code>, <code>httpx</code>, <code>motor</code> (async MongoDB driver), <code>aiomysql</code>, etc. If a library has no async version, offload it to a thread pool using <code>await asyncio.to_thread(sync_func, *args)</code>. It’s not perfect, but at least it won’t block the event loop.</p></li> <li><p><strong>The <code>asyncio.gather</code> exception trap</strong><br><br> By default, <code>gather</code> will immediately propagate any exception thrown by a task, canceling other running tasks in the process. If you want all tasks to finish before handling results, remember to set <code>return_exceptions=True</code> and then manually inspect each returned value to see if it’s an <code>Exception</code> instance.</p></li> <li><p><strong>Don’t abandon tasks created with <code>create_task</code></strong><br><br> If you create a Task with <code>asyncio.create_task</code> but never <code>await</code> it or keep a reference, any exception it raises will be silently swallowed by garbage collection — you won’t even see an error log. Every Task should either be collected with <code>gather</code> or have its exception explicitly checked.</p></li> <li><p><strong>Don’t spawn an unbounded number of coroutines</strong><br><br> Kicking off hundreds of concurrent connections when scraping hundreds of URLs can easily trip the target API’s rate limits. Always cap concurrency — for example, with an <code>asyncio.Semaphore</code> or by tuning your <code>aiohttp</code> connector pool — to avoid getting blocked or throttled.</p></li> </ol>
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。