호출자 정보
class CallerInfo
{
public static void Main()
{
LogMessage("test");
}
//호출자 정보는 '호출하는 측의 정보'를 메서드의 인자로 전달하는 것이다.
//호출자 정보 특성이 명시된 매개변수는 선택적 매개변수 형식이여야 한다.
//컴파일 시점에 값이 치환되어 빌드된다.
static void LogMessage(string text,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
Console.WriteLine("텍스트 : " + text); //"test" 출력
Console.WriteLine("이 메서드를 호출하는 메서드 이름" + memberName); //Main 출력
Console.WriteLine("호출 파일 경로" + filePath); //파일 경로 출력
Console.WriteLine("메서드가 호출된 라인 번호" + lineNumber); //10 출력
}
}
비동기 호출
async/await 예약어
비동기 호출에 await 예약어가 함께 쓰이면 C# 컴파일러는 이를 인지하고 그 이후의 코드를 묶어서 비동기 호출이 끝난 다음에 실행되도록 코드를 변경해서 컴파일한다. async 예약어가 있으면 컴파일러는 await 예약어를 예약어로 인식한다. async 예약어가 없으면 그냥 식별자로 인식한다.
class AsyncAwaitTest
{
//async 예약어가 없이 await를 사용할 수 없다.
private static async void AwaitRead()
{
using (FileStream fs = new FileStream("test.log", FileMode.Open))
{
byte[] buffer = new byte[fs.Length];
Console.WriteLine("실행 전의 스레드 id : " + Thread.CurrentThread.ManagedThreadId);
await fs.ReadAsync(buffer, 0, buffer.Length);
Console.WriteLine("실행 후의 스레드 id : " + Thread.CurrentThread.ManagedThreadId);
//ReadAsync 이하의 처리는 스레드 풀 스레드가 담당하기 때문에 스레드 id가 다르게 나온다.
//이하의 라인은 ReadAsync 비동기 호출이 완료된 후 호출
string txt = Encoding.UTF8.GetString(buffer);
Console.WriteLine(txt);
}
}
public static void Main()
{
AwaitRead();
Console.WriteLine("메인 함수 스레드 id : " + Thread.CurrentThread.ManagedThreadId);
//이게 2번째로 실행된다.
Console.ReadLine();
}
}
class BCLAsyncTest //WebClient 비동기 호출 async/await 예제
{
public static void Main()
{
AwaitDownloadString();
Console.ReadLine();
}
private static async void AwaitDownloadString()
{
WebClient wc = new WebClient();
wc.Encoding = Encoding.UTF8;
//DownloadStringAsync보다 더 간편하게 이용 가능.
string text = await wc.DownloadStringTaskAsync("http://www.naver.com");
Console.WriteLine(text);
}
}
class BCLAsyncTest2 //TCP 서버 비동기 통신 예제
{
public static void Main()
{
TcpListener listener = new TcpListener(IPAddress.Any, 11200);
listener.Start();
while (true)
{
//연결 요청을 받아들인다.
var client = listener.AcceptTcpClient();
ProcessTcpClient(client);
}
}
//NetworkStream 클래스의 ReadAsync와 WriteAsync를 이용하면 간단하게 비동기 통신 구현 가능
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();
}
}
Task, Task<TResult> 타입
Async 메서드의 반환값은 모두 Task 또는 Task<TResult> 유형이다. Task 타입은 반환값이 없는 경우 사용되고, Task<TResult> 타입은 TResult 형식 매개변수로 지정된 반환값이 있는 경우로 구분된다.
class TaskType
{
public static void Main()
{
//스레드 풀에서
ThreadPool.QueueUserWorkItem((obj) => { Console.WriteLine("process workitem"); }, null);
Task task1 = new Task(() => { Console.WriteLine("process taskitem"); });
task1.Start();
Task task2 = new Task((obj) => { Console.WriteLine("process taskitem(obj)"); }, null);
task2.Start();
task1.Wait(); //task1의 작업이 끝날 때까지 현재 스레드를 대기한다.
task2.Wait();
//task 객체를 생성할 필요 없이 바로 작업 시작 가능.
Task.Factory.StartNew(() => { Console.WriteLine("process taskitem StartNew"); });
//Task<TResult> 타입은 값을 반환할 수 있다.
Task<int> task3 = new Task<int>(() =>
{
Random rand = new Random((int)DateTime.Now.Ticks);
return rand.Next();
}
);
task3.Start();
task3.Wait();
Console.WriteLine("무작위 숫자 값 : " + task3.Result);
//StartNew<TResult> 도 반환.
Task<int> taskReturn = Task.Factory.StartNew<int>(() => 255);
taskReturn.Wait();
Console.WriteLine(taskReturn.Result);
}
}
Async 메서드가 아닌 경우의 비동기 처리
class TaskAsync
{
//비동기로 처리할 ReadAllTextAsync라는 메서드를 만들어서 파일 경로를 넘겨준다.
private static async void AwaitFileRead(string filePath)
{
string fileText = await ReadAllTextAsync(filePath);
Console.WriteLine(fileText);
}
//Task를 이용하여 넘겨받은 파일 경로로 들어가 텍스트를 읽고 리턴한다.
static Task<string> ReadAllTextAsync(string filePath)
{
return Task.Factory.StartNew(() =>
{
return File.ReadAllText(filePath);
});
}
public static void Main()
{
string filePath = "test.log";
//비동기로 처리되므로 바로 다음 줄 실행
AwaitFileRead(filePath);
Console.ReadLine();
}
}
비동기 호출의 병렬 처리
//비동기 처리 담당 메서드
private static async void DoAsyncTask()
{
var task3 = Method3Async();
var task5 = Method5Async();
//task3, task5가 끝날 때까지 대기한다.
await Task.WhenAll(task3, task5);
//그 이후 실행
Console.WriteLine(task3.Result + task5.Result);
}
//3초 후 3리턴
private static Task<int> Method3Async()
{
return Task.Factory.StartNew( () =>
{
Thread.Sleep(3000);
return 3;
});
}
//5초 후 5리턴
private static Task<int> Method5Async()
{
return Task.Factory.StartNew(() =>
{
Thread.Sleep(5000);
return 5;
});
}
public static void Main()
{
DoAsyncTask();
Console.WriteLine("먼저 출력되고, 비동기 값 나중에 출력됨.");
Console.ReadLine();
}
}
확장메서드
Static 메서드를 인스턴스 메서드처럼 클래스명 없이 바로 호출할 수 있는 메서드이다. 메서드의 첫 번째 인자에 확장하려는 타입을 넣고 그 앞에 this 키워드를 붙이면 된다.
//현재 날짜로부터 n일을 뺀 날짜를 구해주는 메서드를 가진 헬퍼 클래스.
public static class DateTimeHelper
{
//기존 헬퍼 메서드
public static DateTime MinusDays(DateTime dt, int days)
{
DateTime d = dt.AddDays(-days);
return d;
}
//확장 메서드
//확장하려는 타입이 DateTime이고 그 앞에 this를 붙였다.
public static DateTime MinusDaysEx(this DateTime dt, int days)
{
DateTime d = dt.AddDays(-days);
return d;
}
}
class ExtensionMethod
{
public static void Main()
{
//DateHelper라는 클래스 이름과 함께 MinusDays 메서드를 호출해야한다.
//날짜 연산의 대상이 되는 current 객체를 넘겨줘야 한다.
DateTime current = DateTime.Now;
DateTime dt1 = DateTimeHelper.MinusDays(current, 3);
Console.WriteLine(dt1.ToString());
//확장 메서드를 이용하면 헬퍼 클래스의 이름을 기억해서 호출할 필요가 없다.
//대상이 되는 객체를 인자로 넘겨줄 필요가 없다.
DateTime dt2 = current.MinusDaysEx(3);
Console.WriteLine(dt2.ToString());
Console.ReadLine();
}
}
람다 식
코드로서의 람다 식
익명 메서드를 이용하면 델리게이트에 전달되는 메서드가 일회성으로 사용될 때 간편하게 나타낼 수 있다.
//익명 메서드를 이용한 예시
Thread thread = new Thread(
delegate(object obj)
{
Console.WriteLine("익명 메서드로 호출!");
});
위의 코드를 람다 식을 이용하면 더욱 간편하게 표기가능하다.
Thread thread = new Thread(
//object obj 처럼 더 명확하게 나타낼 수 있다. 전달되는 매개변수가 없으면 ()로 표기.
(obj) =>
{
Console.WriteLine("람다 식으로 호출!");
});
매개변수가 2개 이상이면 다음과 같이 표기한다.
delegate int? MyDivide(int a, int b);
MyDivide myFunc = (a,b) =>
{
if (b == 0)
{
return null;
}
return a / b;
};
return 문을 생략도 가능하다.
delegate int MyAdd(int a, int b);
MyAdd myFunc = (a, b) => a + b;
람다 식을 위한 전용 델리게이트
람다식은 델리게이트랑 대응된다. 하지만 일회성으로 사용되는 간단한 람다 식을 쓸 때마다 델리게이트를 정의해야 한다면 불편할 것이다. 그래서 자주 사용되는 델리게이트 형식을 제공한다.
public delegate void Action<T>(T obj);
-> 반환값이 없는 델리게이트로서 T 형식 매개변수는 입력될 인자 1개의 타입을 지정.
public delegate TResult Func<TResult>();
-> 반환값이 있는 델리게이트로서 TResult 형식 매개변수는 반환될 타입을 지정.
class ActionFunc
{
public static void Main()
{
Action<string> printText =
(txt) =>
{
Console.WriteLine("매개변수로 입력된 텍스트는 : " + txt);
};
printText("좋은 하루입니다!");
//Func<T, TResult>(T arg);
Func<int, int> square = (num) => num* num;
Console.WriteLine(square(5));
}
}
인자를 16개까지 받을 수 있는 Action과 Func가 미리 정의되어 있다.
컬렉션과 람다 식
class CollectionLambda
{
public static void Main()
{
List<int> list = new List<int>() { 1, 2, 3, 4, 5 };
//ForEach 메서드를 이용해 간단하게 컬렉션에서의 연산 가능.
//컬렉션에서 요소를 하나씩 꺼내서 element로 전달. Action<T> 연산 수행.
list.ForEach((element) => { Console.WriteLine(element + " * 2 == " + (element * 2)); });
Array.ForEach(list.ToArray(), (element) => { Console.WriteLine(element + " * 2 == " + (element * 2)); });
//FindAll 메서드를 이용하면 특정 조건에 맞는 요소만 고를 수 있다.
List<int> evenList = list.FindAll((element) => element % 2 == 0);
evenList.ForEach(Console.WriteLine);
//Count 메서드를 이용해서 특정 조건을 만족하는 요소들의 개수를 셀 수 있다.
int count = list.Count((element) => element > 3);
Console.WriteLine("3보다 큰 요소의 개수 : " + count);
}
}
LINQ 메서드식 표현
LINQ 쿼리는 컴파일 될 때 LINQ 확장 메서드로 자동 변경되서 호출된다. 다음의 세 가지 코드는 완전히 동일한 역할을 한다.
표현 | 코드 |
---|---|
LINQ 표현 | from person in people select person; |
확장 메서드 표현 | people.Select( (elem) => elem ); |
일반 메서드 표현 | IEnumerable SelectFunc(List people) { foreach (var item in people) { yield return item; } } |
LINQ 확장 메서드 중 Where()과 FirstOrDefault()의 예제 코드이다.
//public static IEnumerable<TSource> Where<TSource>(
//this IEnumerable<TSource> source, Func< TSource, bool> predicate)
//Where 메서드는 IEnumerable<T> 인터페이스를 지원하는 모든 타입에 사용 가능하다.
//predicate가 True를 리턴하는 요소만 모아서 IEnumerable<T>로 열거형을 반환한다.
IEnumerable<int> enumList = list.Where((element) => element % 2 == 0);
Array.ForEach(enumList.ToArray(), (element) => { Console.WriteLine(element); });
var enumList2 = list.Where((element) => element % 2 == 1);
enumList2.ToList<int>().ForEach(n => Console.WriteLine("홀수는 = " + n));
//public static TSource FirstOrDefault<TSource>(
//this IEnumerable<TSource> source)
//FirstOrDefault 메서드는 조건식에 맞는 처음 요소를 찾는 경우에 사용한다.
//조건에 맞는 요소가 여러 개일 경우 첫 요소만 반환하며, 하나도 없을 경우는 기본값(보통 null)을 반환한다.
List<string> stringList = new List<string>() { "Apple", "AOA", "Banana", "Grape" };
var result = stringList.Where((element) => element.StartsWith("A")).FirstOrDefault();
Console.WriteLine("A로 시작하는 첫 번째 요소는 : " + result);
var result2 = stringList.Where((element) => element.StartsWith("C")).FirstOrDefault();
//그냥 null로 체크하는 것 보다는 default(type)으로 체크하는게 더 안전하다.
//밸류 타입의 경우는 null로 체크하면 에러를 반환한다.
if (result2 == default(string))
{
Console.WriteLine("C로 시작하는 요소가 없습니다.");
}
else
{
Console.WriteLine("C로 시작하는 첫 번째 요소는 : " + result2);
}
질문사항
- await를 사용하면 항상 쓰레드가 자동으로 만들어지는가?
await가 보장하는 것은, Task가 UI Thread에 돌던지, Worker thread에서 돌던지 상관없이 Task 완료 후 await 이후의 실행문들을 디폴트로 원래 await를 실행하기 전의 Thread에서 실행하도록 보장하는 것이다.
하지만 내가 실험하는 환경은 콘솔 어플리케이션이다. 콘솔 어플리케이션은 Thread pool SynchronizationContext를 가지기 때문에 await 이후의 부분이 thread pool thread에 스케쥴링된다. 그래서 이곳 코드를 보면 실행 전, 후의 ThreadId가 다르다. 이것 때문에 await가 스레드를 만드는 것으로 보였던 것이다.
async/await 관련 글
SynchronizationContext 관련 글
비동기 메서드는 비차단 작업입니다. 비동기 메서드의 await 식은 대기한 작업이 실행되는 동안 현재 스레드를 차단하지 않습니다. 대신에 이 식은 메서드의 나머지를 연속으로 등록하고 제어 기능을 비동기 메서드 호출자에게 반환합니다. async 및 await 키워드로 인해 추가 스레드가 생성되지 않습니다. 비동기 메서드는 자체 스레드에서 실행되지 않으므로 다중 스레드가 필요하지 않습니다. 메서드는 현재 동기화 컨텍스트에서 실행되고 메서드가 활성화된 경우에만 스레드에서 시간을 사용합니다. Task.Run을 사용하여 CPU 바인딩 작업을 백그라운드 스레드로 이동할 수 있지만 백그라운드 스레드는 프로세스를 지원하지 않고 결과를 사용할 수 있을 때까지 기다립니다.(MSDN)
-
await Task.Delay() 했을때와 await fs.ReadAsync() 했을 때 blocking 동작이 다른 이유는 무엇인가?
class AsyncTimeTest { private static async void AsyncRead(int i) { using (FileStream fs = new FileStream("test.log", FileMode.Open, FileAccess.Read)) { byte[] buffer = new byte[fs.Length]; await fs.ReadAsync(buffer, 0, buffer.Length);
Console.WriteLine("ReadAsync({0})", i);
string txt = Encoding.UTF8.GetString(buffer);
Console.WriteLine(txt);
}
}
private static async void TaskDelay(int i)
{
await Task.Delay(1000);
Console.WriteLine("Task.Delay({0})", i);
}
public static void Main()
{
Console.WriteLine("메인 시작 시간 : " + DateTime.Now.Ticks / 10000);
for (int i = 0; i < 100; i++)
{
TaskDelay(i);
//AsyncRead(i);
Console.WriteLine(i + "번 돌았다!");
//위 출력이 없으면 Delay(1)일 때 실행되지 않음.
//Delay(1000)으로 하면 i번 돌았다! 만 출력됨.
//AsyncRead는 평균 20번 정도 실행됨.
//그러므로 await로 인한 대기 시간은 ReadAsync < Delay(1) 이다. (test.log을 읽을때 기준)
//ReadAsync와 Delay의 blocking 동작이 다른게 아니라 1번만 실행했을 때,
//Main()이 종료되기 전에 출력되기도 하고, 출력되기 전에 Main()이 끝나기도 해서 동작이 달라보였던 것.
}
//Console.ReadLine();
Console.WriteLine("메인 끝 시간 : " + DateTime.Now.Ticks / 10000);
}
}
3. 한 메소드 안에 await가 두개 이상이면 어떻게 동작하는가?
```c#
class MultiAwaitTest
{
private async void Run()
{
await Task.Run(() => Do1()); //Do1의 실행이 끝나야 다음으로 넘어간다.
Console.WriteLine("중간");
await Task.Run(() => Do2());
Console.WriteLine("끝"); //Do2가 끝나야 실행된다.
}
private void Do1()
{
Console.WriteLine("DO1");
for (int i = 0; i < 10; i++) Console.WriteLine(i + "번");
; }
private void Do2()
{
Console.WriteLine("DO2");
for (int i = 0; i < 10; i++) Console.WriteLine(i + "번");
}
public static void Main()
{
MultiAwaitTest test = new MultiAwaitTest();
test.Run(); //첫번째 await를 만나면 다음으로 넘어간다.
Console.WriteLine("test.Run() 다음 줄 실행"); //Do1과 Do2의 실행 속도에 따라서 결과가 달라진다.
Console.ReadLine();
}
}
- Task에 대한 스터디 보강 (thread는 리턴값이 있는가? Task와 thread 차이는 무엇인가? 왜 Task 타입을 정의했을까?)
- Random 클래스의 seed 역할은 무엇인가?
- 람다식의 변수 스코프는?
- IEnumerable 변수를 foreach 키워드로 순회하도록 변경 바랍니다.
- Where().FirstOrDefault() 하지 말고 곧장 FirstOrDefault() 할 수 없나요?
- Network endian 스터디 보강
- ContinueWhenAny 메소드 스터디 보강
- stread.Read 메소드를 while 루프로 여러번 읽는 이유는? (TCP stream 이야기)
- LINQ 결과인 IEnumerable를 for 문으로 루프 돌때, breakpoint 걸어서 확인해보기