삽질의 현장/- .NET

#078_닷넷(.NET)_.Net Framework 기본 - 비동기 대리자(delegate) & 멀티 쓰레드(Multi Thread)

shovelman 2015. 11. 15. 18:06


안녕하세요 삽잡이입니다.


이번 시간은 아마 매우 어려운 시간이 될 수도 있을 것 같습니다.

이해가 안가시더라도 천천히 여러번 읽어보시고... 

제가 잘못되게 서술했을 수는 있으나...(-_-;;)

어떠한 느낌인지를 느껴보시길 바랍니다...



쓰레드는 OS의 자원입니다.

그런데, 이 쓰레드에 대해서 공부를 하고자할 때 

띄우고, 동기화 제어하는 것이 어렵다면 어려운 부분입니다.

이를 쓰레드의 동시성 문제라고 합니다.


쓰레드의 동시성 문제에 관련되어 

'휘발성', '원자적'이라는 단어는 중요한 용어가 됩니다.


해당 용어들이 주가되어 쓰레드 동시성에 문제가 생기게 될 수 있습니다.

그러면 하나씩 살펴보도록 하겠습니다.


'volatile'을 우리말로 휘발성이라고 합니다.


여러 쓰레드가 접근하는 코드의 경우를 생각해보시길 바랍니다.

내용물에 대한 연산의 과정에 대해서 

RAM에서 CPU로, CPU에서 RAM 으로 반복적으로 처리해야하는 과정을

수 없이 많이 반복한다면... 

Read와 Write를 하는데 매우 많은 시간이 소요됩니다.


따라서, 전부 처리를 하고 RAM에 가져다 쓰는 기능을 제공해주는데,

수 없이 많은 반복을 걸치는 변수가 있다면 

를 CPU에 있는 register에서 연산을 다하고 반환해주는 변수를 register 변수라고 합니다.


즉, 동작할 때에 변수는 CPU내에서만

즉, 레지스터 내에서만 연산을 하고,

모든 행동이 완료됬다는 검증이 된다면 RAM에 다가 내려다 쓴다 이겁니다.


그런데, 효율적으로 사용하기 위해 제공해주던 register 변수가

어떤 문제의 원인이 되는 경우도 있습니다.


휘발하다는 것은 없어지기 쉽다는 뜻입니다.

레지스터 변수는 휘발성이 없는 변수인데, 

다중 쓰레드를 사용할 경우 레지스터 변수에 대해서 문제가 발생할 수 있습니다.


CPU의 레지스터 값이 모두 한 쓰레드의 것이었다가,

다중 쓰레드에 의해서 공유하게 된다면 문제가 생길 수 있다 이것입니다.


따라서, 이럴 때에는 CPU 내에 보관하지 않도록,

해당 변수를 휘발하게 만들기위해 volatile 키워드를 명시해주는 것이 좋습니다.

즉, CPU 내에 보관하지 않는다는 뜻이 되지요.


지금까지 휘발성에 관련되어 설명 드렸습니다.

정리해보겠습니다.

'쓰레드들 사이에 공유되는 변수'는 무조건 Volatile하게 만들어야된다는 것입니다.

이것이 기본이 됩니다.


다음으로 '원자적인 연산'에 대한 문제점입니다.

이전에 알아본 개념인데 간단하게 설명해보겠습니다.


다른 쓰레드에 영향받지 않고 실행되는 최소의 단위를 '원자적'이라고 합니다.

연산을 하다가 도중에 스케줄러에 의해 다른 쓰레드가 개입하게 된다면,

해당 연산이 엉망이 될 수 있습니다.

따라서, 멀티 쓰레드를 사용할 경우 원자적인 연산을 보장해줘야합니다.


이와 같이, 멀티 쓰레드가 난리를 치는 떄에는

반드시, 응용프로그램의 리소스를 오류의 가능성으로부터 보호해줘야합니다.

즉, 쓰레드의 동기화가 중요하다 이겁니다.


닷넷에서의 동기화를 위해서는 

'락, 모니터, [Synchronization]' 과 같은 특성을 많이 사용합니다.




이제부터 본격적으로 쓰레드에 대해서 알아보려고하는데요...

(이제...?)

어디 한번 기억을 떠올려봅시다..


대리자에 대해서 배웠을 때 동기적 호출과 비동기적 호출에 대해서 

알아보신 것에 대해서 기억을 하시련지요...

그 때에는 동기적인 호출만을 배웠었는데, 

이제부터 자동으로 쓰레드를 띄울 때 사용하는 

비동기적 호출에 대해서 알아보려고 하는 것입니다.


비동기 대리자는 내부적으로 쓰레드를 띄웁니다.

따라서 아주 쉽게 쓰레드를 띄울 수 있다는 장점을 가지고 있지만,

제어할 수 없다는 단점이 있습니다. 

왜냐하면, 쓰레드의 참조가 없기 때문이지요.


비동기적으로 어떤 기능을 수행하고 싶다면 비동기 대리자를 사용하는데,

만약, 사용자가 명령을 내린 쓰레드가 실행되고 있는지,

멈추고 싶을 때 멈추도록 관리하고 싶다면

쓰레드를 그냥 띄우는 게 바람직합니다.


아무튼... 대리자를 다시한번 생각해봅시다.

대리자는 동기적, 비동기적인 대리자로 나뉠 수 있었는데,

