지난 시간에는 쓰레드를 통한 비동기 프로그래밍의 원리와 구현에 대해 알아봤다. 이번 시간에는 쓰레드를 효과적으로 관리하기 위한 쓰레드 풀과 함께 서버 제작에 필요한 네트워크 기술을 설명한다. 쓰레드 풀을 이용한 비동기 프로그래밍과 소켓의 개념에 대해 알아 보고 최종적으로 게임 서버를 완성해 보자.
비동기 프로그래밍은 내부적으로 쓰레드를 이용한다. 그런데 비동기 호출을 할 때마다 새로운 쓰레드를 생성해서 작업을 하게 되면, 많은 비동기 호출이 일어날 때에는 쓰레드의 수가 너무 많아져서 오히려 컨텍스트 스위칭(context switching)하는 데 시간이 더 걸리는 현상이 일어난다. 이러한 현상을 해결하기 위해서는 적절한 쓰레드 수를 유지하는 것이 해결 방법이라 할 수 있을 것이다. 닷넷에서는 그러한 관리 방법으로 쓰레드 풀이라는 것을 이용한다. 이를 이용해 시스템의 CPU 사용량에 따라 항상 적절한 쓰레드 수를 유지시켜 준다.
쓰레드 풀이란 먼저 풀(pool)의 사전적인 의미는 스위밍 풀(swimming pool)처럼 물 웅덩이, 저수지라는 뜻이 있다. 다른 뜻으로는 카 풀(car pool)처럼 공동으로 이용하는 것이라는 뜻이 있다. 여기서는 두 번째의 공동으로 이용한다는 의미이다. 카 풀이라는 것이 에너지 절약을 위해서 이웃끼리 통근 시간 같은 때에 차를 같이 이용하는 것을 말한다. 쓰레드 풀도 이와 비슷한 것으로 쓰레드들이 시스템의 효율성을 높이기 위하여 집합적으로 모여 있는 것을 쓰레드 풀이라고 부른다.
쓰레드 풀은 쓰레드 생성 요청이 있을 때마다 그 쓰레드를 바로 생성하는 것이라 일단 큐에 그 쓰레드를 넣어 두었다가 쓰레드 풀이 그 요청을 처리할 수 있는 여유가 있을 때 큐에서 하나씩 꺼내서 처리를 한다. 닷넷 환경에서는 기본적으로 쓰레드 풀 안에서의 최대 25개의 쓰레드를 넣어 둘 수 있다. 이를 그림으로 나타내면 <그림 1>과 같다.
| <그림 1> 쓰레드 풀 |
쓰레드 풀의 사용 방법 닷넷에서 쓰레드 풀을 이용하기 위해서는 쓰레드 풀 클래스를 이용하면 된다. <리스트 1>은 쓰레드 풀 클래스의 메쏘드들이다.
| { // Constructors // Methods public static bool BindHandle(IntPtr osHandle); public virtual bool Equals(object obj); public static void GetAvailableThreads(ref Int32 workerThreads, ref Int32 completionPortThreads); public virtual int GetHashCode(); public static void GetMaxThreads(ref Int32 workerThreads, ref Int32 completionPortThreads); public Type GetType(); public static bool QueueUserWorkItem( System.Threading.WaitCallback callBack); public static bool QueueUserWorkItem( System.Threading.WaitCallback callBack, object state); public static System.Threading.RegisteredWaitHandle RegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, int millisecondsTimeOutInterval, bool executeOnlyOnce); public static System.Threading.RegisteredWaitHandle RegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, UInt32 millisecondsTimeOutInterval, bool executeOnlyOnce); public static System.Threading.RegisteredWaitHandle RegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, long millisecondsTimeOutInterval, bool executeOnlyOnce); public static System.Threading.RegisteredWaitHandle RegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, TimeSpan timeout, bool executeOnlyOnce); public virtual string ToString(); public static bool UnsafeQueueUserWorkItem( System.Threading.WaitCallback callBack, object state); public static System.Threading.RegisteredWaitHandle UnsafeRegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, int millisecondsTimeOutInterval, bool executeOnlyOnce); public static System.Threading.RegisteredWaitHandle UnsafeRegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, UInt32 millisecondsTimeOutInterval, bool executeOnlyOnce); public static System.Threading.RegisteredWaitHandle UnsafeRegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, long millisecondsTimeOutInterval, bool executeOnlyOnce); public static System.Threading.RegisteredWaitHandle UnsafeRegisterWaitForSingleObject( System.Threading.WaitHandle waitObject, System.Threading.WaitOrTimerCallback callBack, object state, TimeSpan timeout, bool executeOnlyOnce); } // end of System.Threading.ThreadPool
|
| |
<리스트 1>을 보면 거의 모든 멤버가 static이고 public constructor가 없음을 볼 수 있을 것이다. 이는 닷넷에서는 하나의 프로세스당 한 개의 풀만을 허용하기 때문이다. 즉 모든 비동기 호출은 같은 하나의 풀을 통해 이뤄진다. 따라서 제 3자의 컴포넌트가 새로운 풀을 만들어서 기존의 풀과 함께 돌아감으로써 생기는 오버헤드를 줄일 수 있는 것이다. 쓰레드 풀의 큐에 새로운 쓰레드를 추가시키려면 다음과 같은 메쏘드를 이용한다.
public static bool QueueUserWotkItem ( WaitCallBack callBack ,object state);
우선 WaitCallBack이라는 대리자를 이용하여 처리할 함수를 등록하고, state를 이용하여 함께 넘길 파라미터를 지정해 준다.
public delegate void WaitCallBack( object state );
WaitCallBack 대리자의 형식이 반환 값은 없고, 인자로는 state 하나만을 받는 형식이다. 따라서 쓰레드를 사용할 함수는 이와 같은 signature를 가져야 한다. 이를 이용하여 0부터 3까지 출력하는 3개의 작업을 만들어 보자. 이를 실행하면 다음과 같이 보통 일반 쓰레드를 이용하는 것과 비슷한 결과 화면을 볼 수 있다. 3개의 작업이 동시에 이뤄지고 있다.
1번 작업 : 0 2번 작업 : 0 1번 작업 : 1 3번 작업 : 0 2번 작업 : 1 1번 작업 : 2 3번 작업 : 1 2번 작업 : 2 1번 작업 : 3 3번 작업 : 2 2번 작업 : 3 1번 작업 끝 3번 작업 : 3 2번 작업 끝 3번 작업 끝
이번에는 Thread.IsThreadPoolThread이라는 속성을 이용하여 정말 쓰레드 풀을 이용하고 있는지와, 현재 쓰레드의 고유 번호를 나타내주는 메쏘드인 GetHashCode를 이용하여 그 값을 확인해 보자.
| <리스트 2> 0부터 3까지 출력하는 4개의 작업 |
| |
| class Class1 { [STAThread] static void Main(string[] args) { WaitCallback callBack;
callBack = new WaitCallback(Calc); ThreadPool.QueueUserWorkItem(callBack,1); ThreadPool.QueueUserWorkItem(callBack,2); ThreadPool.QueueUserWorkItem(callBack,3); Console.ReadLine(); } static void Calc(object state) { for(int i= 0; i < 4; i++) { Console.WriteLine(“{0}번 작업: {1}”,state,i); Thread.Sleep(1000); } Console.WriteLine(“{0}번 작업 끝”,state); } }
|
| |
<리스트 2>에 <리스트 3>과 같은 코드를 추가한다. 결과는 다음과 같다.
Main thread. Is Pool thread:False, Hash : 2 1번 작업 thread. Is Pool thread:True, Hash : 7 1번 작업 : 0 2번 작업 thread. Is Pool thread:True, Hash : 8 2번 작업 : 0 1번 작업 : 1 3번 작업 thread. Is Pool thread:True, Hash : 9 3번 작업 : 0 2번 작업 : 1 1번 작업 : 2 3번 작업 : 1 2번 작업 : 2 1번 작업 : 3 3번 작업 : 2 2번 작업 : 3 1번 작업 끝 3번 작업 : 3 2번 작업 끝 3번 작업 끝
즉 메인 쓰레드는 쓰레드 풀에서 하는 작업이 아니며, 나머지는 쓰레드 풀 내에서 작업하고 있음을 볼 수 있을 것이다. 그리고 각자 다른 해시코드를 가지고 있으므로 각자 새로운 쓰레드를 생성해서 작업하고 있는 것이다. 이는 현재 CPU 사용량에 여유가 있었기 때문에 각자 하나씩의 쓰레드를 생성해서 작업을 한 것이다.
| // 메인 부분에 추가 Console.WriteLine(“Main thread. Is Pool thread:{0}, Hash: {1}”, Thread.CurrentThread.IsThreadPoolThread, Thread.CurrentThread.GetHashCode());
// 쓰레드 작업 부분에 추가 Console.WriteLine(“{0}번 작업 thread. Is Pool thread:{1}, Hash: {2}”, state, Thread.CurrentThread.IsThreadPoolThread, Thread.CurrentThread.GetHashCode());
|
| |
만약 CPU 사용량이 많아져서 컨텍스트 스위칭 시간이 더 걸릴거라 판단되면, 다른 쓰레드들은 큐에서 대기하다가 기존 작업이 끝나고 그 쓰레드를 재사용해서 작업을 하게 된다. 이에 대한 예를 보자. CPU 사용량을 높이려면 다음과 같은 함수를 추가한다.
int ticks = Environment.TickCount; while( Environment.TickCount - ticks < 500 );
Environment.TickCount 속성은 마지막 리부팅한 후부터의 시간을 millisecond 단위로 리턴해 준다. Thread.Sleep(1000)이라는 부분 대신 이 함수를 넣고 실행해 보면 <화면 1>과 비슷한 결과를 볼 수 있다.
| <화면 1> CPU 사용량을 높인 후의 쓰레드 푸 작동 화면 |
<화면 1>을 보면 CPU 사용량이 100%임을 확인할 수 있다. 그리고 결과를 보면 3번 작업이 1번 작업과 같은 해시코드를 사용하고 있다. 즉 같은 쓰레드를 재사용하고 있는 것이다. 그래서 1번 작업이 끝난 후에, 1번 작업이 쓰던 쓰레드를 3번 작업이 다시 사용하고 있는 것이다. 이처럼 쓰레드 풀이라는 것은 현재 시스템의 상황에 따라 적절히 쓰레드 개수를 유지시켜 줌으로써 효율성을 높이고 있다. 그럼 이제 정말 비동기 호출이 쓰레드 풀을 이용하는지 확인해 보자.
| class Class1 { public static void Calc() {
Console.WriteLine(“Is pool:{0}”, Thread.CurrentThread. IsThreadPoolThread); for(int i=1; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“SubWork :{0}”,i); } }
[STAThread] static void Main(string[] args) { SubWork d = new SubWork(Calc); d.BeginInvoke(null,null); for(int i=0; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0}”,i); } }
|
| |
<리스트 4>는 지난 시간에 했던 예제이다. 그곳에 단지 쓰레드 풀임을 확인할 수 있는 문장을 하나 추가했을 뿐이다. 결과는 다음과 같다.
Is pool:True SubWork : 1 MainWork : 0 SubWork : 2 MainWork : 1 SubWork : 3 MainWork : 2 SubWork : 4 MainWork : 3 MainWork : 4 SubWork : 5 SubWork : 6 MainWork : 5 SubWork : 7 MainWork : 6 SubWork : 8 MainWork : 7 SubWork : 9 MainWork : 8 MainWork : 9
이제 비동기 호출이 쓰레드 풀을 이용하는 것이 확실해졌다. 그러면 모든 비동기 호출은 이와 같이 델리게이트를 만들어서 해야 할까? 그것은 아니다. 우리가 매번 비동기 호출을 위해서 델리게이트를 만들어야 한다면 그것 또한 귀찮은 일일 것이다.
그래서 닷넷에서는 미리 비동기 호출을 위한 함수들을 마련해 두고 있다. Begin×××, End×××로 표기되는 메쏘드들이 비동기 호출을 위해 미리 만들어 둔 함수들이다. 델리게이트의 BeginInvoke도 이와 같은 연장선에 보면 될 것이다. 우리는 소켓을 이용한 비동기 통신 방법에 대해 알아볼 것이므로 소켓과 관련된 비동기 함수들을 살펴볼 것이다. 그전에 소켓의 기본 개념부터 설명하겠다.
소켓이란? 일반적인 의미로 소켓이란, 전구의 소켓처럼 꽂는 구멍을 말한다. 즉 다른 것과 연결시켜 주는 구멍이다. 컴퓨터에서의 소켓도 이와 비슷한 의미이다. 네트워크의 다른 대상과 정보를 교환하기 위한 구멍인 것이다. 일종의 네트워크 자료 교환을 위한 파이프라인(pipeline)으로 생각하면 된다.
일반적으로 네트워크에서 정보를 주고받기 위한 주소로 IP 어드레스라는 것을 사용한다. 그런데 이 주소는 대개 하나의 컴퓨터에 한 개의 주소가 할당된다. 그런데 네트워크 정보 교환은 하나의 컴퓨터뿐만 아니라 여러 컴퓨터와 정보를 주고받아야 하므로 하나의 IP 주소로는 이 정보를 어디로 보내야 하는지 구분할 수 없다.
그래서 포트(Port)라는 개념을 쓴다. 이는 항구라는 뜻으로 각 네트워크 정보들이 통신하는 입구인 것이다. 일반적으로 HTTP는 80 포트를 사용하고, FTP는 21 포트를 사용한다. 그래서 어떤 한 컴퓨터에 네트워크 데이터를 보내더라도 포트 번호가 다르므로, HTTP용 데이터와 FTP용 데이터가 각각 제 자리를 찾아가는 것이다. 이를 그림으로 나타내면 <그림 2>와 같다.
| <그림 2> 포트의 개념 |
일반적으로 포트 번호는 0∼65535까지 쓸 수 있지만 0∼1024번까지는 80번이나 21번처럼 미리 정해진 포트 번호를 사용하므로 사용자가 임의의 포트 번호를 사용하려면 그 이상의 번호를 사용하면 된다.
<그림 2>를 보면 포트에 소켓이 연결되어 있음을 볼 수 있을 것이다. 특히 서버쪽을 보면 하나의 포트에 여러 개의 소켓이 달려있음을 볼 수 있을 것이다. 이는 다중의 클라이언트가 하나의 포트로 접속하기 때문이다. 각 클라이언트마다 이들의 데이터를 맡아서 중개해주는 파이프라인(소켓)이 따로 있어야 하기 때문에 하나의 포트에 여러 개의 소켓이 달려 있는 것이다.
그런데 여기서 한 가지 의문점이 있을 수 있다. 하나의 포트에 여러 개의 네트워크 데이터들이 몰려들어 올텐데 서버는 이를 어떻게 구분해서 각자의 전담 파이프라인(소켓)으로 보내주는 것일까? 이는 TCP/IP의 헤더를 보면 쉽게 해결이 된다.
| <표 1> TCP/IP 헤더 |
<표 1>을 보면 IP 프로토콜의 헤더에는 보내는 곳과 받는 곳의 IP 주소가 들어 있다. 한편 TCP 헤더에는 보내는 곳과 받는 곳의 포트 번호가 들어 있다. 이들 4가지의 정보는 서로의 데이터를 확실히 구분하는 기준이 되므로, 서버측에서는 이 헤더를 보고 각자에 맞는 소켓으로 데이터를 보내주는 것이다.
다시 <그림 2>를 보면 서버측의 포트 번호는 지정되어 있는 반면에 클라이언트측의 포트 번호는 일관성 없이 중구난방으로 아무 번호나 할당되어 있음을 볼 수 있을 것이다. 그 이유는 클라이언트 입장에서는 데이터를 보내야 하는 서버측의 포트 번호는 알아야 하지만 자신의 포트 번호는 그냥 비어있는 아무 번호나 써도 상관없다. 굳이 자신의 포트 번호를 미리 정하지 않아도 되는 것이다. 그래서 클라이언트가 서버로 연결할 때, 자신의 남는 포트 번호 중 아무나 한 개를 할당해서 소켓과 연결시켜 주는 것이다.
소켓의 구현 과정 소켓은 <그림 3>과 같은 일련의 과정을 거쳐 작업이 진행된다. 먼저 서버측에서는 소켓을 생성하고 그 소켓을 특정 포트 번호와 연결(bind)시킨다. 그리고 상대방으로 연결이 오기를 허락하는 듣기(listen) 작업을 수행한다. 그러다가 클라이언트가 접속을 하게 되면 서버는 이를 받아들이고(accept) 새로운 소켓을 만들어서 그 새로운 소켓이 계속 통신을 담당하게 하고 자신은 다시 듣기(lisetn)상태로 들어간다.
| <그림 3> 소켓의 구현 과정 |
그런데 이 때 한 가지 주의할 것이 있다. 하나의 포트에는 한 개의 소켓만 bind할 수 있다는 것이다. 여기서 조심해야 할 것이 bind라는 말이다. 하나의 포트에 여러 개의 소켓이 있을 수는 있지만 bind는 오직 한 개만 된다. 하나의 포트에 두 개의 소켓을 bind하려 하면 에러가 나면서 bind가 실패하게 된다.
그럼 왜 bind는 하나만 되는 것일까? 그 이유는 앞에서 보았듯이 데이터를 구분할 방법이 없기 때문이다. 데이터를 구분할 때 TCP/IP 헤더를 보고 구분한다고 했다. 그런데 하나의 포트에 두 개 이상의 소켓이 bind되면 이들 데이터를 구분할 방법이 없는 것이다.
예를 들어 <그림 2>에서 80번 포트에 HTTP와 FTP용 소켓 두 개를 bind시켰다고 해보자. 그러면 서버는 포트로 들어오는 패킷의 TCP/IP 헤더 정보를 보고 데이터를 구분하는데 그 헤더에는 IP와 포트 번호밖에 없다. 그래서 이 패킷이 HTTP용인지 FTP용인지 구분할 방법이 없는 것이다. 그래서 하나의 포트번호에는 하나의 소켓만 bind할 수 있다. 그러면 이제 실제로 간단한 소켓 통신 프로그램을 만들어 보자.
| [STAThread] static void Main(string[] args) { Socket listeningSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress hostadd = Dns.Resolve(“localhost”).AddressList[0]; IPEndPoint EPhost = new IPEndPoint(hostadd, 7000);
listeningSocket.Bind(EPhost);
listeningSocket.Listen( 10 ); Console.WriteLine(listeningSocket.LocalEndPoint + “에서 접속을 listening하고 있습니다.”); Socket newSocket;
while(true) { newSocket = listeningSocket.Accept(); // blocking Console.WriteLine(newSocket.RemoteEndPoint.ToString() + “에서 접속하였습니다.”); byte[] msg = Encoding.Default.GetBytes(“접속해 주셔서 감사합니다.”); int i = newSocket.Send(msg); } }
|
| |
간단한 소켓 예제 <리스트 5>는 서버 소켓 예제이다. 먼저 TCP 방식의 소켓을 생성하고 7000번 포트에 bind한 후 listen하고 있다. 그러다가 클라이언트가 접속을 하게 되면, 클라이언트의 주소를 표시해 주고 메시지를 전송해 주고 있다.
| [STAThread] static void Main(string[] args) { Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress hostadd = Dns.Resolve(“localhost”).AddressList[0]; IPEndPoint EPhost = new IPEndPoint(hostadd, 7000); s.Connect(EPhost); // blocking if ( s.Connected == true) { byte[] bytes = new byte[1024]; s.Receive(bytes); // blocking Console.WriteLine(Encoding.Default.GetString(bytes)); s.Shutdown(SocketShutdown.Both); s.Close(); } }
|
| |
<리스트 6>은 클라이언트의 코드이다. 클라이언트는 특정 포트와 bind할 필요가 없으므로 connect할 때 자동으로 임의의 포트가 할당된다. 서버로의 접속이 성공하면 메시지를 받아서 화면에 표시해 준다. 그럼 이제 이들의 결과 화면을 보자.
◆ 서버 화면 127.0.0.1:7000에서 접속을 listening하고 있습니다. 127.0.0.1:3912에서 접속하였습니다.
◆ 클라이언트 화면 접속해 주셔서 감사합니다.
서버 화면을 보면 클라이언트 측에서는 임의의 포트 번호에 소켓을 할당해서 접속하고 있다는 것을 확인할 수 있을 것이다. 그러면 이제 정말 하나의 포트에 여러 개의 소켓이 존재하는지 보자. 먼저 클라이언트를 두 개 실행시켜 서버에 접속하도록 하자. 다음은 서버 화면이다.
127.0.0.1:7000에서 접속을 listening하고 있습니다. 127.0.0.1:3916에서 접속하였습니다. 127.0.0.1:3917에서 접속하였습니다.
두 개의 클라이언트를 실행시켜서 7000번 포트에 두 개의 클라이언트가 접속을 했다. 이제 netstat -a라는 명령어를 ‘명령프롬프트’창에서 입력해 네트워크 상태를 확인해 보자.
C:\>netstat -a
Active Connections
Proto Local Address Foreign Address State TCP 한용희:7000 한용희:0 LISTENING TCP 한용희:7000 한용희:3916 CLOSE_WAIT TCP 한용희:7000 한용희:3917 CLOSE_WAIT
앞의 화면에서 다른 부분은 생략하고, 우리가 보기를 원하는 화면만 표시를 했다. 현재 로컬 컴퓨터의 7000번 포트의 상태를 보면 listening하는 상태가 있고, 이미 연결된 두 개의 정보가 나온다. 모두 같은 7000번 포트에 연결된 것들이다. 이로써 하나의 포트에 여러 개의 소켓이 있을 수 있다는 것을 확인할 수 있을 것이다.
이제 소켓에 대한 궁금증을 풀었다. 그런데 앞의 예제를 응용해서 게임 서버로 만들기에는 무리가 있다. 왜냐하면 accept할 때나 receive할 때 블러킹이 걸려서 다른 일을 하지 못하기 때문이다. 그러므로 우리가 지금껏 익혀온 비동기 호출을 이용해서 이 문제를 해결해 보자.
비동기 소켓 통신을 이용해 블러킹 해결 앞서 닷넷에서는 델리게이트를 따로 이용하지 않고서도 미리 준비된 Begin×××와 End×××를 이용해서 비동기 프로그래밍을 할 수 있다고 했다. 이를 이용해 앞서 만든 예제에 적용해 보자(<리스트 7>).
| class Class1 { static void AcceptCallBack(IAsyncResult ar) { Socket listener = (Socket)ar.AsyncState; Socket newSocket = listener.EndAccept( ar ); Console.WriteLine(newSocket.RemoteEndPoint.ToString() + “에서 접속하였습니다.”); byte[] msg = Encoding.Default.GetBytes(“접속해 주셔서 감사합니다.”); int i = newSocket.Send(msg);
} [STAThread] static void Main(string[] args) { Socket listeningSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress hostadd = Dns.Resolve(“localhost”).AddressList[0]; IPEndPoint EPhost = new IPEndPoint(hostadd, 7000); listeningSocket.Bind(EPhost);
listeningSocket.Listen( 10 ); Console.WriteLine(listeningSocket.LocalEndPoint + “에서 접속을 listening하고 있습니다.”);
while(true) { IAsyncResult ar = listeningSocket.BeginAccept ( new AsyncCallback(AcceptCallBack), listeningSocket); // non-blocking ar.AsyncWaitHandle.WaitOne(); } } }
|
| |
먼저 예제에서 블러킹이 되었던 accept 부분을 비동기 함수인 BeginAccept로 바꾸었을 뿐 결과는 동일하다. 만약 이 프로그램을 윈도우폼으로 만들었다면 accept할 때 윈도우가 움직이는 것을 보면 확실히 블러킹되지 않았다는 것을 확인할 수 있을 것이다. 그러나 여기서는 간결한 예제를 위해서 콘솔 프로그램으로 만들었다.
<리스트 8>은 클라이언트를 비동기 방식으로 수정한 것이다. 이번에는 connect와 receive 두 개를 비동기 방식으로 만들었다. 결과는 먼저 예제와 동일하다. 이 예제들은 간단하기 때문에, 별 어려움이 없을 것이라 생각한다. 그러면 이 비동기 통신이 쓰레드 풀을 이용하는지 직접 확인해 보고 쓰레드 풀에 남아 있는 쓰레드의 갯수에 대해 알아보자.
| class Class1 { static byte[] bytes = new byte[1024]; static void ConnectCallBack(IAsyncResult ar) { Socket s = (Socket)ar.AsyncState;
if ( s.Connected == true) { s.BeginReceive(bytes, 0, bytes.Length, SocketFlags.None, new AsyncCallback( ReceiveCallBack) , s); // non-blocking } } static void ReceiveCallBack(IAsyncResult ar) { Socket s = (Socket)ar.AsyncState;
int nLength = s.EndReceive(ar); if ( nLength > 0 ) // 0보다 작다면 접속이 끊어진 것이다. { Console.WriteLine(Encoding.Default.GetString( bytes ) ); } }
[STAThread] static void Main(string[] args) { Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPAddress hostadd = Dns.Resolve(“localhost”).AddressList[0]; IPEndPoint EPhost = new IPEndPoint(hostadd, 7000);
s.BeginConnect(EPhost, new AsyncCallback(ConnectCallBack) , s); // non-blocking
Console.ReadLine(); s.Shutdown(SocketShutdown.Both); s.Close(); } }
|
| |
I/O completion ports <리스트 8>에 다음과 같은 코드를 추가해서 <리스트 9>와 같이 현재 쓰레드의 상태에 대해 알아보자. ShowThreadInfo()라는 함수를 만들었다. 이는 현재 쓰레드의 해시코드, 쓰레드 풀인지 여부, 그리고 남아있는 쓰레드 풀의 여분 갯수를 표시한다. 앞서 쓰레드 풀은 시스템에 따라 적절한 쓰레드 갯수를 유지시켜 준다고 했다.
| <리스트 9> 쓰레드의 상태를 알아보기 위한 코드 |
| |
| static void ShowThreadsInfo() { int workerThreads, completionPortThreads; Console.WriteLine(“Thread HashCode: {0}”, Thread.CurrentThread.GetHashCode()); Console.WriteLine(“Is Thread Pool? : {0}”, Thread.CurrentThread.IsThreadPoolThread);
ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads); Console.WriteLine(“Available Threads”); Console.WriteLine(“WorkerThreads: {0}, CompletionPortThreads: {1}”, workerThreads, completionPortThreads); Console.WriteLine(); }
static void ConnectCallBack(IAsyncResult ar) { Socket s = (Socket)ar.AsyncState; ShowThreadsInfo(); if ( s.Connected == true) { s.BeginReceive(bytes, 0, bytes.Length, SocketFlags.None, new AsyncCallback( ReceiveCallBack) , s); // non-blocking } Thread.Sleep(2000); }
static void ReceiveCallBack(IAsyncResult ar) { Socket s = (Socket)ar.AsyncState;
ShowThreadsInfo();
int nLength = s.EndReceive(ar); if ( nLength > 0 ) // 0보다 적다면 접속이 끊어진 것이다. { Console.WriteLine(Encoding.Default.GetString( bytes ) ); } }
|
| |
기본적으로 25개가 최대인데 쓰레드 풀에서 쓰레드를 하나씩 돌릴 때마다 이 최대 수치는 줄어들게 된다. 이를 표시해 주는 함수가 GetAvailableThreads라는 함수이다. 앞에서 Connect의 콜백 함수의 경우 비동기 호출 후 바로 끝나는 것을 막기 위해서 2초간 잠시 잠을 재웠다. 어떤 결과가 나올 것인가? 그냥 생각하기로는 connect에서 쓰레드 하나 쓰고 receive에서 쓰레드 하나 쓰니 남아있는 쓰레드 갯수는 23개가 돼야 할 것이다. 과연 그럴까?
Thread HashCode : 30 Is Thread Pool? : True Available Threads WorkerThreads : 24, CompletionPortThreads : 25
Thread HashCode : 33 Is Thread Pool? : True Available Threads WorkerThreads : 24, CompletionPortThreads : 24
쓰레드 풀인 것은 확인이 됐고 문제는 남아있는 쓰레드 개수이다. 비동기 호출인 receive를 했는데도 WorkerThread 개수가 변함이 없다. 대신 completionPortThread에서 숫자가 하나 줄었다. 왜 이런 현상이 일어나는 것일까? 그것은 또 다른 쓰레드 풀을 사용했기 때문이다.
앞에서 프로세스당 하나의 쓰레드 풀이 존재한다고 했는데 사실 하나가 더 존재한다. 그것은 바로 I/O 전용으로 또 하나의 쓰레드 풀, 즉 I/O completion 포트용 쓰레드 풀이다. 이는 I/O 작업 전용의 쓰레드 풀로서 I/O 작업을 완료했는지 안 했는지에 대한 체크를 담당하게 된다. 그럼 왜 I/O 전용 쓰레드 풀을 사용하는 것일까? 이것을 쓰는 것이 성능이 더 좋기 때문이다.
그러나 이 기능을 사용하려면 Winsock에서 이 기능을 지원해야만 한다. 그래서 앞 프로그램을 윈도우 95나 윈도우 98에서 실행하면 이들 운영체제의 Winsock에는 이 기능이 없기 때문에 닷넷에서는 자동으로 I/O completion 포트용 쓰레드 풀 대신에 workerThread를 이용해서 처리를 하게 해 준다.
그러나 윈도우 NT/2000/XP의 경우 Winsock2가 설치돼 있는데 이 Winsock2가 IOCP 기능을 지원하므로 별도의 IOCP용 쓰레드 풀을 가동해서 일을 처리하게 된다. 과거 비주얼 C++로 IOCP를 구현하려면 복잡하게 코딩을 해야 했으나 닷넷에서는 손쉽게 비동기 호출 중, 네트워크 I/O 관련 함수를 호출하면 자동으로 IOCP를 이용하게 되어 있어 보다 손쉽게 코딩을 할 수 있다.
Deadlocks 비동기 함수를 이용하는 데 있어 한 가지 주의 사항이 있다. <리스트 10>을 보자.
| class ConnectionSocket { public void Connect() { IPHostEntry ipHostEntry = Dns.Resolve( “localhost”); IPEndPoint ipEndPoint = new IPEndPoint( ipHostEntry.AddressList[0], 7000 );
Socket s= new Socket( ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp ); IAsyncResult ar = s.BeginConnect(ipEndPoint, null, null); s.EndConnect(ar); Console.WriteLine(“비동기 호출 완료”); } }
class Class1 { [STAThread] static void Main(string[] args) { for(int i=0; i < 30 ; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback(PoolFunc) ); } Console.WriteLine(“ThreadPool큐에 30개 적재”); Console.ReadLine(); } static void ShowThreadsInfo() { int workerThreads, completionPortThreads;
Console.WriteLine(“Thread HashCode: {0}” ,Thread.CurrentThread.GetHashCode()); ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads); Console.WriteLine(“WorkerThreads: {0}, CompletionPortThreads: {1}”, workerThreads, completionPortThreads); Console.WriteLine(); }
static void PoolFunc(object state) { ShowThreadsInfo(); ConnectionSocket connection = new ConnectionSocket(); connection.Connect(); } }
|
| |
먼저 비동기 호출을 하는 connectionSocket이라는 클래스를 만들었다고 하자. 그런데 어떤 사람이 이 클래스를 쓰면서 이를 쓰레드 풀 내에서 호출하기로 했다. 그래서 그는 30개 쓰레드를 연속으로 만들고 이를 쓰레드 풀에 적재하였다. 그리고 각 쓰레드가 비동기 호출을 하는 이 클래스를 사용하였다. 서버는 먼저 만든 서버를 그대로 이용하기로 하자. 어떤 결과가 나올 것인가? 다음 결과를 보자.
ThreadPool큐에 30개 적재 Thread HashCode : 3 WorkerThreads : 24, CompletionPortThreads : 25 Thread HashCode : 18 WorkerThreads : 23, CompletionPortThreads : 25
Thread HashCode : 19 WorkerThreads : 22, CompletionPortThreads : 25
Thread HashCode : 1 WorkerThreads : 21, CompletionPortThreads : 25
Thread HashCode : 20 WorkerThreads : 20, CompletionPortThreads : 25
Thread HashCode : 21 WorkerThreads : 19, CompletionPortThreads : 25 Thread HashCode : 22 WorkerThreads : 18, CompletionPortThreads : 25
Thread HashCode : 23 WorkerThreads : 17, CompletionPortThreads : 25
Thread HashCode : 24 WorkerThreads : 16, CompletionPortThreads : 25 Thread HashCode : 25 WorkerThreads : 15, CompletionPortThreads : 25
Thread HashCode : 26 WorkerThreads : 14, CompletionPortThreads : 25
Thread HashCode : 29 WorkerThreads : 11, CompletionPortThreads : 25
Thread HashCode : 30 WorkerThreads : 10, CompletionPortThreads : 25
Thread HashCode : 31 WorkerThreads : 9, CompletionPortThreads : 25
Thread HashCode : 32 WorkerThreads : 8, CompletionPortThreads : 25
Thread HashCode : 33 WorkerThreads : 7, CompletionPortThreads : 25 Thread HashCode : 34 WorkerThreads : 6, CompletionPortThreads : 25 Thread HashCode : 35 WorkerThreads : 5, CompletionPortThreads : 25
Thread HashCode : 36 WorkerThreads : 4, CompletionPortThreads : 25
Thread HashCode : 17 WorkerThreads : 3, CompletionPortThreads : 25
Thread HashCode : 4 WorkerThreads : 2, CompletionPortThreads : 25
Thread HashCode : 5 WorkerThreads : 1, CompletionPortThreads : 25
Thread HashCode : 7 WorkerThreads : 0, CompletionPortThreads : 25
실행을 해 보면 프로그램이 멈춰버릴 것이다. WorkerThread가 0이 되면서 프로그램이 더 이상 작동 안 하는 데드록(deadlock) 현상이 일어난다. 왜 이런 현상이 일어나는 것일까? 먼저 어떤 사용자가 쓰레드 풀을 사용하면서 30개의 쓰레드를 쓰레드 풀에 적재를 했다. 쓰레드 풀의 기본적인 최대치는 25인데 한꺼번에 30개의 쓰레드를 적재해 버린 것이다.
그래서 이미 쓰레드 풀은 포화 상태가 되었다. 그런데 비동기 호출인 BeginConnect를 하려고 큐에 적재를 했는데, 이미 차지하고 있는 쓰레드 풀에서 빈 공간이 나올 기미가 안 보이는 것이다. 이미 connect 함수에서는 EndConnect 함수를 이용해 비동기 호출이 끝나기를 블럭되면서 기다리고 있는데 끝나질 않으니 한없이 기다리게 된다. 그렇다고 끝날 수도 없다. 이미 쓰레드 풀은 포화 상태이기 때문에 더 이상의 비동기 호출이 끼어들 자리가 없기 때문이다.
이 문제를 해결하기 위해서는 BeginConnect라는 비동기 호출을 동기 호출 함수로 바꿔주거나 처음 쓰레드 30개를 적재할 때, 한꺼번에 적재하지 말고 비동기 함수가 실행될 여지를 남겨주기 위해서 앞의 for문에서 Thread.Sleep(1000);이라는 문장을 주어 잠시 기다려 주면 비동기 호출이 실행될 여지가 있어서 데드록이 발생하지 않는다. 이러한 현상은 일반적으로 쓰레드 풀 내의 쓰레드가 비동기 호출이 끝나기를 기다릴 때 발생한다. 그러므로 쓰레드 풀과 비동기 호출을 같이 쓸 때는 주의해야 한다.
게임 서버 소개 지금까지 소개한 내용을 가지고 본격적으로 온라인 게임 서버를 만들어 보겠다.
네트워크 데이터 통신 방법 플래시와 소켓 통신을 하는데, 데이터 통신 방법은 단순하게 문자열로 보내고 받는 방법을 택하였다. 원래 플래시에는 XMLSocket이라는 것을 제공한다. 이는 XML 데이터를 위한 소켓으로 데이터를 XML 방식으로 보내야만 하는 것이다. 그러나 게임과 같이 속도가 중요한 프로그램에서는 XML로 데이터를 처리하면 이를 파싱하는 데 오버헤드가 있어 바람직하지 않다. 그래서 플래시의 XMLSocket을 이용하기는 하지만 이를 파싱하지 않고 데이터를 콤마로 구분한 문자열로 보내서 쓰는 방법을 택하였다.
이때 주의할 것은 플래시의 XMLSocket은 맨 마지막에 문자열의 끝임을 나타내주는 ‘\0’ 표시가 있어야 제대로 받아들인다. 그래서 서버에서 데이터를 전송할 때 데이터의 끝에 ‘\0’을 추가해 주었다. 서버에서 네트워크 데이터를 보낼 때는 보통 서버에 접속한 모든 사용자에게 데이터를 전송하는데 자기 자신을 포함하는 경우가 있고, 자기 자신을 제외한 나머지에게 데이터를 전송할 경우가 있어 브로드캐스트(broadcast) 함수를 두 가지로 만들었다.
| <그림 4> 로그인 부분 |
| <그림 5> 대기실 부분 |
사용자 처리 방법 각 사용자마다 이를 담당하는 user 클래스를 따로 만들었다. 이 클래스의 멤버는 <리스트 11>과 같다.
| class User { private Socket m_sock; // Connection to the user private byte[] m_byBuff = new byte[50]; // Receive data buffer public string m_sID; // ID 이름 public string m_sTank; // 탱크 종류 public string m_sTeam; // 팀 종류 public int m_nLocation; // 방에서의 자신의 위치 public Point m_point; // 자신의 위치 }
|
| |
각 사용자마다 자신의 네트워크 데이터를 처리할 소켓을 가지고 있고, 자신의 각종 정보를 가지고 있다. 메인에서는 이들을 arraylist로 유지해 새로운 사용자가 들어올 때마다 리스트에 추가해 준다.
방 관리 본 게임 서버에는 방이 하나밖에 없다. 최초에 들어온 사람이 방장이 되는 것이다. 이렇게 만든 이유는 간단하게 만들기 위해서이다. 본 게임 서버를 소개하는 목적이 소스를 이해하는 데 있으므로 가능한 최소한의 기능만 구현하여 소스 코드 크기를 줄였다. 아마 이 소스를 분석해 보면 쉽게 여러 개의 방도 만들 수 있을 것이다. 방이 하나밖에 없으므로 이미 게임중이면 다른 사용자가 들어오지 못하게 하였다.
| <그림 6> 게임 시작전 초기화 부분 |
happy4u
2004. 8. 25. 14:57
2004. 8. 25. 14:57
내용이 상당이 많으므로 관심 있는 분만 클릭하세요~
..more
>접기 비동기 프로그래밍이라는 것은 한 가지 일을 할 때 그 일이 끝날 때까지 기다리는 것이 아니라 그 일은 그 일 나름대로 진행하면서 동시에 자신의 일을 계속 할 수 있는 것을 말한다. 이러한 기능이 가능하게 하려면 결국 쓰레드를 써야만 한다. 이 쓰레드를 이용해 비동기 프로그래밍 기법을 흉내내 보자.
쓰레드를 이용한 비동기 프로그래밍 <리스트 1>은 쓰레드를 이용해 다른 작업을 동시에 하는 것을 보여준 예이다. 0부터 9까지 출력하는 프로그램으로 별개의 쓰레드를 하나 더 돌려서 이들을 동시에 처리하고 있다.
|
<리스트 1> 0부터 9까지 출력하는 프로그램 |
| |
| class Class1 { public void DoSubWork() { // 서브 작업 for(int i =0 ; i < 10 ; i++) { // 많은 계산을 요구하는 작업 for(int j=0; j < 10000000 ; j++) {} Console.WriteLine(“SubWork :{0} “,i); } Console.WriteLine(“부 작업 완료”); }
// 해당 응용 프로그램의 주 진입점이다. [STAThread] static void Main(string[] args) { // TODO: 여기에 응용 프로그램 시작 코드를 추가 Thread t = new Thread (new ThreadStart(Class1.DoSubWork)); t.Start();
// 메인 작업 for(int i =0 ; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0} “,i); } Console.WriteLine(“메인 작업 완료”); } }
|
|
이 프로그램의 결과는 컴퓨터 사양에 따라 차이가 나고, 할 때마다 다른 결과가 나올 수 있으나 대략 다음의 결과와 비슷할 것이다. MainWork : 0 SubWork : 0 SubWork : 1 MainWork : 1 SubWork : 2 MainWork : 2 MainWork : 3 SubWork : 3 MainWork : 4 SubWork : 4 MainWork : 5 SubWork : 5 MainWork : 6 SubWork : 6 SubWork : 7 MainWork : 7 SubWork : 8 MainWork : 8 MainWork : 9 메인 작업 완료 SubWork : 9 부 작업 완료
즉 쓰레드를 이용하면 이처럼 두 가지 작업을 동시에 처리할 수 있다. 그런데 쓰레드의 생성자를 보면 ThreadStart라는 델리게이트를 취하고 있음을 볼 수 있을 것이다. 이 ThreadStart 델리게이트의 형식을 보면 다음과 같다. public delegate void ThreadStart();
리턴형은 void이고, 인자로는 아무런 값을 받지 않는 델리게이트이다. 따라서 우리가 쓰레드를 사용해 다른 함수를 가동할 때는 인자도 없고 리턴 값도 없는 함수만을 사용해야 된다는 얘기다. 그런데 세상사라는 것이 그리 간단하지 않은 것이, 앞의 경우만 보더라도 0부터가 아닌 임의의 숫자로 시작하고 싶어서 그 숫자를 인자로 넘기고 싶을 때가 있을 것이다. 이럴 때에는 어떻게 해야 할 것인가? <리스트 2>의 예제를 보자.
<리스트 2> 원하는 수부터 9까지 출력하는 프로그램 |
| |
| class CSubWork { private int start; public CSubWork(int i) { start = i; } public void DoSubWork2() { // 서브 작업 for(int i =start ; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“SubWork :{0} “,i); } Console.WriteLine(“부 작업 완료”); } }
class Class1 { [STAThread] static void Main(string[] args) { CSubWork = new CSubWork(5); Thread t = new Thread ( new ThreadStart (c.DoSubWork2) ); t.Start();
// 메인 작업 for(int i =0 ; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0} “,i); } Console.WriteLine(“메인 작업 완료”); } }
|
|
즉 비동기적으로 함수를 랩핑할 수 있는 클래스를 새로 만들어서 데이터 관리를 하면 되는 것이다. 그러므로 따로 인자를 넘기지 않더라도 클래스에 그 값을 줘서 해결할 수 있다. 다음은 <리스트 2>의 결과이다. MainWork : 0 SubWork : 5 MainWork : 1 SubWork : 6 MainWork : 2 SubWork : 7 MainWork : 3 SubWork : 8 MainWork : 4 SubWork : 9 부 작업 완료 MainWork : 5 MainWork : 6 MainWork : 7 MainWork : 8 MainWork : 9 메인 작업 완료
인자가 필요할 때에는 이렇게 해서 문제를 해결했는데, 이제 남은 또 다른 가능성은 리턴 값이 필요할 때이다. 되돌려 받는 값이 없다면 caller 측에서는 호출하고 잊어버리면 그만이다. 이를 ‘fire-and-forget style programming’이라고 한다. 그런데 되돌려 받는 값이 있다면 문제가 되기 시작한다. 상대방에게 비동기적으로 수행하라고 일을 시켜 놓았으니, 언제 끝날지 모르기 때문이다. 이를 확인하는 방법은 크게 두 가지가 있는데, caller 측에서 비동기 호출이 끝났는지 확인해서 끝났다면 리턴 값을 받아오는 방법과 caller 측에서 델리게이트를 넘겨주고 호출된 쪽에서 연산이 다 끝나면 그 델리게이트를 호출해줘서 리턴 값을 받아오는 방법이 있다. 이를 그림으로 나타내면 <그림 1>과 같다. | <그림 1> 비동기 호출로부터 결과값을 받는 방법 |
이번에는 비동기 부분에서 넘겨온 리턴 값을 가지고, 메인 부분에서 그 부분을 시작 값으로 하여 출력하는 프로그램을 만들 것이다. 지금까지 쓰레드 부분을 메인 부분에서 만들어 줬는데, 이번에는 메인 부분의 코드를 간결하게 하기 위하여 새로운 랩핑 클래스를 만들고 그곳에서 쓰레드를 담당하게 할 것이다. 쓰레드를 시작하는 함수는 Begin×××라는 이름을 붙여주고 결과 값을 받아 오는 함수는 End×××라고 붙여주자. <리스트 3>을 보자. | // 콜백 함수 델리게이트 public delegate void CallBack(int result);
// 리턴 값을 처리하기 위한 클래스 class CSubWork2 { private int start; // 시작 값 private CallBack callback; // 콜백 함수 델리게이트 public bool isCompleted; // 연산이 끝났는가? caller 측에서 물어볼 때 대답해 주기 위해서 public int ret; // 결과 값
public CSubWork2(int i, CallBack d) { start = i; callback = d; isCompleted = false } public void DoSubWork2() { ret = Calc(start); isCompleted = true if ( callback != null ) callback(ret ); }
protected int Calc(int s) { // 서브 작업 for(int i =s ; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“SubWork :{0} “,i); } Console.WriteLine(“부 작업 완료”); return s+1; } }
class CAsync { CSubWork2 w; public void BeginWork(int i, CallBack d) { w = new CSubWork2(i, d); Thread t = new Thread(new ThreadStart ( w.DoSubWork2 )); t.Start(); }
public int EndWork() { // 결과 값이 나올 때까지 블럭 do { if ( w.isCompleted == true ) break }while(true); return w.ret; }
public bool IsCompleted() { return w.isCompleted; } }
|
|
리턴 값이 준비돼 있는지 안 돼 있는지 확인하기 위해 IsCim pleted하는 메쏘드를 준비했다. 또한 콜백(callback) 형식으로 델리게이트를 넘길 때를 위해 그에 대한 델리게이트도 마련해뒀다. CAsync 클래스의 BeginWork 함수에서 인자와 콜백 함수를 넘겨주게 되는데 이때 만약 콜백 함수가 필요없다면 null을 넘겨주면 된다. 그러면 대신 IsCompleted하는 함수로 비동기 연산의 종료 여부를 확인할 수 있다.
한편 EndWork 함수에서는 비동기 연산이 종료되지도 않았는데 이 함수를 호출하면 준비되지 않는 결과 값을 가져가는 오류를 미연에 방지하기 위해 결과 값이 나올 때까지 블럭되게 한 후, 결과 값이 나오면 리턴하도록 했다. 그럼 먼저 caller 측에서 비동기 연산의 종료를 확인하는 예제를 보자(<리스트 4>). <리스트 4> 비동기 연산 종료 여부 확인하기 |
| |
| CAsync a = new CAsync(); a.BeginWork(4,null);
while(!a.IsCompleted()) { // 메인 작업 Console.Write(“.”);
} for(int i = a.EndWork() ; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0} “,i); } Console.WriteLine(“메인 작업 완료”);
|
|
이번 예제에서는 콜백 함수가 필요없으므로 BeginWork 함수에서 null을 넘겨줬다. 그리고 대신 IsCompleted 함수를 이용해 종료 여부를 확인하는 동안 메인에서는 자신의 일을 할 수 있도록 했다. 종료된 후에는 EndWork 함수를 이용하여 결과 값을 가져와서 메인 부분의 일을 처리했다. 이 프로그램의 결과는 다음과 같다.
................................................................ ................................................................ ................................................SubWork : 4 SubWork : 5 SubWork : 6 SubWork : 7 ................................................................. ................................................................. ..................................SubWork : 8 SubWork : 9 부 작업 완료 MainWork : 5 MainWork : 6 MainWork : 7 MainWork : 8 MainWork : 9 메인 작업 완료
이번에는 콜백 함수를 이용해 결과 값을 받아 오는 예제를 살펴보자(<리스트 5>). 콜백 함수에서는 비동기 함수의 리턴 값을 인자로 받아 와서 그 일을 하고 수행하고 있다.
| class Class1 { // 비동기 함수가 대신 호출해 줄 콜백 함수 public static void CallMe(int ret) { // 메인 작업 for(int i = ret; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0} “,i); } Console.WriteLine(“메인 작업 완료”); }
[STAThread] static void Main(string[] args) { CAsync b = new CAsync(); b.BeginWork(4,new CallBack(CallMe));
for(int i=0; i<400; i++) { // 아무 작업 수행 Console.Write(“.”); } } }
|
|
이 방법을 쓰면 caller 측에서 일일이 확인하지 않아도, 호출된 비동기 함수 부분에서 다 끝났다고 알려주는 격이 된다. 결과는 다음과 같다. ................................................................................ ................................................................................ ....................................................SubWork : 4 SubWork : 5 SubWork : 6 SubWork : 7 ................................................................................ ................................................................................ ............................SubWork : 8 SubWork : 9 부 작업 완료 MainWork : 5 MainWork : 6 MainWork : 7 MainWork : 8 MainWork : 9 메인 작업 완료
이상이 비동기 프로그래밍의 대략적인 내부 구현이다. 실제 닷넷 프레임워크 내부적으로는 이보다 훨씬 더 복잡하게 진행이 되지만 대략적인 것은 이와 비슷하다. 그럼 이제는 실제 델리게이트를 이용해 비동기 프로그래밍을 해 보자. 델리게이트를 이용한 비동기 프로그래밍의 실제 이번 예제는 앞서 우리가 만든 프로그램과 비슷하다. 시작 숫자를 인자로 넘겨주면 그 숫자부터 프린트하는 프로그램이다. 이를 비동기 호출로 하기 위해 그에 대한 델리게이트를 다음과 같이 선언했다. public delegate void SubWork(int i);
반환형을 다루는 예제는 조금 뒤에 다룰 것이므로, 지금은 반환형이 없는 델리게이트를 이용하자. 인자로는 시작 숫자를 넘겨줬다. 우리가 이렇게 델리게이트를 만들면 지난 시간에 소개했듯이 컴파일러는 이를 바탕으로 하여 다음과 같은 클래스를 만들게 된다. public class SubWork : System.MulticastDelegate { // 생성자 public SubWork (object target, int32 methodPtr);
public void virtual Invoke( int i );
public virtual IAsyncResult BeginInvoke( int i, AsyncCallback callback, Object object);
public virtual void EndInvoke( IAsyncResult result); }
BeginInvoke는 앞에서 BeginWork와 비슷한 역할을 한다. 먼저 델리게이트가 받을 인자가 오고, 그 다음에 콜백 함수가 오고, 추가로 상태를 지정할 수 있는 인자를 쓸 수 있다. 이는 추가 정보를 넘겨줄 필요가 있을 때에만 쓰는 것이므로, 필요없다면 안 써도 된다. 그럼 이를 실제로 테스트해 보자(<리스트 6>). <리스트 6> 델리게이트를 이용한 비동기 호출 |
| |
| public delegate void SubWork(int i); class Class1 { public static void DoSubWork(int start) {
for(int i=start; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“SubWork :{0}”,i); } }
[STAThread] static void Main(string[] args) { SubWork d = new SubWork(DoSubWork); d.BeginInvoke(3,null,null);
for(int i=0; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0}”,i); } } }
|
|
앞서 우리가 한 예제와 동일한 기능을 하는 예제이다. 델리게이트에는 기본적으로 BeginInvoke라는 메쏘드가 있어 프로그래머가 손수 쓰레드 관련 코딩을 하지 않고도 손쉽게 비동기 호출을 할 수 있게 해 준다. 결과는 다음과 같다. MainWork : 0 SubWork : 3 MainWork : 1 SubWork : 4 SubWork : 5 MainWork : 2 MainWork : 3 SubWork : 6 MainWork : 4 SubWork : 7 MainWork : 5 SubWork : 8 MainWork : 6 SubWork : 9 MainWork : 7 MainWork : 8 MainWork : 9
이와 같이 fire-and-forget형 프로그래밍의 경우 간단하지만 만약 반환 값을 다뤄야 할 경우는 약간 복잡해진다. 닷넷 플랫폼에서는 반환 값을 다루기 위한 4가지 스타일의 프로그래밍 기법을 제공한다. 1. Use Callbacks : 콜백 델리게이트를 이용해 비동기 부분에서 연산이 다 끝나면 델리게이트를 호출해 주는 방식 2. Poll Completed : caller 부분에서 연산이 다 끝났는지 IsCompleted라는 속성을 이용하여 계속 확인해 보는 방식 3. Begin Invoke, End Invoke : caller 측에서 결과 값을 받기 위하여 블러킹돼 기다리는 방식 4. Begin Invoke, Wait Handle, End Invoke : 앞의 방식과 비슷하나 wait handle에서 제한 시간을 설정해 줌으로써 계속 블러킹되는 것을 방지할 수 있다.
그럼 이들에 대한 예제를 하나씩 살펴보도록 하자. 이 예제는 SubWork에서 반환 값을 주는데, 역시 앞의 예제와 비슷하게 main 부분에서는 시작 값으로 사용될 값을 넘겨주게 된다. 먼저 <리스트 7>을 통해 1번의 경우부터 보도록 하자.
| public delegate int SubWork2(int i); class Class1 { public static int DoSubWork2(int start) { for(int i=start; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“SubWork :{0}”,i); } return start + 1; }
public static void CallMe(IAsyncResult ar) { SubWork2 d = (SubWork2) ((AsyncResult)ar) .AsyncDelegate; int result = d.EndInvoke(ar); for(int i=result; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0}”,i); } }
[STAThread] static void Main(string[] args) { // 여섯번째 예제 Console.WriteLine(“*** 여섯번째 예제 ***”); SubWork2 d2 = new SubWork2(DoSubWork2); d2.BeginInvoke(3,new AsyncCallback(CallMe),null);
// 아무런 작업 for(int i =0; i<300;i++) Console.Write(“.”);
// 그냥 끝나면 안되므로 키입력 까지 대기 Console.Read (); } }
|
|
이번에는 콜백 함수를 이용해서 함께 넘겨주고 있다. 이 콜백 델리게이트의 형식을 보면 다음과 같다. public delegate void AsyncCallback(IAsyncResult ar)
즉 리턴 값은 없고, 인자로 IAsyncResult라는 것을 받고 있다. 이는 닷넷 프레임워크에서 콜백 함수를 호출할 때 그 인자를 자동으로 넘겨주므로 걱정하지 않아도 된다. 이때 넘어오는 인자에서 AsyncDelegate라는 속성을 이용하면 해당 델리게이트를 받아올 수 있다. 따라서 이를 이용해 EndInvoke를 호출하여 결과 값을 받아오는 것이다. 결과는 다음과 같다.
SubWork : 3 SubWork : 4 SubWork : 5 ................................................................................ ................................................................................ .............................SubWork : 6 SubWork : 7 SubWork : 8 SubWork : 9 ................................................................................ ...............................MainWork : 4 MainWork : 5 MainWork : 6 MainWork : 7 MainWork : 8 MainWork : 9
<리스트 8>은 2의 경우로서 caller 측에서 계속 폴링(polling)하면서 연산이 다 끝났는지 확인하는 방법이다.
| SubWork2 d3 = new SubWork2(DoSubWork2); IAsyncResult ar = d3.BeginInvoke(3,null,null); while( !ar.IsCompleted ) { Console.Write(“.”); }
int ret = d3.EndInvoke(ar);
for(int i= ret; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0}”,i); }
|
|
caller 측에서 IAsyncResult의 IsCompleted 속성을 이용하여 계속적으로 연산이 끝났는지 안 끝났는지 확인하고 있다. 확인하는 동안 caller 측에서는 계속 다른 작업을 할 수 있다. 결과는 다음과 같다. ................................................................................ ................................................................................ ...........................................MainWork : 5 MainWork : 6 MainWork : 7 MainWork : 8 ................................................................................ ................................................................................ ......................................MainWork : 9 SubWork : 3 SubWork : 4 SubWork : 5 ................................................................................ ................................................................................ ......................................SubWork : 6 SubWork : 7 SubWork : 8 SubWork : 9 MainWork : 4 MainWork : 5 MainWork : 6 MainWork : 7 MainWork : 8 MainWork : 9
<리스트 9>는 3의 경우인데, 이는 결국 caller 측에서 비동기 호출된 부분이 끝날 때까지 블러킹되고 있으므로, 별로 권장하는 방법은 아니다. 이렇게 되면 비동기 호출을 할 의미가 없기 때문이다. 그러나 이도 결과 값을 받는 방법의 하나이므로 알아두자. 이번 예제는 콘솔 프로그램이 아닌 윈도우용 프로그램으로 만들었다. EndInvoke를 실행했을 때 caller 측이 블러킹된다는 것을 보여주기 위해서 윈도우용 프로그램으로 만들었다.
<리스트 9> Begin Invoke, End Invoke |
| |
| public class Form1 : System.Windows.Forms.Form { private System.Windows.Forms.TextBox textBox1; private System.Windows.Forms.Button button1; public delegate void SubWork(); public void DoSubWork() { // 서브 작업 string str; for(int i =0 ; i < 100 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 str = String.Format(“SubWork :{0}\r\n”,i); textBox1.Text += str; } }
[STAThread] static void Main() { Application.Run(new Form1()); }
private void button1_Click(object sender, System.EventArgs e) { SubWork d = new SubWork(DoSubWork); IAsyncResult r = d.BeginInvoke(null,null); d.EndInvoke(r); } }
|
|
이 프로그램을 실행하고 ‘비동기 연산 시작’ 버튼을 누르면, 비동기 연산을 시작한다. 그러나 연산 결과가 바로 나오지도 않을 것이며 윈도우가 움직이지도 않을 것이다. 모든 연산이 끝난 후에 한꺼번에 나올 것이다. 이는 EndInvoke라는 함수 때문에 블러킹돼버려서 그런 것이다. 그래서 연산 결과를 표시하기 위해 윈도우 화면 갱신도 못하고 마치 다운된 것처럼 멈췄다가 결과가 나오게 된다. <화면 1>이 그 결과 화면이다. | <화면 1> 윈도우에서 비동기 연산을 한 결과 화면 |
이번에는 마지막 방법인 4에 대해 알아보자. 이 방법은 3번 방법과 비슷한데 한 가지 다른 점은 time out을 정해줄 수 있어서 마냥 블러킹되는 것이 아니라 일정 시간이 넘어버리면 끊어버릴 수 있는 기능을 제공한다. 이번에는 비동기 연산을 0.1초 안에 해내지 못하면 블러킹을 해제하고 main 부분을 실행하는 프로그램을 만들어 본다.
<리스트 10> Begin Invoke, Wait Handle, End Invoke |
| |
| SubWork2 d4 = new SubWork2(DoSubWork2); IAsyncResult ar2 = d4.BeginInvoke(3,null,null);
if ( ar2.AsyncWaitHandle.WaitOne(100,false) == false ) { ret = 4; Console.WriteLine(“도중 차단”); } else { ret = d4.EndInvoke(ar2); }
for(int i= ret; i < 10 ; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 Console.WriteLine(“MainWork:{0}”,i); }
|
|
<리스트 10>은 caller 측에서 결과 값을 받기 위해서 0.1초간만 기다리고 그 안에 결과 값을 받지 못하면 블러킹을 중지하고 자신의 코드를 수행하는 예제이다. 결과는 다음과 같다. 결과에서 확인할 수 있는 것처럼 비동기 호출이 0.1초 안에 연산을 끝내지 못했기 때문에 main은 블러킹을 멈추고 자신이 할 일을 하고 있다. SubWork : 3 SubWork : 4 SubWork : 5 SubWork : 6 SubWork : 7 도중 차단 MainWork : 4 MainWork : 5 MainWork : 6 SubWork : 8 SubWork : 9 MainWork : 7 MainWork : 8 MainWork : 9 리턴 값을 이용하지 않을 경우 이상으로 비동기 호출에서 결과 값을 얻기 위한 4가지 방법을 소개했다. 그러나 비동기 호출에서 결과 값을 받기 위해 꼭 리턴 값을 이용해야 하는 것은 아니다. 우리가 인자로 넘겨줄 때 ref형이나, out형으로 넘겨주면 꼭 리턴 값을 이용하지 않더라도 그 결과 값을 받는 방법이 된다. 그러나 여기서 한 가지 주의할 것은 인자(parameter) 값의 업데이트도 비동기적으로 일어난다는 것이다. <리스트 11>을 보자. | public delegate void SubWork3(ref int i);
public static void DoSubWork3(ref int i) { i *= 2; }
[STAThread] static void Main(string[] args) { Console.WriteLine(“*** 열번째 예제***”); int v = 42; SubWork3 d5 = new SubWork3(DoSubWork3); IAsyncResult ar3 = d5.BeginInvoke(ref v,null,null); ar3.AsyncWaitHandle.WaitOne(); Console.WriteLine(“EndInvoke를 호출하기 이전의 값 :{0}”,v); d5.EndInvoke(ref v,ar3); Console.WriteLine(“EndInvoke를 호출하고 난 후의 값:{0}”,v); }
|
|
이번에는 값을 ref형으로 넘기고 있다. 만약 비동기 호출이 아닌 동기 호출이라면, 처음에 결과 값이 84가 나와야 할 것이다. 그러나 비동기 호출이기 때문에, 파라미터값의 업데이트도 비동기적으로 하므로 EndInvoke를 호출해야만 값의 업데이트가 일어난다. 그 결과는 다음과 같다. EndInvoke를 호출하기 이전의 값 : 42 EndInvoke를 호출하고 난 후의 값 : 84
그런데 여기서 만약 reference type을 파라미터로 넘기게 되면 비동기 호출 부분에서는 이 값을 실시간적으로 업데이트를 한다. 즉 EndInvoke를 호출할 필요없이, 자신이 연산을 하면서 값을 지속적으로 갱신을 하는 것이다. 이번에는 reference type인 byte형 배열을 파라미터로 넘겨서 그 값을 확인해 보겠다. <리스트 12>를 보자.
<리스트 12> reference type을 파라미터로 넘긴 예 |
| |
| public delegate void SubWork4(byte[] b);
public static void DoSubWork4(byte[] b) { for(byte i=0; i < b.Length; i++) { for(int j=0; j < 10000000 ; j++) {} // 많은 계산을 요구하는 작업 b[i] = (byte)(i*i); } }
[STAThread] static void Main(string[] args) { Console.WriteLine(“*** 열한번째 예제***”); byte[] b = new byte[10]; SubWork4 d6 = new SubWork4(DoSubWork4); IAsyncResult ar4 = d6.BeginInvoke(b, null, null); // ar4.AsyncWaitHandle.WaitOne(); for(int i=0; i< b.Length; i++) Console.WriteLine(“b[{0}]={1}”,i,b[i]); }
|
|
이 예제는 byte형 배열을 만든 후(이때 모든 배열의 값은 0으로 자동 초기화된다) 이를 비동기 호출로 넘겨주면, 비동기 연산 부분에서는 이 배열의 값을 넣어주는 작업을 하는 예제이다. <리스트 12>에서 결과 값이 다 나오도록 기다리는 부분을 주석 처리했는데, 그렇게 한 이유는 비동기 호출이 값을 실시간으로 고치는지 확인해 보기 위해서이다. 그 주석 처리한 부분의 주석을 없애 버리면 연산이 다 끝날 때까지 기다리기 때문에 언제나 정확한 값을 얻을 수 있을 것이다. 그러나 우리는 과연 비동기 호출이 값을 실시간으로 바꾸는지 확인하기 위한 것이므로 주석 처리를 했다. 결과 값은 컴퓨터 사양에 따라 다르고 할 때마다 다른 값이 나온다. 다음은 필자 컴퓨터에서 실행한 결과이다. 결과를 보면 5번 방까지는 제대로 들어갔는데, 그 이후로 값이 안 들어와 있다. 이는 caller 측에서 비동기 연산이 아직 다 끝나지 않았는데, 이는 그 값을 꺼내봤기 때문이다. b[0]=0 b[1]=1 b[2]=4 b[3]=9 b[4]=16 b[5]=25 b[6]=0 b[7]=0 b[8]=0 b[9]=0
쓰레드 풀을 이용하자 이번 호에서는 쓰레드를 이용한 비동기 프로그래밍의 원리와 4가지 구현 방법에 대해 알아봤다. 그런데 여기서 아직 해결하지 못한 문제점이 남아 있다. 닷넷 프레임워크는 우리가 매번 비동기 호출을 할 때마다 쓰레드를 새로 만들어서 하는 것일까? 만약 그렇다면, 쓰레드라는 것이 적당한 수가 유지된다면 문제가 안 되지만 과도한 쓰레드의 생성은 오히려 쓰레드를 교체하면서 생기는 컨텍스트 체인지 오버헤드(context change overhead)가 있을 것이다. 닷넷 프레임워크에서는 이 문제를 쓰레드 풀(pool)을 이용해서 해결하고 있다. 다음 연재에서는 이 쓰레드 풀을 이용한 비동기 호출에 대해 알아보고, 최종적으로 게임 서버를 완성할 것이다. @
happy4u
2004. 6. 24. 15:55
2004. 6. 24. 15:55
음... 뭐가 어떻게 된건지 모르겠지만... 하여간... 예전에 엑셀 파일을 컨트롤 하는 프로그램을 전면적으로 수정하는 작업 도중에 새로 프로젝트 생성해서 그 프로젝트에서 Excel을 사용할 필요가 있어서, 참조에 Microsoft Excel을 참조를 했는데, Excel 객체 생성이 안되는 것이다... 예전 소스랑 동일하게 했는데 --;; 헤매다가 MS 문서를 찾았는데, 다음과 같이 using문을 써서 해결했다. using Excel = Microsoft.Office.Interop.Excel; using System.Reflection; 쩝... 걍 줄여서 쓰는거였군... using Microsoft.Office.Interop.Excel; 걍 이것만 참조하고 Application mXls = new Application(); 이렇게 생성해서 사용해도 문제 없음 출처 : http://support.microsoft.com/default.aspx?scid=kb;en-us;302084
happy4u
2004. 6. 8. 15:50
2004. 6. 8. 15:50
구성 요소 서비스에서 응용 프로그램을 만들고 Drag & Drop으로 dll 파일을 등록하는 것과 command line에서 RegSvcs.exe fileName.dll로 등록하는 것과 동일하다. [assembly: ApplicationName("NewComTest")] 에 입력한 이름이 자동으로 응용프로그램 이름으로 등록된다. 현재 작업 중이라 성공적으로 완료되면 추가로 올릴 예정
happy4u
2004. 6. 4. 22:31
2004. 6. 4. 22:31
ADO.NET and SQL Server Performance Tips ADO.NET provides several different methods to access SQL Server data, including OLE DB.NET, ODBC.NET, SQLXML, and the SQL Server .NET data provider. Of all of these, the SQL Server .NET data provider is the fastest, as much as 30-40% faster than the others. The SQL Server .NET provider uses TDS (Tabular Data Stream, which is the native SQL Server data format) to communicate with SQL Server. The SQL Server .NET provider can be used to connect to SQL Server 7.0 and SQL Server 2000 databases, but not SQL Server 6.5 databases. If you need to connect to a SQL Server 6.5 database, the best overall choice is the OLE DB.NET data provider. [7.0, 2000] Added 2-25-2002 ADO.Net은 SQL Server에 접근하기 위한 방법으로 OLE DB.NET, ODBC.NET, SQLXML, SQL Server .NET와 같은 data provider를 제공한다. 이 중 Server .NET data provider가 가장 빠르며, 다른 방법에 비해 약 30~40% 빠르다. Server .NET data provider는 SQL Servr와의 통신에 TDS(Tabular Data Stream, which is the native SQL Server data format)를 사용한다. SQL Server .NET provider는 SQL Server 7.0과 SQL Server 2000 데이터베이스 접속에 사용할 수 있으나 SQL Server 6.5에는 사용할 수 없다. 만약 SQL Server 6.5 접속이 필요하다면 가장 좋은 방법은 OLE DB.NET data provider를 이용하는 것이다. ***** When using ADO.NET to make connections to SQL Server, always be sure you explicitly close any Connection, Recordset, or Command objects you have opened. While letting an object go out of scope will in affect close the object, it is not the same as explicitly closing an object. By explicitly closing these objects and setting them to nothing, you do two things. First, you remove the object sooner than later, helping to free up resources. Second, you eliminate the possibility of "connection creep". Connection creep occurs when connection or resource pooling is used and when connections are not properly closed and released from the pool. This helps to defeat the purpose of pooling and reduces SQL Server's performance. [7.0, 2000] Added 2-25-2002 ***** When you specify a server in an ADO.NET connection string, use the server's IP address, not the server's DNS name. By using an IP address instead of a DNS name, name resolution does not have to occur, reducing the amount of time it takes for a connection to be made. A server's IP address can be used to specify either a default or named instance of a server running SQL Server. [ 7.0, 2000] Added 2-25-2002 ADO.Net의 Connection String에 서버를 기술할 때, DNS 이름을 사용하지 말고 서버의 IP address를 사용하라. DNS이름 대신 IP address를 사용하게 되면, 이름 풀이(name resolution)를 필요로 하지 않기 때문에, 연결을 만드는 시간을 줄일 수 있다. 서버의 ip address를 기본 인스턴스 혹은 동작중인 SQL Server의 명명된 인스턴스를 기술하는 데 사용할 수 있다. ***** While SQL Server application roles are handy, they can also negatively affect your application's performance. The reason for this is that a connection to SQL Server using an application role cannot take advantage of connection pooling. In effect, connection pooling is turned off for any connections using application roles. If your application will be making many connections to SQL Server, avoid applications roles for your application's connections. [ 7.0, 2000] Added 2-25-2002 ***** To get the most out of connection pooling in ADO.NET, keep the following in mind when developing your applications: - Be sure than your connections use the same connection string each time. Connection pooling only works if the connection string is the same. If the connection string is different, then a new connection will be opened.
- Only open a connection when you need it, not before.
- Close your connection as soon as you are done using it.
- Don't leave a connection open if it is not being used.
- Be sure to drop any temporary objects before closing a connection.
- Be sure to close any user-defined transactions before closing a connection.
- Don't use application roles if you want to take advantage of connection pooling.
[ 7.0, 2000] Added 2-25-2002 ***** Disconnected recordsets in ADO.NET outperform disconnected recordsets in traditional ADO. ADO.NET is faster than ADO for disconnected recordsets because under ADO, COM marshalling between tiers requires that values in a recordset be converted to values recognized by COM. Under ADO.NET, data type conversion is not required, boosting performance. [ 7.0, 2000] Added 2-25-2002 ADO.NET의 비연결형 recordsets의 성능이 기존의 ADO의 비연결형 recordset보다 성능이 뛰어나다. ADO.NET의 비연결형 recordset이 ADO의 비연결형 recordset보다 빠르다. 이유는 ADO 하위 계층에서 recordset이 COM이 인식할 수 있는 값으로 변환되는 tier 간의 COM 마샬링(marshalling) 때문이다. ADO.NET 하위 계층에서는 데이터 타입 변환이 필요 없기 때문에 보다 좋은 성능을 발휘한다. ***** When possible, use the ExecuteNonQuery method with SQLCommand objects, as this is the most efficient way to execute queries from ADO.NET. Use output parameters with SQLCommand objects if you need to retrieve just a few values, or a single data row, instead of using more expensive techniques, such as a SQLDataAdapter, a SQLDataReader, or a strongly typed DataSet. [2000] Added 3-27-2002 Read an article about ADO.NET. 가급적 ExecuteNonQuery 메소드와 SQLCommand 객체를 사용하라. 이것이 ADO.Net에서 쿼리를 실행하는 가장 효과적인 방법이다. 몇개의 값을 얻어오거나 하나의 data row를 얻어와야 할 경우에는 SQLDataAdapter, SQLDataReader 혹은 DataSet과 같은 비용이 큰 방법 대신 SQLCommand 객체와 output parameters를 사용하라. 출처 : http://www.sql-server-performance.com
happy4u
2004. 6. 4. 22:07
2004. 6. 4. 22:07
7. ADO.Net을 ADO처럼 취급하지 말라 ADO와는 달리 ADO.Net은 비연결 모드에서 동작하도록 디자인 되었으며, 각 클라이언트를 위해 데이터의 독립적인 복사본을 유지 관리한다. ADO.Net 응용프로그램들은 캐시된 데이터 저장소들과 동작하도록 디자인되었으며, 데이터를 검색하고 수정하기 위해 신속하게 데이터베이스에 연결하고 연결을 해제한다. 6. SqlCommandBuilder를 사용할 필요가 없다. SQLCommandBuilder는 원본 데이터베이스에 DataSet 업데이트들을 전파하기 위해 DataAdapter가 사용하는 INSERT, UPDATE, 그리고 DELETE 문을 자동으로 생성한다. 그러나 SqlCommandBuilder를 사용하지 않고 대신에 여러분의 저장 프로시저를 DataAdapter의 InsertCommand, UpdateCommand, 그리고 DeleteCommand 속성에 연결함으로써 성능을 향상시킬 수 있다. 5. DataView를 간과하지 말라 DataView는 웹과 WinForms 응용프로그램으로 바인딩을 해 주고 DataTable로부터 정보의 일부를 얻을 수 있게 한다. 여러분은 계산된 컬럼을 사용함응로써 DataTable에서 데이터를 확장하기 위하여 DataView Expressions 속성을 사용할 수 있다. 4. DataSet을 사용할 필요가 없다. 만일 단일 테이블로부터 데이터를 검색하는 중이라면 DataSet에 독립적인 DataTable의 한 인스턴스를 생성함으로써 DataSet의 오버헤드를 피할 수 있다. 그 다음에 DataSet의 필요 없이 DataTable에서 데이터에 접근할 수도 있고 그것에 바인드를 할 수도 있다. 3. DataSet을 작은 데이터베이스로 여기지 말라 종종 초보 ADO.Net 프로그래머들은 DataSet을 기반 데이터베이스의 작은 버전으로 만들려고 시도한다. DataSet은 단지 응용프로그램이 필요로 하는 데이터를 담고 있는 로컬 데이터 캐시라고 갖누한는 것이 훨씬 좋다. 2. 연결 문자열을 내장하지 말라 여러분의 응용프로그램이 연결 문자열을 내장하면 보안에 문제를 야기할 수 있다. 만일 가능하다면 통합된 보안의 장점을 이용한다. 그것이 불가능하다면 Active Directory나 보안된 구성 파일에 응용프로그램의 연결 문자열을 저장하기 위해 통합된 보안을 사용한다. 1. 불필요한 데이터를 검색하지 않는다. 너무 많은 데이터를 검색하는 것은 응용프로그램의 성능을 저하시키는 가장 좋은 방법이다. 과도한 데이터는 네트워크를 혹사시키며 대규모의 웹 응용프로그램들이 필요로 하는 귀중한 시스템 자원을 낭비한다. 당신의 SELECT 구문들을 재검토하여 그것들이 적절한 WHERE 절을 사용하고 있으며 필요로하는 컬럼들만을 검색하는지 확인한다. 출처 : Windows & .Net Magazine 2003. 7
happy4u
2004. 5. 31. 16:08
2004. 5. 31. 16:08
jetbrains에서 나온 제품으로... Visual Stdio .Net 2003 Add-in 제품으로 아래의 기능을 가지고 있습니다 ReSharper is an add-in for Visual Studio .NET 2003 that brings intelligent C# code editing, highlighting and refactoring features to Visual Studio. ReSharper aims to provide C# developers with the same level of power and productivity enhancements that have been available to Java developers using IntelliJ IDEA. 사이트에 나온 설명입니다. 대충 보면... ReSharper는 VS .NET 2003의 add-in되는 것으로, 지능적인 C# 편집, highlighting , refactoring 기능을 가지고 있습니다. ReSharper는 C# 개발자들에게 IntelliJ IDEA를 사용하는 자바 개발자들과 동일한 수준의 향상된 개발 생산성을 제공하는데 역점을 두었습니다. (제대로 번역했나 몰겠네요 ^^'') 하여간... freeware인거 같구요. C# 개발자이시라면 당장 설치해서 써 보세요. 그 편리함에 감동하실껍니다. 지금도 Eclipse 쓰면서 계속 감동 중인데... 동일한 수준은 아니더라도 대폭 향상된 기능을 사용하실 수 있으실껍니다. 제공되는 기능 1. Navigation 2. Coding Assistance * Reformat code * 기타 등등 3. Highlighting 4. Refactoring * Rename * Move Type * Change Signature * Introduce Variable * Extract Method Download와 보다 자세한 내용은 아래 페이지 참조 출처 : http://www.jetbrains.com/resharper/index.html
happy4u
2004. 5. 19. 17:39
2004. 5. 19. 17:39
.. | | CSharpFriends - http://www.csharpfriends.com C# community site which provides forums, article discussions, tutorials, internal member messaging and live chat. | | C# (C Sharp) Tutorial and Test Engine - http://www.whizlabs.com/products/c-sharp/c-sharp.html Interactive and customizable C# (C Sharp) tutorial and test engine for quick learning. | | C Sharp (CSharp) Help : For C# Developers - http://www.csharphelp.com/ C# help with reviews of books on the subject and online information | | c-sharpcenter.com - http://www.c-sharpcenter.com/ Free resource of article and tutorials on C#. | | Training on c sharp - http://www.FiniteStates.com/ Advanced training in emergineg technologies. Training on .NET - Visual Studio.NET, C# (C sharp), ASP.NET, VB.NET |
| The C# Tutorial @ C# Station - http://www.csharp-station.com/Tutorial.aspx This set of lessons teaches the basics of the C# programming language. They are appropriate for beginning programmers and developers who are experienced in other languages and would like a quick overview of C# programming. | | Rotor: You spin me right round - http://www.macadamian.com/column/tinymessenger.html A tutorial on starting .NET development using C#. Writing TinyMessenger, a simple MSN Messenger client for Microsoft .NET on FreeBSD and Windows. By Fran?is Jacques and Jean-Claude Batista. | | Free tutorial "Learning C#" - http://www.managedworld.com/articles/0002/article.aspx Introductory tutorial to C#. | | Introduction to C# - http://www.simonrobinson.com/DotNET/Articles/Languages/IntroCSh.aspx A brief introduction to where C# fits in. Samples are included. | | Learn-C-Sharp.Com - http://www.learn-c-sharp.com/ Site containing articles and complete online courses for both beginner and advanced C# programmers. |
| C# Tutorial Presentation - http://www.jaggersoft.com/csharp.html In HTML format and links for information about the programming language. Compiled by Jon Jagger. | | Sample Programs in Generic C# - http://www.dina.kvl.dk/~sestoft/gcsharp/ Show how to use the programming language Generic C#, an extension of C# with parametric polymorphism, developed at Microsoft Research, Cambridge UK. | | C# FAQ - http://www.geocities.com/csharpfaq/ Tips over avoiding common pitfalls in C#. Critique of new language features. Best practices. | | C# Computing - http://csharpcomputing.com/ Tutorials on C# and .NET. | | C# Notebook - http://www.componentsnotebook.com/notebooks/csharp/index.html Example programs for using Windows Forms, the .NET Framework and C#. |
| TrooBloo: C# Tutorials - http://www.troobloo.com/tech/csharp.shtml Comprehensive listing of C# tutorials and articles. | | Hyperlinked C Sharp Grammar - http://www.jaggersoft.com/csharp_grammar.html Generated directly from the C# grammar in the .NET documentation (after a few typos were corrected) using a simple PERL script. | | Findtutorials.com: C# - http://tutorials.findtutorials.com/index/category/87 Several C# tutorials, in what is claimed to be the largest database of tutorials available on the Internet. | | Tetris Source Code in C# - http://rain.prohosting.com/kakasoft/cs.htm It is the best way to learn C#(C Sharp) programming quickly. | | C# Tutorial And Insights - http://www.al-maqsood.org/developer/csharp/tutorial_on_csharp.htm .NET insights and C# tutorial expanding on a baseline application and delving into .Net internals. |
| .NET Fever - http://sanjayahuja.tripod.com/tech/net/ For programmers trying to learn C# and .Net technology. Simple C# program sources. | | Achieve Programmers' Page - C# - http://www.achievecomputers.com/programmer/csharp.html Some common questions for learning C#. | | TutorGig.com C-Sharp Tutorials - http://www.tutorgig.com/showurls.jsp?group=4886&index=0 Collection of C-Sharp tutorial links. | | C# Frequently Asked Questions for C++ Programmers - http://www.eponymous.eclipse.co.uk/csharpfaq.htm Questions that C++ developers have when they first come across C#. By Andy McMullan. | | Useful Methods in C# - http://aspalliance.com/olson/methods A collection of useful, practical methods in C#. Descriptions, sources, examples of use. |
|
|
happy4u
2004. 5. 4. 16:17
2004. 5. 4. 16:17
윈도우 서비스 프로젝트를 배포 프로젝트로 만드는데, 서비스 등록까지 자동으로 되야 하는데 하는 방법을 Devpia에서 찾아보니 바로 나와있더군요. 내용은... 매우 간단~ ------------------------------ 간단합니다. 윈도우 서비스를 배포프로젝트로 만드시면 됩니다. 일단 설치 프로젝트를 만드신후 솔루션 탐색기를 보시면 조그마한 아이콘 7개가 있습니다. 거기서 첫번째 파일시스템 편집기->제일 왼쪽 화면에서 응용프로그램 폴더->마우스 오른쪽->추가 ->프로젝트 출력->인스톨을 원하는 프로젝트를 선택하신후->기본출력->다시 조그만 아이콘에서 5번째 사용자 지정작업 편집기선택->거기보시면(설치,커밋,설치제거등4개가 있습니다) 각각을 선택하시구 마우스 오른쪽 버튼을 누르면 사용자지정작업추가가 있습니다.거기서 응용프로그램 출력에 보시면 (프로젝트이름)의기본출력(활성)이라고 있습니다. 그걸 4개 모두 지정해 주시고 빌드하시면 됩니다. 그럼 그럼 설치 프로그램이 생성됩니다. 설치하시면 윈도우서비스가 자동으로 등록됩니다. 하지만 처음에는 윈도우서비스로 가셔서 시작을 해주시던지 아니면 리부팅을 해줘야 서비스가 시작됩니다. 구럼 ^^; 시간이되면 그림을 올려보죠~
happy4u
2004. 5. 3. 20:38
2004. 5. 3. 20:38
아주 간단하다... System.Web.Mail.SmtpMail.Send("n-cash.net", "goodfeel@test.com", "test", "test"); System.Diagnostics.Process.Start("iisreset.exe");
happy4u
2004. 4. 22. 22:09
2004. 4. 22. 22:09
쩝... 이런게 있는지 오늘 알았다 --;; 올만에 머리 썼다. 알고보면 별것도 아닌데... 분명히 대학때 배웠을텐데... 네트워크 쪽을 등한시 한 내 잘못이려니... 시스템의 내부적인 데이터 표현인데... 크게 Big-Endian과 Little-Endian이 있다. 0x12345678이라는 32비트 값을 표현했을 떄, Big-Endian : 0x12 0x34 0x56 0x78 Little-Endian : 0x78 0x56 0x34 0x12 이다. 시스템이 내부적으로 데이터를 처리하는데 있어서 Big-Endian 방식을 쓰느냐, Little-Endian 방식을 쓰느냐는 시스템의 CPU에 따라 달라진다. 이를 '호스트 바이트 순서 (Host Byte Ordering)'라고 하는데 문제는 호스트 바이트 순서가 일정치 않다는 것이다. Motorola 68000 계열이 주로 Big-Endian 방식을 쓰고 있고, 가장 많이 사용되는 Inter X86 계열은 Little-Endian 방식을 쓰고 있다. 이러한 문제 때문에 네트워크를 통해 데이터를 전송할 때는 통일된 방식을 이용해서 데티어를 전송하기로 약속을 하였는데, 이것이 바로 '네트워크 바이트 순서(Network Byte Ordering)'이다. 따라서 시스템이 Little-Endian 방식을 사용할 경우, 네트워크를 통해 데이터를 전송하기 전에 Big-Endian 방식으로 데이터를 변경해서 보내야만 하고 , 받을 때도 Little-Endian 시스템은 전송되어 오는 데이터를 역순으로 조합해야 한다. .Net Framework의 관련 Class IPAddress.HostToNetworkOrder() IPAddress.NetworkToHostOrder()
happy4u
2004. 4. 22. 16:52
2004. 4. 22. 16:52
휴.... .Net Framework의 클래스 라이브러리를 한번이라도 ?어 봤다면 이렇게 오래 걸리지는 안았을텐데 --;; int를 byte array로 변환하는 것이나 byte array를 int로 변환할 일이 있었는데... 위와 같은 역할을 해 주는 class를 찾지 못해서 만들어서 사용하려고 했다. 하기 싫은 bit 연산까지 해 가며 byte array를 int로 변환하는 함수를 만들고 거꾸로 int 에서 byte array로 변환하는 함수를 만들기 전 MSDN을 좀 더 뒤져보다가 BitConverter Class를 발견하고 말았다 --;; 웅.. 여기 다 있다. 기본적인 getBytes()부터 ToChar(), ToDouble(), 물론 ToInt() .... 기쁘면서 허무하다... .Net Framework의 관련 Class Int -> bytes BitConverter.GetBytes() byte[] -> int BitConverter.ToInt32() string -> bytes Encoding.ASCII.GetBytes()
happy4u
2004. 2. 16. 11:24
2004. 2. 16. 11:24
요즘 C# 프로그래밍에 재미를 더하고 있다... 최근에 두번째로 만든 프로그램이 있다. 특정 파일을 모니터링하다가 파일 갱신 내용으로 특정 웹페이지는 call하는 단순한 프로그램이다. 덕분에 첨으로 쓰레드도 함 써보고, 이벤트도 만들어서 함 해 보고... 재미있었다. 그런데 오늘 FileSystemWatcher의 존재에 대해 아는 강군으로부터 전해 들었다. 충격... 역시 두루두루 많이 알아야 한다는 생각이 들었다. FileSystemWatcher이 클래스는 .Net에 있는 것으로, MSDN의 개요를 보면 ---------------------------------------------------------------------------------------- 파일 시스템 변경 알림을 수신하면서 디렉터리 또는 디렉터리의 파일이 변경되면 이벤트를 발생시킵니다. FileSystemWatcher를 사용하여 지정된 디렉터리의 변경 내용을 조사합니다. 지정된 디렉터리에 있는 하위 디렉터리 및 파일의 변경 내용을 조사할 수 있습니다. 구성 요소는 로컬 컴퓨터, 네트워크 드라이브, 또는 원격 컴퓨터에 있는 파일을 조사할 수 있습니다. 참고 FileSystemWatcher 에서는 전환되거나 제거되지 않은 디스크도 조사할 수 있습니다. 타임스탬프 및 속성은 변경될 수 없으므로 FileSystemWatcher 는 CD와 DVD에 대한 이벤트는 발생시키지 않습니다. 구성 요소가 올바르게 작동되도록 하려면 원격 컴퓨터에 이러한 플랫폼 중 하나가 설치되어 있어야 합니다. 하지만 Windows NT 4.0 컴퓨터에서 원격 Windows NT 4.0 컴퓨터를 조사할 수는 없습니다.
---------------------------------------------------------------------------------------- 라고 나와있다. 이 클래스를 이용하면 이벤트 수신만으로 쓰레드 없이 내가 한 작업을 할 수 있는 것이다. 함 써보고 사용 내용을 올려 볼란다.
happy4u
2004. 2. 15. 09:34
2004. 2. 15. 09:34
좋은 사이트 정보 하나 소개글입니다. Microsoft patterns & practices guides contain specific recommendations illustrating how to design, build, deploy, and operate architecturally sound solutions to challenging business and technical scenarios. They offer deep technical guidance based on real-world experience that goes far beyond white papers to help enterprise IT professionals, information workers, and developers quickly deliver sound solutions. http://www.microsoft.com/resources/practices/default.asp
happy4u
2004. 2. 5. 17:34
2004. 2. 5. 17:34
여러 줄 주석화 할때 /* */ 를 이용해도 되지만.. 이클립스에서 사용하던 방식으로 블럭 지정한 줄 앞에 모두 //를 붙여주는 단축키가 있습니다. Ctrl + K + C : 주석 붙이기 Ctrl + K + U : 주석 해제
happy4u
2004. 2. 2. 16:21
2004. 2. 2. 16:21
happy4u
2004. 2. 1. 09:35
2004. 2. 1. 09:35
음... 글 쓰다 키를 잘못 눌러서 날렸네요 --;; 다시 쓰려니 정말 귀찮지만... csUnit은 Visual Stdio .Net에서 Unit Test를 하기 위한 Add-in software입니다. 자세한 소개는 아래 post를 참고하시구요.. http://blog.naver.com/goodfeel/80000706176 자 그럼 csUnit의 간단한 사용법 먼저 보시죠. 일단 csUnit을 다운 받은 후 설치 하고나서 Visual Stdio(이하 VS) .Net을 실행합니다. 그 다음 '새 프로젝트' 버튼을 누르면 아래와 같이 보입니다. 뭔가 바뀐게 있죠? 예.. 'csUnit Test library'라는 템플릿이 하나 추가 되어 있습니다.
자 그럼 그 'csUnit Test library'를 선택한 후 프로젝트 이름 적당히 바꾸시고 '확인'을 눌러 보시죠. 아래 그림과 같이 프로젝트가 만들어 지고 'TestFixture1.cs'라는 샘플 코드가 자동 작성 됩니다. 그 Sample코드를 간단히 테스트 해 보죠.
테스트 하는 방법은... 오른쪽 '솔루션 탐색기' 부분에서 프로젝트 이름을 선택한 후 오른쪽 마우스 버튼을 누르시면 왼쪽 화면과 같은 메뉴들을 볼 수 있습니다. 예전과 다를겁니다. 역시 메뉴 하나가 추가 됐죠?? 'Build And Run Tests'를 선택해 보세요. 자 새 프로그램이 하나 자동으로 뜹니다. 우리가 처음에 설치한 csUnit이라는 프로그램이 자동으로 뜨면서 1개의 테스트 중 1개가 성공했다는 의미로 녹색으로 표시 됩니다. 테스트 중 하나라도 실패할 경우 빨간색 줄을 볼 수 있습니다.
그럼 테스트에 오류가 있는 경우 어떻게 표시 되는지 함 보죠. 맨 마지막 줄 코드를 보면, 기존 코드는 'Assert.Equals(0, 0, "Zero no longer equals zero.");' 새 코드는 'Assert.Equals(0, 1, "Zero no longer equals zero.");' 자세한 내용은 추후 메뉴얼을 참고 하시면 되는데.. Assert.Equals에서 앞에 인자는 Expected값.. 그러니까 이 값이 나와야 맞다라는 거구요. 뒤 인자는 Actual값 그러니까 실제 프로그래밍 상 이 값이다. 이런 의미 입니다. 그 두 값이 틀리게 코드를 수정했으니 에러가 날텐데.. 위에 설명드린데로 다시 한번 테스트를 해 보시면... 아래 그림과 같이 빨간색으로 오류가 있음을 나타내게 됩니다.
|
|