이 문서에서는 비동 기 식과 유사하지만 .NET 작업을 직접 작성할 수 있는 작업 식에 대한 F#의 지원을 설명합니다. 비동기 식과 마찬가지로 작업 식은 다른 작업의 실행을 차단하지 않고 코드를 비동기적으로 실행합니다.
비동기 코드는 일반적으로 비동기 식을 사용하여 작성됩니다. .NET 작업을 만들거나 사용하는 .NET 라이브러리와 광범위하게 상호 운용하는 경우 작업 식을 사용하는 것이 좋습니다. 작업 식은 성능 및 디버깅 환경을 향상시킬 수도 있습니다. 그러나 작업 식에는 문서의 뒷부분에 설명된 몇 가지 제한 사항이 있습니다.
문법
task { expression }
이전 구문에서 나타내는 expression
계산은 .NET 작업으로 실행되도록 설정됩니다. 이 코드가 실행된 직후 작업이 시작되고 첫 번째 비동기 작업이 수행될 때까지 현재 스레드에서 실행됩니다(예: 비동기 절전 모드, 비동기 I/O 또는 기타 기본 비동기 작업). 식의 형식은 Task<'T>
, 여기서 '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>.
- 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 ..
이 포함됩니다.
with
및 finally
처리기를 제외하고 동기적으로 실행되는 추가 작업 구문을 포함할 수 있습니다. 비동기 try .. finally ..
가 필요한 경우, 형식 use
의 개체와 결합하여 IAsyncDisposable
바인딩을 사용하십시오.
use
및 use!
바인딩
작업 식 내에서 바인딩은 use
형식 IDisposable 또는 IAsyncDisposable.의 값에 바인딩할 수 있습니다. 후자의 경우 삭제 정리 작업이 비동기적으로 실행됩니다.
let!
외에도 use!
사용하여 비동기 바인딩을 수행할 수 있습니다.
let!
use!
차이는 let
use
간의 차이와 동일합니다.
use!
의 경우, 개체는 현재 범위가 종료될 때 처리됩니다. F# 6에서는 use!
가 값을 null로 초기화하는 것을 허용하지 않지만 use
는 가능합니다.
open System
open System.IO
open System.Security.Cryptography
task {
// use IDisposable
use httpClient = new Net.Http.HttpClient()
// use! Task<IDisposable>
use! exampleDomain = httpClient.GetAsync "https://example.com/data.enc"
// use IDisposable
use aes = Aes.Create()
aes.KeySize <- 256
aes.GenerateIV()
aes.GenerateKey()
// do! Task
do! File.WriteAllTextAsync("key.iv.txt", $"Key: {Convert.ToBase64String aes.Key}\nIV: {Convert.ToBase64String aes.IV}")
// use IAsyncDisposable
use outputStream = File.Create "secret.enc"
// use IDisposable
use encryptor = aes.CreateEncryptor()
// use IAsyncDisposable
use cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)
// do! Task
do! exampleDomain.Content.CopyToAsync cryptoStream
}
가치 작업
값 작업은 작업 기반 프로그래밍에서 할당을 방지하는 데 사용되는 구조체입니다. 값 작업은 .AsTask()
을 사용하여 실제 작업으로 전환되는 임시 값입니다.
작업 식에서 값 태스크를 만들려면 사용 |> ValueTask<ReturnType>
하거나 |> ValueTask
. 다음은 그 예입니다.
let makeTask() =
task { return 1 }
makeTask() |> ValueTask<int>
and!
바인딩(F# 10부터 시작)
작업 식 내에서 여러 비동기 작업(Task<'T>
ValueTask<'T>
Async<'T>
등)을 동시에 대기할 수 있습니다. 비교하다:
// We'll wait for x to resolve and then for y to resolve. Overall execution time is sum of two execution times.
let getResultsSequentially() =
task {
let! x = getX()
let! y = getY()
return x, y
}
// x and y will be awaited concurrently. Overall execution time is the time of the slowest operation.
let getResultsConcurrently() =
task {
let! x = getX()
and! y = getY()
return x, y
}
취소 토큰 및 취소 검사 추가
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 }
백그라운드 작업은 SynchronizationContext.Current
이 null이 아닌 스레드에서 시작된 경우, SynchronizationContext.Current
를 사용하여 스레드 풀의 백그라운드 스레드로 전환합니다. null SynchronizationContext.Current
이 있는 스레드에서 시작된 경우 동일한 스레드에서 실행됩니다.
비고
ConfigureAwait(false)
실제로 이는 F# 작업 코드에서 호출이 일반적으로 필요하지 않음을 의미합니다. 대신 백그라운드에서 실행하려는 작업은 .를 사용하여 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