.NET의 비동기 패턴에 대한 간략한 기록:
- .NET Framework 1.0에서는 IAsyncResultAPM(비동기 프로그래밍 모델) 또는
Begin/End
패턴이라고도 하는 패턴을 도입했습니다. - .NET Framework 2.0은 이벤트 기반 EAP(비동기 패턴)를 추가했습니다.
- .NET Framework 4는 APM과 EAP를 모두 대체하고 이전 패턴에서 마이그레이션 루틴을 쉽게 빌드할 수 있는 기능을 제공하는 TAP(작업 기반 비동기 패턴)를 도입했습니다.
작업 및 APM(비동기 프로그래밍 모델)
APM에서 TAP으로
APM(비동기 프로그래밍 모델) 패턴은 구조화되어 있으므로 APM 구현을 TAP 구현으로 노출하는 래퍼를 쉽게 빌드할 수 있습니다. .NET Framework 4 이상 버전에는 이 변환을 제공하는 메서드 오버로드 형식의 FromAsync 도우미 루틴이 포함됩니다.
Stream 클래스와 BeginRead 및 EndRead 메서드를 고려하세요. 이는 동기 Read 메서드에 해당하는 APM을 나타냅니다.
public int Read(byte[] buffer, int offset, int count)
Public Function Read(buffer As Byte(), offset As Integer,
count As Integer) As Integer
public IAsyncResult BeginRead(byte[] buffer, int offset,
int count, AsyncCallback callback,
object state)
Public Function BeginRead(buffer As Byte, offset As Integer,
count As Integer, callback As AsyncCallback,
state As Object) As IAsyncResult
public int EndRead(IAsyncResult asyncResult)
Public Function EndRead(asyncResult As IAsyncResult) As Integer
다음과 같이 이 메서드를 사용하여 TaskFactory<TResult>.FromAsync 이 작업에 대한 TAP 래퍼를 구현할 수 있습니다.
public static Task<int> ReadAsync(this Stream stream,
byte[] buffer, int offset,
int count)
{
if (stream == null)
throw new ArgumentNullException("stream");
return Task<int>.Factory.FromAsync(stream.BeginRead,
stream.EndRead, buffer,
offset, count, null);
}
<Extension()>
Public Function ReadAsync(strm As Stream,
buffer As Byte(), offset As Integer,
count As Integer) As Task(Of Integer)
If strm Is Nothing Then
Throw New ArgumentNullException("stream")
End If
Return Task(Of Integer).Factory.FromAsync(AddressOf strm.BeginRead,
AddressOf strm.EndRead, buffer,
offset, count, Nothing)
End Function
이 구현은 다음과 유사합니다.
public static Task<int> ReadAsync(this Stream stream,
byte [] buffer, int offset,
int count)
{
if (stream == null)
throw new ArgumentNullException("stream");
var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer, offset, count, iar =>
{
try {
tcs.TrySetResult(stream.EndRead(iar));
}
catch(OperationCanceledException) {
tcs.TrySetCanceled();
}
catch(Exception exc) {
tcs.TrySetException(exc);
}
}, null);
return tcs.Task;
}
<Extension()>
Public Function ReadAsync(stream As Stream, buffer As Byte(), _
offset As Integer, count As Integer) _
As Task(Of Integer)
If stream Is Nothing Then
Throw New ArgumentNullException("stream")
End If
Dim tcs As New TaskCompletionSource(Of Integer)()
stream.BeginRead(buffer, offset, count,
Sub(iar)
Try
tcs.TrySetResult(stream.EndRead(iar))
Catch e As OperationCanceledException
tcs.TrySetCanceled()
Catch e As Exception
tcs.TrySetException(e)
End Try
End Sub, Nothing)
Return tcs.Task
End Function
TAP에서 APM으로
기존 인프라가 APM 패턴을 기대하는 경우에는 TAP 구현을 사용하여 APM 구현이 필요할 때 사용할 수 있습니다. 작업을 구성하고 클래스가 Task 구현 IAsyncResult하므로 간단한 도우미 함수를 사용하여 이 작업을 수행할 수 있습니다. 다음 코드에서는 클래스의 Task<TResult> 확장을 사용하지만 제네릭이 아닌 작업에는 거의 동일한 함수를 사용할 수 있습니다.
public static IAsyncResult AsApm<T>(this Task<T> task,
AsyncCallback callback,
object state)
{
if (task == null)
throw new ArgumentNullException("task");
var tcs = new TaskCompletionSource<T>(state);
task.ContinueWith(t =>
{
if (t.IsFaulted)
tcs.TrySetException(t.Exception.InnerExceptions);
else if (t.IsCanceled)
tcs.TrySetCanceled();
else
tcs.TrySetResult(t.Result);
if (callback != null)
callback(tcs.Task);
}, TaskScheduler.Default);
return tcs.Task;
}
<Extension()>
Public Function AsApm(Of T)(task As Task(Of T),
callback As AsyncCallback,
state As Object) As IAsyncResult
If task Is Nothing Then
Throw New ArgumentNullException("task")
End If
Dim tcs As New TaskCompletionSource(Of T)(state)
task.ContinueWith(Sub(antecedent)
If antecedent.IsFaulted Then
tcs.TrySetException(antecedent.Exception.InnerExceptions)
ElseIf antecedent.IsCanceled Then
tcs.TrySetCanceled()
Else
tcs.TrySetResult(antecedent.Result)
End If
If callback IsNot Nothing Then
callback(tcs.Task)
End If
End Sub, TaskScheduler.Default)
Return tcs.Task
End Function
이제 다음과 같은 TAP 구현이 있는 경우를 고려합니다.
public static Task<String> DownloadStringAsync(Uri url)
Public Shared Function DownloadStringAsync(url As Uri) As Task(Of String)
이 APM 구현을 제공하려고 합니다.
public IAsyncResult BeginDownloadString(Uri url,
AsyncCallback callback,
object state)
Public Function BeginDownloadString(url As Uri,
callback As AsyncCallback,
state As Object) As IAsyncResult
public string EndDownloadString(IAsyncResult asyncResult)
Public Function EndDownloadString(asyncResult As IAsyncResult) As String
다음 예제에서는 APM으로 한 번의 마이그레이션을 보여 줍니다.
public IAsyncResult BeginDownloadString(Uri url,
AsyncCallback callback,
object state)
{
return DownloadStringAsync(url).AsApm(callback, state);
}
public string EndDownloadString(IAsyncResult asyncResult)
{
return ((Task<string>)asyncResult).Result;
}
Public Function BeginDownloadString(url As Uri,
callback As AsyncCallback,
state As Object) As IAsyncResult
Return DownloadStringAsync(url).AsApm(callback, state)
End Function
Public Function EndDownloadString(asyncResult As IAsyncResult) As String
Return CType(asyncResult, Task(Of String)).Result
End Function
작업 및 EAP(이벤트 기반 비동기 패턴)
EAP 패턴은 APM 패턴보다 변형이 많고 구조가 적기 때문에 이벤트 기반 EAP(비동기 패턴) 구현 래핑은 APM 패턴을 래핑하는 것보다 더 많이 관련됩니다. 이를 보여주기 위해 다음 코드가 DownloadStringAsync
메서드를 감쌉니다.
DownloadStringAsync
는 URI를 수락하며, 진행 상황에 대한 여러 통계를 보고하기 위해 다운로드 중에 DownloadProgressChanged
이벤트를 발생시키고, 완료되면 DownloadStringCompleted
이벤트를 발생시킵니다. 최종 결과는 지정된 URI에서 페이지의 내용을 포함하는 문자열입니다.
public static Task<string> DownloadStringAsync(Uri url)
{
var tcs = new TaskCompletionSource<string>();
var wc = new WebClient();
wc.DownloadStringCompleted += (s,e) =>
{
if (e.Error != null)
tcs.TrySetException(e.Error);
else if (e.Cancelled)
tcs.TrySetCanceled();
else
tcs.TrySetResult(e.Result);
};
wc.DownloadStringAsync(url);
return tcs.Task;
}
Public Shared Function DownloadStringAsync(url As Uri) As Task(Of String)
Dim tcs As New TaskCompletionSource(Of String)()
Dim wc As New WebClient()
AddHandler wc.DownloadStringCompleted, Sub(s, e)
If e.Error IsNot Nothing Then
tcs.TrySetException(e.Error)
ElseIf e.Cancelled Then
tcs.TrySetCanceled()
Else
tcs.TrySetResult(e.Result)
End If
End Sub
wc.DownloadStringAsync(url)
Return tcs.Task
End Function
작업 및 대기 핸들
대기 핸들에서 TAP로
대기 핸들이 비동기 패턴을 구현하지는 않지만, 고급 개발자는 WaitHandle 클래스와 ThreadPool.RegisterWaitForSingleObject 메서드를 대기 핸들이 설정될 때 비동기 알림을 위해 사용할 수 있습니다. 메서드를 캡슐화하여 대기 핸들에서의 모든 동기 대기를 작업 기반 접근 방식으로 대체할 수 있습니다.
public static Task WaitOneAsync(this WaitHandle waitHandle)
{
if (waitHandle == null)
throw new ArgumentNullException("waitHandle");
var tcs = new TaskCompletionSource<bool>();
var rwh = ThreadPool.RegisterWaitForSingleObject(waitHandle,
delegate { tcs.TrySetResult(true); }, null, -1, true);
var t = tcs.Task;
t.ContinueWith( (antecedent) => rwh.Unregister(null));
return t;
}
<Extension()>
Public Function WaitOneAsync(waitHandle As WaitHandle) As Task
If waitHandle Is Nothing Then
Throw New ArgumentNullException("waitHandle")
End If
Dim tcs As New TaskCompletionSource(Of Boolean)()
Dim rwh As RegisteredWaitHandle = ThreadPool.RegisterWaitForSingleObject(waitHandle,
Sub(state, timedOut)
tcs.TrySetResult(True)
End Sub, Nothing, -1, True)
Dim t = tcs.Task
t.ContinueWith(Sub(antecedent)
rwh.Unregister(Nothing)
End Sub)
Return t
End Function
이 메서드를 사용하면 비동기 메서드에서 기존 WaitHandle 구현을 사용할 수 있습니다. 예를 들어 특정 시간에 실행되는 비동기 작업의 수를 제한하려는 경우 세마포( System.Threading.SemaphoreSlim 개체)를 활용할 수 있습니다. 세마포 수를 N 으로 초기화하고, 작업을 수행할 때마다 세마포를 대기하고, 작업을 완료할 때 세마포를 해제하여 동시에 실행되는 작업 수를 N으로 제한할 수 있습니다.
static int N = 3;
static SemaphoreSlim m_throttle = new SemaphoreSlim(N, N);
static async Task DoOperation()
{
await m_throttle.WaitAsync();
// do work
m_throttle.Release();
}
Shared N As Integer = 3
Shared m_throttle As New SemaphoreSlim(N, N)
Shared Async Function DoOperation() As Task
Await m_throttle.WaitAsync()
' Do work.
m_throttle.Release()
End Function
대기 핸들을 사용하지 않고 작업과 완전히 작동하는 비동기 세마포를 빌드할 수도 있습니다. 이렇게 하려면 위에 데이터 구조를 빌드Task에서 설명한 것과 같은 기술을 사용할 수 있습니다.
TAP에서 대기 핸들까지
앞에서 설명한 대로, Task 클래스는 IAsyncResult을 구현하고, 해당 구현에는 IAsyncResult.AsyncWaitHandle가 완료되면 설정될 대기 핸들을 반환하는 속성이 노출됩니다. 다음과 같이 WaitHandle를 위한 Task을 얻을 수 있습니다.
WaitHandle wh = ((IAsyncResult)task).AsyncWaitHandle;
Dim wh As WaitHandle = CType(task, IAsyncResult).AsyncWaitHandle
참고하십시오
.NET