본문 바로가기

C#

2023.09.14 BCL - 파일, 스레드, 네트워크 통신 (포트 까지)

파일

System.IO.FileStream

파일을 다루기 위한 BCL의 가장 기본적인 타입. MemoryStream의 부모 클래스와 동일한 Stream 타입을 상속받았고

전체적인 동작 방식도 유사하다. 디스크 파일을 대상으로 읽기/쓰기 작업을 한다. 

class Program
    {
        static void Main(string[] args)
        {
            using (FileStream fs = new FileStream("test.log", FileMode.Create))
            {
                StreamWriter sw = new StreamWriter(fs, Encoding.UTF8);
                sw.WriteLine("Hello World");
                sw.WriteLine("Anderson");
                sw.Write(32000);
                sw.Flush();
            }
        }

    }

생성된 test.log파일

결국 메모장 프로그램이 바이트 내용을 읽어서 보여준 것과 같다.

이번에는 StreamWriter가 아닌 BinaryWriter를 사용한 예제를 작성해 보자.

using (FileStream fs = new FileStream("bTest.log", FileMode.Create))
            {
                BinaryWriter bw = new BinaryWriter(fs);
                bw.Write("Hello World" + Environment.NewLine);
                bw.Write("Anderson" + Environment.NewLine);
                bw.Write(32000);
                bw.Flush();
            }

bTest.log 파일

32000에 대한 값이 메모장으로 열어보면 공백과 } 문자로 출력된다. 이는 2진 데이터로 출력된 00 7D 00 00 부분을

문자열로 취급하기 때문이다.

여기서 알 수 있는 것은 FileStream을 이용해 파일을 읽고 쓸 때 사람이 읽을 수 있게 하려면 StreamWriter를 사용하고, 

가독성을 무시하고 효율적으로 데이터를 기록하려면  BinaryWriter를 사용하면 된다.

 

FileStream 생성자의 인자로 전달되는 FileMode, FileAccess, FileShare가 있다. 이 세 가지 모두 열거형이며,

각 인자의 사용법은 다음과 같다.

 

FileMode

Append 해당 파일이 있을 경우 파일을 열고 파일의 끝까지 검색하거나 새 파일을 만듭니다. Append 권한이 필요합니다. FileMode.Append는 FileAccess.Write와 함께만 사용할 수 있습니다. 파일이 끝나기 이전 위치까지 검색하려고 하면 IOException 예외를 throw하고 읽기 시도가 실패하면 NotSupportedException 예외를 throw합니다.
Create 운영 체제에서 새 파일을 만들도록 지정합니다. 파일이 이미 있으면 덮어씁니다. Write 권한이 필요합니다. FileMode.Create는 파일이 없으면 CreateNew를 사용하고, 파일이 있으면 Truncate를 사용하도록 요청하는 것과 마찬가지입니다. 파일이 이미 있지만 숨김 파일이면 UnauthorizedAccessException 예외가 throw됩니다.
CreateNew 운영 체제에서 새 파일을 만들도록 지정합니다. Write 권한이 필요합니다. 파일이 이미 있으면 IOException 예외가 throw됩니다.
Open 운영 체제에서 기존 파일을 열도록 지정합니다. 파일을 열 수 있는지 여부는 FileAccess 열거형에서 지정된 값에 따라 달라집니다. 파일이 없으면 FileNotFoundException 예외가 throw됩니다.
OpenOrCreate 파일이 있으면 운영 체제에서 파일을 열고 그렇지 않으면 새 파일을 만들도록 지정합니다. FileAccess.Read를 사용하여 파일을 여는 경우 Read 권한이 필요합니다. 파일 액세스가 FileAccess.Write이면 Write 권한이 필요합니다. FileAccess.ReadWrite를 사용하여 파일을 여는 경우 Read와 Write 권한이 모두 필요합니다.
Truncate 운영 체제에서 기존 파일을 열도록 지정합니다. 파일을 열면 크기가 0바이트가 되도록 잘라야 합니다. Write 권한이 필요합니다. FileMode.Truncate로 연 파일에서 읽으려고 하면 ArgumentException 예외가 발생합니다.

 