동기적인 방식으로는 InVoke() 메서드가 있었습니다.

비동기적인 방식으로는 BeginInvoke() 메서드와 EndInVoke() 메서드가 있었지요.



BeginInvoke()는 IAsyncResult 인터페이스를 return 합니다.

그리고 EndInVoke()의 return타입은 InVoke()의 return 타입과 같습니다.

InVoke() 메서드는 BeginInvoke()로 시작하여 

EndInVoke()로 끝나는 과정과 같기 때문이지요.


그런데, BeiginInvoke()의 경우 인자로 대리자를 받습니다.

대리자는 대리자인데, 완료 콜백으로 사용하기 위한 대리자를 받습니다.

그리고 인수에 대한 상태값인 object형의 인자를 받을 수 있습니다.


비동기 입출력은 BeginInVoke() 메서드를 시작으로 

EndInVoke()로 끝이 나는 과정을 가지고 있습니다.

EndInvoke()를 호출하면 return 값을 가져올 것입니다.


그런데 시작된 BeginInVoke()로부터 작업이 끝나지도 않았는데

EndInVoke()를 통해 return 값을 가져올 수 있을까요?

완료가 되야지 return 값을 반환하지 않겠습니까?

작업이 완벽하게 완료가 되야지 return 값을 가져올 수 있을 것입니다.

따라서, EndInvoke() 메서드는 

BeginInVoke() 메서드가 완료될 때 까지 Block상태가 됩니다.

따라서, CallBack 개념이 있는 것입니다.


즉, 호출한 메서드가 종료되면 호출해달라고 하는 것이지요.

이는 BeginInvoke()에 있는 대리자에서 호출하는 것입니다.

Callback 메서드이기 때문이지요.


완료가 됬다면 BeginInvoke() 메서드에서는 

IAsyncResult 형식의 return 값이 return 됩니다.

그리고 EndInVoke() 메서드에서는 해당 return 값을 인자로 받고 호출을 하면

결과치를 가져오게 되는 것이지요.



그런데, EndInVoke()를 통해서 

결과치를 받아와야되는 방법 이외에도 다른 방법이 있습니다.

EndInvoke()를 호출하기 위해서 대기하지 않아도 된다 이겁니다. 


BeginInVoke() 메서드에서 AsyncCallback이라는 대리자를 넣고

callback 메서드를 넣어주는 것입니다.

자세한 내용은 아래의 예시를 통해서 설명하도록 하겠습니다.


비동기 대리자의 Callback 함수의 시그니처는 정해져있습니다.



이와 같은 형식의 메서드를 callback 해달라고 명하는 것이지요.


예시를 살펴보시겠습니다.



이와 같이 callback 함수를 등록해두고, 

비동기 입출력을 통해 'Add'라는 함수의 작업이 완료가 되면,

Callback함수가 호출되게 됩니다.

호출이 됨으로써 비동기 호출이 완료되었다는 통지를 받을 수 있게 되는 것이죠.


그런데, 함수가 완료되었다는 결과치는 

대리자를 접근할 수 없기 때문에 실제 결과를 받을 수 는 없습니다.

왜냐하면, 호출된 callback 함수에서는 delegate를...

즉, 위에 예시에서 b를 알 수 없기 때문입니다.




마지막 인자인 callback 함수에 전달하기 위한 상태값

즉, IAsyncResult라는 인터페이스의 속성으로써 받을 수 있습니다.

따라서, 비동기 딜리게이트를 구현한 객체의 인터페이스를 통해 

결과치를 받을 수 있습니다.


그런데, 다른 방법도 있습니다.



 좀 어렵지요...  허허...



EndInVoke()를 호출하기 위해서는 delegate의 참조가 필요합니다.

이를 어떻게 받겠냐 해서... 아까는 state 값을 넘긴 것이고...



사실은, 인수로 넘어오는 IAsyncResult 인터페이스...

이 비동기 딜리게이트를 구현한 객체의 인터페이스를 통해 

비동기 딜리게이트를 받아올 수 있다는 것입니다.


항상 인터페이스를 받더라도 객체를 받고,

이 객체는 인터페이스로 구현을 했으니까 받을 수 있게 되는 것입니다.

다운 케스트를 하면 되는 것이죠.


따라서 delegate의 참조를 받아올 수 있는 것입니다.

AsyncDelegate라는 속성이 있기 때문이지요.

이것은 delegate를 호출한 참조자를 반환하지요.



이 AsyncResult 객체는 그런데 뜬금없이 뭐지...? 할 수 있는데 

호출하는 순간 BeginInVoke()에서는 AsyncResult 객체를 만들어내는 것입니다.

그리고 이 객체 안에는 AsyncDelegate라는 속성이 있는데,

이 속성은 delegate 참조자를 반환할 수 있지요.

따라서 EndInvoke()를 호출할 수 있게 되는 것입니다.


정리해보자면..

BeginInvoke()가 완료됬을 때 

ASyncCallback의 인자로 받는 callback 함수가 호출되고,

해당 Callback 함수는 비동기 쓰레드가 완료한 결과값을 받아와야되는데,

인터페이스로 전달되서 오기 때문에

다운 케스트를 해서 받아올 수 있다 이겁니다.


이번 시간은 여기까지 하도록 하겠습니다.

이상 삽잡이였습니다!