본문 바로가기

C#/이것이 C#이다

[ 이것이 C#이다 ] Chapter 19. 스레드와 태스크

 [ 학습 흐름 ]
  1. 프로세스와 스레드
  2. 스레드 시작하기
  3. 스레드 임의로 종료시키기
  4. 스레드 일생과 상태 변화
  5. 인터럽트
  6. System.Threading.Tasks.Task 클래스
  7. Task<TResult>클래스
  8. Parallel 클래스
  9. async 한정자와 await 연산자로 만드는 비동기 코드

[ 프로세스와 스레드 ]

[ 프로세스 ]

- 프로세스란 실행파일이 실행되어 메모리에 적재된 인스턴스

- 즉 , 실행파일에 담겨있는 데이터와 코드가 메모리에 적재되어 동작하는 것이 프로세스 

- 프로세스는 하나 이상의 스레드로 구성된다 .

- 스레드는 운영체제가 CPU 시간을 할당하는 기본 단위


[ 멀티 스레드의 장점 ]

  1. 응답성을 높일 수 있다 . ex ) 파일 복사 - 사용자 대화 두개의 스레드가 있다면 파일복사를 하는 동안 사용자 대화가    가능하다 .
  2. 자원 공유가 쉽다 . ex ) 멀티 프로세스에 비해 멀티 스레드가 자원공유가 쉽다 . 멀티 프로세스는 서버용 애플리케이션에서 많이 취하는 구조로 자원 공유시 소켓/공유 메모리 같은 IPC 방식을 이용한다 .반면 멀티 스레드는 스레드끼리 코드내 변수를 같이 사용하는 것만으로 데이터 교환이 가능하다 .
  3. 경제적이다 . ex ) 프로세스를 띄우기 위해 메모리와 자원을 할당하는 작업은 비용이 비싼데 , 스레드를 띄울때는 이미 프로세스에 할당된 메모리와 자원을 그대로 쓰기에 메모리와 자원을 할당하는 비용을 지불하지 않는다 .

[ 멀티 스레드의 단점 ]

  1. 구현이 복잡하다 ex ) 구현하기 까다롭고 테스트 역시 쉽지 않다 . 멀티 스레드 기반 소프트 웨어 디버깅 역시 어렵다 .
  2. 소프트 웨어 안정성을 저해한다 ex ) 멀티 프로세스 기반의 소프트웨어는 자식 프로세서중 하나가 죽어도 그 이상의 영향은 없지만 , 멀티 스레드는 하나라도 죽는다면 전체 프로세스에 영향을 미친다 .
  3. 과용시 성능저하 ex) 스레드가 CPU를 사용하기 위해 작업간 전환을 해야하는데 , 적잖은 비용을 소모한다 . 많은 스레드가 작업간 전환을 자주 수행하면 앱이 일하는 시간보다 전환시간이 더 길어 성능이 저하된다 .

[ 스레드 시작하기 ]

[ 스레드 시작 ]

static void DoSomething()
{
	//스레드가 실행할 메소드
}

static void Main(string[]args)
{
    // 1 . Thread의 인스턴스 생성 . 생성자의 인수로 스레드가 실행할 메소드를 넘긴다 .
    Thread t1 = new Thread(new ThreadStart(DoSomething));	
    // 2 . 스레드 시작
    t1.Start();
    // 3 . 스레드 종료 대기
    t1.Join();
}

- .Net은 스레드를 제어하는 클래스로 System.Threading.Thread를 제공한다 .

- 실제 스레드가 메모리에 적재되는 시점은 t1.Start()메소드의 호출시다 .

- 호출되면 CLR은 스레드를 생성 , DoSomething 메소드를 호출한다 .

- Join메소드는 스레드의 실행이 끝나면 반환되어 다음코드를 실행하게 한다 .

- Join은 밧줄에서 실이 한갈래 빠져나왔다 Join()메소드 반환시점에 합류한다 생각하면 쉽다 .

using System;
using System.Linq;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Collections.Generic;
using System.Threading;

namespace ConsoleApp;

