- 프로세스와 스레드
- 스레드 시작하기
- 스레드 임의로 종료시키기
- 스레드 일생과 상태 변화
- 인터럽트
- System.Threading.Tasks.Task 클래스
- Task<TResult>클래스
- Parallel 클래스
- async 한정자와 await 연산자로 만드는 비동기 코드
[ 프로세스와 스레드 ]
[ 프로세스 ]
- 프로세스란 실행파일이 실행되어 메모리에 적재된 인스턴스
- 즉 , 실행파일에 담겨있는 데이터와 코드가 메모리에 적재되어 동작하는 것이 프로세스
- 프로세스는 하나 이상의 스레드로 구성된다 .
- 스레드는 운영체제가 CPU 시간을 할당하는 기본 단위
[ 멀티 스레드의 장점 ]
- 응답성을 높일 수 있다 . ex ) 파일 복사 - 사용자 대화 두개의 스레드가 있다면 파일복사를 하는 동안 사용자 대화가 가능하다 .
- 자원 공유가 쉽다 . ex ) 멀티 프로세스에 비해 멀티 스레드가 자원공유가 쉽다 . 멀티 프로세스는 서버용 애플리케이션에서 많이 취하는 구조로 자원 공유시 소켓/공유 메모리 같은 IPC 방식을 이용한다 .반면 멀티 스레드는 스레드끼리 코드내 변수를 같이 사용하는 것만으로 데이터 교환이 가능하다 .
- 경제적이다 . ex ) 프로세스를 띄우기 위해 메모리와 자원을 할당하는 작업은 비용이 비싼데 , 스레드를 띄울때는 이미 프로세스에 할당된 메모리와 자원을 그대로 쓰기에 메모리와 자원을 할당하는 비용을 지불하지 않는다 .
[ 멀티 스레드의 단점 ]
- 구현이 복잡하다 ex ) 구현하기 까다롭고 테스트 역시 쉽지 않다 . 멀티 스레드 기반 소프트 웨어 디버깅 역시 어렵다 .
- 소프트 웨어 안정성을 저해한다 ex ) 멀티 프로세스 기반의 소프트웨어는 자식 프로세서중 하나가 죽어도 그 이상의 영향은 없지만 , 멀티 스레드는 하나라도 죽는다면 전체 프로세스에 영향을 미친다 .
- 과용시 성능저하 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 블록안에서 호출해야 한다 .
- Wait()메서드는 스레드를 WaitSleepJoin 상태로 만든다 .
- 해당 스레드는 동기화를 위해 갖고 있던 lock을 내려놓고 Waiting Queue에 입력 , 다른 스레드가 락을 얻는다 .
- 작업을 수행하던 스레드가 일을 마치고 Pulse를 호출하면 Qaiting Queue에 있던 스레드는 Ready Queue에 입력된다
- 차례에 따라 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();
}
}
'C# > 이것이 C#이다' 카테고리의 다른 글
[ 이것이 C#이다 ] Chapter 22. 가비지 컬렉션 (0) | 2024.04.22 |
---|---|
[ 이것이 C#이다 ] Chapter 20. WinForm으로 만드는 사용자 인터페이스 (0) | 2024.04.16 |
[ 이것이 C#이다 ] Chapter 18. 파일 다루기 (0) | 2024.04.11 |
[ 이것이 C#이다 ] Chapter 16 . 리플렉션과 애트리뷰트 (0) | 2024.04.02 |
[ 이것이 C#이다 ] Chapter 15 . LINQ (0) | 2024.03.27 |