Asynchronous: Python's asyncio/greenlet, Java's CompletableFuture
"TLDR: This article introduces several methods for implementing asynchronous programming in Python and Java, including threading, asyncio, and greenlet in Python, as well as CompletableFuture in Java. It provides a detailed comparison of these tools in terms of control, scheduling mechanisms, and ecosystem support, and includes corresponding code examples to illustrate their usage."
Python: Implementing Asynchrony with threading and asyncio
Simulating Asynchrony with threading
import time
import threading
def simulate_task(task_id):
print(f"Task {task_id} started.")
time.sleep(2) # Simulate a time-consuming operation
print(f"Task {task_id} finished.")
start_time = time.time()
threads = []
for i in range(150): # 150 tasks
thread = threading.Thread(target=simulate_task, args=(i,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
end_time = time.time()
print(f"Total time taken with threading: {end_time - start_time} seconds")
This is a fairly conventional implementation. Although Python has a global GIL lock, it is not a major issue in I/O-bound tasks; it mainly affects CPU utilization across multiple cores in CPU-bound tasks.
In multi-threaded implementations for I/O-bound tasks, having too many threads can also degrade performance due to frequent context switching.
Implementing Asynchrony with asyncio
import time
import asyncio
async def simulate_task(task_id):
print(f"Task {task_id} started.")
await asyncio.sleep(2) # Simulate a time-consuming operation
print(f"Task {task_id} finished.")
start_time = time.time()
async def main():
tasks = []
for i in range(150): # 150 tasks
tasks.append(simulate_task(i))
await asyncio.gather(*tasks)
asyncio.run(main())
end_time = time.time()
print(f"Total time taken with asyncio: {end_time - start_time} seconds")
Asyncio is implemented based on coroutines and an event loop. Due to the nature of coroutines, multiple coroutines only switch within a single thread, without the overhead of context switching. Therefore, you can create as many coroutines as you want, fully utilizing the CPU. The event loop primarily manages switching between multiple coroutines. Compared to the Threading version above, it is about 0.02 seconds faster.
Nested Asynchronous Tasks
import asyncio
async def task1():
print("Task 1 start")
await asyncio.sleep(1)
print("Task 1 end")
async def task2():
print("Task 2 start")
await task1() # Call another async function
print("Task 2 end")
async def other_task():
for i in range(3):
print(f"Other task is running {i+1}")
await asyncio.sleep(0.5)
async def main():
# Run multiple tasks concurrently
await asyncio.gather(
task2(),
other_task(),
)
asyncio.run(main())
When a coroutine is blocked, it automatically yields the CPU to execute other tasks. As shown above, when task1 is blocked, the event loop will run other_task. (task2 will not execute because it depends on task1's completion and is blocked at the await task1() line.)
Python: Implementing Coroutines with greenlet
from greenlet import greenlet
def task_1():
print("Task 1: Start")
g2.switch() # Switch to task_2
print("Task 1: End")
def task_2():
print("Task 2: Start")
g1.switch() # Switch back to task_1
print("Task 2: End")
g1 = greenlet(task_1)
g2 = greenlet(task_2)
g1.switch() # Start task 1
greenlet is also an asynchronous tool based on coroutines, but with the advent of asyncio, it is less commonly used.
Comparison between greenlet and asyncio:
- Control:
greenletuses explicit control, requiring developers to manually specify when to switch tasks in the code. In contrast,asyncioautomatically manages task switching through the event loop andawait. - Scheduling:
greenletuses cooperative scheduling, requiring manual task switching.asynciouses event-driven automatic scheduling, which is suitable for I/O-bound tasks and is both concise and efficient. - Ecosystem and Support:
asynciois part of the Python standard library and enjoys broader support.greenletis typically used in lower-level implementations with higher performance requirements.
Java: Implementing Asynchrony with CompletableFuture
import java.util.concurrent.CompletableFuture;
public class AsyncExample {
public static CompletableFuture<Void> task1() {
return CompletableFuture.runAsync(() -> {
try {
System.out.println("Task 1 started");
Thread.sleep(2000); // Simulate a time-consuming operation
System.out.println("Task 1 finished");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
public static CompletableFuture<Void> task2() {
return task1().thenRun(() -> { // Call another async function
System.out.println("Task 2 started");
System.out.println("Task 2 finished");
});
}
public static void main(String[] args) {
task2().join(); // Start task 2 and wait for completion
}
}
CompletableFuture vs. Coroutines: CompletableFuture is not a coroutine; it uses thread pools and callback mechanisms to implement asynchronous operations, with each asynchronous task typically executed by an independent thread. Asynchrony in Java is usually managed through ExecutorService (such as ForkJoinPool) rather than coroutines.
Summary:
- Python:
asynciois the standard asynchronous programming approach, based on coroutines and an event loop, suitable for I/O-bound tasks.greenletprovides a low-level coroutine implementation where control is explicitly managed by the developer. Both have different use cases in terms of ecosystem and support. - Java:
CompletableFutureis not a coroutine but an asynchronous implementation based on thread pools and callbacks, commonly used for handling asynchronous tasks. Unlike Python's coroutines, Java tends to manage concurrent tasks through thread pools.