졸업 작품/서버

Boost Strand 사용

장형이 2019. 7. 17. 20:39

이 포스팅은 위의 두 주소에서 Strand를 공부하고 사용해본 기록이다.

 

strand? : http://blog.naver.com/PostView.nhn?blogId=njh0602&logNo=220715956896&parentCategoryNo=&categoryNo=&viewDate=&isShowPopularPosts=false&from=postView

 

[boost.asio] strand는 어떻게 동작하고 왜 사용해야 하는가?

How strands work and why you should use themIf you ever used Boost Asio, certainly you used or a...

blog.naver.com

strand 직접 사용 : https://chipmaker.tistory.com/entry/boost-asio-%EC%A0%95%EB%A6%AC-5-work-%EA%B3%A0%EC%B0%B0

 

boost asio 정리 - 5 (work 고찰)

1. 목적 이제 마무리 단계에 왔다. 바로 work의 사용법이다. work는 io_service start 전에 post()에 기술된 handler함수가 먼저 종료되는 상황에서 io_service start를 차 후에 호출하더라도 실제 post의 핸들러..

chipmaker.tistory.com

 

개요

 

Strand는 어떤 작업을 핸들러 단위로 쪼개서 멀티 스레드로 수행하는 것이다.

예를 들어 일반적인 온라인 게임 서버에서 동기화 단위는 Room일 것이다. Room 밖에서 서로 통신하는 일이 없는 이상, Room 별로 동기화하고, 나머지는 굳이 동기화하지 않아도 되기 때문이다.

그래서 이 Room을 핸들러 단위로 하고, Strand를 사용하면 명시적인 Lock 없이 멀티스레딩 수행이 가능해진다.

그림으로 보면 이해가 더 쉬울테니 그림으로 나타내 보았다.

 

Lock을 사용한 Room 동기화

 

Lock을 사용하면 그림과 같이 같은 핸들러(mutex)를 동기화시키기 위해서 끝날 때까지 대기해야 한다. 이때, Strand를 사용하면 다음과 같이 수행이 가능하다.

 

Strand를 사용한 Room 동기화

 

Strand를 사용하면 같은 핸들러(Room)가 겹치지 않게 Strand가 알아서 함수를 실행시켜 준다. 심지어 Strand 별로 호출 순서도 보장해준다.

 

지금은 간단히 룸이 2개에 스레드가 2개여서 굳이? 스럽지만, 룸이 몇백몇천개고 스레드가 몇십 개가 됐을 때 Room을 동기화 하기 위해서 여기저기 Lock을 거는 것은 끔찍한 일이 아닐 수 없는데, Strand를 사용하면 명시적인 Lock 없이도 멀티 스레드 프로그래밍이 가능해진다.

 

Strand 사용법 (개념)

 

솔직히 필자는 Boost에 익숙하지 않아서, IO_Service가 무엇을 하는지 Context가 무엇인지 잘 모른다. 그래서 Strand를 사용하기 위해서 최소한 쉽게 정리하면 다음과 같다.

 

strand를 사용하기 위해서 사용하는 boost 클래스들

 

이제 정말 소스를 보자.

 

Strand 사용법 (소스)

 

먼저 Main 부분은 다음과 같이 작성하였다.

int main(int argc, char* argv[])
{
	CManager manager;

	char opt;
	while (1)
	{
		std::cin >> opt;

		if (opt == 'x')
		{
			break;
		}

		// 1을 입력 받으면 Room1에서 PrintRoom(5)를 명령한다.
		else if (opt == '1')
		{
			manager.PostPrintRoom1(5);
		}
		
		// 2를 입력 받으면 Room1에서 PrintRoom(5)를 명령한다.
		else if (opt == '2')
		{
			manager.PostPrintRoom2(5);
		}
	}

	return 0;
}

 

Room 부분은 다음과 같다.

Strand 즉 동기화 핸들러를 사용하기 위해서는 생성자에서 io_service를 받아서 strand에 넣어줘야 한다.

PrintRoom 함수는 간단하니까 보고 가자.

 

class CRoom {
public:
	CRoom(boost::asio::io_service& service, const int& number) : m_strand(service), roomNumber(number) {}
	boost::asio::io_service::strand m_strand;

	void PrintRoom(int n) {
		for (int i = 0; i < n; i++)
		{
			std::printf("Room:%d - %d\n", roomNumber, i);
			boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
		}
	}

	int roomNumber;
};

 

마지막으로 Manager 부분.

주석을 보면서 이해하자.

