작업 식
이 문서에서는 비동기 식과 유사하지만 .NET 작업을 직접 작성할 수 있는 작업 식 에 대한 F#의 지원을 설명합니다. 비동기 식과 마찬가지로 작업 식은 다른 작업의 실행을 차단하지 않고 코드를 비동기적으로 실행합니다.
비동기 코드는 일반적으로 비동기 식을 사용하여 작성됩니다. .NET 작업을 만들거나 사용하는 .NET 라이브러리와 광범위하게 상호 운용하는 경우 작업 식을 사용하는 것이 좋습니다. 작업 식은 성능 및 디버깅 환경을 향상시킬 수도 있습니다. 그러나 작업 식에는 문서의 뒷부분에 설명된 몇 가지 제한 사항이 있습니다.
구문
task { expression }
이전 구문에서 나타내는 expression
계산은 .NET 작업으로 실행되도록 설정됩니다. 이 코드가 실행된 직후 작업이 시작되고 첫 번째 비동기 작업이 수행될 때까지 현재 스레드에서 실행됩니다(예: 비동기 절전 모드, 비동기 I/O 또는 기타 기본 비동기 작업). 식의 형식은 Task<'T>
키워드(keyword) 'T
사용할 때 return
식에서 반환되는 형식입니다.
let!
작업 식에서 일부 식과 작업은 동기식이고 일부는 비동기식입니다. 비동기 작업의 결과를 기다리는 경우 일반 let
바인딩 let!
대신 . 그 효과는 let!
계산이 수행될 때 다른 계산 또는 스레드에서 실행을 계속할 수 있도록 하는 것입니다. 바인딩의 오른쪽이 let!
반환되면 나머지 작업은 실행을 다시 시작합니다.
다음 코드에서는 차이점과 let!
.let
사용하는 let
코드 줄은 작업을 나중에 대기할 수 있는 개체로 만듭니다(예 task.Wait()
: 또는 task.Result
.). 사용하는 let!
코드 줄은 작업을 시작하고 결과를 기다립니다.
// let just stores the result as a task.
let (result1 : Task<int>) = stream.ReadAsync(buffer, offset, count, cancellationToken)
// let! completes the asynchronous operation and returns the data.
let! (result2 : int) = stream.ReadAsync(buffer, offset, count, cancellationToken)
F# task { }
식은 다음과 같은 종류의 비동기 작업을 대기할 수 있습니다.
- .NET 작업 Task<TResult> 및 제네릭 Task이 아닌 .
- .NET 값 태스크 ValueTask<TResult> 및 제네릭 ValueTask이 아닌 .
- F# 비동기 계산 .
Async<T>
- F# RFC FS-1097에 지정된 "GetAwaiter" 패턴을 따르는 모든 개체입니다.
return
식
작업 식 return expr
내에서 작업의 결과를 반환하는 데 사용됩니다.
return!
식
작업 식 return! expr
내에서 다른 작업의 결과를 반환하는 데 사용됩니다. 결과를 사용한 let!
후 즉시 반환하는 것과 같습니다.
제어 흐름
작업 식에는 제어 흐름 구문for .. in .. do
, while .. do
, try .. with ..
, try .. finally ..
if .. then .. else
및 if .. then ..
. 동기적으로 실행되는 처리기 및 finally
처리기를 제외하고 with
추가 작업 구문을 포함할 수 있습니다. 비동try .. finally ..
기 항목이 필요한 경우 형식IAsyncDisposable
의 개체와 함께 바인딩을 사용합니다use
.
use
및 use!
바인딩
작업 식 내에서 바인딩은 use
형식 IDisposable 또는 IAsyncDisposable.의 값에 바인딩할 수 있습니다. 후자의 경우 삭제 클린up 작업이 비동기적으로 실행됩니다.
또한 let!
비동기 바인딩을 수행하는 데 사용할 use!
수 있습니다. 사이의 let!
차이와 use!
은(는) 사이의 let
차이와 use
같습니다. 의 경우 use!
개체는 현재 범위의 닫기에서 삭제됩니다. F# 6 use!
에서는 값이 null use
로 초기화되는 것을 허용하지 않습니다.
값 작업
값 작업은 작업 기반 프로그래밍에서 할당을 방지하는 데 사용되는 구조체입니다. 값 작업은 을 사용하여 .AsTask()
실제 작업으로 전환되는 임시 값입니다.
작업 식에서 값 태스크를 만들려면 사용 |> ValueTask<ReturnType>
하거나 |> ValueTask
. 예시:
let makeTask() =
task { return 1 }
makeTask() |> ValueTask<int>
취소 토큰 및 취소 검사 추가
F# 비동기 식과 달리 작업 식은 취소 토큰을 암시적으로 전달하지 않으며 취소 검사 암시적으로 수행하지 않습니다. 코드에 취소 토큰이 필요한 경우 취소 토큰을 매개 변수로 지정해야 합니다. 예시:
open System.Threading
let someTaskCode (cancellationToken: CancellationToken) =
task {
cancellationToken.ThrowIfCancellationRequested()
printfn $"continuing..."
}
코드를 취소할 수 있도록 하려면 취소를 지원하는 모든 .NET 라이브러리 작업에 취소 토큰을 전달하는지 신중하게 검사. 예를 들어 Stream.ReadAsync
여러 오버로드가 있으며, 그 중 하나는 취소 토큰을 허용합니다. 이 오버로드를 사용하지 않으면 특정 비동기 읽기 작업을 취소할 수 없습니다.
백그라운드 작업
기본적으로 .NET 작업은 있는 경우를 사용하여 SynchronizationContext.Current 예약됩니다. 이렇게 하면 태스크가 UI를 차단하지 않고 사용자 인터페이스 스레드에서 실행되는 협조적이고 인터리브된 에이전트 역할을 할 수 있습니다. 없는 경우 작업 연속 작업은 .NET 스레드 풀로 예약됩니다.
실제로 작업을 생성하는 라이브러리 코드는 동기화 컨텍스트를 무시하고 필요한 경우 항상 .NET 스레드 풀로 전환하는 것이 좋습니다. 다음을 사용하여 이 작업을 수행할 수 있습니다.backgroundTask { }
backgroundTask { expression }
백그라운드 작업은 nullSynchronizationContext.Current
이 SynchronizationContext.Current
아닌 스레드에서 시작된 경우 다음을 사용하여 Task.Run
스레드 풀의 백그라운드 스레드로 전환합니다. null SynchronizationContext.Current
이 있는 스레드에서 시작된 경우 동일한 스레드에서 실행됩니다.
참고 항목
실제로 이는 F# 작업 코드에서 호출이 일반적으로 필요하지 않음을 의미 ConfigureAwait(false)
합니다. 대신 백그라운드에서 실행하려는 작업은 .를 사용하여 backgroundTask { ... }
작성해야 합니다. 백그라운드 작업에 대한 모든 외부 작업 바인딩은 백그라운드 작업이 완료될 때 다시 동기화 SynchronizationContext.Current
됩니다.
tailcall과 관련된 작업의 제한 사항
F# 비동기 식과 달리 작업 식은 tailcall을 지원하지 않습니다. 즉, 실행될 때 return!
현재 작업은 결과가 반환되는 작업을 기다리는 것으로 등록됩니다. 즉, 작업 식을 사용하여 구현된 재귀 함수 및 메서드는 바인딩되지 않은 태스크 체인을 만들 수 있으며, 바인딩되지 않은 스택 또는 힙을 사용할 수 있습니다. 예를 들어, 다음 코드를 고려하세요.
let rec taskLoopBad (count: int) : Task<string> =
task {
if count = 0 then
return "done!"
else
printfn $"looping..., count = {count}"
return! taskLoopBad (count-1)
}
let t = taskLoopBad 10000000
t.Wait()
이 코딩 스타일은 작업 식과 함께 사용하면 안 됩니다. 그러면 10000000개의 태스크 체인이 만들어지고 발생합니다 StackOverflowException
. 각 루프 호출에 비동기 작업이 추가되면 코드는 기본적으로 바인딩되지 않은 힙을 사용합니다. 예를 들어 명시적 루프를 사용하도록 이 코드를 전환하는 것이 좋습니다.
let taskLoopGood (count: int) : Task<string> =
task {
for i in count .. 1 do
printfn $"looping... count = {count}"
return "done!"
}
let t = taskLoopGood 10000000
t.Wait()
비동기 tailcall이 필요한 경우 tailcall을 지원하는 F# 비동기 식을 사용합니다. 예시:
let rec asyncLoopGood (count: int) =
async {
if count = 0 then
return "done!"
else
printfn $"looping..., count = {count}"
return! asyncLoopGood (count-1)
}
let t = asyncLoopGood 1000000 |> Async.StartAsTask
t.Wait()
작업 구현
작업은 F# 6의 새로운 기능인 다시 실행 가능한 코드를 사용하여 구현됩니다. 작업은 F# 컴파일러에 의해 "다시 열 수 있는 상태 컴퓨터"로 컴파일됩니다. 이러한 내용은 다시 실행 가능한 코드 RFC 및 F# 컴파일러 커뮤니티 세션에서 자세히 설명합니다.
참고 항목
.NET