FileAccess

Read 파일에 대한 읽기 액세스입니다. 데이터를 파일에서 읽을 수 있습니다. 읽기/쓰기 액세스에 대한 Write와 함께 사용합니다.
ReadWrite 파일에 대한 읽기 및 쓰기 액세스입니다. 데이터를 파일에 쓰고 파일에서 읽을 수 있습니다.
Write 파일에 대한 쓰기 액세스입니다. 데이터를 파일에 쓸 수 있습니다. 읽기/쓰기 액세스에 대한 Read와 함께 사용합니다.

 

FileShare

Delete 파일의 후속 삭제를 허용합니다.
Inheritable 파일 핸들을 자식 프로세스에서 상속할 수 있도록 합니다. Win32에서는 이러한 방식이 직접 지원되지 않습니다.
None 현재 파일의 공유를 거절합니다. 파일을 닫아야만 이 프로세스나 다른 프로세스에서 파일을 열려는 요청이 수행됩니다.
Read 다음에 파일을 읽기용으로 여는 것을 허용합니다. 이 플래그가 지정되어 있지 않은 경우 파일을 닫아야만 이 프로세스나 다른 프로세스에서 파일을 읽기용으로 열려는 요청이 수행됩니다. 그러나 이 플래그가 지정되어 있으면 파일에 액세스하는 데 추가 권한이 필요할 수도 있습니다.
ReadWrite 다음에 파일을 읽기용 또는 쓰기용으로 여는 것을 허용합니다. 이 플래그가 지정되어 있지 않은 경우 파일을 닫아야만 이 프로세스나 다른 프로세스에서 파일을 읽기용 또는 쓰기용으로 열려는 요청이 수행됩니다. 그러나 이 플래그가 지정되어 있으면 파일에 액세스하는 데 추가 권한이 필요할 수도 있습니다.
Write 다음에 파일을 쓰기용으로 여는 것을 허용합니다. 이 플래그가 지정되어 있지 않은 경우 파일을 닫아야만 이 프로세스나 다른 프로세스에서 파일을 쓰기용으로 열려는 요청이 수행됩니다. 그러나 이 플래그가 지정되어 있으면 파일에 액세스하는 데 추가 권한이 필요할 수도 있습니다.

 

자주 쓰는 옵션

FileMode.Append 로깅(logging) 목적의 파일 쓰기를 하는 경우 사용
(FileMode,Append인 경우 FileAccess는 Write만 허용된다. 또한 FileShare의 기본값은
Read이므로 굳이 지정할 필요가 없다. )
FileMode.OpenCreate
FileAccess.ReadWrite
FileShare.None
재사용되는 전용 데이터를 입/출력하는 목적인 경우 사용
FileMode.Create
FileAccess.ReadWrite
FileShare.None
임시로 사용되는 데이터를 입/출력하는 목적인 경우 사용

System.IO.File / System.IO.FileInfo

File 타입은 자주 사용되는 파일 조작 기능을 담은 정적 클래스다. 따라서 File 타입에서 제공되는 모든 메서드는

정적 메서드다. 자주 사용되는 기능을 나열해 보면 다음과 같다.

Copy 파일 복사
Exist 파일 존재 여부 true/false로 반환
Move 파일 이동
ReadAllBytes 파일의 모든 내용을 읽어 byte배열로 반환
ReadAllLines 텍스트 파일의 모든 내용을 string 배열로 반환. 한 줄당 문자열 하나로 대응된다.
ReadAllText 텍스트 파일의 모든 내용을 읽어 string 객체로 반환
WriteAllBytes 지정된 byte 배열을 모두 파일에 쓴다.
WriteAllLines 지정된 string 배열의 모든 내용을 개행 문자와 함께 파일에 쓴다.
WriteAllText 지정된 string 인자의 값을 모두 파일에 쓴다.

 File.Copy 메서드는 복사되는 위치에 이미 파일이 있으면 IOException이 발생한다. 덮어쓰고 싶다면 3개의 인자를 갖는

