11import asyncio
2- import random
3- import time
2+ import time
43
5- async def main ():
6- user_ids = [1 , 2 , 3 ]
4+
5+ async def task_function (name , duration , fail = False ):
6+ print (f" Task { name } : Starting (will sleep for { duration } s)" )
7+ await asyncio .sleep (duration )
8+ if fail :
9+ raise ValueError (f"Task { name } failed!" )
10+ print (f" Task { name } : Finished" )
11+ return f"Result of { name } "
12+
13+
14+ # ─────────────────────────────────────────────
15+ # asyncio.gather
16+ # ─────────────────────────────────────────────
17+ # • Available since Python 3.4
18+ # • Accepts coroutines or Tasks
19+ # • Returns a list of results in the same order as inputs
20+ # • Error behaviour (default): if one task raises, the exception is
21+ # propagated immediately to the caller, but the OTHER tasks keep
22+ # running in the background (they are NOT cancelled).
23+ # • return_exceptions=True: exceptions are returned as result values
24+ # instead of being raised, so all tasks always run to completion.
25+ # • Good for fire-and-forget fan-out or when you need return_exceptions.
26+
27+ async def demo_gather ():
28+ print ("\n === asyncio.gather (no failures) ===" )
729 start = time .perf_counter ()
8- await asyncio .gather (
9- * (get_user_with_posts (user_id ) for user_id in user_ids ), return_exceptions = True
30+ results = await asyncio .gather (
31+ task_function ("A" , 3 ),
32+ task_function ("B" , 1 ),
33+ task_function ("C" , 2 ),
1034 )
11- end = time .perf_counter ()
12- print (f"\n ==> Total time: { end - start :.2f} seconds" )
13-
14- async def get_user_with_posts (user_id ):
15- user = await fetch_user (user_id )
16- await fetch_posts (user )
17-
18- async def fetch_user (user_id ):
19- delay = random .uniform (0.5 ,2.0 )
20- print (f"User coro: fetching user by { user_id } " )
21- await asyncio .sleep (delay )
22- user = {"id" : user_id , "name" : f"User{ user_id } " }
23- print (f"User coro: fetched user with { user_id } (done in { delay :.2f} seconds)" )
24- return user
25-
26- async def fetch_posts (user ):
27- delay = random .uniform (0.5 ,2.0 )
28- print (f"Posts coro: retrieving posts for { user ['name' ]} " )
29- await asyncio .sleep (delay )
30- posts = [f"Post { i } by { user ['name' ]} " for i in range (1 , 3 )]
31- print (
32- f"Post coro: got { len (posts )} posts by { user ['name' ]} "
33- f" (done in { delay :.1f} seconds)"
35+ print (f" Done in { time .perf_counter () - start :.2f} s | Results: { results } " )
36+
37+ print ("\n === asyncio.gather with return_exceptions=True ===" )
38+ start = time .perf_counter ()
39+ results = await asyncio .gather (
40+ task_function ("A" , 1 ),
41+ task_function ("B" , 0.5 , fail = True ), # will raise
42+ task_function ("C" , 1 ),
43+ return_exceptions = True , # exceptions become values
3444 )
35- for post in posts :
36- print (f" - { post } " )
45+ print (f" Done in { time .perf_counter () - start :.2f} s | Results: { results } " )
46+
47+
48+ # ─────────────────────────────────────────────
49+ # asyncio.TaskGroup (Python 3.11+)
50+ # ─────────────────────────────────────────────
51+ # • Context-manager-based structured concurrency.
52+ # • Tasks are created with tg.create_task() inside the `async with` block.
53+ # • When the block exits all tasks are AWAITED automatically.
54+ # • Error behaviour: if ANY task raises, ALL remaining tasks are
55+ # cancelled immediately, then the exception is re-raised.
56+ # Multiple simultaneous exceptions are collected into an ExceptionGroup.
57+ # • Cleaner resource management; preferred for structured concurrency.
58+
59+ async def demo_task_group ():
60+ print ("\n === asyncio.TaskGroup (no failures) ===" )
61+ start = time .perf_counter ()
62+ results = []
63+ async with asyncio .TaskGroup () as tg :
64+ t1 = tg .create_task (task_function ("A" , 3 ))
65+ t2 = tg .create_task (task_function ("B" , 1 ))
66+ t3 = tg .create_task (task_function ("C" , 2 ))
67+ # All tasks are guaranteed finished here
68+ results = [t1 .result (), t2 .result (), t3 .result ()]
69+ print (f" Done in { time .perf_counter () - start :.2f} s | Results: { results } " )
70+
71+ print ("\n === asyncio.TaskGroup with a failing task ===" )
72+ try :
73+ async with asyncio .TaskGroup () as tg :
74+ tg .create_task (task_function ("A" , 1 ))
75+ tg .create_task (task_function ("B" , 0.1 , fail = True )) # raises quickly
76+ tg .create_task (task_function ("C" , 1 )) # will be cancelled
77+ except* ValueError as eg :
78+ print (f" Caught ExceptionGroup errors: { eg } " )
79+
80+
81+ # ─────────────────────────────────────────────
82+ # Summary
83+ # ─────────────────────────────────────────────
84+ # | Feature | gather | TaskGroup (3.11+) |
85+ # |--------------------------|-------------------------|-------------------------|
86+ # | API style | function call | async context manager |
87+ # | Task creation | pass coroutines/tasks | tg.create_task() |
88+ # | On first exception | others keep running * | others are cancelled |
89+ # | Multiple exceptions | only first propagated | ExceptionGroup |
90+ # | return_exceptions | yes | no (use try/except) |
91+ # | Structured concurrency | no | yes |
92+ # | Python version | 3.4+ | 3.11+ |
93+ #
94+ # * Unless return_exceptions=False (default) and you shield tasks.
95+
96+ async def main ():
97+ await demo_gather ()
98+ await demo_task_group ()
99+
37100
38101if __name__ == "__main__" :
39- random .seed (42 )
40- asyncio .run (main ())
102+ asyncio .run (main ())
103+
104+
0 commit comments