{"id":2026,"date":"2025-03-29T01:43:56","date_gmt":"2025-03-28T22:13:56","guid":{"rendered":"https:\/\/mshaeri.com\/blog\/?p=2026"},"modified":"2026-05-19T02:27:09","modified_gmt":"2026-05-19T12:27:09","slug":"multi-threading-multi-processing-and-async-event-loop-in-python","status":"publish","type":"post","link":"https:\/\/mshaeri.com\/blog\/multi-threading-multi-processing-and-async-event-loop-in-python\/","title":{"rendered":"Multi-Threading, Multi-Processing, Async and Event Loop in Python"},"content":{"rendered":"\n<p>In Python, you&#8217;ve probably come across terms like <strong>multi-threading, multi-processing, async and event loops<\/strong>. They can be confusing<strong> <\/strong>at first. What should we use? When? Why does Python have multiple ways to do the same thing?<\/p>\n\n\n\n<p>In this post, I&#8217;ll break it all down in a way that actually makes sense, and to wrap it up, I&#8217;ll show you real-world code examples that demonstrate how these tools can improve performance in your system.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Multi-Threading  (Good for I\/O-Bound Tasks)<\/strong><\/h2>\n\n\n\n<p>Multi-threading is when you run <strong>multiple threads inside the same process<\/strong>. But because of Python&#8217;s <strong>Global Interpreter Lock (GIL)<\/strong>, only <strong>one thread can execute Python bytecode at a time<\/strong>. This means multi-threading is NOT good for CPU-heavy tasks but can be useful for I\/O-bound operations like <strong>web scraping, file I\/O, and API calls<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Example: Multi-Threading for Downloading Web Pages<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">import threading\nimport time\n\ndef download_page(url):\n    print(f\"Downloading {url} ...\")\n    time.sleep(2)  # Simulate network delay\n    print(f\"Finished {url}\")\n\nurls = [\"http:\/\/example.com\/page1\", \"http:\/\/example.com\/page2\", \"http:\/\/example.com\/page3\"]\n\nthreads = [threading.Thread(target=download_page, args=(url,)) for url in urls]\n\nfor thread in threads:\n    thread.start()\n\nfor thread in threads:\n    thread.join()\n\nprint(\"All downloads complete!\")\n\n\n<\/code><\/pre>\n\n\n\n<ul>\n<li>We\u2019re just waiting for the network, so using threads allows the OS to switch between them while a thread is waiting for a I\/O task to be finished.<\/li>\n\n\n\n<li>Threads share memory, making it lightweight.<\/li>\n<\/ul>\n\n\n\n<p>\u26d4 <strong>Downside:<\/strong> GIL prevents true parallel execution for CPU-bound tasks. So, again, don&#8217;t use it for calculation or image\/data processing tasks. <\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Multi-Processing \ud83d\udda5\ufe0f (Best for CPU-Bound Tasks)<\/strong><\/h2>\n\n\n\n<p>Multi-processing, on the other hand, <strong>spawns multiple processes<\/strong>, each with its <strong>own memory space<\/strong>. This means Python can actually run code in <strong>parallel<\/strong> on multiple CPU cores.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Example: Multi-Processing for CPU-Heavy Work<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">import multiprocessing\n\ndef compute_square(n):\n    return n * n\n\nif __name__ == \"__main__\":\n    numbers = [1, 2, 3, 4, 5]\n\n    with multiprocessing.Pool(processes=3) as pool:\n        results = pool.map(compute_square, numbers)\n\n    print(\"Squares:\", results)\n\n\n<\/code><\/pre>\n\n\n\n<ul>\n<li>Each process runs independently, <strong>bypassing the GIL<\/strong>.<\/li>\n\n\n\n<li>Ideal for CPU-heavy tasks like <strong>image processing, machine learning, and data analysis<\/strong>.<\/li>\n<\/ul>\n\n\n\n<p>In multi-processing, processes don\u2019t share memory, so communication between them requires extra effort. Each process is actually a new instance of the Python interpreter, and each one has its own <strong>private memory area<\/strong>. This is different from multi-threading, where threads share the same memory within a single process.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Async Event Loop \u26a1 (Best for I\/O-Heavy &amp; High-Concurrency Tasks)<\/strong><\/h2>\n\n\n\n<p>Async programming uses an <strong>event loop<\/strong> to handle <strong>thousands of tasks<\/strong> efficiently <strong>without blocking<\/strong>. Instead of waiting (like threads do), an event loop will <strong>switch to another task<\/strong> while a task is waiting for I\/O. In other word, instead of relying on OS-level thread management like what happens in multi-threading, an event loop <strong>switches between tasks cooperatively<\/strong>, meaning a task gives up control voluntarily to the main thread when it encounters an <code>await<\/code> statement, then the main thread can pay to another task while the waiting task is not backed. All is done in a single thread.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><a href=\"https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2025\/03\/image.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"383\" src=\"https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2025\/03\/image-1024x383.png\" alt=\"\" class=\"wp-image-2051\" srcset=\"https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2025\/03\/image-1024x383.png 1024w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2025\/03\/image-300x112.png 300w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2025\/03\/image-768x287.png 768w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2025\/03\/image.png 1432w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Event loop fetches task from queue, give it CPU until finished or blocked by I\/O operation<\/figcaption><\/figure><\/div>\n\n\n<h3 class=\"wp-block-heading\"><strong>Example: Async Event Loop for Non-Blocking Tasks<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">import asyncio\n\nasync def task():\n    print(\"Start Task\")\n    await asyncio.sleep(3)  # Non-blocking wait\n    print(\"Task Complete\")\n\nasync def main():\n    print(\"Before Task\")\n    await task()\n    print(\"After Task\")\n\nasyncio.run(main())\n\n\n<\/code><\/pre>\n\n\n\n<p>Since all tasks run in the same process and thread, they can access the same global variables or objects in memory. But, just like with regular Python code, if you want to safely share data between tasks, you need to manage synchronization or use other mechanisms like locks or other safe data structures.<\/p>\n\n\n\n<ul>\n<li>It\u2019s <strong>single-threaded but non-blocking<\/strong>.<\/li>\n\n\n\n<li>Ideal for <strong>web scraping, API calls, database queries, and file I\/O<\/strong>.<\/li>\n<\/ul>\n\n\n\n<p>\u26d4 Like multi-threading it&#8217;s not good for CPU-heavy tasks (multi-processing is better for that).<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Running Multiple Async Tasks (Concurrency)<\/strong><\/h2>\n\n\n\n<p>Here is an example of two tasks that <strong>asyncio<\/strong> will accomplish them and waits for both to be completed using <code>gather()<\/code> method.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Example: Running Multiple Async Tasks in Parallel<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">import asyncio\n\nasync def task1():\n    print(\"Task 1 Start\")\n    await asyncio.sleep(2)\n    print(\"Task 1 Done\")\n\nasync def task2():\n    print(\"Task 2 Start\")\n    await asyncio.sleep(3)\n    print(\"Task 2 Done\")\n\nasync def main():\n    await asyncio.gather(task1(), task2())  # Run both tasks concurrently\n\nasyncio.run(main())\n\n\n<\/code><\/pre>\n\n\n\n<ul>\n<li>Task 1 takes <strong>2 seconds<\/strong>.<\/li>\n\n\n\n<li>Task 2 takes <strong>3 seconds<\/strong>.<\/li>\n\n\n\n<li>Total time:<strong> Only 3 seconds instead of 5<\/strong>.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>When to Use What?<\/strong><\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th><strong>Use Multi-Threading \ud83e\uddf5 If:<\/strong><\/th><th><strong>Use Multi-Processing \ud83d\udda5\ufe0f If:<\/strong><\/th><th><strong>Use Async \u26a1 If:<\/strong><\/th><\/tr><\/thead><tbody><tr><td>You have <strong>I\/O-bound<\/strong> tasks<\/td><td>You have <strong>CPU-bound<\/strong> tasks<\/td><td>You have <strong>high concurrency I\/O<\/strong> tasks<\/td><\/tr><tr><td>Need <strong>lightweight concurrency<\/strong><\/td><td>Need <strong>true parallel execution<\/strong><\/td><td>Need <strong>thousands of async operations<\/strong><\/td><\/tr><tr><td>Examples: <strong>Web scraping, file I\/O, database queries<\/strong><\/td><td>Examples: <strong>Machine learning, image processing, data analysis<\/strong><\/td><td>Examples: <strong>APIs, web scraping, real-time applications<\/strong><\/td><\/tr><\/tbody><\/table><figcaption class=\"wp-element-caption\">Multi-threading, Multi-processing and Async Event-loop comparision<\/figcaption><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Real World Example Combining Multi-Processing and Async for Heavy I\/O + CPU Tasks<\/strong><\/h2>\n\n\n\n<p>To help you fully grasp the advantages of using these tools in real-world scenarios, let&#8217;s look at a practical example: fetching <strong>shopping cart data<\/strong> from an API (I\/O-bound) and then <strong>calculating the total price<\/strong> of each cart (CPU-heavy).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Example: Web Scraping + CPU-Intensive Processing<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">import asyncio\nimport time\nimport aiohttp\nimport multiprocessing\n\nasync def fetch_cart(session, cart_id):\n    url = f\"https:\/\/dummyjson.com\/carts\/{cart_id}\"\n    await asyncio.sleep(cart_id) # Simulate network delay for each cart\n    async with session.get(url) as response:\n        return await response.json()\n\ndef calculate_cart_total_price(cart):\n    products = cart[\"products\"]\n    time.sleep(cart[\"id\"]) # Simulate CPU-heavy work for each cart\n    return cart[\"id\"], sum(product[\"total\"] for product in products)\n\nasync def main():\n    start_time = time.time()\n    card_ids = [1, 2, 3, 4, 5]\n    async with aiohttp.ClientSession() as session:\n        tasks = [fetch_cart(session, url) for url in card_ids]\n        responses = await asyncio.gather(*tasks)\n        fetching_elapsed_time = time.time() - start_time\n        print(\"All carts fetched in {} seconds, instead of ~{}\".format(fetching_elapsed_time, sum(card_ids)))\n\n    # Use multi-processing for CPU-intensive processing\n    with multiprocessing.Pool(processes=5) as pool:\n        results = pool.map(calculate_cart_total_price, responses)\n\n    print(\"Total price of all carts:\", sum(result[1] for result in results))\n\n    processing_elapsed_time = time.time() - start_time - fetching_elapsed_time\n    print(\"Calculation done in {} seconds instead of ~{}\".format(processing_elapsed_time, sum(card_ids)))\n\n    total_elapsed_time = time.time() - start_time\n    print(\"Total elapsed time: {} seconds, instead of ~{}\".format(total_elapsed_time, sum(card_ids)*2))\n\nasyncio.run(main())<\/code><\/pre>\n\n\n\n<p>In this example, multiple shopping carts are fetched concurrently instead of waiting for each request to complete one by one. This significantly reduces the total time spent on I\/O operations. Once all the data is retrieved, we used multi-processing to perform CPU-heavy calculations in parallel across multiple processes, making full use of the available CPU cores. <\/p>\n\n\n\n<p>If we were to fetch carts sequentially without async, each request would block execution until it completed, resulting in a total wait time of approximately 15 seconds (1+2+3+4+5). Similarly, if we processed each cart\u2019s total price one after another without multiprocessing, it would add another 15 seconds, leading to an overall execution time of around <strong>30 seconds<\/strong>. Thanks to async-io and multi-processing, now our optimized approach reduces this to roughly <strong>5-6 seconds<\/strong>. You can try running this code on your machine to experience firsthand how async I\/O and multi-processing work together to optimize performance.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Long story short<\/strong><\/h2>\n\n\n\n<ul>\n<li>\ud83e\uddf5 <strong>Multi-threading<\/strong> is great for <strong>I\/O-bound<\/strong> tasks but is <strong>limited by GIL<\/strong>.<\/li>\n\n\n\n<li>\ud83d\udda5\ufe0f <strong>Multi-processing<\/strong> is best for <strong>CPU-bound<\/strong> tasks and <strong>bypasses GIL<\/strong>.<\/li>\n\n\n\n<li>\u26a1 <strong>Async programming<\/strong> is perfect for <strong>high concurrency I\/O<\/strong> (e.g., handling thousands of requests).<\/li>\n<\/ul>\n\n\n\n<p>So next time you\u2019re wondering <strong>\u201cWhich one should I use?\u201d<\/strong>, just ask yourself:<\/p>\n\n\n\n<ul>\n<li>Is it <strong>I\/O-heavy?<\/strong> \u2192 Use <strong>multi-threading<\/strong> or <strong>async<\/strong>.<\/li>\n\n\n\n<li>Is it <strong>CPU-heavy?<\/strong> \u2192 Use <strong>multi-processing<\/strong>.<\/li>\n\n\n\n<li> You need to handle <strong>thousands of concurrent I\/O-heavy tasks?<\/strong> \u2192 Use <strong>async<\/strong> because it is more efficient than making thousand threads.<\/li>\n<\/ul>\n\n\n\n<p>Happy coding! <\/p>\n","protected":false},"excerpt":{"rendered":"<p>In Python, you&#8217;ve probably come across terms like multi-threading, multi-processing, async and event loops. They can be confusing at first. What should we use? When? &hellip; <\/p>\n","protected":false},"author":1,"featured_media":2030,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1,35,41],"tags":[284,285,281,286,283,39,282],"_links":{"self":[{"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/posts\/2026"}],"collection":[{"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/comments?post=2026"}],"version-history":[{"count":5,"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/posts\/2026\/revisions"}],"predecessor-version":[{"id":2053,"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/posts\/2026\/revisions\/2053"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/media\/2030"}],"wp:attachment":[{"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/media?parent=2026"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/categories?post=2026"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/tags?post=2026"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}