class MainApp
{ 
    static void DoSomething()
    {
        for(int i=0;i<5;i++)
        {
            Console.WriteLine("DoSomething : {0}", i);
            Thread.Sleep(10);//인수만큼 CPU 사용을 멈춘다 . 인수단위는 밀리초 .
        }
    }
    static void Main(string[] args)
    {
        Thread t1 = new Thread(new ThreadStart(DoSomething));

        Console.WriteLine("Starting Thread ..");
        t1.Start();

        for(int i=0;i<5;i++)
        {
            Console.WriteLine($"Main : {i}");
            Thread.Sleep(10);
        }

        Console.WriteLine("Waiting until thread stops");
        t1.Join();

        Console.WriteLine("Finished");
    }
}

 


[ 스레드의 임의 종료 ]

- Thread.Abort()메소드는 .Net 프레임워크에서만 지원된다 .

- 프로세스는 사용자가 작업관리자를 통해 임의로 죽일 수 있지만 스레드는 Thread 객체의 Abort()메소드를 호출해야 한다.

-  Abort() 메소드는 호출과 동시에 스레드를 종료하는게 아니다 .

- 호출시 CLR은 해당 스레드가 실행중인 코드에 ThreadAbortException을 던진다 .

- 이때 해당 예외를 catch 하는 코드가 있다면 , 예외 처리 이후 Finally 블록까지 실행한후 스레드가 완전히 종료된다 .

- Abort/ResetAbort함수는 .NET 5.0부터 폐기되었지만 , 4.8에서는 지원한다 .

using System;
using System.Linq;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Collections.Generic;
using System.Threading;
using System.Collections.Specialized;

namespace ConsoleApp;
class SideTask
{
    int count;
    public SideTask(int count)
    {
        this.count = count;
    }
    public void KeepAlive()
    {
        try
        {
            while(count>0)
            {
                Console.WriteLine($"{count--} left");
                Thread.Sleep(10);
            }
            Console.WriteLine("Count : 0");
        }
        catch(ThreadAbortException e)
        {
            Console.WriteLine(e);
            Thread.ResetAbort();
        }
        finally
        {
            Console.WriteLine("Clearing resources ...");
        }
    }
}
class MainApp
{ 
    
    static void Main(string[] args)
    {
        SideTask task = new SideTask(100);
        Thread t1 = new Thread(new ThreadStart(task.KeepAlive));
        t1.IsBackground = false;

        Console.WriteLine("Starting Thread");
        t1.Start();
        Thread.Sleep(100);
        Console.WriteLine("AbortingThread...");
        t1.Abort();

        Console.WriteLine("Wait until thread stops ...");
        t1.Join();
        Console.WriteLine("Finished");
    }
}

[ 스레드의 일생과 상태 변화 ]

상태 설명
Unstarted 스레드 객체를 생성한 후 Thread.Start()메소드가 호출되기 전의 상태
Running 스레드가 시작하여 동작중인 상태 . Unstarted 상태의 스레드를 Thread.Start()를 통해 이 상태로 만든다 .
Suspended 스레드의 일시 중단 상태 . 스레드를 Thread.Suspend()메서드로 이상태를 만들며 ,Suspended 상태의 스레드는 Thread.Resume()메서드로 다시 Running 상태로 만들 수 있다 .
WaitSleepJoin 스레드가 블록된 상태를 나타냄 .Block이 아닌 이유는 스레드에 대해 Monitor.Wait() / Thread.Sleep() / Thread.Join() 메소드를 호출하면 이 상태가 되기 때문이다 .
Aborted 스레드가 취소된 상태 . Aborted 상태가 된 스레드는 Stopped 상태로 전환 , 완전히 중지된다 .
Stopped 중지된 스레드의 상태 . Abort 메소드를 호출 / 스레드가 실행중인 메소드가 종료되면 이상태가 된다 .
Background 스레드가 백그라운드로 동작되고 있음을 나타냄 . 포어그라운드 스레드는 하나라도 살아 있는 한 프로세스가 죽지 않지만 , 백그라운드는 열개가 살아도 프로세스가 죽고 사는것에 영향미치지 않는다 . 프로세스가 죽으면 백그라운드 스레드들도 모두 죽는다 . Thread.IsBackground 속성에 True 값을 입력함으로써 스레드를 이 상태로 바꾼다 .