Copy 메서드를 사용하면 된다.

// 경로가 지정되지 않으면 Enviroment.CurrentDirectory가 기본 경로로 사용됨
// 대상 폴더에 파일이 없다면,
File.Copy("test.log", "test.dat");

// 대상 폴더에 파일이 있고, 덮어 쓸 의도라면,
File.Copy("test.log", "test.dat", true);

3번째 인자의 불린 값이 true면 대상 경로에 이미 파일이 있어도 덮어쓴다.

 

File.Move 메서드는 원본과 대상을 가리키는 2개의 인자만 갖는다. 파일의 위치를 옮기는 목적 말고도 Move의 특성상

폴더가 같은 경우 파일명을 변경하는 용도로 사용된다.

// 폴더가 동일하다면 파일명 변경
File.Move("test.log", "test.dat");

// 폴더가 다르다면 파일 이동
File.Move("test.log", "c:\\temp\\test.dat");

주의할 점은 대상이 되는 경로에 같은 이름의 파일이 존재하면 IOException이 발생한다. File.Copy처럼 덮어쓰는 옵션이 

없기 때문에 파일 유무를 먼저 확인하고 삭제하는 작업을 진행해야 한다.

string target = "c\\temp\\test.dat";
if (File.Exists(target) == true)
{
    File.Delete(target);
}
File.Move("test.log", target);

File 타입은 정적 클래스다. FileInfo타입은 File 타입의 기능을 인스턴스 멤버로 일부 구현하고 있다는 차이점을 제외하고 거의 모든 면에서 사용법이 같다. 위 예제를 FileInfo로 구현해 보자

string target = "c\\temp\\test.dat";
if (File.Exists(target) == true)
{
    File.Delete(target);
}
File.Move("test.log", target);

스레드

스레드는 명령어를 실행하기 위한 스케줄링 단위이며, 프로세스 내부에서 생성할 수 있다.

윈도우 프로세스를 생성할 때 기본적으로 한 개의 스레드를 함께 생성하며, 이를 주 스레드라고 한다.

 

스레드는 CPU 명령어 실행과 관련된 정보를 보관하고 있는데 이를 스레드 문맥이라 한다.

운영체제의 스케줄러는 실행돼야 할 적절한 스레드를 골라서 CPU로 하여금 실행되게 만드는데 

이때 두 가지 동작을 수행한다.

  1. CPU는 현재 실행 중인 스레드를 다음에 이어서 실행할 수 있게 환경 정보를 스레드 문맥에 보관
  2. 운영체제로부터 할당받은 스레드의 문맥 정보를 다시 CPU 내부로 로드해서 마치 해당 스레드가
    실행되고 있었던 상태인 것처럼 복원한 다음, 일정 시간 동안 실행

멀티스레딩의 필요성에 대한 예를 들어보면, 여러 기능이 있는 프로그램이 있을 때 한 동작을 수행하는 동시에 다른 동작을 수행하려 할 때 스레드가 하나면 이전 동작을 모두 완료할 때까지 기다려야 한다. 그러나 멀티 스레딩이 지원된다면 마치 동시에 여러 작업을 하는 것처럼 작동한다.

 

System.Threading.Thread

지금까지 실습한 모든 예제는 주스레드가 하나 생성되어 실행된 것이다.

Thread 타입에 현재 명령어를 실행 중인 스레드 자원에 접근할 수 있는 정적 속성을 제공한다.

이를 활용하여 프로그램을 실행하고 있는 스레드의 상태를 알아보자.

// Thread 타입을 사용하려면 using System.Threading을 추가해야 한다.

Thread thread = Thread.CurrentThread;
Console.WriteLine(thread.ThreadState); // Running

자주 사용되는 Thread 정적 메서드로는 Sleep 메서득가 있다. 이를 사용하면 현재 Running 상태의 스레드를 지정된

