Programming/.NET

실행중인 프로그램은 하나의 프로세스로 볼 수 있고 프로세스는 시작될때 기본적으로 하나의 스레드를 생성하게 됩니다. 스레드가 없는 프로세스는 존재하지 않습니다. 프로세스가 무엇인가 작업을 수행하려면 반드시 해당 작업을 수행하는 스레드를 생성해야 합니다. 스레드는 프로세스의 작업단위입니다. 예를 들어 사용자로부터 특정값을 입력받아 합계를 계산하는 아래와 같은 프로그램이 있습니다.

 

static void Main(string[] args)
{
    Console.Write("첫번째 값 : ");
    string i = Console.ReadLine();

    Console.Write("두번째 값 : ");
    string j = Console.ReadLine();

    int sum = int.Parse(i) + int.Parse(j);

    Console.WriteLine("결과 : {0}", sum);

    Console.Read();
}

 

이 프로그램은 입력과 계산, 결과출력에 대한 일련의 작업이 main 메서드에 정의되어 있고 따라서 메서드 실행을 위한 스레드를 생성해야 하는 것입니다. 프로세스는 반드시 하나이상의 스레드를 생성하며 기본적으로 생성되는 최초의 스레드를 주 스레드(main thread)라고 합니다.

 

CPU는 각 프로세스마다 생성한 스레드를 하나씩 돌아가며 실행합니다. 물론 단일 CPU는 하나의 스레드만을 실행할 수 있었지만 내부에 4코어, 8코어처럼 멀티코어를 가진 CPU라면 코어수만큼의 여러 스레드를 동시에 실행할 수도 있을 것입니다. 만약 4코어라면 4개의 스레드를 동시에 순회하면서 실행하는 것이 가능합니다.  그러나 일반적으로 CPU가 실행해야할 스레드의 수는 수십개나 그 이상이기에 모든 스레드를 동시에 실행할 수는 없습니다. 4개나 8개의 스레드를 동시에 실행한다고 하더라도 이들 스레드를 실행한 뒤 종료될때까지 기다릴 수 없기에 시간을 조금씩 할당해서 대기중인 각 스레드를 모두 돌아가며 실행해야 합니다.

 

스레드안에는 CPU가 읽어 실행될 명령어집합이 존재하는데 이를 스레드문맥(thread context)이라고 합니다. 메서드로 비유하자면 메서드의 시작부터 마지막까지 작성된 일련의 코드에 해당하는 것입니다. CPU는 스레드의 문맥을 읽어 실행하다가 다른 스레드를 실행하기 직전에 현재 실행중인 스레드의 상태를 스레드문맥에 저장합니다. 그런뒤 다른 스레드로 실행을 이동하게 되는데, 현재상태를 문맥에 저장해야 하는 이유는 스레드의 실행계획이 담긴 스케쥴링에 따라 CPU가 다시 해당 스레드를 실행할때 이전에 저장된 상태를 스레드문맥으로부터 읽어들여서 마치 이전부터 계속 실행중이었던것처럼 스레드의 실행을 이어나갈 수 있기 때문입니다.

 

Thread는 기본적으로 현재 실행중인 스레드에 접근할 수 있는 몇가지 속성을 제공하며 필요한 경우 새로운 새로운 스레드를 생성할 수도 있습니다.

 

static void Main(string[] args)
{
    Thread t = new Thread(myThread);
    t.Start();

    Console.Read();
}

static void myThread()
{
    Console.WriteLine("hahaha");
}

 

스레드를 생성할때는 스레드를 통해 실행할 메서드를 제공해야 합니다. 이때 메서드는 스레드로서 실행하는 하나의 작업단위, 즉. 명령의 집합이라고 볼 수 있습니다.

 

스레드가 생성되 Start() 메서드가 호출되면 이제 하나의 프로세스는 2개의 스레드가 생성된 것이며 CPU에 의해 각 스레드는 별도의 작업으로서 실행될 것입니다.

 

여기서 스레드가 왜 필요한지는 스레드의 작업단위인 메서드를 보면 알 수 있습니다. 현재 메서드의 명령을 그대로 실행하면서 동시에 다른 작업을 해야하는 경우가 발생하는 것입니다. 예를 들어

 