- 스레드의 상태변화에는 규칙이 있다 .

- Aborted 상태의 스레드는 절대 Running로 전이되지 않고 , Running 상태의 스레드는 Unstarted 상태로 바뀌지 않는다 .

- 또한 ThreadState는 Flags 애트리뷰트를 가지고 있다 .

- Flags 어트리뷰트는 자신이 수식하는 열거형을 비트필드(플래그 집합)으로 처리 할 수 있다 .

[Flags]
enum MyENum{
	Apple=1<<0,
    Orage=1<<1,
    Kiwi=1<<<2,
    Mango=1<<3
}

Console.WriteLine((MyEnum)1|4);	//Apple / Kiwi 열거형 요소의 집합으로 구성되는 값 표현 가능

 

- 그렇기에 스레드는 둘 이상의 상태일 수 있다 .

- ex ) Suspended면서 WaitSleepJoin / Backgroud면서 Stopped 상태 등 ..

상태 10진수 2진수
Running 0 000000000
StopRequested 1 000000001
SuspendRequest 2 000000010
Background 4 000000100
Unstarted 8 000001000
Stopped 16 000010000
WaitSleepJoin 32 000100000
Suspended 64 001000000
AbortRequested 128 010000000
Aborted 256 100000000

- Thread 객체의 ThreadState 필드를 통해 상태 확인시 반드시 비트 연산을 이용해야 한다 .

if(t1.ThreadState&ThreadState.Aborted==Thread.Aborted)
//스레드 정지
else if(t1.ThreadState&ThreadState.Stopped == ThreadState.Stopped)
//스레드 취소

- 위의 경우 ThreadState에 Aborted가 포함되어 있다면 참이다 . 즉 , 복수의 상태중 Aborted가 포함되어 있다면 참인것 .


[ 인터럽트 : 스레드를 임의로 종료하는 다른 방법 ]

- Abort()메소드는 강제로 중단되더라도 프로세스 자신이나 시스템에 영향을 받지 않는 작업에 한해 사용하는게 좋다 .

- Thread.Interput()는 스레드가 동작중인 상태를 피해 WaitJoinSleep 상태에 들어갈때 중지시킨다 .

- 최소한 코드가 절대로 중단되면 안되는 작업을 할때는 중단되지 않는다는 보장을 받을 수 있다 .

using System;
using System.Linq;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Collections.Generic;
using System.Threading;
using System.Collections.Specialized;

namespace ConsoleApp;
class SideTask
{
    int count;
    public SideTask(int count)
    {
        this.count = count;
    }
    public void KeepAlive()
    {
        try
        {
            Console.WriteLine("Running Thread isnt gonna be interrupted)");
            Thread.Sleep(100);

            while(count>0)
            {
                Console.WriteLine($"{count--} left");
                Thread.Sleep(10);
            }
            Console.WriteLine("Count : 0");
        }
        catch(ThreadInterruptedException e)
        {
            Console.WriteLine(e);
        }
        finally
        {
            Console.WriteLine("Clearing resources ...");
        }
    }
}
class MainApp
{ 
    
    static void Main(string[] args)
    {
        SideTask task = new SideTask(100);
        Thread t1 = new Thread(new ThreadStart(task.KeepAlive));
        t1.IsBackground = false;

        Console.WriteLine("Starting Thread");
        t1.Start();
        Thread.Sleep(100);
        Console.WriteLine("Interruot thread...");
        t1.Interrupt();

        Console.WriteLine("Wait until thread stops ...");
        t1.Join();
        Console.WriteLine("Finished");
    }
}

 


[ 스레드 간 동기화 ]

[ 스레드 간 동기화 ]

- 각 스레드는 여러 자원을 공유하는 경우가 많다 . ex) 파일 핸들 / 네트워크 커넥션 / 메모리에 선언한 변수

- 스레드들이 순서를 갖춰 자원을 사용하게 하는 것을 동기화라고 한다 .