class CManager
{
public:
	// 생성자에서 여러가지들을 초기화 해줘야 한다.
    // service는 독립적으로 초기화가 가능하고,
    // work는 io 스레드를 블락시켜줄 도구이다. 이 도구를 초기화 할땐 service를 넣는다.
    // room에서는 strand를 사용하므로 service를 넣어줘야한다. 동적할당으로 생성을 미뤄도 된다.
	CManager() : service(), work(service), room1(service, 1), room2(service, 2)
	{
    	// io 스레드를 미리 돌려 준다.
		for (int i = 0; i < 4; i++)
		{
			io_threads.create_thread(boost::bind(&boost::asio::io_service::run, &service));
		}
	}

	~CManager() 
	{
    	// boost io_service 종료를 보장하는 방법은 다음과 같다.
		service.stop();
		io_threads.join_all();
	}

	// boost bind를 통해서 함수를 맵핑하고 strand로 비동기 수행을 요청하는 방법이다.
	void PostPrintRoom1(int n) {
		service.post(room1.m_strand.wrap(boost::bind(&CRoom::PrintRoom, room1, n)));
	}
    
	// boost bind를 사용하지 않고, 람다를 사용하여 직접 함수를 만든 뒤, room2의 strand로 비동기 수행을 요청한다.
	void PostPrintRoom2(int n) {
		service.post(room2.m_strand.wrap([this, n]() {
			for (int i = 0; i < n; i++)
			{
				std::printf("Room:%d - %d\n", room2.roomNumber, i);
				boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
			}
			}
		));
	}

private:
	boost::asio::io_service service;
	boost::thread_group io_threads;
	boost::asio::io_service::work work;

	CRoom room1, room2;
};

 

소스 자체는 생각보다 매우 간단하다.

 

아래와 같이 호출해보면 다음과 같이 출력이 된다.

 

 

결과를 관찰해보면, Room1은 5-10-15, Room 2는 5-10으로 호출 순서가 보장되고, 같은 핸들러가 동시에 실행되지 않는 모습을 볼 수 있었다.

 

졸작 서버에 Strand 적용 후기

 

기존 졸작 서버에 이 strand를 넣으면서, 모든 동기 함수를 비동기 함수화 시키는 게 매우 힘들었다... ㅠㅜ

하지만 겨우겨우 수정한 덕분에 다음 그림처럼 구성하는데 완료했다..!!

 

 

Strand의 존재를 알았다면 미리미리 비동기를 걱정하고 프로그램을 짰을 텐데... ㅠㅜ

 

Ps. 복붙의 편의를 위해서 풀 소스는 아래에다가...

 

#include <iostream>
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/thread.hpp>

class CRoom {
public:
	CRoom(boost::asio::io_service& service, const int& number) : m_strand(service), roomNumber(number) {}
	boost::asio::io_service::strand m_strand;

	void PrintRoom(int n) {
		for (int i = 0; i < n; i++)
		{
			std::printf("Room:%d - %d\n", roomNumber, i);
			boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
		}
	}

	int roomNumber;
};

class CManager
{
public:
	CManager() : service(), work(service), room1(service, 1), room2(service, 2)
	{
		for (int i = 0; i < 4; i++)
		{
			io_threads.create_thread(boost::bind(&boost::asio::io_service::run, &service));
		}
	}

	~CManager() 
	{
		service.stop();
		io_threads.join_all();
		service.reset();
	}

	void PostPrintRoom1(int n) {
		service.post(room1.m_strand.wrap(boost::bind(&CRoom::PrintRoom, room1, n)));
	}

	void PostPrintRoom2(int n) {
		service.post(room2.m_strand.wrap([this, n]() {
			for (int i = 0; i < n; i++)
			{
				std::printf("Room:%d - %d\n", room2.roomNumber, i);
				boost::this_thread::sleep_for(boost::chrono::milliseconds(100));
			}
			}
		));
	}

private:
	boost::asio::io_service service;
	boost::thread_group io_threads;
	boost::asio::io_service::work work;

	CRoom room1, room2;
};

int main(int argc, char* argv[])
{
	CManager manager;

	manager.PostPrintRoom1(5);
	manager.PostPrintRoom2(5);
	manager.PostPrintRoom1(10);
	manager.PostPrintRoom1(15);
	manager.PostPrintRoom2(10);

	char opt;
	while (1)
	{
		std::cin >> opt;

		if (opt == 'x')
		{
			break;
		}

		// 1을 입력 받으면 Room1에서 PrintRoom(5)를 명령한다.
		else if (opt == '1')
		{
			manager.PostPrintRoom1(5);
		}
		
		// 2를 입력 받으면 Room1에서 PrintRoom(5)를 명령한다.
		else if (opt == '2')
		{
			manager.PostPrintRoom2(5);
		}
	}

	return 0;
}