비동기적 함수 호출: 파이썬 코루틴과 고의 고루틴

This post is being written.

스레드, 프로세스

WIP

Python's Coroutine

파이썬 인터프리터 락 (GIL)

asyncio

async/await 문법으로 선언할 수 있는 coroutine은 asyncio 프로그램을 작성하는 데 사용된다. asyncio란 동시성(Concurrent) 코드를 사용할 수 있는 파이썬 라이브러리로, 주로

  • 고성능 네트워크/서버
  • 데이터베이스
  • 분산 작업 큐

와 같은 병렬 작업을 수행하는데 사용된다. 파이썬은 기본적으로 싱글 스레드로 작동하기 때문에 다른 언어와 비교해 병렬 작업의 속도가 느리다는 단점이 있었는데, 이러한 작업을 더 빠르게 실행할 수 있도록 도와주는 라이브러리라고 생각하면 된다. asyncio는 파이썬만의 특징인 Coroutine을 직접 이용하는 고수준(high level) API와 라이브러리/프레임워크 개발자를 위한 저수준(low level) API의 두가지로 나뉘어 있다.

고수준이 더 높은 단계가 아니라, 좀더 추상적이고 직관적이라는 뜻으로, 세부적인 기능을 몰라도 프로그램을 작성하는 데 문제가 없도록 구성되었다는 의미이다. 반대로 저수준이란 높은 퍼포먼스와 정확성을 위해 아주 자세한 내용까지 설정할 수 있도록 제공되는 API라는 의미이다.

저수준 API 중 유명한 것으로는 future가 있다.

coroutine

실제 coroutine을 이용한 코드 샘플을 실행해 보자.

import asyncio
async def main():
    print("Hello")
    await asyncio.sleep(1)
    print("World")
asyncio.run(main())

Hello가 출력되고 1초 후에 World가 출력된다. main() 함수를 그냥 호출할 경우 coroutine 객체가 리턴된다. 좀더 자세히 설명하면, await 키워드는 다른 coroutine에서 기다릴 수 있고(be awaited), 모든 coroutine이 종료될 때까지 함수는 더이상 진행되지 않는다. 따라서 이를 이용해 동시에 여러 함수를 호출하는 게 가능하다.

await 표현식 다음에 올 수 있는 객체는 세 가지가 있다.

Coroutine : async def로 선언된 함수, 또는 이 함수를 호출해 반환된 coroutine 객체

Task : coroutine을 동시에 예약하는 데 사용됨

import asyncio

async def nested():
    return 42

async def main():
    # Schedule nested() to run soon concurrently
    # with "main()".
    task = asyncio.create_task(nested())

    # "task" can now be used to cancel "nested()", or
    # can simply be awaited to wait until it is complete:
    await task

asyncio.run(main())

Future : 비동기 연산의 최종 결과를 나타내는 객체. 일반적으로 프로그래밍 할 때 Future 객체를 이용할 일은 없다.

태스크 동시 실행

asyncio.gather에 coroutine인 awaitable 객체를 시퀀스로 전달하면 된다. 결과는 모든 리턴이 리스트로 연결되어 리턴된다.

import asyncio

async def factorial(name, number):
    f = 1
    for i in range(2, number + 1):
        print(f"Task {name}: Compute factorial({number}), currently i={i}...")
        await asyncio.sleep(1)
        f *= i
    print(f"Task {name}: factorial({number}) = {f}")
    return f

async def main():
    # Schedule three calls *concurrently*:
    L = await asyncio.gather(
        factorial("A", 2),
        factorial("B", 3),
        factorial("C", 4),
    )
    print(L)

asyncio.run(main())

결과는 다음과 같다.

Task A: Compute factorial(2), currently i=2...
Task B: Compute factorial(3), currently i=2...
Task C: Compute factorial(4), currently i=2...
Task A: factorial(2) = 2
Task B: Compute factorial(3), currently i=3...
Task C: Compute factorial(4), currently i=3...
Task B: factorial(3) = 6
Task C: Compute factorial(4), currently i=4...
Task C: factorial(4) = 24
[2, 6, 24]

Thread blocking and coroutine

https://elizarov.medium.com/blocking-threads-suspending-coroutines-d33e11bf4761

goroutine

Go 런타임에서 관리하는 lightweight thread로 OS의 thread보다 훨씬 가볍다. goroutine은 함수를 동시에(Concurrently) 비동기적으로(asynchronously) 실행하는 데 사용된다. goroutine간 통신은 채널(channel)을 통해 이루어진다.

package main
 
import (
    "fmt"
    "time"
)
 
func print(s string, index int) {
    for i := 0; i < 10; i++ {
        fmt.Println(s, index, ",", i)
    }
}
 
func main() {
    print("Sync")
 
    for i:=0; i < 3; i++ {
      go print("Async ", i)
    }
 
    time.Sleep(time.Second * 3)

다만 주의할 점은 goroutine을 호출한 다음 순서로 반드시 다른 함수가 와야 한다는 것이다. 만일 위 예제에서 time.Sleep() 이 호출되지 않는다면 goroutine이 끝나기 전에 Go 런타임이 종료될 수 있기 때문이다.

goroutine에도 익명함수를 사용할 수 있다.

package main
 
import (
    "fmt"
    "sync"
)
 
func main() {
    var wait sync.WaitGroup
    wait.Add(3)
 
    for i:=0; i < 3; i++ {
      go func() {
          defer wait.Done()
          fmt.Println("Hello")
      }()
    }
 
    wait.Wait()
}

goroutine은 기본적으로 단일 코어에서 실행된다. 멀티코어 환경에서 goroutine을 실행하려면 다음과 같이 명시한다.

package main

import (
	"fmt"
	"runtime"
)

func main() {
	runtime.GOMAXPROCS(runtime.NumCPU())

	fmt.Println(runtime.GOMAXPROCS(0))

	s := "Hello, world!"

	for i := 0; i < 100; i++ {
		go func(n int) {
			fmt.Println(s, n)
		}(i)
	}

	fmt.Scanln()
}

go runtime scheduler

그렇다면 go runtime이란 무엇일까?

Concurrency와 Parallelisum

동시성은 서로 독립적인 프로세스를 실행하는 것을 지칭하는 말이다. 병렬성은 동시에 연관된 작업을 수행하는 것을 말한다. 다시 말해 동시성은 여러 작업을 동시에 실행하는 것처럼 하는 방식이고, 병렬성은 실제로 여러 작업이 동시에 수행되는 것이다.

동시성 병렬성
동시에 실행되는 것 같이 보이는 것 실제로 동시에 여러 작업이 처리되는 것
(싱글 코어에서) 멀티 스레드(Multi thread)를 동작 시키는 방식 멀티 코어에서 멀티 스레드(Multi thread)를 동작시키는 방식
한번에 많은 것을 처리(lots of things at once) 한번에 많은 일을 처리(lots of works at once)
Software, 논리적(logical), 독립적(independent) Hardware, 물리적(physical)