밀리초만큼 중단할 수 있다.

Console.WriteLine(DateTime.Now);
Thread.Sleep(1000);
Console.WriteLine(DateTime.Now);



-----------------출력 결과--------------------
2023-09-14 오후 1:11:29
2023-09-14 오후 1:11:30

약 1초간 멈춘 후 다시 실행되었다.

 

이제 새로운 스레드를 하나 생성해 보자. 스레드는 실행될 명령어가 필요하므로 명령어의 묶음인 메서드를

Thread 생성자에 전달해야 한다. 일단 스레드 개체가 생성되면 Start 메서드를 호출하는 것으로 스레드가 시작된다.

static void Main(string[] args)
}
    Thread t = new Thread(threadFunc);
    t.Start();
}
private static void threadFunc()
{
    Console.WriteLine("threadFunc run!");
}

위 동작을 그림으로 설명하면 다음과 같다.

새롭게 생성된 스레드는 별도로 명령어를 실행해 나간다.

최근의 다중 코어 CPU에서는 실제로 주스레드와 t 스레드의 코드를 동시에 실행할 수 있다.

 

기본적으로 프로그램은 생성된 모든 스레드가 실행을 종료해야만 프로그램도 종료할 수 있다.

따라서 다음과 같이 새롭게 생성된 스레드에서 실행을 계속하고 있으면 EXE 프로세스는 해당 스레드가 

끝날 때까지 종료하지 않는다.

static void Main(string[] args)
}
    Thread t = new Thread(threadFunc);
    t.Start();
    // 주 스레드가 실행할 명령어가 없으므로 주 스레드는 제거 됨
}
private static void threadFunc()
{
     Console.WriteLine("60초 후에 프로그램 종료");
     Thread.Sleep(1000 * 60); // 60초 동안 실행 중지
                              // 현재 주 스레드는 종료됐어도 t스레드는 존속
     Console.WriteLine("스레드 종료!");
}

이처럼 프로그램의 실행 종료에 영향을 미치는 스레드를 전경 스레드 (foreground thread)라고 한다.

대조되는 의미의 배경 스레드(background thread)도 있으며, 이 유형은 실행 종료에 영향을 미치지 않는다.

Thread 타입의 IsBackground 속성을 true로 바꿔 전경 스레드 동작을 배경 스레드로 바꿀 수 있다.

Thread t = new Thread(threadFunc);
t.IsBackground = true;
t.Start();

이 코드를 실행하면 아무것도 출력되지 않고 종료되거나 아주 낮은 확률로 threadFunc 메서드의 첫 번째 메서드가

수행되는 것을 볼 수 있다. 왜냐하면 새롭게 생성된 스레드의 종료 여부에 상관없이 Main 메서드를 실행하는

스레드가 완료되면 프로세스 자체가 종료되기 때문이다. 여기서 의문이 생긴다.

주 스레드에서 분명히 스레드 객체의 Start 메서드를 실행했는데 어째서 threadFunc에 있는 단 한 줄의 코드도

실행이 안 되는 것일까? 그 이유는 스레드가 CPU에 의해 선택되어 실행될 수 있는 단계까지 시간이 걸리기 때문이다.

즉, threadFunc을 실행해야 할 스레드가 운영체제의 스케줄러에 의해 선택되기도 전에 Main 메서드를 실행하는 스레드가 종료됐으므로 그런 현상이 발생한 것이다.

 

때로는 다른 스레드의 실행이 종료되기까지 기다려야 할 수도 있다. 이를 위해 Thread 타입의 Join 메서드를

사용할 수 있다. 다음 코드는 새로운 스레드 t가 배경 스레드임에도 주 스레드가 Join 메서드를 호출해 t 스레드의

실행이 종료될 때까지 기다린다.

