VisualStudio/C#

[C#] Task async awiat

usingsystem 2022. 8. 10. 15:46
728x90

Task

 

이 글은 C#에서 Task 클래스와 async, await 사용법에 대한 글입니다.

 

제가 이전 글에서 Thread에 대해서 설명한 적이 있습니다.

링크 - [C#] 36. 스레드(Thread)를 사용하는 방법, Thread.Sleep 함수 사용법

 

쓰레드라는 건 병렬 처리라는 건 몇 번을 설명했는지, 이번 글에서는 생략하겠습니다.

Thread를 생성할 때는 시스템 상에서 여러가지 리소스를 사용하고 지나치게 많이 사용되면 반대로 시스템의 성능이 느려집니다. 그래서 쓰레드 풀을 생성해서 쓰레드의 개수 제한과 쓰레드 리소스의 재활용을 통해서 시스템의 성능을 향상 시킬수 있는데 스레드 상태를 제어할 수 없어서 쓰레드가 종료할 때까지 기다리는 것을 별도로 구현을 해야 하는 불편함이 있습니다.

Task는 ThreadPool 안에서 움직이는 쓰레드이고 Thread처럼 쉽게 생성하고 Join 기능까지 사용할 수 있는 기능이 있습니다.

 

 [소스 보기] Program.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;

namespace Example
{
// 쓰레드로 넘기는 파라미터 클래스
class Node
{
// 콘솔 출력시 사용되는 텍스트
public string Text { get; set; }
// 반복문 횟수
public int Count { get; set; }
// Sleep의 시간틱
public int Tick { get; set; }
}
class Program
{
// 실행 함수
static void Main(string[] args)
{
// ThreadPool의 최소 쓰레드는 0, 최대 쓰레드는 2개로 설정
ThreadPool.SetMinThreads(0, 0);
ThreadPool.SetMaxThreads(2, 0);
// 리스트에 Task를 설정한다.
var list = new List<Task<int>>();
// Task에 사용될 람다식 입력 값은 object 반환값은 int형
var func = new Func<object, int>((x) =>
{
// object를 Node타입으로 강제 캐스팅
var node = (Node)x;
// 변수
int sum = 0;
// 설정된 반복문의 횟수만큼
for (int i = 0; i <= node.Count; i++)
{
// 값을 더하기
sum += i;
// 콘솔 출력
Console.WriteLine(node.Text + " = " + i);
// 설정된 Sleep 시간틱
Thread.Sleep(node.Tick);
}
// 콘솔 출력
Console.WriteLine("Completed " + node.Text);
return sum;
});
// 리스트에 Tack 추가
list.Add(new Task<int>(func, new Node { Text = "A", Count = 5, Tick = 1000 }));
list.Add(new Task<int>(func, new Node { Text = "B", Count = 5, Tick = 10 }));
list.Add(new Task<int>(func, new Node { Text = "C", Count = 5, Tick = 500 }));
list.Add(new Task<int>(func, new Node { Text = "D", Count = 5, Tick = 300 }));
list.Add(new Task<int>(func, new Node { Text = "E", Count = 5, Tick = 200 }));
// list에 넣은 Task를 실행
list.ForEach(x => x.Start());
// list에 넣은 Task를 종료될 때까지 실행
list.ForEach(x => x.Wait());
// 쓰레드의 합을 출력
Console.WriteLine("Sum = " + list.Sum(x => x.Result));
// 아무 키나 누르면 종료
Console.WriteLine("Press Any key...");
Console.ReadLine();
}
}
}

위 결과를 보면 먼저 ThreadPool에서 설정하는 쓰레드 제한 설정이 Task로 선언한 쓰레드에도 영향이 가는 것을 확인할 수 있습니다.

즉, 구현은 Thread 처럼 간단하게 사용할 수 있지만, 내용은 ThreadPool에서 움직이는 것을 확인 할 수 있습니다.

 

그리고 ThreadPool과 다르게 return 값을 받을 수 있어서 1부터 5까지 더하면 15, 쓰레드가 5개이니깐 총합의 75의 결과가 나오는 것을 확인할 수 있습니다.

lock를 사용하지 않아도 각 쓰레드에서 결과 값을 받아서 각 메인 프로세스에서 쓰레드의 값을 받아 사용할 수 있습니다.

 

그리고 Task의 다른 기능은 async, await 키워드와 매우 밀접한 관계가 있습니다.

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

namespace Example
{
// 쓰레드로 넘기는 파라미터 클래스
class Node
{
// 콘솔 출력시 사용되는 텍스트
public string Text { get; set; }
// 반복문 횟수
public int Count { get; set; }
// Sleep의 시간틱
public int Tick { get; set; }
}
class Program
{
private static async Task<int> RunAsync(Node node)
{
var task = new Task<int>(() =>
{
// 변수
int sum = 0;
// 설정된 반복문의 횟수만큼
for (int i = 0; i <= node.Count; i++)
{
// 값을 더하기
sum += i;
// 콘솔 출력
Console.WriteLine(node.Text + " = " + i);
// 설정된 Sleep 시간틱
Thread.Sleep(node.Tick);
}
// 콘솔 출력
Console.WriteLine("Completed " + node.Text);
return sum;
});
// Task 실행
task.Start();
// task가 종료될 때까지 대기
await task;
// task의 결과 리턴
return task.Result;
}
// 실행 함수
static void Main(string[] args)
{
// ThreadPool의 최소 쓰레드는 0, 최대 쓰레드는 2개로 설정
ThreadPool.SetMinThreads(0, 0);
ThreadPool.SetMaxThreads(2, 0);
// 리스트에 Task를 설정한다.
var list = new List<Task<int>>();
// 리스트에 Tack 추가
list.Add(RunAsync(new Node { Text = "A", Count = 5, Tick = 1000 }));
list.Add(RunAsync(new Node { Text = "B", Count = 5, Tick = 10 }));
list.Add(RunAsync(new Node { Text = "C", Count = 5, Tick = 500 }));
list.Add(RunAsync(new Node { Text = "D", Count = 5, Tick = 300 }));
list.Add(RunAsync(new Node { Text = "E", Count = 5, Tick = 200 }));
// 쓰레드의 합을 출력
Console.WriteLine("Sum = " + list.Sum(x => x.Result));
// 아무 키나 누르면 종료
Console.WriteLine("Press Any key...");
Console.ReadLine();
}
}
}

첫번째 예제와 결과는 같은 결과입니다만, Task를 다루기 쉽게 구현 되었습니다.

async가 선언된 함수에서 Task를 생성하고 실행하고, await으로 쓰레드가 종료될 때까지 대기합니다.

그리고 결과를 리턴하게 되면 Main 프로세스에서 결과를 합산하여 나오는 것을 확인할 수 있습니다.

 

여기까지가 Task와 async, await의 기본 구조입니다.

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

namespace Example
{
// 쓰레드로 넘기는 파라미터 클래스
class Node
{
// 콘솔 출력시 사용되는 텍스트
public string Text { get; set; }
// 반복문 횟수
public int Count { get; set; }
// Sleep의 시간틱
public int Tick { get; set; }
}
class Program
{
private static async Task<int> RunAsync(Node node)
{
var task = new Task<int>(() =>
{
// 변수
int sum = 0;
// 설정된 반복문의 횟수만큼
for (int i = 0; i <= node.Count; i++)
{
// 값을 더하기
sum += i;
// 콘솔 출력
Console.WriteLine(node.Text + " = " + i);
// 설정된 Sleep 시간틱
Thread.Sleep(node.Tick);
}
// 콘솔 출력
Console.WriteLine("Completed " + node.Text);
return sum;
});
// Task 실행
task.Start();
// task가 종료될 때까지 대기
await task;
// task의 결과 리턴
return task.Result;
}
// 실행 함수
static void Main(string[] args)
{
// ThreadPool의 최소 쓰레드는 0, 최대 쓰레드는 2개로 설정
ThreadPool.SetMinThreads(0, 0);
ThreadPool.SetMaxThreads(2, 0);
// 리스트에 Task를 설정한다.
var list = new List<Task<int>>();

// 리스트에 Tack 추가
// ContinueWith 함수를 사용하여 쓰레드의 결과를 받으면 계산을 추가합니다.
list.Add(RunAsync(new Node { Text = "A", Count = 5, Tick = 1000 }).ContinueWith(x => x.Result * 100));
list.Add(RunAsync(new Node { Text = "B", Count = 5, Tick = 10 }).ContinueWith(x => x.Result * 100));
list.Add(RunAsync(new Node { Text = "C", Count = 5, Tick = 500 }).ContinueWith(x => x.Result * 100));
list.Add(RunAsync(new Node { Text = "D", Count = 5, Tick = 300 }).ContinueWith(x => x.Result * 100));
list.Add(RunAsync(new Node { Text = "E", Count = 5, Tick = 200 }).ContinueWith(x => x.Result * 100));
// 쓰레드의 합을 출력
Console.WriteLine("Sum = " + list.Sum(x => x.Result));
// 아무 키나 누르면 종료
Console.WriteLine("Press Any key...");
Console.ReadLine();
}
}
}

Task에서 ContinueWith의 함수를 제공하는데, 이는 각 쓰레드가 종료되면 이어서 처리되는 람다식 처리입니다.

상황에 따라서는 다른 Task 쓰레드를 붙일 수도 있고 여러가지 실행을 연결해서 구현할 수 있는 함수입니다.

출처: https://nowonbun.tistory.com/415 [명월 일지:티스토리]

 

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

 

 

이 글에서는 async와 await를 소개하겠습니다.

async와 await은 Task 클래스와 매우 밀접한 관계가 있으므로 먼저 Task 클래스의 글을 함께 보면 이해하기 쉽습니다.

링크 - [C# 강좌 - 54]Task 클래스

 

먼저 async 키워드는 void로 사용하는 방법과 Task클래스와 사용하는 방법이 두가지로 나누어져 있습니다.

먼저 void로 사용하는 방법입니다.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using System.Threading;



namespace Example

{

class Program

{

// 단독으로 사용할 때는 void를 사용한다.

static async void AsyncTest()

{

var task = new Task<int>(() =>

{

int sum = 0;

for (int i = 0; i < 10; i++)

{

sum += i;

// 0.1초 단위로 1씩 증감

Console.WriteLine(i);

Thread.Sleep(100);

}

return sum;

});

task.Start();

}

static void Main(string[] args)

{

// Task 리턴식이 없기 때문에 제어할 수가 없다.

AsyncTest();

Console.WriteLine("Press Any Key...");

Console.ReadKey();

}

}

}

 

위 소스를 보면 async 메서드에 task를 넣고 실행했습니다. 사실 async - void는 그냥 비동기입니다. ThreadPool로 만드는 것과 차이가 없습니다.

별 의미도 없고 그냥 위처럼 만든다면 그냥 async없이 Task 클래스만 사용하는 게 맞습니다. 실제 저렇게 코딩하면 Visual studio에서도 warning 메시지 나옵니다.

 

 

warning 메시지 뜻은 조금 다른 내용인데 비슷한 말입니다.

 

그러므로 async 키워드는 리턴 값이 Task으로 사용하는게 맞는 것 같습니다.

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

namespace Example
{
class Program
{
// 단독으로 사용할 때는 void를 사용한다.
static async Task<int> AsyncTest()
{
var task = new Task<int>(() =>
{
int sum = 0;
for (int i = 0; i < 10; i++)
{
sum += i;
// 0.1초 단위로 1씩 증감
//Console.WriteLine(i);
Thread.Sleep(100);
}
return sum;
});
task.Start();
//외부에서 await에서 기다린다.
await task;
// Wait가 호출되면 통과된다.
Console.WriteLine(task.Result);
return 10;
}

static void Main(string[] args)
{
var task = AsyncTest();
Console.WriteLine("pass await 1");
// Wait
task.Wait();
Console.WriteLine("pass await 2");
//결국 Return까지 기다린다.
int result = task.Result;
//결과 같은 10이다.
Console.WriteLine(result);
Console.WriteLine("Press Any Key...");
Console.ReadKey();
}
}
}

 

우리가 쓰레드를 만들 때 실제 호출하는 쪽에서 쓰레드 안의 제어를 하기 쉽지 않습니다. 문제는 동기화 때문입니다.

그러나 async - await을 쓰면 제어가 가능해 집니다.

 

위 예제는 Main에서 AsyncTest를 호출했습니다. AsyncTest 메서드 안에서는 task가 실행됩니다. 물론 비동기로 실행됩니다. 여기서 이 쓰레드를 잠깐 멈추게 하고 싶습니다. 그럼 외부에서 Wait함수를 부르면 되는데 이 때 await 키워드로 멈추는 포인트를 지정할 수 있는 것입니다.

이렇게 되면 순서는 결과의 순서는 「pass await 1」 => 「45」 => 「pass await 2」 => 「10」 으로 출력이 됩니다.

 

 

다음은 async와 더불어 많이 사용하는 ContinueWith 함수를 소개하겠습니다.

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

namespace Example
{
class Program
{
static async Task<int> AsyncTest()
{
var task = new Task<int>(() =>
{
int sum = 0;
for (int i = 0; i < 10; i++)
{
sum += i;
// 0.1초 단위로 1씩 증감
Console.WriteLine(i);
Thread.Sleep(100);
}
return sum;
});

task.Start();
await task;
task = new Task<int>(() =>
{
int sum = 0;
for (int i = 10; i < 20; i++)
{
sum += i;
Thread.Sleep(100);
}
return sum;
});
task.Start();
// await이 위에 있지만 결국에는 145가 리턴된다.
return task.Result;
}

static void Main(string[] args)
{
//AsyncTest 끝나면 이어서 실행된다. 즉, AsyncTest가 종료되면 실행되는 Thread
var continueTask = AsyncTest().ContinueWith(task =>
{
return task.Result;
});
//continueTask 끝날때까지 기다린다.
Console.WriteLine(continueTask.Result);

Console.WriteLine("Press Any Key...");
Console.ReadKey();
}
}
}

ContinueWith 함수는 Task를 연달아 붙여서 사용할 때 사용합니다. 이 뜻은 Task => Wait => Result => Task => Wait => Result 이지만 위처럼 ContinueWith함수를 써서 간단하게 처리를 했습니다.

ContinueWith는 C#의 콜백 함수라고도 부르는 사람도 있습니다.

 

여기까지 async와 await 설명이었습니다.

 

궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.

출처: https://nowonbun.tistory.com/419 [명월 일지:티스토리]

728x90