블럭,논블럭,동기,비동기 이야기
블럭,논블럭,동기,비동기 이야기
블록,논블럭,동기,비동기를 구분하는 것에 대한 글들이 많이 있는데, 어렵게 풀어내는 거 같아서 나름 간단하고 분명하게 구분해 보는 글을 작성 해 본다. 근데 함정이 있는데 분명하게 정답을 말해 준다는게 아니다. 분명하게 정답이 없으며, 불분명하다고 말해주려는 것이다. ㅎㅎ 면접시나 시험지에 적을 정확한 정답을 몰라서 혹시 불안해 하시는 분이 있다면 이 글을 읽고 안심하셔도 될 것이다.(뭐 시험관이 잘못알고 있는것 까지 책임지진 못하겠다. 과감히 논쟁하시라~~ㅎ) 구체적으로 블럭/논블럭에 대한 구분은 비교적 명확하다. 동기/비동기로 넘어가면 말하는 상황에 따라서 조금 달라지기 시작한다. 이제 조합하기 시작하면 문제가 발생하기 시작한다. 이 글에서는 이것의 구분에 대한 설명을 해드릴 것이지만, 구분을 굳이 왜 해야하는가? 라는 의문이 생길 수도 있을 것이다. 그렇다면 내 글의 의도가 먹힌 것이다. 사실 이것은 구분을 명확히 해야하는 문제라기 보단, 이런식으로 시스템이 작동 하기도 하는 구나 라는 "감" 을 잡으면 되는 문제이다.
여기서 내가 내리는 정의 또한 "정답" 이 아니다. 애초에 정답이 없는 문제라고 생각하고 유연하게 바라보자. 즉 1+1=2 같은 종류의 문제라거나, 자바에서 class 의 정의는? 같은 문제와는 다르다. 디자인패턴 같은 느낌이다. "의도"는 있지만 "구현"은 제각각인..
처음에는 각각의 정의를 내려보고, 그 담엔 우체국을 예로 들어서 이야기 식으로 구분을 해 보며, 마지막으로는 코드를 통한 예를 통해 그 "감"을 잡아내는 수확을 얻도록 하자.
@ 다 읽기 귀찮고 감 만잡으려면 3번 우체국이야기는 꼭 읽자
@ 애초에는 I/O 과 연관되어서 정의내리는 것이었는데, I/O 상관없는 비동기가 자주 사용되면서 더 희미해졌다.
1. 개별 정의
블럭/논블럭
- 블럭/논블럭는 함수호출에서의 이야기이다.(기술적으로 명확히 구분된다.)
- A 라는 함수를 호출했을때, A라는 함수를 호출 했을 때 기대하는 행위를 모두 끝마칠때까지 기다렸다가 리턴되면, 이것은 블로킹 되었다고 한다.
- A 라는 함수를 호출 했는데, A라는 함수를 호출 했을 때 기대하는 어떤 행위를 요청 하고 "바로" 리턴되면 이것은 논블럭킹 되었다고 한다.
동기/비동기
- 동기/비동기는 행위에 대한 이야기이다.
- 여기서 "행위"는 단순히 서로 다른 쓰레드 or 프로세스 or 서버에서 일어나는 일련의 동작들 이라고 치환해서
생각하면 이해하기는 쉽다.
- 동시성(concurrent) 의 문제이지 병행성(parallel ) 과는 무관하다. (병행성에 대한 의식은 잠시 잊어버리자)
- A 라는 행위와 B 라는 별개의 행위가 있다고 하자. A 라는 행위와 B 라는 행위가 동시(or 순차적이지 않다면)에 실행되고 있으면 비동기라고 한다. 여기서 제약이 하나 있는데 A,B 행위 사이에는 인과관계가 있어야 한다. 즉 웹서버를 예로 들어서 멀티쓰레드로 각각 A와B가 다른 클라이언트와 작업 할 때 둘은 동시에 작업하고 있지만, 둘의 인과관계는 없지 않나? 이땐 비동기라고 볼 수 없다. A라는 행위의 결과를 B라는 행위에서 언젠간 이용하게 될 때 비동기라고 본다.
- A라는 행위와 B라는 행위가 순차적으로 작동한다면 동기라고 한다.
- 동기적 행동에는 하나가 더 있다. A라는 행위가 별개의 것이 아니라, B라는 행위를 관찰하는 행위라면 이것이 동시에 일어나더라도 동기이다. 기술적으로 말해서 A라는 쓰레드와 B라는 쓰레드가 따로 돌아 간다고 해도, 어떤 하나의 행위가 다른 행위에 밀착되어 있다면 두 행위가 다른 쓰레드에서 벌어지더라도 동기란 말이다. 관찰하는 행위라는 말자체가 정확한 기술적 구분이 되는게 아니기 때문에 추상적이라는 표현을 사용한 것이며, 이 글의 가장 불분명한 요소 중 하나이니 잘 기억해 두도록하자. 이해가 안가면 다음의 우체국 예제와 실제 코드예제를 통해 이해할 수 있을 것이다.
사실 여기까지만 읽으셔도 된다. "굳이" 조합까지는 생각 할 필요는 없다. 조합은 내 개인적 상상력의 산물이다.
2.조합 정의
블럭/논블럭과 동기/비동기를 조합해서 상상을 해보자.
블럭/동기
A가 실행되다가 B라는 일을 수행하는 함수를 호출해서 B를 시작한다. B라는 일이 끝나면 함수를 리턴한다. A와 B는 순차적으로 진행되기 때문에 동기이며, B라는 일을 하는 함수를 호출하고 그 일이 끝나고 나서야 리턴되므로 블럭된 것이다. 따라서 블럭/동기
블럭/비동기
어떻게 블럭되었는데 A,B라는 일이 동시에 일어나는가? 설명을 들어보고 이런 경우를 말하는구나라는 "감"을 잡아보자.
일단 A는 B라는 일을 시킨다. 그리고 바로 리턴하고 (여기서는 논블럭) B는 일을 시작하고, A도 자신의 일을 한다. A는 중간에 B라는 일이 하는 중간 결과를 보고 받아서 처리해야한다. A는 B에게 요청을 해서 중간결과를 기다린다(블록), 요청의 결과를 받고 나서 그 결과를 이용해서 A는 자신의 일을 처리한다. 동시에 B 는 또 자신의 일을 동시에 한다. (비동기) A는 다시 B에게 중간결과를 요청해서 기다린다 (블록) , 요청의 결과를 받고 A는 자신의 일을 , B는 자신의 일을 한다. 반복된다.
이 글을 읽고, 사실 갸우뚱 해야한다. 중간에 블록되는 동안에는 "동기" 라고 말 할 수 있기 때문이다. 즉 어느 한 순간에 대해 해석하자면 틀릴 수도 있는것이다. 즉 처음부터 말해왔듯이 "정답"이 존재하지 않는다. 다만 이런 패턴들이 분명히 사용되고 있구나라고 감을 잡는게 목적이다.
논블럭/동기
이것이 예도 위의 블럭/비동기와 비슷한데 조금 다른 늬앙스에 대해서 "감"을 잡아보자.
논블럭/비동기
간단하다. A는 B의 일을 시작시키고 바로 리턴한다 (논블럭) 그리고 A와B는 각자 자신의 일을 한다 (비동기)
2. 실행활에서 일어나는 우체국 이야기로 풀어보자.
블럭/동기
우체국에 배달 트럭들이 줄을 서 있다. 우체국에 들어오는 물품들을 싣기 위해서인데,
- 1번 트럭이 우체국에 내 것들을 가져와주세요 요청하고 기다린다. (블럭)
- 우체국은 1번 트럭에게 주기 위한 물건들을 찾아서 싣기 시작한다.
- 2번트럭은 1번트럭에 물건이 다 싣기를 기다린다. (블럭)
- 3번 트럭도 기다린다. (블럭)
- 1번트럭이 물건을 싣고 떠나면, 우체국은 이제 2번 트럭의 물건을 찾아서 싣는다. (동기)
모든 일들이 순차적으로 일어 난다 (동기)
블럭/비동기
우체국에 가서 내가 필요한 물품은 무엇이라고 접수원에게 말을 하고 집으로 돌아온다.
- 우체국은 물품을 준비하고, 나는 집에서 집안 청소를 한다. (비동기)
- 우체국에 전화 해서 접수원과 통화한다. 물품이 준비되었냐고 물어본다. 접수원은 준비될 때 까지 기다리라고 한다. 나는 하염없이 기다린다 (블럭)
- 접수원이 준비됬다고 말한다. 나는 트럭을 가지고 우체국으로 가서 물건을 싣고 온다.
- 우체국은 자신의 일을 하고, 나는 싣고 온 물건을 배달한다 (비동기)
중간에 블럭되는 지점이 있지만, 그 이전과 이후에는 각자 자신의 일을 한다.
논블럭/동기
우체국에 가서 내가 필요한 물품은 무엇이라고 접수원에게 말을 하고 집으로 돌아온다.
- 우체국은 물품을 준비하고, 나는 전화기를 붙잡는다.
- 우체국에 전화 해서 접수원과 통화한다. 물품이 준비되었냐고 물어본다. 접수원은 안됬다고 말한다. 나는 전화를 바로 끊는다. (논블럭)
- 전화를 끊고, 집안 청소를 하는게 아니라, 다시 우체국에 전화한다. 안됬다고 하면 바로 끊는다 (논블럭)
- 계속 반복적으로 전화한다 (논블럭이며, 나는 내 일을 하는게 아니라 우체국의 일에 매달리고 있으므로 동기)
- 이번 전화에는 접수원이 준비됬다고 말한다. 나는 트럭을 가지고 우체국으로 가서 물건을 싣고 온다.
- 나는 싣고 온 물건을 배달한다.
중간 중간 논블럭으로 전화를 바로 끊지만, 끊고 나서 바로 또 전화를 하므로 동기
* 이 경우에 내가 배달하는 동안에는 현실과 좀 다르지만 우체국은 쉰다고 생각 해야한다. (동기)
논블럭/비동기
우체국에 가서 내가 필요한 물품은 무엇이라고 접수원에게 말을 하고 트럭을 놓고 집에 온다. (논블럭)
트럭(버퍼) 크기가 크다면 우체국에서 많이 채워 줄 것이다. (하지만 좀 더 시간이 걸리겠지)
- 우체국은 물품을 준비하고, 나는 집에 와서 내일 을 한다 (비동기)
- 전화 따위는 하지 않는다. 우체국에서 알아서 트럭에 짐을 채워서 나에게 트럭이 준비됬으면 연락 할 것이기 때문이다.
- 트럭이 가득 찼다고 연락이 왔다. 나는 트럭을 가지고서 배달을 시작하고 우체국은 자신의 일을 한다.
이것이 논블럭/비동기이다. 완전 효율적이지 않는가?
이렇게 되면 배달일 끝날 쯤에는 우체국에 가있는 트럭은 가득 차 있을 것이고, 나는 연속적으로 배달을 할 수 있어서 돈을 많이 벌 수 있을 것이다.
3. 실제 코드 예제로 풀어보자.
블럭되어 동기식으로 일처리 - javascript
const fs = require('fs');
const data = fs.readFileSync('/file.md');
파일 다 읽을 때 까지 함수가 멈춰져 있으며, (블럭) 다른일도 못한다 (동기)
논블럭되어서 비동기식으로 일처리 - javascript
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
// readFile 호출해 놓고 바로 리턴한다.
if (err) throw err; // 하지만 이 일에 대한 인과관계 장치를 마련해 둔다.
}); ... 다른일을 한다 ...
func start_server() {
l, err := net.Listen(CONN_TYPE, CONN_HOST+":"+CONN_PORT)
defer l.Close()
for {
conn, err := l.Accept()
if err != nil {
log.Print(err)
continue
}
go handle_client(conn)
}
}
아래의 예처럼 go 루틴으로 분기시킨 후에 go 채널로써 상호작용(인과관계)를 발생시키는 경우에는 비동기식이라 할 수 있을 것이다.
sigs := make(chan os.Signal, 1)
signal.Notify(sigs)
go func() {
s := <-sigs
log.Printf("RECEIVED SIGNAL: %s",s)
AppCleanup()
os.Exit(1)
}()
val myFuture: Future[String] = Future {
val f = Source.fromFile("build.sbt")
try
f.getLines.mkString("\n")
finally
f.close()
}
if myFuture.isCompleted { .... }
Thread.sleep(100)
if myFuture.isCompleted { ... }
Thread.sleep(100)
if myFuture.isCompleted { ... }
물론 실제 저런식으로 무식하게 코딩을 하진 않는다.
// 실제로는 아래처럼 onComplete 에 콜백을 등록해 준다.
// 더 콜백들을 편하게 조작하기 위해서, 많은 언어에서 지원하는 async/await 또한 지원한다.
val file = Future { Source.fromFile(".gitignore-SAMPLE").getLines.mkString("\n") }
file onComplete {
case Success(text) => log(text)
case Failure(t) => log(s"Failed due to $t")
} 이렇게 사용하면 논블럭/비동기라 할만하다.
블럭/비동기 - JAVA (NIO)
Selector selector = Selector.open();
ServerSocketChannel mySocket = ServerSocketChannel.open();
InetSocketAddress myAddr = new InetSocketAddress("localhost", 1111);
mySocket.bind(myAddr);
mySocket.configureBlocking(false);
int ops = mySocket.validOps();
SelectionKey selectKy = mySocket.register(selector, ops, null);
while (true) {
selector.select();
Set<SelectionKey> myKeys = selector.selectedKeys();
Iterator<SelectionKey> myIterator = myKeys.iterator();
while (myIterator.hasNext()) {
SelectionKey myKey = myIterator.next();
if (myKey.isReadable()) {
SocketChannel myClient = (SocketChannel) myKey.channel();
ByteBuffer myBuffer = ByteBuffer.allocate(256);
myClient.read(myBuffer);
String result = new String(myBuffer.array()).trim();
}
crunchifyIterator.remove();
}
}
논블럭/비동기 - C++ (IOCP)
while (! isStop()) {
위의 이야기에서는 한명의 트럭기사가 처리했지만, 4명의 오너배달부들이 트럭을 감시 할 수도 있다.
그럼 한명이 병목되더라도 나머지 기사들이 들어온 짐들을 처리 할 수 있을 것이다.
if (GetQueuedCompletionStatus(m_hIOCP, &dwTrans, &pKey, (LPOVERLAPPED*)&pOV, 64))
{
if (pOV)
{
MySession* sess = pOV->m_sess; ....
if (pOV == &sess->m_recv1) //트럭1(버퍼1)을 다 실었다고 우체국으로 부터 연락옴
{
bool error = false;
sess->m_recv1.m_size = dwTrans; // 받은 데이터 사이즈
sess->m_recv2.Reset(); //recv1 버퍼를 처리할것이고, recv2 버퍼는 OS한테 넘긴다.
//트럭을 가져온다(기술적으로는 os버퍼에서 응용버퍼로 데이터를 이동한다)
if (! ReadFile((HANDLE)sess->m_sock, (LPVOID)sess->m_recv2.m_data.c_str(), (DWORD)sess->m_recv2.m_data.size(), &dwTrans, &sess->m_recv2))
{
if (GetLastError() != ERROR_IO_PENDING)
error = true;
}
OnData(*sess, sess->m_recv1);// 트럭에 실린 짐을 처리하기 시작한다.
if (error)
OnClose(*sess);
}
else if (pOV == &sess->m_recv2)
{
//우체국에서 트럭2에 짐을 다 실었을 경우에 처리한다.
//트럭만 바뀌었을 뿐이지 위와 동일
}
}
}
}
bool MyServer::onData(MySession& sess, MyPacket& data)
{
std::string cmd, dat;
while (recvPacket(sess, cmd, dat))
{
//우체국으로 부터 받은 트럭으로 부터 배달을 시작함
//여기서 멀티쓰레드 사용하면 (알바배달부를 더 고용하면) 더 효율적이게 됨
//물론 너무 많이 만들어도 곤란하다.
}
return false;
}
// 이 코드는 트럭에 실린 짐을 확인하는 코드이다.
// 즉 트럭에 실린 짐이 김연아에게 갈 짐이 맞는지 확인하는 것이다.
// 트럭에 실린 짐이 김연아에게 갈 짐 중에서 50% 밖에 실리지 않았다면
// 김연아의 짐이 100% 될 때까지 트럭을 다시 우체국에 보내고 처리하지 않는다.
// 기술적으로는 "syn:: ~~~ ::end" 까지의 패킷이 완성되야 일 처리를 시작한다는 것이다.
static bool recvPacket(MySession& sess, std::string& cmd, std::string& data)
{
size_t st = sess.m_recvs.find("syn::");
if (st == std::string::npos)
return false; // [패킷작업] 패킷의 끝인 ::end 를 찾는 작업 등
if (dt)
data = sess.m_recvs.substr(sz+1, dt);
return true;
}
* 참고로 자바로도 가능하다 (NIO2)
이제 정답이 없다는데 이해했으리라 보고 굳이 구분을 하자면 나는 이렇게 구분한다.
@ 먼저 블로킹/논블로킹은 함수 호출에 관해서 국한 한다.
A가 B를 호출 했을 때 B가 A가 원하는 모든 일을 다 마치고 리턴하면 블로킹이고 다 마치기 전에 리턴하면 논블로킹이다.
함수가 작동하는 시간하고는 무관하다. 1+1만 리턴하는 함수면 엄청 빠르게 리턴 할 지라도 원하는 행위를 다 했기 때문에 블로킹이다.
@ 동기/비동기는 각기 다른 쓰레드/프로세스/서버에서 일어나는 행위에 대한 동시성에 관한 이야기이다.
즉 쓰레드 혹은 프로세스가 분리되서 행위가 일어나는 데, A쓰레드가 B쓰레드의 결과를 계속 대기하고 있으면 동기이다.
A(쓰레드,프로세스, 서버)가 자신의 일을 하다가 B의 결과를 이벤트로 받아서 처리하면 비동기이다.
이런 구분방법은 거의 대부분의 경우에서 의사소통을 하기에 매우 분명하며 적절하다고 생각한다.
위의 글은 아래에도 실려있으며 Q/A 가 추가되어 있으니 참고하십시요. https://okky.kr/article/442803