static void Main(string[] args)
}
    Thread t = new Thread(threadFunc);
    t.IsBackground = true;
    t.Start();
    
    t.Join(); // t 스레드가 종료될 때까지 현재 스레드를 무한 대기
    Console.WriteLine("주 스레드 종료!");
}
private static void threadFunc()
{
     Console.WriteLine("60초 후에 프로그램 종료");
     Thread.Sleep(1000 * 60); // 60초 동안 실행 중지
                              // 현재 주 스레드는 종료됐어도 t스레드는 존속
     Console.WriteLine("스레드 종료!");
}

--------------출력 결과------------------
60초 후에 프로그램 종료
스레드 종료!
주 스레드 종료!

 

스레드를 시작하는 측에서 인자를 전달하는 것도 가능하다. object 타입의 인자를 하나 전달받는 스레드 메서드를

준비하고 Thread.Start 메서드에 직접 값을 넣으면 된다.

static void Main(string[] args)
        {           
            // 인자가 있는 메서드의 경우 Thread 생성자는
            // ParameterizedThreadStart 델리게이트 타입을 허용한다.
            Thread t = new Thread(threadFunc);
                        // 따라서 C# 컴파일러는 위의 코드를 다음과 같이 번역해 컴파일한다.
                        // new Thread(new ParameterizedThreadStart(threadFunc));

            t.Start(10);           
        }

        private static void threadFunc(object initialValue)
        {
            int intValue = (int)initialValue;
            Console.WriteLine(intValue);   // 출력 10       
        }

전달할 값이 여러 개라면 어떻게 할까? threadFunc 메서드의 인자 타입이 object인 것을 감안하면 

전달할 값의 수만큼 필드를 포함한 클래스를 만들어 그 객체를 전달하면 된다.

    class ThreadParam
    {
        public int Value1;
        public int Value2;
    }
    
    static void Main(string[] args)
    {           
        Thread t = new Thread(threadFunc);

        ThreadParam param = new ThreadParam();
        param.Value1 = 10;
        param.Value2 = 20;

        t.Start(param);         
    }

    private static void threadFunc(object initialValue)
    {
        ThreadParam value = (ThreadParam)initialValue;
        Console.WriteLine("{0}, {1}", value.Value1, value.Value2);  // 출력 10, 20   
    }

 

System.Threading.Monitor

다중 스레드 생성은 다음과 같이 Thread 개체를 여러 번 생성하면 된다.

static void Main(string[] args)
{
    for(int i = 0; i < 10; i++)
    {
        Thread t = new Thread(threadFunc);
        t.Start(i);
    }
}
private static void threadFunc(object value)
{
    Console.WriteLine(value + "번째 스레드");
}

----------------출력 결과--------------------
6번째 스레드
7번째 스레드
0번째 스레드
1번째 스레드
2번째 스레드
3번째 스레드
4번째 스레드
5번째 스레드
8번째 스레드
9번째 스레드

출력은 되지만 순서는 보장되지 않는다.

다중 스레드를 사용할 때 주의할 점은 또 있다. 예를 들어 2개의 스레드를 생성하는 프로그램을 만들고 실행해 보자.

using System;
using System.Threading;

namespace MultiThread
{
    class Program
    {
        int number = 0;

        static void Main(string[] args)
        {
            Program pg = new Program();

            Thread t1 = new Thread(threadFunc);
            Thread t2 = new Thread(threadFunc);

            t1.Start(pg);
            t2.Start(pg); // 2개의 스레드를 시작하고,

            t1.Join();
            t2.Join(); // 2개의 스레드 실행이 끝날 때까지 대기한다.

            Console.WriteLine(pg.number);

        }

        private static void threadFunc(object inst)
        {
            Program pg = inst as Program;

            for (int i = 0; i < 10; i++)
            {
                pg.number = pg.number + 1; // Program 객체의 number 필드 값을 증가
                                           // 출력 결과 대부분 20
            }
        }
    }
}

2개의 스레드가 Program 객체의 number 필드를 루프를 돌면서 1씩 증가시키고 있다.

하지만 반복 횟수가 많아지면 결괏값이 20000, 2000000 등이 아닌 엉뚱한 값이 나온다.

