코루틴과 동시성
Subroutine
A subroutine in programming is a common form of function or method. Subroutines have the following characteristics
Single Entry Point: Each time it is called, it always starts execution from the beginning.
Sequential Execution: It executes sequentially from the beginning and, upon completion, returns control to the calling location.
Stack-Based: Subroutines use a call stack to manage the calling location and local variables. Once a subroutine is completed, it is removed from the stack, and the flow of execution returns to the previous subroutine (or caller).
Coroutine
A coroutine offers a more flexible execution flow than a subroutine, simplifying and making efficient the handling of asynchronous operations, event processing, and concurrency.
Multiple Entry Points: Coroutines have multiple entry points. They operate on the concept of lazy evaluation, pausing execution at suspension points and resuming from that point when called again.
State Preservation: The state of a coroutine can be stored in heap memory, allowing it to maintain its state (local variables, execution point, etc.) at suspension points in a different manner than the typical call stack.
Cooperative Multitasking: Coroutines allow for suspension of execution at programmer-defined points and times, yielding control to other coroutines or event loops as needed.
Coroutines are sometimes defined as lightweight threads. While threads ensure concurrency with the cost of context switching and memory usage, coroutines achieve concurrency within a single thread through multiple units of work, thus reducing the cost of context switching and memory usage.
Concurrency
Concurrency is a concept different from parallel processing, characterized by rapid switching between tasks. In coroutines, control can be yielded to other coroutines at suspension points, allowing multiple coroutines to alternate execution and effectively handle I/O-bound tasks. Typically, accessing the file system (hard disk) or network communication (network card) involves hardware operations, which take longer than CPU operations. In the absence of asynchronous processing, the program remains in a waiting state during I/O operations, known as blocking I/O. Asynchronous programming allows the CPU to perform other commands during this time, effectively reducing waiting time.
Coroutines in Python
In Python, coroutines are implemented using the 'asyncio' library, along with the 'async' and 'await' syntax. Functions such as 'asyncio.gather' and 'asyncio.wait' are used to execute coroutines and wait for their completion.
import asyncio
async def count():
print("One")
await asyncio.sleep(1)
print("Two")
async def main():
await asyncio.gather(
count(),
count(),
count()
)
asyncio.run(main())
'asyncio.gather' is used to call the count coroutine three times simultaneously. During the 'await' in the 'sleep', there is a second wait. During this waiting period, each of the different count coroutines is executed, and after a 1-second wait, each 'count' coroutine finishes its wait and prints "Two".
result
One
One
One
(wait for a sec)
Two
Two
Two
Goroutines in Go
Instead of coroutines, Go uses a concept called goroutines. Python's coroutines are fundamentally executed in a single thread, and therefore do not offer parallelism, making them primarily suitable for I/O-bound tasks.
Goroutines support both concurrency and parallelism, and can efficiently handle various types of tasks in a multi-core environment. They stand out not only in I/O-bound tasks but also in CPU-bound tasks.
Goroutines are started with the 'go' keyword, and the completion of asynchronous tasks can be awaited using 'sync.WaitGroup' or channels.
package main
import (
"fmt"
"time"
)
// A goroutine that performs an independent task
func worker(id int, done chan<- bool) {
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d finished\n", id)
done <- true // Send a completion signal to the channel
}
func main() {
done := make(chan bool, 3) // Channel to receive completion signals
// Start 3 goroutines in parallel
for i := 1; i <= 3; i++ {
go worker(i, done)
}
// Wait for the completion of all goroutines
for i := 1; i <= 3; i++ {
<-done
}
}
The go worker(i, done) statement executes multiple goroutines in parallel. This means that on a multi-core CPU, multiple goroutines can actually run simultaneously. Goroutines in Go are managed in a multi-threaded environment by the Go runtime. Therefore, goroutines used with go worker(i, done) execute in parallel, providing parallelism. Each worker goroutine performs its task for one second and then prints "starting" and "finished" messages. Once the task is completed, each goroutine sends a task completion signal to the done channel. The main function waits for the completion of all goroutines through this channel.
result
Worker 1 starting
Worker 2 starting
Worker 3 starting
(wait for a sec)
Worker 1 finished
Worker 2 finished
Worker 3 finished
서브루틴
서브루틴은 프로그래밍에서 일반적인 함수, 또는 메서드의 형태이다.
서브루틴은 다음과 같은 특징을 지닌다.
단일 진입점. 호출될때마다 항상 처음부터 실행을 시작한다.
순차적 실행. 처음부터 순차적으로 실행되며, 완료시 제어를 호출한 곳으로 반환한다.
스택기반. 서브루틴은 호출 스택을 사용하여 호출된 위치와 지역변수를 관리한다.
하나의 서브루틴이 완료되면 스택에서 제거되고 실행흐름은 이전 서브루틴(또는 호출자)로 돌아간다.
코루틴
코루틴은 서브루틴보다 더 유연한 실행 흐름을 가지고 있으며, 비동기처리와 이벤트 처리, 동시성을 단순하고 효율적으로 만들어준다.
다중 진입점. 코루틴은 여러 진입점을 지닌다. 게으른 연산의 개념으로 중단점에서 실행을 중지하고, 나중에 호출될때 그지점부터 다시 시작할 수 있다.
상태 유지. 코루틴의 상태는 힙메모리에 저장될 수 있으며, 잠시 중단되었을때, 다시돌아가기 위해 일반적인 호출스택과 다른방식으로 중단시점의 상태(지역 변수, 실행지점)등을 유지한다.
Cooperative Multitasking. 코루틴은 프로그래머의 의도대로 지정된 위치에서 지정된 시점에 실행을 중단하고 제어를 다른 코루틴이나 이벤트루프등에 양도할 수 있다.
코루틴은 경량 스레드라고 정의 되기도 하는데, 스레드는 동시성을 보장하는데 있어서 스레드간 context switching 이라는 비용과 메모리 사용이 발생하는데 반해, 코루틴은 한 스레드 안에서 여러 work단위로 비동기 처리하기 때문에 context switching 비용과 메모리 사용을 줄인 채로 동시성을 보장하기 떄문이다.
동시성
병렬처리와는 다른 개념이며 작업간 전환이 빠르게 일어난다.
코루틴에서는 중단점에서 다른 코루틴으로 제어를 양도하고, 이렇게 여러 코루틴이 번갈아 실행되며 I/O 바운드 작업을 효과적으로 처리한다.
보통 파일 시스템 접근(하드디스크), 네트워크 통신(네트워크 카드) 등은 cpu가 아닌 하드웨어를 통해 작업하는데, 이는 CPU작업보다 더 오랜시간이 걸린다.
비동기 처리가 적용되지 않은 경우 I/O작업이 진행 되는동안 프로그램은 대기상태에 머무르게 되는데 이를 블로킹 I/O라고 한다.
비동기 프로그래밍에서는 이 시간동안 CPU가 다른 명령을 수행할 수 있게 해 대기시간을 효과적으로 줄인다.
파이썬에서의 코루틴
asyncio 라이브러리를 통해, async, await 구문을 사용하여 구현된다.
asyncio.gather, asyncio.wait와 같은 함수를 사용하여, 코루틴을 실행하고, 완료를 기다린다.
import asyncio
async def count():
print("One")
await asyncio.sleep(1)
print("Two")
async def main():
await asyncio.gather(
count(),
count(),
count()
)
asyncio.run(main())
One
One
One
(약 1초 대기)
Two
Two
Two
asyncio.gather는 count 코루틴을 세번 동시에 호출하고, await에서 sleep에 의해 1초간 대기한다. 이 대기시간동안, 각 다른 count 코루틴을 실행시키고, 각 count코루틴들은 1초후 대기를 마치고 two를 출력한다.
go에서의 고루틴
고에서는 코루틴 대신 고루틴이라는 개념을 사용한다.파이썬의 코루틴은 기본적으로 싱글 스레드에서 실행되며, 따라서 병렬성을 제공하지 않기 떄문에 I/O바운드 작업에 주로 사용된다.
고루틴은 동시성과 병렬성을 모두 지원하며, 멀티코어 환경에서도 다양한 종류의 작업을 효율적으로 처리할 수 있다. I/O뿐만 아니라 CPU바운드 작업에서도 그 장점이 두드러진다.
go 키워드로 코루틴을 시작하며, sync.waitGroup 이나 채널을 사용해 비동기 작업의 완료를 기다린다.
package main
import (
"fmt"
"net/http"
)
func fetchURL(url string, done chan<- bool) {
resp, err := http.Get(url)
if err != nil {
fmt.Println(err)
done <- false
return
}
defer resp.Body.Close()
fmt.Println(url, "status:", resp.Status)
done <- true
}
func main() {
urls := []string{
"http://example.com",
"http://example.org",
"http://example.net",
}
done := make(chan bool)
for _, url := range urls {
go fetchURL(url, done)
}
for i := 0; i < len(urls); i++ {
<-done
}
}
위 예에서는 done이라는 채널을 생성하고. fetchURL이라는 고루틴이 완료될떄까지 채널에서 신호를 기다리고 있다.