ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • C# 프로세스( 멀티 스레드)
    .NET/C# Basic 2008. 10. 27. 23:52
    반응형
    - 닷넷 프로그래밍 정복 참고


    멀티스레드
    스레드의 생성

    스래드는 코드의 실행흐름이다 보통의  응용프로그램은 Main으로 부터 시작하는 하나의 실행 흐름을 가지며 Main의 선두에서부터 물 흐르듯이 순서대로 코드를 실행한다. 이런 방식을 싱글 스레드라고 하는데 실행 흐름이 하나밖에 없으므로 한번에 하나의 작업밖에 하지 못한다. 윈도우즈 95부터는 하나의 응용프로그램에 두개 이상의 스레드가 동시에 실핼 되루 수 있는 멀티스레드를 지원하며 닷넷도 멀티스레딩을 기본적으로 지원한다. 우리가사용하는 대부분의 응용프로그램들도 멀티스레드로 실행되고 있다.

    두개 이상의 스레드를 동시에 실행할 수 있으므로 하나의 응용 프로그램이 두개의 작업을 병렬적으로 처리하는 것이 가능핟. 예를 들어 백그라운드에서 틈틈이 해야한 작업이라든가 시간이 오래 거릴는 계산작업. 인쇄를 위한 스풀링 작업등이 좋은 예이다 백그라운드의 스레드가 필요한 연산을 하는 동안 주 스레드는 계속 사용자의 입력을 받아 처리할 수 있으므로 프로그램의 반응성이 좋아지고 여러가지 작업을 동시 다발적으로 처리할 수 잇다

    닷넷은 응용프로그램이 시작된면 Main 으로부터 시작되는 주 스레드를 기본적으로 생성하여 즉시 실행한다 . 주스레드는 필요한 경우 작업 스레드를 얼마든지 더 만들수 있다
    C# 에선 스레드는 System.Threading 네임스페이스에 정의된 Thread 클래스로 표현하며 Thread 타입의 객체를 생성하면 새로운 스레드가 생성된다 
    Thread의 생성자는 다음 두가지와 스택의 최대 크기를 전달받은 두가지가 더 있다 스택 크기는 디폴트가 무난하게 정의되어 있으므로 보통 생략한다.

    public Thread(ThreadStart start [,int maxStackSize])
    pulbic Thread (ParameterizedThreadStart start [,int maxStackSize])



     생성자의 인수로 ThreadStart 또는 ParameterizedThreadStart 델리게이트를 넘기는데 이델리게이트를 통해 스레드의 진입 메서드를 전달한다. 스레드는 델리게이트가 가리키는 메서드에서부터 실행을 시작하며 이메서드가 종료되면 스레드도 종료된다. 마치 주 스레드가 Main 에서 시작해서 Main 이 끝나면 종료되는 것과 같다. 각 생성자가 인수로 받아들이는 델리게이트 타입은 다음과 같이 정의되어있다

    public delegate void ThreadStart()
    public delegate void ParameterizedThreadStart(Object obj)

    ThreadStart 타입은 인수를 받지 않으며 ParametherizedThreadStart는 obj 인수를 하나 받아들인다. 둘다리턴값은 없는데 스레드는 생성후 독립적으로 실행되다가 작업이 완료 되면 자동으로 파괴되는것이므로 작업 결과를 리턴하지 않는다 
    스레드로 전달할 작업내용이 없으면 인수가 없는 델리게이트 타입을 사용하고 전달할 인수가 있으면 인수가 있는 델리게이트 타입을 사용한다.

    생성자는 스레드 객체를 만들기만 하고 실행을 즉시 시작하지는 않는다. 스레드는 정지된 상태로 생성한 되며 이후 Start 메서드를 호출하여 시작한다. 특별한 이유가 없는 한 스레드는 생성후 곧바로 실행해야하므로 Start메서드를 호출해야한다. 인수를 받아들이는 스레드는 Start 메서드를 통해 인수를 전달받는다 Suspend는스레드의 실행을 일시 중지하며Resume은 일시 중지된 스레드를 실행을 재개한다

    public void Start()
    public void Suspend()
    public void Resume()
    public static void Sleep(int millisecondsTimeout)

    Sleep 정적 메서드는 스레드의 실행을 일정시간 동안 잠시 중하는데 단위는 1/1000이다 이 기간동안 스레드는 실행을 중지하고 다른스레드를 위해 CPU 시간을 양보한다 Sleep 은백그라운드 스레드의 실행속도를 제어하는 주요한 수단이다. 다음예제는 주 스레드외에 하나의 작업스레드를 더 생성하여 카운터를 출력한다. 그래픽 환경이라면 굉장히 재미있는 예제들을 만들어 보겠지만 콘솔환경이다 보니 예제가 조금 썰렁한 감이있다

    using System;
    using System.Threading;

    class CStest
    {
    //작업스레드
    static void ThreadProc()
    {
    for(int i = 0 ; i <10 ; i++)
    {
    Console.WriteLine(i);
    Thread.Sleep(500);
    }
    Console.WriteLine("작업 스레드 종료");

    }
    //주스레드
    static void Main()
    {
    Thread T = new Thread (new ThreadStart(ThreadProc));
    T.Start();
    for(;;)
    {
    ConsoleKeyInfo cki;
    cki = Console.ReadKey();
    if(cki.Key == ConsoleKey.A)
    {
    Console.Beep();
    }
    if(cki.Key == ConsoleKey.B)
    {
    break;
    }
    }
    Console.WriteLine("주 스레드 종료");
    }

    }

    Main 에서 시작하는 주 스레드는 실행 직후에 ThreadProc 을 진입점으로하는 새로운 작업 스레드를 생성하고 이 스레드의 Start 메서드를 호출하여 실행을 즉시 시작한다. 그리고 무한 루프를 돌며 사용자의 키입력을 받아 A를 누르면 소리를 내고 B를 누르면 프로그램을 종료한다. 실행해 보면 0~9까지 숫자 카운터가 촉당 2회씩 증가할 것이며 중간에 언제든지 A나 B키의 입력을 받아들일수 있다.

    비록 아주 간단한 작업이기는 하지만 카운트 증가와 키입력 대기라는 두개의 작업이 병렬적으로 잘 실행된다. 멀티 스레드가 아니라면 주 스레드에서 카운터를 직업 출력해야하는데 이렇게 되면 주스레드가 입력을 대기할 수 없으므로 두가지 작업을 동시에 처리할 수 없을 것이다 두스레드가 모두 종료되어야 응용프로그램이 종료되므로 카운터 출력 완료후 B키를 눌러 주 스레드를 종료해야한다.

    작업 스레드의 진입 함수는 필요할  경우 주 스레드로부터 작업에 필요한 인수를 받아들일 수 있다
    ParameterizedThreadStart  델리게이트 타입은 object 타입의 인수를 하나 받아들으므로 모든 타입의 인수를 다 받아들일 수있는 셈이다 예를 들어 위 예제의 ThreadProc 작업 스레드가 무조건 0~9까지 카운트를 하는 것이 아니라  호출원에서 지정한 만큼 카운트를 하고 싶다면 다음과 같이 작성하면 된다

    // 작업 스렏
    static void ThreadProc(object count)
    {
    for(int  i = 0 ; i (int)count ; i ++)
    {
    Consoel.WriteLine(i);
    Thread.Sleep(500);

    }
    Cosole.WriteLine("작업스레드 종료");
    }

    //주스레드

    static void Main()
    {
    Thread T = new Thread(new ParameterizedThreadStart(ThreadProc)
    T.Start(5);
    for(;;)
    ....
    ...


    ThreadProc은 object 타입 (곧 임의의 타입)의 count 를 인수를 받아 count가 지시하는 범위까지 숫자를센다 주스레드는 ThreadProc을 진입점으로하는 스레드를 생성하고 Start메서드를 인수로 스레드에게 작업 거리를 전달한다. 스레드의 진입점으로는 단하나의 인수만 넘길수 있지만 필요할 경우 클래스를 정의해서 객체를 넘기면 여러개의 작업거리를 전달할 수 도 있다. 예를 들어 카운터의 시작과 끝 주기등을 맴버로 가지는 클래스를 정의하고 이 클래스의 객체를 진입함수로 전달하면 된다 
    작업스레드는 생성후 자신의 소임을 다하면 자동으로 종료된다 .통상 백그라운드 작업이란 영원히 실해오디는 것이 아니라 주 스레드를 대신하여 시간이 걸리는 작업을 대신하는 것으로 진입함수가 종료되면 스레드도 끝나는 것이 자연스럽다. 만약외부에서 스레드를 강제로 종료하고 싶다면 다음 메서드를 사용한다

    Thread.Abort();
    Thread.Join();

    Abort 는 실행중인 스레드를 강제 종료하고 필요한 정리 작업을 수행한다 . 스레드가 생성한 자원이나 객체등이 이과정에서 모두 정리되는데 아주 복잡한 작업을 하고 있었다면 스레드를 정리하는 데 다소 시간이 걸릴 수도 있다Join 은 스레드가 완전히 종료되고 자원을 해제할 때까지 대기하는 역할을 한다. 스레드 종료 후 스레드의 작업 결과를 가지고 어떤 작업을 하고자 한다면 주 스레드는 Join  메슫로 잠시 대기해야한다. 그렇지 않으면 스레드가 사용하던 자원이 아직 덜 해제되어 문자가 발생할 수도 있다.



    스레드의 프로퍼티

    다음은 Thread 클래스의 프로퍼티에 대해 알아보자 Thread는 자신의 현재 상태와 동작방식을 지정하는 많은 프로퍼티를 제공하는데 주 스레드는 이 프로퍼티를 통해 스레드의 상태를 알아내고 동작을 제어할 수있다

    Name : 스레드의 이름을 지정하는 문자열이다 문법적으로는 큰의미가 없으며 이름을 지정하지 않아도 실행에는 아무런 제약이 없다 하지만 디버깅목정이나 스레드 간의 구분을 위해 이름을 붙일 수 있도록 되어 있다 문자열 형식으므로 이름은 자유롭게 붙일 수 있으며 이름을 지정하지 않으면 null을 가진다 . 이름은 최초 닥한번만 지정할 수 있으며  실행 중에는 변경하지는 못한다

    IsAlive : 스레드가 아직 살아 있는지를 조사한다. 정상적으로 시작되었고 종료나 중단되지 않았다면 true 를 리턴하고 그러히 않다면 false를 리턴한다 주 스레드는 이 프로퍼티를 주기적으로 검사함을로써 작업 스레드의 완료 여부를 감시할 수 잇다. 스레드의 상태를 조사하기만 하는 읽기전용 속성이다 

    IsBackground : 배경 스레드인지 , 전경 스레드인지를 조사또는 지정한다
    두종류의 스레드는 실행 중일대 응용 프로그램의 종료 가능성이 다르다. 주 스레드종료되었을때 모든 전경 스레드가 종료되어야 응용프로그램이 무사히 종료할 수 있다. 만약 전경 스레드 중 하나라도 실행 중이라면 응용 프로그램은 종료되지 않고 대기한다. 이에비해 배경 스레드는 실행 중이더라도 응용 프로그램 종료에는 별 영향을 주지 않는다

    이 프로퍼티의 디폴트는 false이며 전경 스레드로 생성된다 앞 예제를 실행해 보자., 작업 스레드가 카운터를 출력하고 있는 중에 B키를 눌러 주 스레드를 종료해도 카운터는 계속 출력된다 왜냐하면 ThreadProc 이 전경 스레드으므로 이 스레드가 실행을 완전히 마칠때까지 응용 프로그램이 종료되지않고 대기하기 때문이다 

    Thread  T = new Thread (new ThreadStart(TheadProc));
    T.IsBackground = true;
    T.Start();

    작업 스레드의 IsBackground 프로퍼티를 true로 변경하여 배경 스레드로 만들면 주 스레드 졸료시 카운터 진행 여부에 상관없이 응용 프로그램이 즉시 종료된다. 스레드의 전경, 배경 프로퍼티는 스레드가 하는 작업의 중요도에 따라서 결정된다. 파일 입출력이나 인쇄 같은 중요한 작업은 조료전에 완전히 끝내야 하므로 전경 스레드로 만들어야 한다. 반면 애니메이션이나 단순한 장식, 캐시 정보관리등의 작업은 응용 프로그램 종료와 별 상관이 없으므로 즉시 종료해도 문제가 없다.또한이런 스레드는 대개 무한 루프를 돌기 때문에 자연 종료되지도 않으며 따라서 배경 스레드로 만드는것이 합리적이다.

    Priority : 스레드의 우선순위를 지정한단. 우선순위는 스레드가 CPU 시간을 얼마나 많이 받아야하는지를 지정하며 우선순위가 높을수록 더 많은 실행 시간을 확보 할 있다. 우선순위는 ThreadPriority 열거형으로 표현하며 기본값을 Nomal 이다

    Highest 가장높은 우선순위
    AboveNormal 보통보다 높은 우선순위
    Normal 보통우선순위
    BelowNomal 보통보다 낮은 우선순위
    Lowest 가장낮은 우선순위

    우선순위에 따라 스레드의 실행 시간을 배분하는 규칙은 비교적 단순하다. 가급적으면 우선 순위가 높은 스레드에게 더 많은 시간을 할당하며 높은 우선순위의 스레드가 실행 중이라면 우선순위가 낮은 나머지 스레드는 실행 시간을 할당받지못한다. 하지만 실제로는 CPU가 충분히 빨라서 대부분의 스레드에게 실행 시간이 조금씩은 돌아가도록 되어있다.

    시스템이 스레드의 실행순서를 정밀하고도 공평하게 제어하므로 가급적이면 우선순위는 조정하지않는것이 좋다. 하지만 백그라운드의 작업스레드보다 사용자의 입력을 받는 스레드는 더 중요한일을 하므로 우선순위를 봎여 반응성을 높이는 것이 바람직핟. 그래야 사용자의 입력에 즉각적인반응을 보일수 있다 이런 스레드는 반응성이 높아야하지마지만 입력처리에 시간을 많이 소모하지는 않기 떄문에 우선순위를 높이더라도 다른작업스레드의 실행을 방해하지는 않느다

    ThreadState : 스레드의 현재상태를 조사한다. IsAlive 프로퍼티보다 휠씬 더 상세한 정보를 제공하는데 ThreadState 열거형 값중하나를 가진다. 스레드가 실행중인지 . 일시 중지중인지 강제 종료되었는지 등의 상세한 정보를 조사할 수있다. 역시 읽기 전요이다.

    CorrentThread : 현재 실행 중인 스레드를 나타내는 정적 프로퍼티이다 스레드 내부에서 자기자신에 대한 참조가 필요할 때 이 프로퍼티를 읽으면 자기 자신에 대한 Thread참조를 얻을 수 잇다


    동기화

    멀티 스레드를 사용하면 동시에 두가지 이상의 작업을 진행 할수 있으므로 병렬성이 향상되고 작업중에도 입력을 처리할 수 있어 반응성이 높아지는 장점이 있다 그러나 스레드를 많이 쓴다고해서 전체적인 처리속도가 향상되는 것은 아니다 CPU의 처리 속도에는 제한이 있고 스레드를 번갈아 실행하는 방식이기 때문에 동시에 여러개의 작업을 하면 스레드 스위칭으로 인한 시간낭비가 생겨 오히려 더 느려질 수도 있다. 물론 CPU가 두개라면 멀티 스래드로 인한 속도상의 이점이 있겠지만 일반적으로는 속 향샹에는 큰 보탬이 되지는 못한다

    멀티스레드를 쓸 때 발생하는 또 다른 복잡한 문제는 바로 동기화이다. 스레드는 프로세스에 속한 전역 정적자원이나 하드웨어 같은 본질적으로 전역인 자원을 공유하기 때문에 하나의 자원을 두고 두개의 스레드가 서로 싸울수 있다. 이를 경쟁상태(Race Condition) 라고 하는데 경쟁 을 해결하려면 한 스레드가 공유자원을 사용하는 동안 다른 스레드는 대기하도록 하는 동기화(Synchronization)가 필요하다 동기화를 하다보면 양쪽이 서로를 기다리는 교착상태(Deadlock)에 빠지기도 하는데 이는 다운되는 현상과 거의 효과가 같다

    멀티 스레드에서 동기화의 문제가 발생하는 근본적인 원인은 스레드의 실행 순서가 비동기적이라는 데있다 시스템은 스레드의 우선순위와 CPU시간의 여유 상황에 따라 다음 실행할 스레드를 선택하며 이런 식으로 스레드들이 번갈아 가며 실행된다. 스레드 간의 샐행 순서나 각 스래드가 얼마만큼의 시간을 사용할 수 있는지를 예측할 수 없으며 그러다 보니 동기화의 문제가 발생하는 것이다 따로 예를 들지 않더라도 한집안 두살림이 얼마나 골치 아픈문제를 야기할 것인가는 쉽게 상상이 갈 것이다. 

    다음 옌제는 스레드끼리의 경쟁 상태를 보여준다. 경쟁상태나 교착사태는 대단히 큰 프로그램에서 아주 민감한 문제로 인해 발생하기 때문에 짧은 예제에서 현실감 잇는 경쟁상태를 보이기는 무척어렵다. 그래서 의도적으로 경쟁상태를 만들어 보일 수 밖에 없는데 실무에서 발생하는 동기화 문제도 원리는 동일하다

    using System;
    using System.Threading;

    class Site
    {
    public string name;
    public Site(string aname) {name = aname;}
    }
    class CSTest
    {
    private static Site site = new Site("www.winapi.co.kr");

    static void ThreadProc()
    {
    for (int i = 0 ; i <= 100; i +=10)
    {
    Console.SetCursorPosition(0,0);
    Console.WriteLine("{0}에서 {1}%다운로드 중",site.name);
    Thread.Sleep(1000);
    }
    }

    static void Dosomething()
    {
    string old = site.name;
    ste.name = "www.loseapi.co.kr";
    for (int i = 0 ; i <=100 ; i+=10)
    {
    Console.SetCurorPosition(0,1);
    Console.WriteLine("{0}에서 {1}%다운로드 중",site.name);
    Thread.Sleep(500);
    }
    site.name = old;
    }
    static void Main()
    {
    Thread T = new Thread (new ThreadStart(ThreadProc));
    T.Start();
    Thread.Sleep(2000);
    DoSomething();

    }


    }


    이 예제는 웹사이트로 부터 어떤 자료를 다운로드 받는다 다운로드는 시간이 오래 걸릴 수 있으므로 주 스레드가 직접 처리하지 않고 ThreadProc 스레드에게 다운로드 작업을 위임했다. ThreadProc은 지시받은 다운로드를 마치면 종료될 것이고 그동안에도 주 스레드는 다른작업을 할 수 있을 것이다.

     Site 클래스는 다운로드 받을 사이트릐 정보를 가지는데 실제 예에서는 주소, 다운로드받을 파일 접속 정보등도 포함되겠지만 단순함을 위해 사이트의 이름만 가지도록 했다. site 정적맴버는 최초winapi라는 사이트로 초기화되며 ThreadProc 에서 이사이트의 자료를 다운로드 받는다 이상태 그대로ThreadProc이 계속 실행될 수 있다면 아무문제가 없을 것이다. 그러나 2초 후에 주 스레드가 loseapi라는 사이트로 부터 병렬적으로 다운로드 를 받기위해 site의 정보를 잠시 변경했다고 해보자

    이렇게 되면 site라는 한의 자원을 두 스레드가 동시에 사용하려고 하는 경쟁 상태가 된다 ThreadProc 이 참조한고 있는 site객체의 정보가 중간에 다른스레드에 의해 다른주소로 변경되어 버렸으므로 이 스레드는 예정된 작업을 정상적으로 실핼할 수 없을 것이다 .실행 보면 처음에는 잘 다운로드 되짐난 중간에 주 스레드가 다운로드를 시작하면 작업스레드도 엉뚱한 사이트에서 다운로드를 하려고 시도한다. 주 스레드는 다운로드 완료 후 site 를 원래의 값으로 복구해 주지만 ThreadProc은 변경된 값으로 계속 실행 되므로 문제가 발생했다

     물론 위 예제에서는 주 스레드가 site라는 공유객체를 사용하지 말고 별도의 객체를 하나 더 만들어서 사용하면 문제가 간단하게 해결된다 그러나 실제 상황에서는 네트워크 연결이나 데이터 베이스접속처럼 사본을 만들수 없는 전역자원들이 더 많고 이런 상황에서는 ㅇ쩔수 없이 경쟁 상태가 발생할 수 밖에 없다. 위예제 이런 상황을 가정하여 만든것이다

    문제를 해결하려면 동기화를 해야한다. 동기화란 거창하게 생각할 필요없이 한쪽에서 쓰는동안 다른쪽을 대기시키는것이라고 생각하면된다 C#은 언어차원에서 동기화기능을 구현하는 lock 구문을 제공한다 공유자원을 액세스하는 문장을 lock 으로 감싸면 동시에 두스레드가 하나의 객체 를 액세스하지 않는다. 예제를 다음과같이 수정해보자.


    class CSTest
    {
    private static Site site = new Site("www.winapi.co.kr");
    static void ThreadProc()
    {
    for (int i =0 ; i <=100; i +=10)
    {
    lock(site)
    {
    Console.SetCurorPosition(0,0);
    Console.WriteLine("{0}에서 {1}%다운로드 중",site.name);
    }
    Thread.Sleep(1000);
    }
    }
    static void DoSomething()
    {
    string old = site.name;
    ste.name = "www.loseapi.co.kr";
    for (int i = 0 ; i <=100 ; i+=10)
    {
    Console.SetCurorPosition(0,1);
    Console.WriteLine("{0}에서 {1}%다운로드 중",site.name);
    Thread.Sleep(500);
    }
    site.name = old;
    }
    static void Maiin()
    {
    Thread T = new Thread (new ThreadStart(ThreadProc));
    T.Start();
    Thread.Sleep(2000);
    lock(site)
    {
    DoSomething();

    }

    }

    }



    lock 블록에 잠그고자 하는 객체를 지정하면 이객체를 액세스하는 스레드들은 한번에 하나씩 순서대로 실행된다 주스레드에서 DoSomething 을 호출하는 동안 lock 블록으로 site객체를 잠근다 이렇게 되면 다운로드 스레드는 lock 블록 안으로 들어가지 못하고 잠시 대기한다. DoSomething 은 이때 site 객체의 정보를 원하는 값으로 잠시 변경한후 작업을 하고 완료 site에 대한 잠금을 풀면 대기하고 있던 스레드도 정상 동작을 계속할 수 있다

    lock 블록에는 값 타입의 변수는 지정할 수 없으며 참조타입의 객체만 지정할 수있다. 이때 lock 에서 잠그는 객체는 동기화가 필요한 블록에 대한 일종의 식별자 역할을 하므로 임의ㅢ 객체이기만 하면 된다. 예를 들어 위 예제에서 꼭 site를 잠그지 않더라도 object타입의 임시 객체 a를 선언한 후 a를 잠글수도 있다 그러나 가급적으면 보호대상 자체를 잠그는 것이 의미를 명확하게 표현 할 수 있다

    C# 에서 동기화를 하는 좀더 일반적인 객체는 Monitor 이다 Monitor 는 Win32의 크리티컬 섹션에 해당하며 동시에 두개의 블록이 같이 실행되지 못하도록 방지한다. 이 객체의 Enter, Exit메서드로 잠그고자 하는 객체와 범위를 지정하면 된다. 이방법을 좀더 쓰기 쉽게 언어차원에서 제공하는 키워드가 바로 lock이며 lock 블록은 내부적으로 Monitor 객체를 사용한다

    이외에 스레드끼리 통신에 사용되는 자동, 수동 이벤트가 있고 프로세스간의 동기화에도 사용할수있는 뮤텍스라는 동기화 객체도 있다. 동기화 객체들은 다양한 대기 함수들과 함께 사용되어 스레드의 실행 순서나 시점을 제어하는데 사용된다 동기화의 방법이나 기술은 사실 Win32나 리룩스등의 운영체제와도 거의 유사하다. 이런 동기화 기법은 콘솔환경에서 실제 예를 보이기가 상당히 어렵고 주제의 부피가 너무 거대하므로 소개 정도만 하기로 한다.


















    반응형

    댓글

Designed by Tistory.