각자 CPU의 코어 수만큼 스레드를 동시에 실행할 수 있다. 컴퓨터에는 코드 외에도 다양한 프로그램들이

동시에 실행되는 것처럼 보이는데 사실 중단과 실행을 순간적으로 계속하면 반복하고 있는 것이다.

여기에 하나 더, 코드에 사용된 "pg.number = pg.number + 1"  명령어를 CPU 입장에서 이해해 보자

  1. 메모리의 힙 영역에서 number 변수에 해당하는 값을 가져온다.
  2. 가져온 값에 1을 더한다.
  3. 1이 더해진 새로운 값을 메모리의 힙 영역에 저장한다.

이 과정을 여러 스레드를 오가며 수행하다 보면 미쳐 저장하지 못하고 다른 스레드를 기다리다가 

한 단계 이전의 값을 메모리에 저장하게 된다. 

이것이 출력값이 예상했던 것보다 적게 나오는 원인이다.

 

이런 상황을 일반적으로 공유 리소스에 대한 스레드의 동기화(synchronization)가 처리되지 않았다고 표현한다.

위 예제에서 보면 공유 리소스는 number필드이다. 2개의 스레드가 number필드에 동시에 접근하기 때문에 오동작이

발생한 것이다. 이런 문제를 해결하기 위해 "동기화 처리"를 해야 한다. 여러 방법이 있지만 이런 경우에는

number 필드를 한순간에 한 개의 스레드만 접근할 수 있게 만드는 수단이 필요하다.

 

BCL에서 제공하는 Monitor 클래스를 이용하면 된다.

공유 자원에 접근하는 앞뒤로 Monitor를 사용하면 문제가 해결된다.

        private static void threadFunc(object inst)
        {
            Program pg = inst as Program;

            for (int i = 0; i < 1000000; i++)
            {
                Monitor.Enter(pg);
                
                try
                {
                    pg.number = pg.number + 1; 
                }
                finally
                {
                    Monitor.Exit(pg);
                }
            }
         }

Monitor.Enter/Exit 코드는 이런 패턴으로 사용하면 된다. Enter와 Exit 코드 사이에 위치한 모든 코드는 한 순간에 스레드 

하나만 진입해서 실행할 수 있다는 점을 기억하자. 또한 Enter와 Exit 메서드의 인자로 전달하는 값은 반드시 참조형

인스턴스여야 한다.

동일한 기능을 제공하는 lock 예약어가 있다. Monitor 예제를 lock을 이용해 바꾸면 다음과 같다.

        private static void threadFunc(object inst)
        {
            Program pg = inst as Program;

            for (int i = 0; i < 1000000; i++)
            {   
                lock (pg)
                {
                   pg.number = pg.number + 1;
                }
            }
         }

구문은 바뀌었지만 lock 예약어를 사용한 블록은 C#컴파일러에 의해 최종적으로 try/finally + Monitor.Enter/Exit 코드로

바뀌기 때문에 완전히 동일하다. 코드가 간결하기 때문에 lock 예약어를 이용한 구문이 더 선호된다.

 

성능상의 이유로 BCL의 모든 타입은 스레드에 안전하지 않다. 동기화가 필요할 때는 개발자가 직접 외부에서

처리해야 한다.

static void Main(string[] args)
{
   MyData data = inst as MyData;
   
   for (int i = 0; i < 100000; i++)
   {
       lock (data) // 스레드에 안전하지 않은 메서드를 외부에서 안전하게 사용
       {
           data.Increment();
       }
    }
}

다중 스레드에서 공유 자원을 사용하는 몇몇 패턴에 대해서는 명시적인 동기화 작업이 필요없도록 정적 메서드를 제공한다. 예를 들어, 32비트/64비트 숫자 형 타입의 더하기 및 증가/감소 같은 일부 연산에 대해서는 lock을 사용하지 않고 InterLocked 타입을 이용해 처리할 수 있다.

 

System.Threading.ThreadPool

