본문 바로가기

C#/Study

[C# 공부] Thread(쓰레드) - 비동기 호출, Delegate - 짱우의 코딩일기 - 티스토리

반응형

  책을 보면서 독학을 하다가 나중에 까먹거나 헷갈릴거 같은 개념들을 적어둘 목적으로 글을 써보았다.


스레딩

System.Threading.EventWaitHandle

  EventWaitHandle은 Monitor 타입처럼 스레드 동기화 수단의 하나다. 스레드로 하여금 이벤트를 기다리게 만들 수 있고, 다른 스레드에서는 원하는 이벤트를 발생시키는 시나리오에 적합하다.

  이때 이벤트 객체는 딱 두 가지 상태만 갖는데, 바로 Signal과 Non-Signal로 나뉘고 서로 간의 상태 변화는 Set, Reset 메서드로 전환할 수 있다.

Set : Non-Signal → Signal / Reset : Signal → Non-Signal

  이와 함께 이벤트 객체는 WaitOne 메서드를 제공한다. 어떤 스레드가 WaitOne 메서드를 호출하는 시점에 이벤트 객체가 Signal 상태이면 메서드에서 곧바로 제어가 반환되지만, Non-Signal 상태였다면 이벤트 객체가 Signal 상태로 바뀔 때까지 WaitOne 메서드는 제어를 반환하지 않는다. 즉, 스레드는 더는 실행되지 못하고 대기 상태로 빠지는 것이다.

using System;
using System.Threading;
using System.IO;

namespace prac1
{
    class Program
    {
        int number = 0;

        static void Main(string[] args)
        {
            // Non-Signal 상태의 이벤트 객체 생성
            // 생성자의 첫 번째 인자가 false이면 Non-Signal 상태로 시작
            //                          true이면 Signal 상태로 시작
            EventWaitHandle ewh =
                new EventWaitHandle(false, EventResetMode.ManualReset);
            Thread t = new Thread(threadFunc);
            t.IsBackground = true;
            t.Start(ewh);

            // Non-Signal 상태에서 WaitOne을 호출했으므로 Signal 상태로 바뀔 때까지 대기
            ewh.WaitOne();
            Console.WriteLine("주 스레드 종료!");
        }

        static void threadFunc(object inst)
        {
            EventWaitHandle ewh = inst as EventWaitHandle;

            Console.WriteLine("2초 후에 종료");
            Thread.Sleep(2000);
            Console.WriteLine("스레드 종료!");

            // Non-Signal 상태의 이벤트를 Singal 상태로 전환
            ewh.Set();

            Console.WriteLine("는 훼이크~!");
        }
    }
}

  threadFunc 메서드에서 ewh.Set() 으로 Signal 상태로 전환한다 해도 메서드가 끝나버리는 건 아니다. Signal 상태로만 바뀌는거고 메서드에 있는 코드가 다 끝난 후에 다시 메인문으로 전환된다.

 

비동기 호출

using System;
using System.Threading;
using System.IO;
using System.Collections;
using System.Text;

namespace prac1
{
    class Program
    {
        int number = 0;

        static void Main(string[] args)
        {
            using (FileStream fs =
                new FileStream(@"C:\windows\system32\drivers\etc\HOSTS", FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                byte[] buf = new byte[fs.Length];
                fs.Read(buf, 0, buf.Length);

                string txt = Encoding.UTF8.GetString(buf);
                Console.WriteLine(txt);
            }
        }
    }
}
  • 여기서 FileStream.Read 메서드는 동기 호출에 속한다. 즉, Read 메서드는 디스크의 파일로부터 데이터를 모두 읽기 전까지는 제어를 반환하지 않는다. 이 때문에 다른 말로 동기 호출을 블로킹 호출(blocking call)이라고도 한다.
  • 쉽게 말해 느린 디스크 I/O가 끝날 때까지 스레드는 아무 일도 못한다는 것이고, 이는 곧 CPU가 일을 하지 않고 놀게 된다는 것을 의미한다.
  • 이러한 동기 호출의 단점을 해결하기 위해 비동기 호출이 제공된다. FileStream은 비동기 호출을 위해 Read/Write 메서드에 대해 각각 BeginRead/EndRead, BeginWrite/EndWrite 메서드를 쌍으로 제공한다.

비동기 방식의 파일

using System;
using System.Threading;
using System.IO;
using System.Collections;
using System.Text;

namespace prac1
{
    class Program
    {
        static void Main(string[] args)
        {
            FileStream fs =
                new FileStream(@"C:\windows\system32\drivers\etc\HOSTS", FileMode.Open, FileAccess.Read, FileShare.ReadWrite);


            FileState state = new FileState();
            state.Buffer = new byte[fs.Length];
            state.File = fs;

            fs.BeginRead(state.Buffer, 0, state.Buffer.Length, readCompleted, state);

            // BeginRead 비동기 메서드 호출은 스레드로 곧바로 제어를 반환하기 때문에
            // 이곳에서 자유롭게 다른 연산을 동시에 진행할 수 있다.

            //Console.ReadLine();

            int result = 0;
            for (int i = 0; i < 99999999; i++)
            {
                result = i;
            }
            Console.WriteLine(result);

            fs.Close();
        }

        static void readCompleted(IAsyncResult ar)
        {
            FileState state = ar.AsyncState as FileState;
            state.File.EndRead(ar);

            string txt = Encoding.UTF8.GetString(state.Buffer);
            Console.WriteLine(txt);
        }
    }

    class FileState
    {
        public byte[] Buffer;
        public FileStream File;
    }
}
  • BeginRead 메서드는 디스크로부터 파일 데이터를 읽어낼 때까지 기다리지 않고 곧바로 스레드에 제어를 반환한다. 따라서 스레드는 이후의 코드를 끊김없이 실행할 수 있다.

  위의 캡쳐화면을 보면 파일을 읽어오는 동안 for문에서 연산이 동시에 이루어지는 걸 확인할 수 있다.

System.Delegate의 비동기 호출

  일반적으로 비동기 호출은 입출력 장치와의 속도 차이에서 오는 비효율적인 스레드 사용 문제를 극복하는데 사용된다. 그런데 닷넷에서는 특이하게도 입출력 장치뿐만 아니라 일반 메서드에 대해서도 비동기 호출을 할 수 있는 수단을 제공하는데, 다름 아닌 델리게이트가 그러한 역할을 한다. 즉, 메서드를 델리게이트로 연결해 두면 이미 비동기 호출을 위한 기반이 마련된 것이나 다름없다.

using System;
using System.Threading;
using System.IO;
using System.Collections;
using System.Text;

namespace prac1
{
    class Program
    {
        public delegate long CalcMethod(int start, int end);

        static void Main(string[] args)
        {
            CalcMethod calc = new CalcMethod(Calc.Cumsum);

            long result = calc(1, 100);
            Console.WriteLine(result);
        }
    }

    public class Calc
    {
        public static long Cumsum (int start, int end)
        {
            long sum = 0;

            for (int i = start; i <= end; i++)
            {
                sum += i;
            }

            return sum;
        }
    }
}

  • 다음 코드에서 calc 델리게이트 수행은 당연히 현재의 스레드에서 수행된다.
  • 하지만 델리게이트의 비동기 호출을 위한 메서드 (BeginInvoke / EndInvoke)를 사용하면 calc 인스턴스에 할당된 Calc.Cumsum 메서드의 수행을 ThreadPool의 스레드에서 실행할 수 있다.
반응형