아래 내용은 아래 책들을 참고하였습니다. 자세한 내용은 아래 책을 참고해 주세요.
Thread
- 필요 자료 전달 가능- 하지만 결과값을 받기 위해서는 main thread를 block 시켜야 함.
- 예외 발생 시 전파 어려움.
Task
- Thread 보다 높은 수준의 abstraction- 하나의 concurrent operation을 대표함.
- 일반적으로 main thread가 아닌 thread pool thread에서 비동기적으로 실행
- Continuation을 통해 Task를 조합할 수 있음.
- TaskComplectionSource와 함께 callback 방식으로 활용가능
- Thread 없이 I/O 한정 연산을 기다리는 것이 가능함.
https://msdn.microsoft.com/ko-kr/library/system.threading.tasks.task(v=vs.110).aspx
- t1, t2, t3, t4 각각의 방법으로 Task를 만들어서 사용하는 예제
- Continuation을 통해 Task를 조합할 수 있음.
- TaskComplectionSource와 함께 callback 방식으로 활용가능
- Thread 없이 I/O 한정 연산을 기다리는 것이 가능함.
https://msdn.microsoft.com/ko-kr/library/system.threading.tasks.task(v=vs.110).aspx
using System; using System.Threading; using System.Threading.Tasks; class Example { static void Main() { Action<object> action = (object obj) => { Console.WriteLine("Task={0}, obj={1}, Thread={2}", Task.CurrentId, obj, Thread.CurrentThread.ManagedThreadId); }; // Create a task but do not start it. Task t1 = new Task(action, "alpha"); // Construct a started task Task t2 = Task.Factory.StartNew(action, "beta"); // Block the main thread to demonstrate that t2 is executing t2.Wait(); // Launch t1 t1.Start(); Console.WriteLine("t1 has been launched. (Main Thread={0})", Thread.CurrentThread.ManagedThreadId); // Wait for the task to finish. t1.Wait(); // Construct a started task using Task.Run. String taskData = "delta"; Task t3 = Task.Run( () => {Console.WriteLine("Task={0}, obj={1}, Thread={2}", Task.CurrentId, taskData, Thread.CurrentThread.ManagedThreadId); }); // Wait for the task to finish. t3.Wait(); // Construct an unstarted task Task t4 = new Task(action, "gamma"); // Run it synchronously t4.RunSynchronously(); // Although the task was run synchronously, it is a good practice // to wait for it in the event exceptions were thrown by the task. t4.Wait(); } } // The example displays output like the following: // Task=1, obj=beta, Thread=3 // t1 has been launched. (Main Thread=1) // Task=2, obj=alpha, Thread=4 // Task=3, obj=delta, Thread=3 // Task=4, obj=gamma, Thread=1 |
- t1, t2, t3, t4 각각의 방법으로 Task를 만들어서 사용하는 예제
: t1, t2, t4 는 action으로 정의된 delegate를 실행하는 task
: t3는 lamda로 정의한 내용을 실행하는 task
- t4의 경우 동기적으로 실행되어 main thread(1번)에서 실행된다.
: 일반적으로 main (application) thread에서 동기적으로 실행하면 화면이 멈추므로 잘 사용하지 않음.
- Task.Run()을 사용하여 별도 설정 없이 task를 실행할 수 있음.
- Task.Wait()을 호출하여 task가 끝날때까지 기다릴 수 있고 호출 thread는 block 됨
- Task<TResult> generic type으로 반환값을 지정할 수 있음.
- Task.GetAwaiter()를 사용하여 대기자(awaiter)객체를 반환 받을 수 있고 Awaiter.OnCompleted() 지정하여 실행 후 처리 가능.
. awaiter.GetResult();는 리턴값이 없을 경우 void임. void라도 불러야 할 필요가 있는게 Task 내에서 exception이 발생할 경우 GetResult()가 exception을 발생 시킴.
. synchronous context를 전달할 필요가 없을 경우 task.ConfigureAwait(false).GetAwaiter()를 사용하면 전달하지 않음.
https://michaelridland.com/xamarin/taskcompletionsource-xamarin-beautiful-async/
using System; using System.Threading.Tasks; public class Example { public static async Task Main() { await Task.Run( () => { // Just loop. int ctr = 0; for (ctr = 0; ctr <= 1000000; ctr++) {} Console.WriteLine("Finished {0} loop iterations", ctr); } ); } } // The example displays the following output: // Finished 1000001 loop iterations |
- Task.Run()을 사용하여 별도 설정 없이 task를 실행할 수 있음.
- Task.Wait()을 호출하여 task가 끝날때까지 기다릴 수 있고 호출 thread는 block 됨
- Task<TResult> generic type으로 반환값을 지정할 수 있음.
- Task.GetAwaiter()를 사용하여 대기자(awaiter)객체를 반환 받을 수 있고 Awaiter.OnCompleted() 지정하여 실행 후 처리 가능.
. awaiter.GetResult();는 리턴값이 없을 경우 void임. void라도 불러야 할 필요가 있는게 Task 내에서 exception이 발생할 경우 GetResult()가 exception을 발생 시킴.
. synchronous context를 전달할 필요가 없을 경우 task.ConfigureAwait(false).GetAwaiter()를 사용하면 전달하지 않음.
TaskCompletionSource
https://msdn.microsoft.com/ko-kr/library/dd449174(v=vs.110).aspxhttps://michaelridland.com/xamarin/taskcompletionsource-xamarin-beautiful-async/
- 적절한 시기에 실행할 수 있는 Task를 생성할 수 있다.
- 좀 말이 어려운데 간단하게 말하면 Task를 한번 감싸는 객체이고 Task 실행을 적절하게 조절할 수 있음.
결과를 .SetResult(), .SetCanceled(), .SetException()를 사용하여 caller에게 전달할 수 있음.
- tcs1은 t1 task의 결과로 1초 뒤 15값을 t1.Result로 전달함.
- 좀 말이 어려운데 간단하게 말하면 Task를 한번 감싸는 객체이고 Task 실행을 적절하게 조절할 수 있음.
결과를 .SetResult(), .SetCanceled(), .SetException()를 사용하여 caller에게 전달할 수 있음.
using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; class TCSDemo { // Demonstrated features: // TaskCompletionSource ctor() // TaskCompletionSource.SetResult() // TaskCompletionSource.SetException() // Task.Result // Expected results: // The attempt to get t1.Result blocks for ~1000ms until tcs1 gets signaled. 15 is printed out. // The attempt to get t2.Result blocks for ~1000ms until tcs2 gets signaled. An exception is printed out. static void Main() { TaskCompletionSource<int> tcs1 = new TaskCompletionSource<int>(); Task<int> t1 = tcs1.Task; // Start a background task that will complete tcs1.Task Task.Factory.StartNew(() => { Thread.Sleep(1000); tcs1.SetResult(15); }); // The attempt to get the result of t1 blocks the current thread until the completion source gets signaled. // It should be a wait of ~1000 ms. Stopwatch sw = Stopwatch.StartNew(); int result = t1.Result; sw.Stop(); Console.WriteLine("(ElapsedTime={0}): t1.Result={1} (expected 15) ", sw.ElapsedMilliseconds, result); // ------------------------------------------------------------------ // Alternatively, an exception can be manually set on a TaskCompletionSource.Task TaskCompletionSource<int> tcs2 = new TaskCompletionSource<int>(); Task<int> t2 = tcs2.Task; // Start a background Task that will complete tcs2.Task with an exception Task.Factory.StartNew(() => { Thread.Sleep(1000); tcs2.SetException(new InvalidOperationException("SIMULATED EXCEPTION")); }); // The attempt to get the result of t2 blocks the current thread until the completion source gets signaled with either a result or an exception. // In either case it should be a wait of ~1000 ms. sw = Stopwatch.StartNew(); try { result = t2.Result; Console.WriteLine("t2.Result succeeded. THIS WAS NOT EXPECTED."); } catch (AggregateException e) { Console.Write("(ElapsedTime={0}): ", sw.ElapsedMilliseconds); Console.WriteLine("The following exceptions have been thrown by t2.Result: (THIS WAS EXPECTED)"); for (int j = 0; j < e.InnerExceptions.Count; j++) { Console.WriteLine("\n-------------------------------------------------\n{0}", e.InnerExceptions[j].ToString()); } } } } |
- tcs1은 t1 task의 결과로 1초 뒤 15값을 t1.Result로 전달함.
- tcs2와 같이 task에서 발생된 exception도 task의 결과로 전달 가능하다.
-------------------------------------------------------------------------------------------------------------------------------
멀티 쓰레드를 사용하는 근본적인 이유들
- Main application thread를 차단(block)하지 않기 위해
- I/O를 완료할 때까지 기다리지 않고 다른 작업을 함께 처리
- 동시 처리해야하는 작업들의 수행
Synchronous operation : 수행 후에 실행의 흐름을 caller에게 반환하는 연산
Asynchronous operation : 할 일을 다 마치기 전에 실행의 흐름을 caller에게 반환한다.
비동기 프로그래밍
- 오래 실행되는 함수는 asynchronous operation 형태로 작성
- 예전에는 동기적인 함수를 작성하고 이 함수를 thread에서 실행했던 것과는 달리 함수 내부에서 동시성을 시작함.
. thread에 묶이지 않고 동시성을 구현할 수 있어 효율적임.
. worker thread를 관리가 줄어들고 thread 안정성이 향상될 수 있음.
. thread에서 분리되어 처리되던 기존 방식에 비해 세밀하게 동시성을 처리할 수 있음.
{
// Do Long Work
return start;
}
void DisplayResult()
{
for (int i = 0; i < 10; i++)
Console.WriteLine($"Long work { i } started, result = { DoLongWork(i) }");
Console.WriteLine("Done");
}
(결과)
Long work 0 started, result = 0
Long work 1 started, result = 1
Long work 2 started, result = 2
Long work 3 started, result = 3
Long work 4 started, result = 4
Long work 5 started, result = 5
Long work 6 started, result = 6
Long work 7 started, result = 7
Long work 8 started, result = 8
Long work 9 started, result = 9
{
return Task.Run(() =>
{
// Do Long Work
return start;
});
}
void DisplayResult2()
{
for (int i = 0; i < 10; i++)
{
var awaiter = DoLongWorkAsync(i).GetAwaiter();
awaiter.OnCompleted(() =>
{
Console.WriteLine($"Long work { i } started, result = { DoLongWork(i) }");
});
}
Console.WriteLine("Done");
}
DoLongWorkAsync() 10개가 모두 병렬로 실행됨.
(결과)
Done
Long work 10 started, result = 10
Long work 10 started, result = 10
Long work 10 started, result = 10
Long work 10 started, result = 10
Long work 10 started, result = 10
Long work 10 started, result = 10
Long work 10 started, result = 10
Long work 10 started, result = 10
Long work 10 started, result = 10
Done이 표시되고 Long work들이 표시 될 수 있음.
그리고 캡쳐링된 i가 10인 상태에서 모두 실행되므로 i를 지역변수로 캡쳐링 하면 정상적으로 나옴.
void DisplayResult2()
{
for (int i = 0; i < 10; i++)
{
int captured = i;
var awaiter = DoLongWorkAsync(i).GetAwaiter();
awaiter.OnCompleted(() =>
{
Console.WriteLine($"Long work { captured } started, result = { DoLongWork(captured) }");
(결과)
Long work 1 started, result = 1
Long work 0 started, result = 0
Done
Long work 3 started, result = 3
Long work 2 started, result = 2
Long work 4 started, result = 4
Long work 5 started, result = 5
Long work 7 started, result = 7
Long work 6 started, result = 6
Long work 8 started, result = 8
Long work 9 started, result = 9
연속적으로 진행하고자 한다면
public void DisplayResult3()
{
DisplayResultFrom(0);
}
void DisplayResultFrom(int i)
{
var awaiter = DoLongWorkAsync(i).GetAwaiter();
awaiter.OnCompleted(() =>
{
Console.WriteLine($"Long work { i } started, result = { DoLongWork(i) }");
if (i++ < 10)
DisplayResultFrom(i);
else
Console.WriteLine("Done");
});
}
DisplayResult 자체를 비동기화하여
DisplayResult에서 작업 완료 시 신호를 보내는 작업 객체를 돌려주게 하려면
Task DisplayResultAsync4()
{
var display = new LongWorkDisplay();
display.DisplayResultFrom(0);
return display.Task;
}
public class LongWorkDisplay
{
TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>();
public Task Task { get => _tcs.Task; }
public void DisplayResultFrom(int i)
{
var awaiter = DoLongWorkAsync(i).GetAwaiter();
awaiter.OnCompleted(() =>
{
Console.WriteLine($"Long work { i } started, result = { awaiter.GetResult() }");
if (i++ < 10)
DisplayResultFrom(i);
else
{
Console.WriteLine("Done");
_tcs.SetResult(null);
}
});
}
}
하지만 C# 비동기 함수 기능을 사용하면 다음과 같음.
public async Task DisplayResultAsync5()
{
for (int i = 0; i < 10; i++)
Console.WriteLine($"Long work { i } started, result = { await DoLongWorkAsync(i) }");
Console.WriteLine("Done");
}
예외 전달
async void ButtonClick(object sender, EventArgs args)
{
await Task.Delay(1000);
throw new Exception("exception");
}
이 때 ButtonClick 실행은 await문을 넘어간 후 메세지 루프로 돌아오므로
얼마 지나서 발생한 예외는 메세지 루프의 catch 블록에 잡히지 않는다고 함.
하지만 synchronous context가 있는 경우 AsyncvoidMethodBuilder는
void async 함수에서 미처리 예외를 잡아서동기화 문맥에 전달한다.
비동기 함수에서 바로 처리가 가능한 경우 result를 받을 수 있다면
다음과 같이 처리할 수 있다고 함.
var awaiter = GetWebPageAsync().GetAwaiter();
if (awaiter.IsCompleted)
Console.WriteLine(awaiter.GetResult());
else
awaiter.OnCompleted(() => ...);
-------------------------------------------------------------------------------------------------------------------------------
멀티 쓰레드를 사용하는 근본적인 이유들
- Main application thread를 차단(block)하지 않기 위해
- I/O를 완료할 때까지 기다리지 않고 다른 작업을 함께 처리
- 동시 처리해야하는 작업들의 수행
Synchronous operation : 수행 후에 실행의 흐름을 caller에게 반환하는 연산
Asynchronous operation : 할 일을 다 마치기 전에 실행의 흐름을 caller에게 반환한다.
비동기 프로그래밍
- 오래 실행되는 함수는 asynchronous operation 형태로 작성
- 예전에는 동기적인 함수를 작성하고 이 함수를 thread에서 실행했던 것과는 달리 함수 내부에서 동시성을 시작함.
. thread에 묶이지 않고 동시성을 구현할 수 있어 효율적임.
. worker thread를 관리가 줄어들고 thread 안정성이 향상될 수 있음.
. thread에서 분리되어 처리되던 기존 방식에 비해 세밀하게 동시성을 처리할 수 있음.
. Synchronous operation
int DoLongWork(int start){
// Do Long Work
return start;
}
void DisplayResult()
{
for (int i = 0; i < 10; i++)
Console.WriteLine($"Long work { i } started, result = { DoLongWork(i) }");
Console.WriteLine("Done");
}
(결과)
Long work 0 started, result = 0
Long work 1 started, result = 1
Long work 2 started, result = 2
Long work 3 started, result = 3
Long work 4 started, result = 4
Long work 5 started, result = 5
Long work 6 started, result = 6
Long work 7 started, result = 7
Long work 8 started, result = 8
Long work 9 started, result = 9
. Asynchronous operation
Task<int> DoLongWorkAsync(int start){
return Task.Run(() =>
{
// Do Long Work
return start;
});
}
void DisplayResult2()
{
for (int i = 0; i < 10; i++)
{
var awaiter = DoLongWorkAsync(i).GetAwaiter();
awaiter.OnCompleted(() =>
{
Console.WriteLine($"Long work { i } started, result = { DoLongWork(i) }");
});
}
Console.WriteLine("Done");
}
DoLongWorkAsync() 10개가 모두 병렬로 실행됨.
(결과)
Done
Long work 10 started, result = 10
Long work 10 started, result = 10
Long work 10 started, result = 10
Long work 10 started, result = 10
Long work 10 started, result = 10
Long work 10 started, result = 10
Long work 10 started, result = 10
Long work 10 started, result = 10
Long work 10 started, result = 10
Done이 표시되고 Long work들이 표시 될 수 있음.
그리고 캡쳐링된 i가 10인 상태에서 모두 실행되므로 i를 지역변수로 캡쳐링 하면 정상적으로 나옴.
void DisplayResult2()
{
for (int i = 0; i < 10; i++)
{
int captured = i;
var awaiter = DoLongWorkAsync(i).GetAwaiter();
awaiter.OnCompleted(() =>
{
Console.WriteLine($"Long work { captured } started, result = { DoLongWork(captured) }");
(결과)
Long work 1 started, result = 1
Long work 0 started, result = 0
Done
Long work 3 started, result = 3
Long work 2 started, result = 2
Long work 4 started, result = 4
Long work 5 started, result = 5
Long work 7 started, result = 7
Long work 6 started, result = 6
Long work 8 started, result = 8
Long work 9 started, result = 9
연속적으로 진행하고자 한다면
public void DisplayResult3()
{
DisplayResultFrom(0);
}
void DisplayResultFrom(int i)
{
var awaiter = DoLongWorkAsync(i).GetAwaiter();
awaiter.OnCompleted(() =>
{
Console.WriteLine($"Long work { i } started, result = { DoLongWork(i) }");
if (i++ < 10)
DisplayResultFrom(i);
else
Console.WriteLine("Done");
});
}
DisplayResult 자체를 비동기화하여
DisplayResult에서 작업 완료 시 신호를 보내는 작업 객체를 돌려주게 하려면
Task DisplayResultAsync4()
{
var display = new LongWorkDisplay();
display.DisplayResultFrom(0);
return display.Task;
}
public class LongWorkDisplay
{
TaskCompletionSource<object> _tcs = new TaskCompletionSource<object>();
public Task Task { get => _tcs.Task; }
public void DisplayResultFrom(int i)
{
var awaiter = DoLongWorkAsync(i).GetAwaiter();
awaiter.OnCompleted(() =>
{
Console.WriteLine($"Long work { i } started, result = { awaiter.GetResult() }");
if (i++ < 10)
DisplayResultFrom(i);
else
{
Console.WriteLine("Done");
_tcs.SetResult(null);
}
});
}
}
하지만 C# 비동기 함수 기능을 사용하면 다음과 같음.
public async Task DisplayResultAsync5()
{
for (int i = 0; i < 10; i++)
Console.WriteLine($"Long work { i } started, result = { await DoLongWorkAsync(i) }");
Console.WriteLine("Done");
}
예외 전달
async void ButtonClick(object sender, EventArgs args)
{
await Task.Delay(1000);
throw new Exception("exception");
}
이 때 ButtonClick 실행은 await문을 넘어간 후 메세지 루프로 돌아오므로
얼마 지나서 발생한 예외는 메세지 루프의 catch 블록에 잡히지 않는다고 함.
하지만 synchronous context가 있는 경우 AsyncvoidMethodBuilder는
void async 함수에서 미처리 예외를 잡아서동기화 문맥에 전달한다.
비동기 함수에서 바로 처리가 가능한 경우 result를 받을 수 있다면
다음과 같이 처리할 수 있다고 함.
var awaiter = GetWebPageAsync().GetAwaiter();
if (awaiter.IsCompleted)
Console.WriteLine(awaiter.GetResult());
else
awaiter.OnCompleted(() => ...);
-------------------------------------------------------------------------------------------------------------------------------
async 및 await를 사용한 비동기 프로그래밍
비동기 프로그래밍의 필요성과 예제 시나리오를 지원하기 위해 동기적, 비동기적 프로그램을 예시를 들어 설명하고 있어 개념이 이해 안된다면 한번 읽어보세요.
댓글 없음:
댓글 쓰기