Greg Ewing
Clarity Consulting Inc.
요약: 이 기사에서는 스레딩의 다른 모델(단일, 아파트 및 자유)과 각 모델의 사용에 대해 설명합니다. 스레드를 이용하는 응용 프로그램을 작성하는 데 도움을 줄 수 있도록 스레드를 사용하는 C# 코드 샘플도 소개합니다. 또한 다중 스레딩 코드에 포함된 중요한 문제에 대해서도 설명합니다(9페이지/인쇄 페이지 기준).
목차
소개
스레딩에 대한 배경 지식
예제 응용 프로그램
다중 스레드 코드의 문제
결론
소개
다중 스레드 MSMQ(Microsoft Message Queuing) 트리거 응용 프로그램을 작성하는 일은 일반적으로 까다로운 작업이었습니다. 그러나 .NET Framework 스레딩 및 메시징 클래스의 도입으로 어느 때보다 쉬워졌습니다. 이 클래스를 사용하면 .NET Framework를 대상으로 하는 모든 언어로 다중 스레드 응용 프로그램을 작성할 수 있습니다. 이전에 Microsoft Visual Basic과 같은 도구는 스레딩에 대한 지원이 매우 제한되어 있었습니다. 따라서 C++을 사용하여 다중 스레드 코드를 작성하거나 Visual Basic에서 여러 프로세스나 ActiveX DLL로 구성되는 이상적이지 않은 솔루션을 작성하거나 또는 다중 스레딩을 완전히 무시하는 수 밖에 없었습니다. .NET Framework에서는 어떤 언어를 사용하는지에 관계없이 풍부한 다중 스레드 응용 프로그램을 작성할 수 있습니다.
이 기사에서는 Microsoft 메시지 대기열의 메시지를 수신하고 처리하는 다중 스레드 응용 프로그램을 작성하는 프로세스를 단계적으로 소개하며, 특히 System.Threading 및 System.Messaging이라는 두 가지 네임스페이스에 초점을 둡니다. 샘플 코드는 C#으로 작성되어 있지만 원하는 다른 언어로 쉽게 변환할 수 있습니다.
스레딩에 대한 배경 지식
Win32 환경에서 스레딩의 기본 모델은 단일, 아파트 및 자유 등 세 가지입니다.
단일 스레딩
처음에 작성한 응용 프로그램은 아마 응용 프로그램의 프로세스에 해당하는 스레드만 포함된 단일 스레드였을 것입니다. 프로세스는 해당 응용 프로그램의 메모리 공간을 차지하는 응용 프로그램의 인스턴스로 정의할 수 있습니다. 대부분의 Windows 응용 프로그램은 단일 스레드에서 모든 작업을 수행하는 단일 스레드 응용 프로그램입니다.
아파트 스레딩
아파트 스레딩은 단일 스레드보다 복잡한 스레딩 모델입니다. 아파트 스레딩으로 표시된 코드는 자체의 아파트로 제한된 고유 스레드에서 실행될 수 있습니다. 스레드는 처리 시간 동안 일어날 프로세스에서 소유하는 엔터티로 정의할 수 있습니다. 아파트 스레딩 모델에서 모든 스레드는 기본 응용 프로그램의 메모리에서 각각의 하위 섹션 내에서만 작동합니다. 이 모델에서는 코드의 여러 인스턴스를 동시에 그리고 독립적으로 실행할 수 있습니다. 예를 들어 .NET 이전의 Visual Basic에서는 아파트 스레드 구성 요소와 응용 프로그램을 만드는 것으로 제한되어 있었습니다.
자유 스레딩
자유 스레딩은 가장 복잡한 스레딩 모델입니다. 자유 스레딩 모델에서는 동시에 여러 스레드가 같은 메서드와 구성 요소로 호출됩니다. 아파트 스레딩과 달리 자유 스레딩은 분리된 메모리 공간에 제한되지 않습니다. 예를 들어 응용 프로그램에서 매우 비슷하지만 독립적인 수학 계산을 대량으로 실행해야 하는 경우에 자유 스레드 개체를 사용할 수 있습니다. 이 경우 같은 코드 인스턴스를 사용하여 계산을 실행하는 여러 스레드를 만듭니다. Visual Basic 6.0과 같은 언어에서는 이와 같은 작업이 거의 불가능하므로 자유 스레드 응용 프로그램을 작성한 경험이 있는 응용 프로그램 개발자는 아마 C++ 개발자뿐일 것입니다.
스레딩 모델 작업
스레딩 모델에 대한 개념의 이해를 돕는 예로 한 집에서 다른 집으로 이사하는 일을 들 수 있습니다. 단일 스레드 방법은 포장에서 상자 운반과 짐 풀기까지의 모든 일을 직접하는 것이라고 볼 수 있습니다. 아파트 스레딩 모델로 작업하는 경우는 절친한 친구 몇에게 도움을 청하는 것과 같습니다. 각 친구는 각기 다른 방에서 일하고 다른 방에서 일하는 사람을 도울 수 없습니다. 그들은 각자의 공간과 이삿짐을 맡습니다. 자유 스레드 방법을 선택하는 경우, 친구들에게 도움을 요청하는 것은 아파트 스레딩 모델과 동일하지만 친구들이 모두 어느 시간이나 어느 방에서든 함께 이삿짐을 꾸릴 수 있다는 점이 다릅니다. 이 비유에서 집은 모든 스레드가 작동하는 프로세스이고 각 친구는 코드의 인스턴스이며 이삿짐은 응용 프로그램의 리소스와 변수입니다.
위의 예에서는 각 모델의 장점과 단점을 보여 줍니다. 아파트 스레딩은 구성 요소의 여러 인스턴스가 작동하므로 단일 스레딩보다 빠릅니다. 자유 스레딩에서는 모든 일이 동시에 일어나고 모든 리소스가 공유되므로 어떤 경우에는 아파트 스레딩보다 빠르고 훨씬 효율적입니다. 그러나 여러 스레드에서 공유 리소스를 변경하는 경우 문제가 일어날 수 있습니다. 한 사람이 상자를 사용해 부엌 물건을 싼 다음 다른 친구가 와서 같은 상자에 침실 물건을 포장하는 경우를 생각해 보십시오. 첫 번째 친구는 상자에 '부엌'이라는 레이블을 붙였는데 뒷 친구가 그 위에 '침실'이라는 레이블을 붙입니다. 결국 짐을 풀 때는 부엌 물건을 침실에서 풀게 될 것입니다.
예제 응용 프로그램
첫 단계로 예제 응용 프로그램의 디자인을 검토합니다. 이 응용 프로그램에서는 여러 스레드를 만들고 각 스레드는 MSMQ 대기열의 메시지를 수신합니다. 이 예제에서는 기본 Form 클래스와 사용자 지정 MQListen
클래스의 두 가지 클래스를 사용합니다. Form 클래스는 사용자 인터페이스를 처리하는 것과 아울러 작업자 스레드를 만들고 관리하고 소멸시킵니다. MQListen
클래스에는 메시지 대기열 항목을 비롯해 작업자 스레드를 실행하는 데 필요한 모든 코드가 포함됩니다.
응용 프로그램 준비
- 응용 프로그램을 시작하려면 Visual Studio .NET을 열고 MultiThreadedMQListener라는 C# Windows 응용 프로그램을 새로 만듭니다. 폼의 속성을 열고 이름을 QueueListenerForm으로 지정합니다. 초기 폼이 그려지면 그 위에 레이블 두 개, 단추 두 개, 상태 표시줄 한 개 및 텍스트 상자 두 개를 끌어 놓습니다. 첫 번째 텍스트 상자는 Server로, 두 번째는 Queue로 이름을 지정합니다. 첫 번째 단추는 StartListening로, 두 번째는 StopListening으로 이름을 지정합니다. 상태 표시줄은 기본 이름인 statusBar1로 그대로 두어도 됩니다.
- 그런 다음 프로젝트 메뉴에서 참조 추가를 클릭하여 System.Messaging 네임스페이스에 대한 참조를 추가합니다. .NET 구성 요소 목록에서 System.Messaging.Dll을 찾아 선택합니다. 이 네임스페이스에는 MSMQ 대기열과 통신하는 데 사용되는 클래스가 포함됩니다.
- 그런 다음 파일 메뉴에서 새 항목 추가를 클릭하여 프로젝트에 새 클래스를 추가합니다. Class 템플릿을 선택하고 이름을 MQListen으로 지정합니다. 클래스 맨 위에 다음과 같은 using 문을 추가합니다.
// C# using System.Threading; using System.Messaging;
System.Threading 네임스페이스를 사용하여 필요한 모든 스레딩 기능(이 경우, Thread 클래스와 ThreadInterruptException 생성자)에 액세스할 수 있습니다. 이 네임스페이스에서 사용할 수 있는 다른 많은 추가 기능이 있으나, 이 기사에서는 이에 대해 다루지 않습니다. System.Messaging 네임스페이스를 사용하면 대기열의 메시지 보내기 및 받기를 포함한 MSMQ 기능에 액세스할 수 있습니다. 이 예제에서는 MessageQueue 클래스를 사용하여 메시지를 수신합니다. 또한 기본 폼 코드에
using System.Threading
도 추가합니다.
모든 참조가 준비가 되면 코드 작성을 시작할 수 있습니다.
작업자 스레드
첫 단계로 모든 스레드 작업을 캡슐화하는 MQListen
클래스를 작성합니다. 다음 코드를 MQListen
대해서도 삽입합니다.
// C# public class MQListen { private string m_MachineName; private string m_QueueName; // 생성자는 필요한 대기열 정보를 적용합니다. public MQListen(string MachineName, string QueueName) { m_MachineName = MachineName; m_QueueName = QueueName; } // 각 스레드에서 MQ 메시지를 수신하는 데 사용하는유일한 메서드입니다.
public void Listen() { // MessageQueue 개체를 만듭니다.
System.Messaging.MessageQueue MQ = new System.Messaging.MessageQueue(); // MessageQueue 개체의 경로 속성을 설정합니다.
MQ.Path = m_MachineName + "\\private$\\" + m_QueueName; // Message 개체를 만듭니다.
System.Messaging.Message Message = new System.Messaging.Message();
// 인터럽트가 수신될 때까지 반복합니다. while (true) { try { // 인터럽트가 throw된 경우 catch하기 위해 대기합니다. System.Threading.Thread.Sleep(100);
// Message 개체를 receive 함수의 결과와 동일하게 설정합니다. // Timespan(일, 시간, 분, 초).
Message = MQ.Receive(new TimeSpan(0, 0, 0, 1)); // 받은 메시지의 레이블을 표시합니다.
System.Windows.Forms.MessageBox.Show(" Label: " + Message.Label); } catch (ThreadInterruptedException e) {
// 기본 스레드에서 ThreadInterrupt를 catch하고 끝냅니다.
Console.WriteLine("Exiting Thread"); Message.Dispose(); MQ.Dispose(); break; } catch (Exception GenericException) { // receive에서 throw된 예외를 catch합니다.
Console.WriteLine(GenericException.Message); } } } }
코드 설명
MQListen 클래스에는
생성자 이외에 함수가 하나 있습니다. 이 함수는 각 작업 스레드가 실행하는 모든 작업을 캡슐화합니다. 스레드가 시작될 때 이 함수가 실행될 수 있도록 기본 스레드에서 이 함수에 대한 참조를 스레드 생성자로 전달합니다. Listen
에서는 우선 메시지 대기열 개체를 설정합니다. MessageQueue 생성자는 세 가지 구현으로 오버로드됩니다. 첫 번째 구현 작업은 수신할 대기열의 위치를 지정하는 문자열 인수와 대기열을 처음으로 액세스하는 응용 프로그램에 대기열에 대한 단독 읽기 권한을 부여해야 하는지 여부를 지정하는 부울 등 두 가지 인수를 취합니다. 두 번째 구현은 대기열 경로 인수만을 취하고 세 번째 구현은 아무 인수도 취하지 않습니다. 간단히 하기 위해 세 번째 구현을 사용하여 다음 줄에 경로를 할당할 수 있습니다.
대기열에 대한 참조를 마쳤으면 메시지 개체를 만들어야 합니다. 메시지 생성자도 세 가지 구현을 가집니다. 처음 두 구현은 대기열에 메시지를 쓸 경우에 사용할 수 있습니다. 이 구현에서는 메시지 본문에 들어갈 개체와 메시지 본문으로 개체가 serialize되는 방법을 정의하는 IMessageFormatter 개체를 취합니다. 여기서는 대기열에서 읽는 중이므로 빈 메시지 개체를 초기화합니다.
개체를 초기화한 후 모든 작업을 수행할 기본 루프를 입력합니다. 나중에 기본 스레드에서 이 스레드를 중지시키기 위해 Interrupt를 호출하면 스레드가 대기, 중지 또는 조인 상태에 있는 경우에만 중단됩니다. 세 가지 상태 중 하나에 있지 않은 경우 다음에 이런 상태로 들어가기 전까지는 중단되지 않습니다. 작업자 스레드가 대기, 중지 또는 조인 상태를 입력하도록 하려면System.Threading 네임스페이스에 있는 Sleep 메서드를 호출합니다. Sleep 메서드는 이전에 Windows API sleep 함수를 사용한 경험이 있는 C++ 개발자와 Visual Basic 개발자에게는 아주 익숙할 것입니다. 이 메서드는 스레드가 대기하는 시간(밀리초)이라는 단일 인수를 취합니다. Sleep 메서드를 호출하지 않으면 작업자는 인터럽트 요청을 받을 수 있는 상태를 입력할 수 없고 프로세스를 수동으로 종료하기 전까지는 무기한 계속됩니다.
MQ Receive 메서드의 구현은 두 가지입니다. 첫 번째 구현은 아무것도 취하지 않으며 메시지를 수신할 때까지 무한히 대기합니다. 이 예제에서 사용되는 두 번째 구현에서는 TimeSpan 개체로 제한 시간을 지정합니다. TimeSpan 생성자에는 일, 시간, 분, 초 등 네 가지 인수가 있습니다. 이 예제에서는 Receive 메서드가 1초 동안 대기한 후 시간이 초과되어 반환됩니다.
메시지가 수신되면 메시지는 앞에서 만든 메시지 개체에 할당되어 처리에 사용될 수 있습니다. 이 예제에서는 해당 레이블이 있는 메시지 상자를 열고 메시지를 삭제합니다. 이 코드를 실제 사용을 위해 변경하려면 여기에 메시지 처리 코드를 넣습니다.
작업자 스레드에서 Interrupt 요청을 수신하면 ThreadInterruptedException 예외를 throw합니다. 해당 예외를 catch하려면 Sleep 및 Receive 함수를 try-catch 블록에 래핑합니다. 두 가지 catch를 지정해야 합니다. 첫째는 인터럽트 예외를 catch하고 둘째는 catch되는 오류 예외를 처리합니다. 인터럽트 예외가 catch되면 우선 스레드가 끝나는 디버그 창에 씁니다. 그런 다음 대기열 개체와 메시지 개체에서 Dispose 메서드를 호출하여 모든 메모리를 정리하고 가비지 수집기로 보냅니다. 마지막으로 while 루프를 빠져 나갑니다.
함수에서 while 루프를 끝내자마자 관련 스레드는 코드 0으로 종료합니다. 디버그 창에 'The thread '<name>' (0x660) has exited with code 0 (0x0)'와 같은 메시지가 나타납니다. 스레드는 이제 컨텍스트를 벗어났으며 자동으로 소멸됩니다. 기본 스레드와 작업 스레드 중 어디에서도 정리하기 위해 별도로 해야 할 일은 없습니다.
기본 폼
다음 단계에서는 폼에 코드를 추가하여 작업자 스레드를 만들고 각 스레드에서 MQListen
클래스를 시작합니다. 우선 폼에 다음과 같은 함수를 추가합니다.
// C# private void StartThreads() { int LoopCounter; // 스레드 수 StopListeningFlag = false;
// 작업자가 중지될지 여부를 추적하는 플래그입니다. // 작업자 스레드가 될 5개의 스레드 배열을 선언합니다.
Thread[] ThreadArray = new Thread[5]; // 작업자 스레드의 모든 코드를 포함하는 클래스를 선언합니다.
MQListen objMQListen = new MQListen(this.ServerName.Text,this.QueueName.Text); for (LoopCounter = 0; LoopCounter < NUMBER_THREADS; LoopCounter++) { // Thread 개체를 만듭니다.
ThreadArray[LoopCounter] = new Thread(new ThreadStart(objMQListen.Listen));
// 스레드를 시작하면 ThreadStart 대리자를 호출합니다.
ThreadArray[LoopCounter].Start(); } statusBar1.Text = LoopCounter.ToString() + " listener threads started"; while (!StopListeningFlag) { // 사용자가 중지 단추를 누를 때까지 대기합니다. // 대기하는 동안 시스템에서는 다른 이벤트를 처리할 수 있습니다.
System.Windows.Forms.Application.DoEvents(); } statusBar1.Text = "Stop request received, stopping threads"; // 인터럽트 요청을 각 스레드에 보냅니다. for (LoopCounter = 0;LoopCounter < NUMBER_THREADS; LoopCounter++) { ThreadArray[LoopCounter].Interrupt(); } statusBar1.Text = "All Threads have been stopped"; }
코드 설명
이 함수에서는 먼저 5개의 항목으로 구성된 스레드 배열을 만듭니다. 이 배열은 나중에 사용할 수 있도록 모든 스레드 개체에 대한 참조를 보관합니다.
MQListen 클래스의
생성자는 메시지 대기열을 호스팅하는 컴퓨터 이름과 수신할 대기열의 이름 등 두 가지 인수를 취합니다. 이 생성자는 텍스트 상자의 값을 사용하여 이 인수를 할당합니다.
스레드를 만들려면 각 스레드 개체를 초기화할 루프를 입력합니다. 스레드의 Start
메서드가 호출될 때 호출될 함수를 가리키는 대리자를 Thread 생성자에 전달해야 합니다. MQListen.Listen 함수를
사용하여 스레드를 시작할 수 있지만 이 함수는 대리자가 아닙니다. 스레드 생성자의 요구 사항을 충족하려면 ThreadStart 개체를 전달하여 지정된 함수 이름으로 대리자를 만들어야 합니다. 여기서는 MQListen.Listen
함수에 대한 참조를 ThreadStart 개체에 전달합니다. 이 배열 요소가 초기화되었으므로 곧바로 Start
메서드를 호출하여 스레드를 시작합니다.
스레드가 모두 시작되면 폼의 상태 표시줄을 적절한 메시지로 업데이트합니다. 스레드가 실행되면서 대기열을 수신하고 있는 동안 기본 스레드는 사용자가 응용 프로그램에 수신을 중단하도록 요청할 때까지 대기합니다. 이렇게 하려면 사용자가 StopListening 단추를 클릭할 때까지 while 루프를 입력하여 StopListeningFlag
값을 변경합니다. 이 대기 루프에서 응용 프로그램은 Forms.Application.DoEvents 메서드를 사용하여 필요한 다른 모든 처리를 수행할 수 있습니다. 이것은 Visual Basic에 익숙한 사람에게는 이전의 DoEvents 메서드와 같고, C++에 익숙한 사람에게는 MSG 펌프를 작성하는 것과 동일합니다.
StopListening 단추를 클릭하면 이 루프가 끝나고 스레드 종료 코드로 진입합니다. 스레드를 모두 종료하기 위해 코드에서는 스레드 배열 내에서 루프하여 각 스레드에 인터럽트 신호를 보냅니다. 이 루프 내에서 배열의 각 스레드 개체에 대해 Interrupt 메서드를 호출합니다. 이 메서드가 호출되기 전까지는 MQListen
클래스의 코드는 정상적으로 실행됩니다. 이 때문에 다른 것을 처리하는 도중에라도 각 작업자에 대해 Interrupt를 호출할 수 있습니다. 스레드 클래스는 스레드가 완료되면 각 스레드의 정리를 처리합니다. 마지막 작업으로 기본 폼의 상태 표시줄을 업데이트하고 종료합니다.
이제 단추 뒤에 코드를 추가해야 합니다. 다음과 같은 코드를 StartListening 단추의 Click 이벤트에 추가합니다.
// C# statusBar1.Text = "Starting Threads"; StartThreads();
이 코드는 상태 표시줄을 업데이트하고 StartThreads
메서드를 호출합니다. 이 코드에서 StopListening 단추의 StopListeningFlag
를 True로 설정하기만 하면 됩니다.
// C# StopListeningFlag = true;
마지막 단계로 StopListeningFlag
에 대해 폼 수준 변수를 추가합니다. 폼 코드 맨 위에 다음 줄을 추가합니다.
// C# private bool StopListeningFlag = false;
응용 프로그램을 테스트하려면 메시지 대기열에 쓰기 위한 예제 응용 프로그램인 MQWrite를 다운로드합니다.
다중 스레드 코드의 문제
샘플 코드 작업을 완료했으므로, 이제 다중 스레드 응용 프로그램을 작성하는 데 필요한 도구를 가지게 되었습니다. 스레딩은 응용 프로그램의 성능과 확장성을 극적으로 개선할 수 있습니다. 이런 강력함과 더불어 스레딩의 위험에 대해서도 알고 있어야 합니다. 어떤 경우에는 스레드 사용으로 응용 프로그램에 해를 끼칠 수 있습니다. 스레드로 인해 작업이 다운되거나 예상치 못한 결과가 일어날 수 있으며 심지어 응용 프로그램이 응답을 중지할 수도 있습니다.
스레드가 여러 개인 경우 스레드 서로가 특정 지점에 도달하거나 종료할 때까지 대기하고 있지 않은지 확인합니다. 제대로 수행하지 않으면 각 스레드가 서로 대기하고 있으므로 아무 스레드도 종료되지 않는 교착 상태를 초래할 수 있습니다.
여러 스레드에서 쉽게 공유될 수 없는 리소스(예: 플로피 디스크 드라이브, 직렬 포트 또는 적외선 포트)에 액세스해야 하는 경우 스레드 사용을 방지하거나 synclock 또는 mutex와 같은 고급 스레딩 도구를 사용하여 동시성을 관리할 수 있습니다. 두 스레드가 이 리소스 중 하나를 동시에 액세스하려 하면 한 스레드에서는 리소스를 얻을 수 없고 데이터 손상이 일어납니다.
스레딩의 또 다른 일반적인 문제는 경쟁 조건입니다. 한 스레드가 파일에 데이터를 쓰는 동안 다른 스레드에서 해당 파일을 읽고 있는 경우 어떤 스레드가 먼저 종료할지 알 수 없습니다. 이 문제는 두 스레드가 파일 끝으로 경주하고 있기 때문에 경쟁 조건이라고 합니다. 읽는 스레드가 쓰는 스레드보다 앞서는 경우 알 수 없는 결과가 반환됩니다.
스레드를 사용할 때 모든 스레드가 다른 스레드와 별도로 완전히 작업을 마칠 수 있을지 여부도 생각해 보아야 합니다. 데이터를 앞뒤로 전달해야 하는 경우 데이터가 비교적 간단해야 합니다. 복잡한 개체를 전달하는 경우 이 개체를 앞뒤로 이동하기 위해 막대한 마샬링 비용이 들어가기 시작합니다. 이에 따라 운영 체제에서 관리하는 데 오버헤드가 생기고 전체 성능이 떨어집니다.
또 다른 문제로는 코드를 다른 개발자에게 넘겨주는 데 따른 전환 비용이 있습니다. .NET으로 스레딩이 훨씬 쉬워졌지만 코드를 유지 관리하는 다음 개발자가 스레딩 작업을 하기 위해서는 스레딩에 대해 알고 있어야 합니다. 그러나 이 점은 스레드 사용을 피해야 할 이유라기 보다는 적절한 코드 주석을 제공해야 할 이유입니다.
이런 문제들로 인해 스레딩 자체를 사용하지 않는 것 보다는 응용 프로그램을 디자인하고 스레드를 사용할지 여부를 결정할 때 모든 문제들을 기억하고 있는 것이 좋습니다. 불행히도 이 백서에서는 이런 문제를 방지하기 위한 방법을 설명하지 않습니다. 스레드를 사용하기로 했으나 위에서 언급한 문제가 발생한 경우 synclock이나 mutex를 검토하여 문제를 해결하거나 다른 솔루션을 찾아 보십시오.
결론
여기에 있는 내용으로 스레딩을 이용하는 응용 프로그램을 작성할 수 있습니다. 그러나 이렇게 하는 동안 관련된 문제를 기억하십시오. 스레딩은 제대로 사용하면 응용 프로그램의 성능과 확장성을 단일 스레드 응용 프로그램에 비해 훨씬 개선할 수 있지만, 올바로 사용하지 못하는 경우 정반대의 결과를 가져올 수 있으며 응용 프로그램이 불안정하게 될 수 있습니다.
출처: MSDN
'IT-개발,DB' 카테고리의 다른 글
[개발] 윈도우2003에서 CVSNT + TortoiseCVS 설치하기 (0) | 2010.11.05 |
---|---|
[개발] CVS 거북이 사용법 강좌 (0) | 2010.11.05 |
[개발] Timer 클래스 (System.Windows.Forms.Timer) (0) | 2010.11.05 |
[개발] 키보드상태 얻기(Ins ' Num Lock' Caps Lock) (0) | 2010.11.05 |
[개발] 익스플로러의 프린터설정(머리글,바닥글,여백) 변경하기 (0) | 2010.11.05 |
댓글