임시적인 목적으로 언제든 원하는 때에 스레드를 사용할 수 있게 CLR은 기본적인 스레드 풀을 마련해 뒀다.

프로그래미에서 pool은 재사용 가능한 자원의 집합을 의미한다.

static void Main(string[] args)
{
    MyData data = new MyData();
    
    ThreadPool.QueueUserWorkItem(threadFunc, data);
    ThreadPool.QueueUserWorkItem(threadFunc, data);
    
    Thread.Sleep(1000);
    
    Console.WriteLine(data.Number);
}

스레드 생성 코드가 생략되는 대신 스레드 생성자에 전달됐던 메서드를 곧바로 ThreadPool 타입의 QueueuserWorkItem

메서드에 전달하고 있다. 이렇게 두 번을 호출했기 때문에 스레드 풀에는 2개의 스레드가 자동으로 생성되고 각 스레드에 threadFunc 메서드가 할당되어 실행된다.

 

비동기 호출

동기 호출과 대비되는 개념. 일반적으로 입출력(I/O) 장치와 연계되어 설명될 때가 많다.

FileStream.Read 메서드는 동기 호출에 속한다. 즉, Read 메서드는 디스크의 파일로부터 데이터를 모두 읽기 전까진

제어를 반환하지 않는다. 이 때문에 동기 호출을 블로킹 호출이라고도 한다. 아래 그림에서 볼 수 있듯이 스레드가 

메서드를 호출한 다음 디스크 I/O가 완료될 때까지는 실행이 차단되므로 그런 이름이 붙은 것이다.

동기 호출 - 파일 읽기

이런 동기 호출의 단점을 해결하기 위해 비동기 호출이 제공된다.

FileStream은 비동기 호출을 위해 Read/Write 메서드에 대해 각각 BeginRead/EndRead, BeginWrite/EndWrite 메서드를 쌍으로 제공한다.

비동기 호출은 I/O 연산이 끝날 때까지 차단되지 않으므로 논블로킹 호출이라고도 한다.

 

System.Delegate의 비동기 호출

일반적으로 비동기 호출은 입출력 장치와의 속도 차이에서 오는 비효율적인 스레드 사용 문제를 극복하는데 사용된다.

그런데 닷넷에서는 입출력 장치뿐 아니라 일반 메서드에 대해서도 비동기 호출을 할 수 있는 수단을 제공하는데,

다름 아닌 델리게이트가 그런 역할을 한다. 즉, 메서드를 델리게이트로 연결해 두면 이미 비동기 호출을 위한 기반이

마련된 것이다.

    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); // 출력 결과: 5050            
        }
    }

    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의 스레드에서 실행할 수 있다.

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

        static void Main(string[] args)
        {
            CalcMethod calc = new CalcMethod(Calc.Cumsum);
            
            // Delegate 타입의 BeginInvoke 메서드를 호출한다.
            // 이 때문에 Calc.Cumsum 메서드는 ThreadPool 스레드에서 실행된다.
            IAsyncResult ar = calc.BeginInvoke(1, 100, null, null);
            
            // BeginInvoke로 반환 받은 IAsyncResult 타입의 AsyncWaitHandle 속성은, 
            // EventWaitHandle 타입이다.
            // AsyncWaitHandle 객체는 스레드 풀에서 실행된 Calc.Cumsum 동작이 완료됐을 때
            // Signal 상태로 바뀐다.
            // 따라서 아래와 같이 호출함면 Calc.Cumsum 메서드 수행이 완료될 때까지
            // 현재 스레드를 대기시킨다.
            ar.AsyncWaitHandle.WaitOne();
            
            // Calc.Cumsum의 반환 값을 얻기 위해 EndInvoke 메서드를 호출한다.
            // 반환값이 없어도 EndInvoke는 반드시 호출하는 것을 권장한다.
            long result = calc.EndInvoke(ar);
            
            Console.WriteLine(result);          
        }
    }

 

네트워크 통신