- 동기화가 잘 이루어져야 완벽한 멀티 스레드 프로그래밍을 하는 길이다 .

- 자원을 한 번에 하나의 스레드가 사용하도록 보장하기 위해 .Net은 lock 키워드와 Monitor 클래스를 제공한다 .


[ lock 키워드로 동기화 하기 ]

class Counter
{
	public int count=0;
    private readonly object thisLock=new Object();
    
    public void Increase()
    {
    	//크리티컬 세션 . 다른 스레드는 절대 이 코드를 실행 못한다 .
    	lock(thisLock)
        {
        	count = count+1;
        }
    }
}

- 크리티컬 섹션은 한 번에 한 스레드만 사용가능한 코드영역을 말한다.

- C#에서는 lock 키워드로 감싼다면 크리티컬 섹션으로 바꿀 수 있다 .


[ lock 키워드의 주의점 ]

- 스레드는 목수 / 스레드들이 실행하는 코드는 공방 / lock 키워드의 매개변수는 망치라 가정하자 .

- 목수들은 필수자원인 망치를 챙겨야 하지만 망치가 하나라면 , 자기만의 공방이 있음에도 망치 하나로 기다려야 한다 .

- lock 키워드를 만나 크리티컬 세션을 생성할때 (스레드는 lock 을 얻어야 크리티컬 섹션을 생성함) 다른 스레드들이 락을 달라고 아우성치며 대기하는 상황이 발생한다 .

- 이 경우 성능저하가 있기에 스레드의 동기화를 설계할 때는 크리티컬 세션을 반드시 필요한 곳에만 사용해야 한다 .

- lock 키워드의 매개변수로 사용하는 객체는 참조형이면 어느것이나 쓸 수 있지만 다음 키워드는 사용하지 말아야 한다 .

  • this : 클래스의 인스턴스는 클래스 내부/외부에서도 자주 사용된다 . lock(this)는 나쁜 버릇이다 .
  • Type 형식 : typeof 연산자 / GetType()메소드는 type 형식의 인스턴스를 반환한다 .즉 ,코드의 어느곳에서나 특정 형식에 대한 type 객체를 얻을 수 있다 . lock(typeof(SomeClass)) / lock(obj.GetType())은 피하자 .
  • string 형식 : "abc"는 어느 코드에서든 얻을수있는 string 객체이다 .

위를 사용시 문법적으로는 문제가 없어 컴파일 검사를 통과하지만 , 다른 자원에 대해 동기화해야 하는 스레드도 예기치않게 대기하는 상황을 만들기 때문에 지양해야 한다 .

using System;
using System.Linq;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Collections.Generic;
using System.Threading;
using System.Collections.Specialized;

namespace ConsoleApp;
class Counter
{
    const int LOOP_COUNT = 1000;

    readonly object thisLock;

    int count;
    public int Count
    {
        get { return count; }
    }
    public Counter()
    {
        thisLock = new object();
        count = 0;
    }
    public void InCrease()
    {
        int loopCount = LOOP_COUNT;
        while(loopCount-- >0)
        {
            lock(thisLock)
            {
                count++;
            }
            Thread.Sleep(1);
        }
    }
    public void DeCrease()
    {
        int loopCount = LOOP_COUNT;
        while (loopCount-- > 0)
        {
            lock (thisLock)
            {
                count--;
            }
            Thread.Sleep(1);
        }
    }
}
class MainApp
{ 
    
    static void Main(string[] args)
    {
        Counter counter = new Counter();
        Thread incThread = new Thread(new ThreadStart(counter.InCrease));
        Thread decThread = new Thread(new ThreadStart(counter.DeCrease));
        incThread.Start();
        decThread.Start();
        incThread.Join();
        decThread.Join();
        Console.Write(counter.Count);

    }
}

[ Monitor 클래스로 동기화 하기 ]

//lock
lock (thisLock)
{
    count--;
}
Thread.Sleep(1);

//Monitor 클래스
Monitor.Enter(thisLock);
try
{
    count--;
}
finally
{
    Monitor.Exit(thisLock);
}
Thread.Sleep(1);

