1. "시작하세요. C# 프로그래밍" 책에서 P416 ~ P449 스터디하시고 정리해 주세요.
스레딩
***스레드(thread)***는 명령어를 실행하기 위한 스케줄링 단위이며, 프로세스 내부에서 생성할 수 있다.
윈도우 운영체제는 프로세스를 생성할 때 기본적으로 한 개의 스레드를 함께 생성하며, 이를 주 스레드(main thread, primary thread)라고 한다.
스레드는 CPU의 명령어 실행과 관련된 정보를 보관하고 있는데, 이를 스레드 문맥(thread context)이라 한다.
운영체제 스케줄러의 스케줄링 알고리즘에 따라 스레드의 문맥 교환(context switching)이 발생한다.
단일 스레딩의 경우 스레드가 작업 중에는 다른 작업을 할 수 없다. 멀티 스레딩은 스레드가 동시에 동작하기 때문에 다중 작업이 가능하다.
System.Threading.Thread
프로그램이 실행되면 주 스레드가 하나 실행되고 새로 생성된 스레드를 포함한 모든 스레드가 종료되어야 프로그램이 종료된다.
- 전경 스레드(foreground thread) : 프로그램 실행 종료에 영향을 미치는 스레드
- 배경 스레드(background thread) : 프로그램 실행 종료에 영향을 미치지 않는 스레드
// 스레드 생성, 시작, 대기
class Program
{
static void Main(string[] args)
{
Thread t = new Thread(threadFunc); // 스레드 생성
t.IsBackground = true; // 배경 스레드로 변경
t.Start(); // t 스레드 시작
t.Join(); // t 스레드가 종료할 때 까지 현재 스레드를 무한 대기
Console.WriteLine("주 스레드 종료");
}
static void threadFunc()
{
Console.WriteLine("60초 후에 프로그램 종료");
Thread.Sleep(1000 * 60); // 60초 동안 실행 중지
Console.WriteLine("스레드 종료!");
}
}
// 60초 후에 프로그램 종료
// 스레드 종료!
// 주 스레드 종료!
System.Threading.Monitor
멀티 스레딩 환경에서 여러 스레드가 동시에 실행되는 경우 여러 스레드에서 공유 리소스(shared resource)에 접근이 자유로우므로 예측할 수 없는 동작이 발생할 수 있다. 따라서 공유 리소스에 대한 스레드 동기화(synchronization) 처리가 필요하다. BCL에서 스레드 동기화를 위한 Monitor 클래스를 제공한다.
메서드도 여러 스레드에서 동시 접근이 가능하므로 동기화가 필요하다. BCL의 모든 정적 멤버는 다중 스레드 접근에 안전하지만, 인스턴스 멤버는 안전하지 않기 때문에, 동기화가 필요할 때는 직접 동기화 처리를 해야 한다.
class Program
{
int number = 0;
static void Main(string[] args)
{
Thread t = new Thread(threadFunc);
t.Start();
}
static void threadFunc(object inst)
{
Program pg = inst as Program;
for (int i = 0; i < 100000; i++)
{
// Monitor 클래스를 이용한 동기화
Monitor.Enter(pg);
try
{
pg.number = pg.number + 1;
}
finally
{
Monitor.Exit(pg);
}
// lock 예약어를 이용한 동기화 간편 표기
lock (pg)
{
pg.number = pg.number + 1;
}
}
}
}
System.Threading.Interlocked
다중 스레드의 공유자원을 사용하는 몇몇 패턴에 대해서 명시적인 동기화 작업을 단순하게 만드는 정적 메서드를 제공한다.
Interlocked
타입의 정적 메서드로 제공되는 연산의 단위를 원자적 연산(atomic operation) 이라고 하는데, 더 이상 쪼갤 수 없는 연산 단위로 이해할 수 있다.
Interlocked 정적 메서드
-
CompareExchange : 두 대상을 비교하여 값이 같으면 지정된 값을 설정하고, 그렇지 않으면 연산을 수행하지 않는다.
-
Decrement : 지정된 변수의 값을 감소시키고 저장한다.
-
Exchange : 변수를 지정된 값으로 설정한다.
-
Increment : 지정된 변수의 값을 증가시키고 저장한다.
System.Threading.ThreadPool
임시적인 목적으로 스레드를 사용할 수 있는 스레드 풀. 스레드를 자주 생성해서 사용하는 경우 Thread 객체를 생성하기보다는 TheadPool로 부터 재사용했을 때 더 나은 성능을 보인다.
TheadPool.QueueUserWorkItem(threadFunc, data);
System.Threading.EventWaitHandle
Monitor 타입처럼 스레드 동기화 수단의 하나다.
이벤트 객체는 Signal과 Non-Signal 상태를 갖는다.
이벤트 객체의 상태는 Set
, Reset
메서드로 전환한다.
WaitOne을 통해 이벤트 객체의 상태를 판단하여 제어를 반환하거나 대기한다.
이벤트는 수동 리셋(manual reset)과 자동 리셋(auto reset)으로 나뉜다.
- 수동 리셋(manual reset) :
Set
메서드를 호출하여 Signal 상태가 된 후에 계속 Signal 상태로 머무는 이벤트.Reset
메서드를 호출하여 Non-Signal 상태로 돌릴 수 있다. - 자동 리셋(auto reset) :
Set
메서드를 호출하여 Signal 상태가 된 후에 바로 Non-Signal 상태로 자동으로 변경되는 이벤트.
비동기 호출(Asynchronous Call)
비동기 호출은 동기 호출(Synchronous Call)에서 발생하는 스레드 점유 문제를 해결하기 위해 제공된다.
C#에서는 스트림 객체의 비동기 호출을 위해 Read/Write 메서드에 대해 각각 BeginRead
/EndRead
, BeginWrite
/EndWrite
메서드를 쌍으로 제공한다.
System.Delegate의 비동기 호출
C#에서는 입출력 장치뿐만 아니라 일반 메서드에 대해서도 델리게이트를 통한 비동기 호출을 제공한다.
델리게이트의 비동기 호출을 위한 메서드 BeginInvoke
/EndInvoke
를 사용하면 ThreadPool을 이용한 비동기 호출을 사용할 수 있다.
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;
}
}
class Program
{
public delegate long CalcMethod(int start, int end);
static void Main(string[] args)
{
CalcMethod calc = new CalcMethod(Calc.Cumsum);
IAsyncResult ar = calc.BeginInvoke(1, 100, null, null);
ar.AsyncWaitHandle.WaitOne();
long result = calc.EndInvoke(ar);
Console.WriteLine(result);
}
}
2. "시작하세요. C# 프로그래밍" 책에서 P651 ~ P667 스터디하시고 정리해 주세요.
비동기 호출
비동기 호출은 별도의 thread를 만들고 callback을 전달하고 thread의 종료 시점을 대기해서 다음 작업을 진행해야 하는 등의 복잡한 흐름으로 코드를 작성하기가 어려운 부분이 있었다.
그래서 C# 5.0에서는 비동기 호출을 동기 방식처럼 호출하는 코드를 작성할 수 있도록 async
/await
예약어 제공한다.
async
: 메서드 선언시 사용되는 예약어로 해당 메서드를 비동기로 실행하라는 의미다.
await
: 비동기 메서드에서만 인식되며 ~Async류의 비동기 호출이 끝날때 까지 대기한다.
class Program
{
static void Main(string[] args)
{
AwaitRead();
Console.ReadLine();
}
private static async void AwaitRead()
{
using (FileStream fs = new FileStream(@"C:\windows\system32\drivers\etc\HOSTS", FileMode.Open))
{
byte[] buf = new byte[fs.Length];
await fs.ReadAsync(buf, 0, buf.Length);
// ReadAsync 메서드가 완료된 후에 아래 코드가 호출된다.
string txt = Encoding.UTF8.GetString(buf);
Console.WriteLine(txt);
}
}
}
닷넷 4.5 BCL에 추가된 Async 메서드
async
/await
의 도입으로 BCL 라이브러리에 제공되던 복잡한 비동기 처리에 비동기 호출이 가능한 메서드들이 추가됐다.
class Program
{
static void Main(string[] args)
{
// WebClinet 타입
AwaitDownloadString();
Console.ReadLine();
// TCP 통신
TcpListener listener = new TcpListener(IPAddress.Any, 11200);
listener.Start();
while (true)
{
var client = listener.AcceptTcpClient();
ProcessTcpClient(client);
}
}
private static async void AwaitDownloadString()
{
WebClient wc = new WebClient();
wc.Encoding = Encoding.UTF8;
string text = await wc.DownloadStringTaskAsync("http://www.naver.com");
Console.WriteLine(text);
}
private static async void ProcessTcpClient(TcpClient client)
{
NetworkStream ns = client.GetStream();
byte[] buffer = new byte[1024];
int received = await ns.ReadAsync(buffer, 0, buffer.Length);
string txt = Encoding.UTF8.GetString(buffer, 0, received);
byte[] sendBuffer = Encoding.UTF8.GetBytes("Hello:" + txt);
await ns.WriteAsync(sendBuffer, 0, sendBuffer.Length);
ns.Close();
}
}
이 밖에도 닷넷 4.5에는 I/O를 담당하는 Stream 기반 클래스를 비롯해 몇몇 타입에 기존 Begin/End 델리게이트로 구현된 비동기 메서드에 대응하는 Async 메서드가 추가됐다.
Task, Task 타입
Task는 닷넷 4.0에 추가된 병렬 처리 라이브러리(TPL:Task Parallel Library)에 속한 타입으로 비동기 작업을 위해 스레드 처럼 사용 가능하다.
class Program
{
static void Main(string[] args)
{
// 기존 ThreadPool QueueUserWorkItem 사용
ThreadPool.QueueUserWorkItem((obj) => { Console.WriteLine("process workitem"); }, null);
// 닷넷 4.0 Task 타입 사용
Task task1 = new Task(() => { Console.WriteLine("process taskitem"); });
task1.Start();
// Task 작업 완료 대기
Task TaskSleep = new Task(() => { Thread.Sleep(5000); });
TaskSleep.Start();
TaskSleep.Wait();
// 객체 생성없이 Task 실행
Task.Factory.StartNew(() => { Console.WriteLine("process taskitem"); });
// 값을 반환하는 Task 실행
Task<int> task = new Task<int>(() => { return new Random((int)DateTime.Now.Ticks).Next(); });
task.Start();
task.Wait();
Console.WriteLine("무작위 숫자 값: " + task.Result);
}
}
Async 메서드가 아닌 경우의 비동기 처리
Task는 Async 처리가 적용되지 않은 메서드와 사용자 정의 메서드를 비동기 호출하는 방법을 제공한다.
class Program
{
static void Main(string[] args)
{
AwaitFileRead(@"C:\windows\system32\drivers\etc\HOSTS");
Console.ReadLine();
}
private static async void AwaitFileRead(string filePath)
{
string fileText = await ReadAllTextAsync(filePath);
Console.WriteLine(fileText);
}
static Task<string> ReadAllTextAsync(string filePath)
{
return Task.Factory.StartNew(() => { return File.ReadAllText(filePath); });
}
}
비동기 호출의 병렬 처리
await
와 Task
의 조합으로 비동기 호출의 병렬 작업이 가능하다.
아래 예제는 1에서 1000사이의 난수를 만들고 평균을 계산하는 난수 생성기를 인스턴스화하여 병렬로 비동기 호출을 하는 예제이다.
public class Example
{
public static void Main()
{
var tasks = new List<Task<long>>(); // 난수 생성기에서 반환한 값을 저장할 리스트 생성
for (int ctr = 1; ctr <= 10; ctr++)
{
int delayInterval = 18 * ctr;
tasks.Add(Task.Run(async () => { // async 이용한 익명 함수를 생성하여 task를 수행
long total = 0;
await Task.Delay(delayInterval); // 랜덤 시드값을 위해 delay
var rnd = new Random();
for (int n = 1; n <= 1000; n++)
total += rnd.Next(0, 1000); // 난수 생성
return total;
}));
}
var continuation = Task.WhenAll(tasks); // Non-Blocking Call
long grandTotal = 0;
foreach (var result in continuation.Result)
{
grandTotal += result;
Console.WriteLine("Mean: {0:N2}, n = 1,000", result / 1000.0);
}
Console.WriteLine("\nMean of Means: {0:N2}, n = 10,000", grandTotal / 10000);
}
}
3. 아래의 EncClientManager 클래스의 SendRecvCommandTimeoutAsync() 메소드를 분석해 주세요.
Thread, Task, await 문법에 집중해서 분석해 주세요.
// Tuple<bool, int>을 반환하고 비동기로 수행될 메서드 선언
public async Task<Tuple<bool, int>> SendRecvCommandTimeoutAsync(string ipAddr, string cmd, int ctag, int timeoutMillis = AppConst.TimeoutDefaultResponse,
int sessionID = AppConst.PduSessionInitial)
{
TcpClient client = new TcpClient();
int calla_id = -1;
bool result = false;
try
{
// SendRecvCommandAsync, Task.Delay 수행
Task<int> cmdTask = SendRecvCommandAsync(client, ipAddr, cmd, ctag, sessionID);
Task timeoutTask = Task.Delay(timeoutMillis);
calla_id = -1;
// cmdTask, timeoutTask가 완료되면 비동기로 호출 될 completedTask 등록
result = await Task.Factory.ContinueWhenAny<bool>(new Task[] { cmdTask, timeoutTask }, (completedTask) =>
{
// cmdTask, timeoutTask 중 먼저 완료된 Task에 의해 아래 작업이 수행된다.
if (completedTask == timeoutTask)
{
/* timeout */
return false;
}
else if (completedTask == cmdTask)
{
calla_id = cmdTask.Result;
// 141203.nicew Calla가 Invalid한 경우 여기서 Close 시킨다.
int isValid = Calla.valid(calla_id);
if (isValid != 1)
{
Calla.close(calla_id);
}
return (isValid == 1);
}
else
{
/* just ignore : timeout */
return false;
}
});
}
catch (Exception /*ex*/)
{
//Trace.TraceInformation(ex.Message);
}
finally
{
client.Close();
}
return new Tuple<bool, int>(result, calla_id);
}
// int를 반환하고 비동기로 수행될 메서드 선언
public async Task<int> SendRecvCommandAsync(TcpClient client, string ipAddr, string cmd, int ctag, int sessionID)
{
// 생략
}