C++에서도 coroutine & yield 를 쓴지 벌써 1년이 지났습니다. 그리고 지난 1년간 C++에도 많은 일이 있었습니다.
그 중 제가 주목한 한가지는 바로 C++ 표준에 드디어 coroutine이 들어갈 수 있는 가능성이 열렸다는 것입니다!
바로 boost 1.53 버전에 coroutine이 정식으로 포함된 것인데요.
오늘은 이 boost 1.53 버전에 포함된 coroutine에 대한 소개를 해보려고 합니다.
소개를 시작하기에 앞서 한가지 알려드리자면..
이 글을 쓰고 있는 2013년 7월 16일 현재 boost는 1.54버전까지 릴리즈 되었습니다.
하지만 불행하게도 boost 1.54버전에는 coroutine에 문제가 있어 coroutine을 사용할 수 없습니다.
(http://boost.2283326.n4.nabble.com/boost-1-54-needs-a-bugfix-td4649209.html 참조)
boost에 정식으로 포함된 coroutine은 비공식 coroutine과는 인터페이스가 약간 다릅니다.
보다 더 명확하고 더 coroutine의 개념을 충실하게 구현하고 있습니다.
간단하게 사용법에 대해서 살펴보도록 하겠습니다.
먼저 아주 단순하게 coroutine 객체를 생성해서 실행하는 과정입니다.
위 코드의 결과는 아래처럼 나오게 됩니다.
coroutine의 실행은 처음 접한 분들에게는 굉장히 생소할 수 있는 동작을 보입니다.
caller()를 호출할때마다 coroutine을 실행한 위치로 빠져나오고 다시 coro() 형태로 coroutine을 실행하면 마지막으로 caller()를 호출한 다음 명령부터 실행되는것을 확인할 수 있습니다.
공식 coroutine은 비공식 coroutine과 다른점이 몇 가지 있는데요.
첫번째는 비공식 coroutine의 경우 생성한 이후에 coro(); 처럼 한번 실행해주어야 switch가 일어나지만 공식 coroutine의 경우 생성하면 바로 switch가 일어나고 바로 TestFunction이 실행됩니다. coroutine 함수가 생성과 동시에 바로 실행이 되면서 밑에서 설명할 coroutine의 반환값에 대해 더 제대로 된 구현이 가능해졌다고 생각됩니다.
두번째는 인터페이스입니다. 비공식 coroutine에서는 coroutine 함수 안에서 self.yield(); 처럼 self 객체의 함수로 호출해주었는데 공식 coroutine에서는 caller(); 와 같은 형태로 실행됩니다. 만약 이름을 caller가 아닌 yield로 한다면 다른 언어에서의 coroutine의 문법과 거의 동일하게 사용이 가능해집니다.
반환값이 있는 coroutine을 사용하는 경우에는 다음과 같이 할 수 있습니다.
역시 이 코드의 결과는 다음과 같습니다.
caller( 반환값 ) 을 호출해주면 coroutine을 호출한 곳에서 get() 를 이용하여 반환값을 받을 수 있습니다. 그리고 coroutine을 실행해주는 operator()는 coroutine객체의 참조를 반환하기 때문에 coro().get() 의 형태로 coroutine을 실행함과 동시에 반환값을 받는 인터페이스 사용이 가능합니다.
주의할 점은 반환값을 주기 위해 coroutine의 마지막에 caller()를 이용하여 반환값을 넘긴 부분인데요. 이렇게 되면 coroutine은 더이상 실행할 명령은 없지만 완전히 끝나지 않은 상태가 됩니다. 끝나기 직전에 switch가 일어난 것이죠. 그래서 마지막에 다시 coroutine을 실행해서 완전히 종료되도록 하기 위해 if( !coro ) 대신에 if( !coro() )를 사용한 것입니다. 물론 coroutine은 완전히 끝나지 않은 상태에서 객체가 삭제되도 문제는 없습니다. coroutine에서 생성한 스택 데이터들도 모두 coroutine이 삭제될때 같이 해제가 되는것을 보장합니다.
coroutine이 실행될 때 인자값을 넣어줄 수도 있습니다.
역시 마찬가지로 다음과 같은 결과를 확인할 수 있습니다.
coroutine을 실행할때마다 coroutine에 새로운 인자값을 넣어줄 수 있고 그 인자값을 받아서 사용할 수 있습니다. 참고로 이러한 동작은 비공식 coroutine에서는 지원하지 않는 내용입니다.
인자값 전달은 여러개(현재 최대 10개)도 가능하지만 이 경우 인터페이스는 약간 달라집니다.
이 코드의 결과는 다음과 같습니다.
여러 인자값의 지원은 boost tuple을 사용하여 구현되어 있습니다.
추가로 많은 분들이 관심가질만한 부분이 바로 coroutine의 성능일 것이라고 생각되는데요
boost coroutine의 performance 페이지에서 대강의 정보를 확인할 수 있습니다.
그래도 조금 더 와닿는 테스트를 원하시는 분들을 위해서 간단하게(그리고 급하게) 테스트 코드를 작성하여 측정해보았습니다.
먼저 1024바이트의 데이터를 coroutine의 stack에 할당하고 switch를 1000000번 일으키는 것을 100번 반복하여 시간을 측정하도록 한 코드입니다.
그리고 그냥 1000000번 for문을 돌면서 위 테스트 코드와 비슷한 로직 처리를 하는 코드입니다.
이것을 Intel i5 CPU에서 10번 반복하여 측정한 결과
coroutine을 사용한 결과는 평균 0.0819799 가 나왔고 그냥 for문을 돌린 결과는 평균 0.0195949 가 나왔습니다. 단순 계산으로 switch 비용은 0.062385 정도라고 생각할 수 있겠네요..
(참고로 win32 API인 Fiber를 사용하는 비공식 boost coroutine 으로 같은 로직을 실행했을 때 평균 0.17250125 이 나왔습니다)
여러분들은 어떻게 생각하실지 모르겠으나 제가 내린 결론은 boost coroutine은 꽤 쓸만한 물건이라고 생각됩니다.