- Monitor.Enter()와 Monitor.Exit()는 lock 키워드와 완전히 똑같은 기능을 한다 . 

- lock 키워드는 Monitor 클래스의 메소드를 바탕으로 구현되었기에 , Monitor.Enter()/Exit()로 동기화 한다면 lock을 쓰는게 좋다 .


[ Monitor.Wait() / Monitor.Pulse()로 하는 저수준 동기화 ]

- Monitor.Wait() / Pulse()는 lock 키워드만 사용할 때보다 더 섬세하게 멀티 스레드간 동기화를 가능하게 한다 .

- 위 메서드는 반드시 lock 블록안에서 호출해야 한다 .

  1.  Wait()메서드는 스레드를 WaitSleepJoin 상태로 만든다 .
  2. 해당 스레드는 동기화를 위해 갖고 있던 lock을 내려놓고 Waiting Queue에 입력 , 다른 스레드가 락을 얻는다 .
  3. 작업을 수행하던 스레드가 일을 마치고 Pulse를 호출하면 Qaiting Queue에 있던 스레드는 Ready Queue에 입력된다 
  4. 차례에 따라 lock을 얻어 Running  상태로 들어간다 .

- Thread.Sleep 메서드도 WaitSleepJoin상태로 만들지만 Monitor.Pulse에 의해 깨어나지 못한다 .

- 다시 Running 상태로 돌아가려면 매개변수에 입력된 시간의 경과 / 인터럽트 예외를 받아야 한다 .

- Monitor.Wait()는 Monitor.Pulse가 호출되면 바로 깨어난다 .

- 이때문에 멀티 스레드 애플리케이션의 성능 향상을 위해 Monitor.Wait()/Pulse()를 사용하는 것이다 .

using System;
using System.Linq;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Collections.Generic;
using System.Threading;
using System.Collections.Specialized;

namespace ConsoleApp;
class Counter
{
    const int LOOP_COUNT = 1000;
    //1 . 동기화 객체 필드를 선언
    readonly object thisLock;
    //2 . 스레드를 WaitSleepJoin 상태로 바꿔 블록할 조건을 결정할 필드 선언
    bool lockedCount = false;
    // 각 스레드가 너무 오랫동안 count 변수를 혼자 사용하는 것을 막는다 .
    private int count;
    public int Count
    {
        get { return count; }
    }
    public Counter()
    {
        thisLock = new object();
        count = 0;
    }
    public void Increase()
    {
        int loopCount = LOOP_COUNT;
        while(loopCount-- > 0)
        {
            //스레드를 블록하고 싶은 곳에서 다음과 같이 lock 블록안 필드를 검사하여 Wait()호출
            lock(thisLock)
            {
                while(count>0||lockedCount==true)
                    Monitor.Wait(thisLock);

                //블록된 스레드가 깨어나면 작업을 수행 ㅣtrue 로 변경시 블록이 걸려 같은 코드를 실행 불가

                lockedCount = true;
                count++;
                lockedCount = false;
                //WaitingQueue에 대기하고 있던 다른 스레드가 깨어나 false로 바뀐 lockedCount를 보고 작업을 수행할것이다 .
                Monitor.Pulse(thisLock);
            }
        }
    }
    public void Decrease()
    {
        int loopCount = LOOP_COUNT;
        while (loopCount-- > 0)
        {
            lock (thisLock)
            {
                while (count < 0 || lockedCount == true)
                    Monitor.Wait(thisLock);

                lockedCount = true;
                count--;
                lockedCount = false;

                Monitor.Pulse(thisLock);
            }
        }
    }
}
class MainApp
{ 
    
    static void Main(string[] args)
    {
        Counter counter = new Counter();

        Thread incThread = new Thread(new ThreadStart(counter.Increase));
        Thread decThread = new Thread(new ThreadStart(counter.Decrease));

        incThread.Start();
        decThread.Start();  
        incThread.Join();
        decThread.Join();
        Console.WriteLine(counter.Count);
    }
}