전화를 이용한 사람 간의 통신 절차는 다음과 같다.

  1. 전화번호부에서 수신자 이름을 찾는다.
  2. 전화번호를 확인한다.
  3. 번호를 눌러 통화한다.

컴퓨터 간의 통신에도 이 방법이 그대로 적용된다.

컴퓨터 간의    
     
     
     

통신에서 서로의 주소를 아는 것과 함께 중요한 것이 어떤 절차를 거쳐 통신할 것인가에 대한 규칙을 

정하는 것이다. 이를 프로토콜이라고 하는데, 현재 인터넷에서 가장 많이 사용하는 프로토콜이 TCP/IP이다.

 

System.Net.IPAddress

현재 널리 사용되는 TCP/IP의 IP는 4번째 버전이다. IPv4라고하며 표현 범위는 약 42억이고 이미

국제 인터넷 번호관리기관(IANA)은 이미 모든 IP 주소가 고갈됐다고 발표했다. 물론 이를 대비해 

최신 표준이 나와있는데 IPv6이고 주소 용량은 총 128비트로 인터넷 환경이 IPv6로 이전한다면 한동안

IP 주소 걱정은 없을 것이다.

C#에서는 System.Net.IPAddress 타입으로 표현된다. Parse 정적 메서드가 제공되므로 문자열로부터 IPAddress

인스턴스로 변환하거나, 직접 숫자에 해당하는 바이트 값을 생성자에 전달하는 것도 가능하다.

IPAddress ipAddr = IPAddress.Parse("202.179.177.21");
Console.WriteLine(ipAddr);

IPAddress ipAddr2 = new IPAddress(new byte[] { 202, 179, 177, 21 });
Console.WriteLine(ipAddr2);

위 주소 형식은 IPv4 주소에 해당하고, 16바이트로 확장된 IPv6 주소 체계는 좀 더 복잡하다.

기본적으로는 2바이트씩 16진수로 표현하고 콜론으로 구분해서 나타낸다.

만약 중간에 0으로 채워져 있으면 2개의 콜론을 사용해 줄이는 것도 가능하다.

예) 2001:0000:0000:0000:0000:0000:0000:7334 ->2001::7334

올바른 형식의 주소를 담은 문자열이라면 어떤 값이든 IPAddress.Parse 메서드로 IPv4/IPv6에 

상관없이 모두 해석할 수 있다.

 

포트

네트워크 통신은 일반적으로 서버와 클라이언트로 나뉜다. 클라이언트/서버 구조라고 하며, 줄여서 C/S라고도 한다.

서버가 통신을 열었을 때 클라이언트는 서버를 구분해서 접속을 시도해야 한다. 여기서 서버란 실제로 TCP/IP

통신을 하는 프로그램을 의미하는데 한 대의 컴퓨터에서 실행되는 서버 프로그램의 종류는 매우 다양할 수 있다.

그래서 포트라는 개념을 추가한다. 포트는 단순히 0~ 65535 사이의 값이므로 BCL에서 따로 정의되진 않았다.

포트가 도입됨으로써 서버 측 프로그램에서는 IP와 함께 포트를 이용햐 통신을 대기할 수 있다.

따라서 원하는 포트 번호를 미리 선점해서 통신을 대기해두면 클라이언트 측에서는 그 번호에 해당하는 포트를

지정해 서버와 연결할 수 있다. 덕분에 한 대의 컴퓨터에서 1개의 IP로 여러 통신 프로그램을 사용할 수 있다.

 

https://www.sysnet.pe.kr/0/0

 

J & J - 정성태의 닷넷 이야기: Digital Stories

 

www.sysnet.pe.kr

https://ridibooks.com/books/1160000100

 

시작하세요! C# 10 프로그래밍

시작하세요! C# 10 프로그래밍 작품소개: 이 책의 목표는 여러분이 C#을 이용해 프로그래밍 기초를 탄탄하게 다질 수 있게 하는 것이다. 이를 위해 C# 언어의 최신 버전인 C# 10의 문법까지 구체적인

ridibooks.com