static void Main(string[] args)
{
    while (true) {
        string s = Console.ReadLine();

        if (s.Trim() == "exit")
            return;

        Console.WriteLine(s);
    }
}

 

위와 같은 프로그램의 경우 사용자로부터 값을 입력받아 입력받은 값을 그대로 뿌려주도록 되어 있습니다. 프로그램을 종료하려면 'exit'를 입력하면 됩니다. 그런데 좀 억지스럽지만 이 프로그램에서 3초씩마다 뭔가를 계산하고 출력하는 기능을 추가해야 한다고 가정해 보겠습니다. 그러면서 본래 프로그램이 수행하는 작업은 계속되어야 합니다.

 

static void Main(string[] args)
{
    myThread();

    while (true) {
        string s = Console.ReadLine();

        if (s.Trim() == "exit")
            return;

        Console.WriteLine(s);
    }
}

static void myThread()
{
    while (true) {
        Thread.Sleep(3000);
        Console.WriteLine("처리됨");
    }
}

 

위 예제는 스레드를 사용하지 않고 만든 경우인데 이렇게 해서는 제대로 프로그램을 사용할 수 없게 됩니다. 주 스레드는 첫명령인 myThread() 메서드를 호출하는데 이 메서드는 무한대로 3초마다 무엇인가를 처리하고 결과를 뿌려주기 때문입니다. 주 스레드가 이 작업에 매달리게 되고 결국 주 스레드는 하위의 while문을 실행할 여지가 없어지게 됩니다.

 

static void Main(string[] args)
{
    Thread t = new Thread(myThread);
    t.Start();

    while (true) {
        string s = Console.ReadLine();

        if (s.Trim() == "exit")
            return;

            Console.WriteLine(s);

    }
}

 

바로 이럴때 스레드가 필요합니다. 주 스레드는 myThread() 메서드 실행을 위한 별도의 스레드를 생성한뒤 자신이 처리해야할 부분을 계속 처리할 수 있게 됩니다.

 

일단 스레드가 생성되 실행되면 해당 스레드를 생성한 프로세스는 모든 스레드가 종료되어야만 종료될 수 있습니다. 이런 형태의 스레드를 전경 스레드(foreground thread)라 하는데 바꾸어 말하면 프로세스가 실행을 종료하기 위해서는 모든 전경 스레드가 종료되어야 하는 것입니다.

 

static void Main(string[] args)
{
    Thread t = new Thread(myThread);
    t.IsBackground = true;
    t.Start();
           

    Console.Read();
}

 

하지만 스레드의 IsBackground속성을 true로 설정하면 해당 스레드를 배경 스레드(background thread)로서 실행할 수 있게 됩니다. 배경 스레드는 전경 스레드와는 달리 해당 스레드의 종료여부와 상관없이 프로세스가 종료되면 같이 종료됩니다. 결국 프로세스가 종료되기 위한 스레드는 전경 스레드에 한정됩니다.

 

하지만 배경 스레드라 하더라도 다음과 같은 방법으로 스레드종료를 대기할 수 있습니다.

 

static void Main(string[] args)
{
    Thread t = new Thread(myThread);
    t.IsBackground = true;
    t.Start();

    t.Join();

    Console.Read();
}

 

스레드 처리중 join() 메서드를 만나면 t에 해당하는 스레드가 종료될때까지 t스레드를 호출한 주 스레드는 무한정 대기하게 됩니다.

 

스레드를 통해 메서드를 호출할때 필요하다면 특정 값을 메서드에 전달할 수도 있습니다.

 

static void Main(string[] args)
{
    Thread t = new Thread(myThread);
    t.Start(100);

    Console.Read();
}

static void myThread(object o)
{
    Console.WriteLine("입력값 : " + o.ToString());
}

 

전달가능한 매개변수는 object형이어야 하며 단 하나의 매개변수만 가능합니다. 여러개의 매개변수를 전달해야 한다면 배열을 사용하거나 사용자 정의 타입을 구현해 필요한 데이터를 전달하면 됩니다.

 

static void Main(string[] args)
{
    myValue mv = new myValue();
    mv.param1 = 100;
    mv.param2 = 200;

    Thread t = new Thread(myThread);
    t.Start(mv);

    Console.Read();
}

static void myThread(object o)
{
    myValue mv = (myValue)o;
    Console.WriteLine("입력값 : " + mv.param2);
}

 

0 0