- 즉 , Increase가 실행시 Decrease 가 블록 되며 Increase의 실행 이후 Pulse 메서드로 Decrease 가 실행 , Increase는 블록의 과정을 거치는 것이다 .


[ Task와 Task<TResult> 그리고 Parallel ]

[ 도입 배경 ]

- 멀티코어 CPU에서 고성능 소프트웨어를 만들려면 여러 코어가 동시에 작업하는 병렬 처리/비동기 처리기법이 필수다.

  • 병렬 처리 : 하나의 작업을 여러 작업자가 나워서 수행후 다시 하나의 결과로 만드는 것
  • 비동기 : 작업A를 시작한 후 A의 결과가 나올 때까지 대기하는 대신 다른 작업 B,C를 수행하다 A가 끝나면 그때 결과를 받아내는 처리 방식

[ System.Threading.Tasks.Task 클래스 ]

  • 동기 코드 : 메소드를 호출한 뒤 이 메소드의 실행이 완전히 끝나야(반환되어야만) 다음 메소드를 호출할 수 있다.
  • 비동기 코드 : Shoot And Forget . 메소드 호출후 메소드의 종료를 기다리지 않고 바로 다음코드를 실행 .
Action someAction = ()=>
{
	Thread.Sleep(1000);
    Console.WriteLine("Printed asynchronously");
};

//Task 클래스는 인스턴스 생성시 Action 대리자를 받음
// 반환형을 갖지 않는 메소드와 익명 메소드 , 무명 함수등을 받는다 .
Task myTask = new Task(someAction);
myTask.Start();

Console.WriteLine("Printed syncronously");

//비동기 호출이 완료될때까지 기다린다
myTask.Wait();

//결과는 ..
Printed syncronously
Printed asynchronously

 

