C++ 11의 lambda와 shared_ptr을 활용한 비동기 프로그래밍에 대해
정답이 있는 질문은 아닐 것 같지만... 다른 분들의 의견이 궁금하여 글을 올려봅니다.
C++11을 사용하면서 람다를 너무 너무 잘 쓰고 있습니다. 특히 비동기 호출을 할 때 아주 유용한데, 문제는 변수의 소멸 시점이더군요.
해당 비동기 함수의 콜백이 완료되는 시점까지 캡쳐한 변수가 살아남아야 하는데, shared_ptr을 이용해서 값 복사로 캡춰하는 것으로 가능하다 싶었습니다.
auto socket = std::make_shared(ios);
auto header = std::make_shared();
socket->async_read_some(boost::asio::buffer(header.get(), sizeof(ST_PACKET_HEADER)),
[header, socket] ()
{
...
이런 식으로 사용을 몇 번 해 봤는데 예상과 달리 그닥 편하지가 않더군요.
해당 변수가 소멸되는 시점을 짐작할 수 없을 뿐더러(이건 당연하지만)
람다를 콜백으로 넘겨받은 함수에서 콜이 완료된 이후에 람다를 소멸시켜준다는 보장이 없다면 결국 shared_ptr의 소멸또한 보장되지 않더라구요.
윗 코드만 봐도 socket에 대해 상호 참조가 이뤄지고 있기 때문에 영 불안하구요.
이런 점들이 번잡스러워 지금은 작은 조각 단위로만 해당 패턴을 사용하고 있습니다.
javascript 처럼 클로저를 활용하고 싶은데 쉽지가 않네요.
이러다 보니 궁금한 점이 2가지가 있습니다.
1. 람다와 shared_ptr을 사용하여 원활하게 코딩한 사례가 있을까요? 있다면 어떠한 패턴일지
2. shared_ptr을 사용하지 않는다면 비동기 프로그래밍 시 변수의 생성과 소멸은 어떻게 제어하는 것이 좋을까요?
읽어주셔서 감사합니다. 비슷한 경험을 해 보신 분들의 조언 기다리겠습니다. ^^;
그냥 포인터 넘겨주고 콜백(람다) 마지막에 포인터
그냥 포인터 넘겨주고 콜백(람다) 마지막에 포인터 해제하면 되지 않나요?
물론 그렇게 할 수 있지만 C++11 스타일의 포인터
물론 그렇게 할 수 있지만 C++11 스타일의 포인터 관리를 사용하고 싶어서요.
게다가 만약 여러 콜백에서 하나의 포인터를 공유한다면 참조 카운팅을 사용할 수 밖에 없기도 하구요.
자바스크립트의 클로져라고 언급하셨는데, 람다 자체가
자바스크립트의 클로져라고 언급하셨는데, 람다 자체가 캡쳐로 클로져를 구현하기 때문에(그리고 람다가 없으면 함수 객체를 이용하면되구요) 자바스크립트에선 되는데 C++에선 안되는 건 없습니다.
캡쳐 자체도 값을 복사해올지, 레퍼런스를 가져올지 다 선택 가능하구요.
솔직히 문제 상황이 잘 이해가 안됩니다.
우선 'C++11 스타일의 포인터 관리'는 스마트 포인터를 쓰고 싶다는 뜻이신거 같네요.
shared_ptr은 자원을 공유하는 스마트 포인터입니다.
만약 자원 공유가 필요없는 상황에서 스마트 포인터를 쓴다면 shared_ptr이 아니라 unique_ptr을 써야겠죠.
그리고 마지막에 언급하신 것처럼 자원 공유가 필요한 상황이라면 shared_ptr을 쓰는게 맞는데, 만약 '어떤 특정 시점에서' shared_ptr로 공유한 객체가 소멸되는게 보장되어야 한다면, 그건 그 시점에서 동기화를 한번 해주어서 해결할 '설계상의 문제'입니다.
비동기로만 작동하게 하려면 객체의 소유권을 완전히 비동기 워커로 이전시키거나, 아예 값 복사를 해줘야죠.
제가 글을 이해하기 좀 어렵게 쓴 것 같네요...
제가 글을 이해하기 좀 어렵게 쓴 것 같네요...
요점만 정리하자면 비동기 프로그래밍에서 객체의 소유권을 어떻게 관리하는게 옳을까? 하는 질문입니다.
람다와 스마트 포인터의 캡춰를 통해 소유권을 관리하는 것이 옳은(많이 사용되는) 방법인지 궁금해서 질문을 드렸는데, 다시 보니 너무 추상적인 질문인 것 같습니다.
패턴이야 그때그때 맞는 걸 쓰면 되는 거니까요... 개인적으로 연구를 좀 더 해봐야 겠습니다.
답변 감사합니다.
이해가 안 가는군요.
Callback 에서 넘겨받은 lambda 를 다시 반환하거나 다른 객체에 쓰지 않으면 callback 종료시점에서 소멸되지 않나요?
Socket 상호참조라는게 무슨 말인지 모르겠습니다만(socket 의 비동기 읽기에 넘겨받는 lambda 가 socket 을 참조하는 것?) 그 역시 필요하니까 쓰시는 걸텐데요.
JavaScript 의 clojure 야 말로 이리저리 얽히면서 쓰지 않나요? 그래서 IE 에서 memory 누수 bug 도 있었고...
설마 GC 를 원하시는건가요?
최근 MSXML 을 쓴 바 있는데 왜 IE 에서 memory 누수가 발생했는지 알겠더군요. 참조계수로 운영되는 COM pointer 로 다루고 있으니...
shared_ptr 에서 상호참조가 발생할 수 있다면 weak_ptr 을 찾아보시기 바랍니다.
boost asio의 경우 콜백 호출 이후에 콜백을
boost asio의 경우 콜백 호출 이후에 콜백을 소멸시켜 주는 듯 합니다.(더 이상 호출할 일 없으니 당연하겠지만)
하지만 모든 라이브러리가 이러한 소멸을 보장해주진 않을 듯 해서요.
만약 호출 이후 함수 객체를 제거하지 않거나, 아예 호출이 일어나지 않는 경우에는 상호 참조가 일어나게 되어있더라구요.
말씀하신대로 socket에 넘겨주는 람다에서 socket을 캡춰하고 있으니 해당 람다가 소멸되지 않는이상 socket은 상호참조로 계속 유지되겠죠.
물론 필요에 의해서 사용하는 것이지만... 영 깔끔하지 않다는 느낌이 들어서요.
javascript와 같이 root 객체가 존재하지 않으니 상호 참조에 의한 누수는 발생할 수 밖에 없다고 생각되긴 합니다.
저도 이러한 문제점들을 모두 해결할 수 있는 대단한 방법을 필요로 하기보다는... 실사용이 가능할 만한 패턴인가? 하는 것에 궁금함을 가지고 질문을 드리는 것입니다.
댓글 달기