var myTask = Task.Run(()=>
	{
    	Thread.Sleep(1000);
        Console.WriteLine("~~~");
    }

- Task.Run()메소드를 이용하는게 일반적인 방법 . 생성과 시작을 단번에 한다 .

using System;
using System.Linq;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Collections.Generic;
using System.Threading;
using System.Collections.Specialized;
using System.Threading.Tasks;

namespace ConsoleApp;

class MainApp
{ 
    
    static void Main(string[] args)
    {
        string srcFile = args[0];

        Action<object> FileCopyAction = (object state) =>
        {
            String[] paths = (String[])state;
            File.Copy(paths[0], paths[1]);

            Console.WriteLine("TaskID:{0} , ThreadID{1},{2} was copied to {3}"
                , Task.CurrentId, Thread.CurrentThread.ManagedThreadId, paths[0], paths[1]);
        };

        Task t1 = new Task(FileCopyAction, new string[] { srcFile, srcFile + ".copy1" });

        Task t2 = Task.Run(() =>
        {
            FileCopyAction( new string[] { srcFile, srcFile + ".copy2" });
        });

        t1.Start();

        //동기
        Task t3 = new Task(FileCopyAction, new string[] { srcFile, srcFile + ".copy3" });
        t3.RunSynchronously();

        t1.Wait();
        t2.Wait();
        t3.Wait();

    }
}

[ 코드의 비동기 실행 결과를 주는 Task<TResult>클래스 ]

- 코드의 비동기 실행 결과를 손쉽게 얻게 해준다 .

- Task<TRresult>클래스는 코드의 비동기 실행결과를 손쉽게 취합하게 한다 .

- myTask.Result프로퍼티는 비동기 작업이 끝나야 반환되므로 myTask.Wait는 호출하지 않아도 되지만 Task 클래스 사용시 Wait를 토출하는 습관을 들이기 위해 항상 호출하는게 좋다 .

- 아래 소수 찾기 프로그램은 m개의 CPU가 있는 시스템에서 m개의 Task가 n개의 수를 m의 범위로 나누고 소수를 찾는다면 더 빠를것이다 .

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ConsoleApp;

class MainApp
{ 
    static bool IsPrime(long number)
    {
        //1은 소수가 아니다
        if(number<2)
            return false;
        //2가 아닌수가 2로 나눠지면 소수가 아니다
        if (number%2==0&&number!=2)
            return false;
        //자기자신을 제외한 아래수들로 나눠지면 소수가 아니다
        for(long i =2;i<number;i++)
        {
            if(number%i==0)
                return false;
        }

        return true;
    }

    static void Main(string[] args)
    {
        long from = Convert.ToInt64(args[0]);
        long to = Convert.ToInt64(args[1]);
        int taskCount = Convert.ToInt32(args[2]);

        //Func 설정
        Func<object, List<long>> FindPrimeFunc =
            (objRange) =>
            {
                long[] range = (long[])objRange;
                List<long> found = new List<long>();

                for (long i = range[0]; i < range[1]; i++)
                {
                    if (IsPrime(i))
                        found.Add(i);
                }
                return found;
            };

        Task<List<long>>[] tasks = new Task<List<long>>[taskCount];
        long currentFrom = from;
        long currentTo = to / tasks.Length;

        //Task 범위 설정
        for (int i = 0; i < tasks.Length; i++)
        {
            Console.WriteLine("Task[{0}] : {1} ~ {2}", i, currentFrom, currentTo);

            tasks[i] = new Task<List<long>>(FindPrimeFunc, new long[] { currentFrom, currentTo });
            currentFrom = currentTo + 1;

            if (i == tasks.Length - 2)
                currentTo = to;
            else
                currentTo = currentTo + (to / tasks.Length);
        }

        Console.WriteLine("Please press enter to start ...");
        Console.ReadLine();
        Console.WriteLine("Started");

        DateTime startTime = DateTime.Now;

        foreach (Task<List<long>> task in tasks)
            task.Start();

        List<long> total = new List<long>();

        foreach (Task<List<long>> task in tasks)
        {
            task.Wait();
            total.AddRange(task.Result.ToArray());
        }
        DateTime endTime = DateTime.Now;
        TimeSpan elapsed = endTime - startTime;

        Console.WriteLine("Primte number count between {0}and {1} : {2}", from, to, total.Count);


        Console.WriteLine("Elapsed Time : {0}", elapsed);
    }
}

[ 손쉬운 병렬 처리를 가능케 하는 Parallel 클래스 ]

void SomeMethod(int i)
{
	//
}

Pararrel.For(0,100,SomeMethod);

- Parallel 클래스는 For() / Foreach() 등의 메소드를 제공함으로써 병렬처리를 더 쉽게 구현하게 해준다 .

- 위 코드에서 메소드를 병렬로 호출하면서 0부터 100사이의 정수를 메소드의 인수로 넘긴다 .

- 몇개의 스레드를 사용할지는 Parallel 클래스가 내부적으로 판단 , 최적화 한다 .

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;

namespace ConsoleApp;

class MainApp
{ 
    static bool IsPrime(long number)
    {
        //1은 소수가 아니다
        if(number<2)
            return false;
        //2가 아닌수가 2로 나눠지면 소수가 아니다
        if (number%2==0&&number!=2)
            return false;
        //자기자신을 제외한 아래수들로 나눠지면 소수가 아니다
        for(long i =2;i<number;i++)
        {
            if(number%i==0)
                return false;
        }

        return true;
    }

    static void Main(string[] args)
    {
        long from = Convert.ToInt64(args[0]);
        long to = Convert.ToInt64(args[1]);

        Console.WriteLine("Please press enter to start ...");
        Console.ReadLine();
        Console.WriteLine("Started");

        DateTime startTime = DateTime.Now;
        List<long> total = new List<long>();

        Parallel.For(from, to, (long i) =>
        {
            if (IsPrime(i))
                lock(total)
                    total.Add(i);
        });

        DateTime endTime = DateTime.Now;
        TimeSpan elapsed = endTime - startTime;

        Console.WriteLine("Primte number count between {0}and {1} : {2}", from, to, total.Count);


        Console.WriteLine("Elapsed Time : {0}", elapsed);
    }
}

[ async 한정자와 await 연산자로 만드는 비동기 코드 ]

[ 도입 배경 ]

- C# 5.0 이전의 BeginInvoke / EndInvoke 메소드를 이용한 비동기 코드 패턴은 사용이 까다롭기에 사용이 드물었다 .

- async 한정자는 메소드/이벤트 처리기/태스트/람다식 등을 수식함으로써 C# 컴파일러가 이들을 호출하는 코드를 만날때

호출 결과를 기다리지 않고 바로 다음 코드로 이동하게 실행코드를 생성하게 한다 .

- async는 한정자로 메소드/태스크를 수식하면 비동기 코드가 만들어진다 .

- 다만 실행후 잊어버릴 작업이라면 void로 ,작업 완료시까지 기다리는 메소드는 반환형식이 Task / Task<Result>여야 한다

- 컴파일러는 Task/Task<Result>반환 형식의 메소드를 async가 한정할때 await 연산자가 해당 메소드 어디에 위치하는지 찾는다 .

- 찾으면 그곳에서 호출자에게 제어를 돌려주도록 실행파일을 만든다 .

- 찾지 못하면 호출자에게 제어를 돌려주지 않기에 메소드/테스크는 동기적으로 실행됨

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;

namespace ConsoleApp;

class MainApp
{ 
    async static private void MyMethodAsync(int count)
    {
        Console.WriteLine("C");
        Console.WriteLine("D");

        await Task.Run(async () =>
        {
            for(int i=1;i<=count;i++)
            {
                Console.WriteLine($"{i}/{count}...");
                //Thread.Sleep의 비동기 버전
                await Task.Delay(100);
            }
        });

        Console.WriteLine("G");
        Console.WriteLine("H");
    }
    static void Caller()
    {
        Console.WriteLine("A");
        Console.WriteLine("B");
        MyMethodAsync(3);
        Console.WriteLine("E");
        Console.WriteLine("F");
    }
    static void Main(string[] args)
    {
        Caller();
        Console.ReadLine();
    }
}

 

- Task.Delay() 함수가 하는 일은 인수로 입력받은 시간이 지나면 Task 객체를 반환한다 .

- Task.Delay()는 Thread.Sleep()과 중요한 차이가 있는데

- Thread.Sleep()은 스레드 전체를 블록시키지만 , Task.Delay()는 스레드를 블록하지 않는다 .

- 사용자 인터페이스 스레드 안에서 Thread.Sleep()을 호출하면 UI 가 Sleep()이 반환되기전까지 사용자에게 응답하지 않지만 , Task.Delay를 사용하면 메소드 반환여부와 상관없이 동작한다 .


[ .NET이 제공하는 비동기 API 맛보기 ]

- 기존 API와 더불어 비동기 버전 API를 새롭게 제공하도록 업그레이드 했다 .

- .NET 클래스 라이브러리 곳곳에 추가된 ~Async()메소드가 그것들이다 .

- System.IO.Stream 클래스가 제공하는 ReadAsync / WriteAsync를 알아보자 .

- 동기 버전과 비동기 버전의 차이는 비동기 버전은 메소드 실행중 사용자인터페이스에 접근하는데 문제가 없다는 것이다

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;

namespace ConsoleApp;

class MainApp
{ 
    //파일 복사 후 복사한 파일 용량 반환
    static async Task<long>CopyAsync(string FromPath,string ToPath)
    {
        using (
            var fromStream = new FileStream(FromPath, FileMode.Open))
        {
            long totalCopied = 0;

            using(
                var toStream = new FileStream(ToPath, FileMode.Create))
            {
                byte[] buffer = new byte[1024];
                int nRead = 0;
                while((nRead=await fromStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
                {
                    await toStream.WriteAsync(buffer,0,nRead);
                    totalCopied += nRead;
                }
            }
            return totalCopied;
        }
    }
    static async void DoCopy(string FromPath,string ToPath)
    {
        long totalCopied = await CopyAsync(FromPath, ToPath);
        Console.WriteLine($"Copied Total {totalCopied} bytes");
    }
   
    static void Main(string[] args)
    {
        if(args.Length<2)
        {
            Console.WriteLine("Usage : AsyncFileIO <source> <Destination>");
            return;
        }
        DoCopy(args[0], args[1]);
        Console.ReadLine();
        
    }
}