F'de zaman uyumsuz programlama#

Zaman uyumsuz programlama, çeşitli nedenlerle modern uygulamalar için gerekli olan bir mekanizmadır. Çoğu geliştiricinin karşılaşacağı iki birincil kullanım örneği vardır:

  • Çok sayıda eşzamanlı gelen isteğe hizmet verebilen bir sunucu işlemi sunmak, istek işleme sırasında kullanılan sistem kaynaklarını en aza indirirken bu işlem dışındaki sistemlerden veya hizmetlerden gelen girişleri bekler
  • Arka plan çalışmasını eşzamanlı olarak ilerlerken duyarlı bir kullanıcı arabirimini veya ana iş parçacığını koruma

Arka plan çalışması genellikle birden çok iş parçacığının kullanımını içerse de, zaman uyumsuz ve çok iş parçacıklı kavramlarını ayrı ayrı göz önünde bulundurmak önemlidir. Aslında bunlar ayrı endişelerdir ve biri diğerini ima etmez. Bu makalede ayrı kavramlar daha ayrıntılı olarak açıklanmaktadır.

Zaman uyumsuz tanımlandı

Bir önceki nokta ( zaman uyumsuzluk birden çok iş parçacığının kullanımından bağımsızdır) biraz daha açıklamaya değer. Bazen birbiriyle ilişkili ancak birbirinden tamamen bağımsız olan üç kavram vardır:

  • Eşzamanlılık; birden çok hesaplama çakışan zaman aralıklarında yürütür.
  • Parallelism; birden çok hesaplama veya tek bir hesaplamanın birkaç bölümü tam olarak aynı anda çalıştırıldığında.
  • Zaman uyumsuz; bir veya daha fazla hesaplama ana program akışından ayrı olarak yürütülebildiğinde.

Üçü de ortogonal kavramlardır, ancak özellikle birlikte kullanıldığında kolayca birleştirilebilir. Örneğin, birden çok zaman uyumsuz hesaplamayı paralel olarak yürütmeniz gerekebilir. Bu ilişki paralellik veya zaman uyumsuzluk birbirini ima etmez.

"Zaman uyumsuz" sözcüğünün etymolojisini dikkate alırsanız, iki parça söz konusudur:

  • "a", "değil" anlamına gelir.
  • "zaman uyumlu", "aynı anda" anlamına gelir.

Bu iki terimi bir araya getirdiğinizde , "zaman uyumsuz" ifadesinin "aynı anda değil" anlamına geldiğini görürsünüz. İşte hepsi bu! Bu tanımda eşzamanlılık veya paralelliğin bir etkisi yoktur. Bu, pratikte de geçerlidir.

Pratik olarak, F# dilindeki zaman uyumsuz hesaplamalar ana program akışından bağımsız olarak yürütülecek şekilde zamanlanır. Bu bağımsız yürütme eşzamanlılık veya paralellik anlamına gelmez ve hesaplamanın her zaman arka planda gerçekleştiği anlamına gelmez. Aslında, zaman uyumsuz hesaplamalar, hesaplamanın doğasına ve hesaplamanın yürütülmekte olduğu ortama bağlı olarak zaman uyumlu olarak bile yürütülebilir.

Sahip olmanız gereken temel nokta, zaman uyumsuz hesaplamaların ana program akışından bağımsız olmasıdır. Zaman uyumsuz hesaplamanın ne zaman veya nasıl yürütülmesi konusunda birkaç garanti olsa da, bunları düzenlemeye ve zamanlamaya yönelik bazı yaklaşımlar vardır. Bu makalenin geri kalanında F# zaman uyumsuzuna yönelik temel kavramlar ve F# içinde yerleşik olan türlerin, işlevlerin ve ifadelerin nasıl kullanılacağı incelenmektedir.

Temel kavramlar

F# dilinde zaman uyumsuz programlama iki temel kavram etrafında ortalanır: zaman uyumsuz hesaplamalar ve görevler.

  • Bir Async<'T> görev oluşturmak için başlatılabilen, birleştirilebilir zaman uyumsuz hesaplamayı temsil eden ifadeleri olan async { } tür.
  • Task<'T> Yürütülen bir .NET görevini temsil eden ifadeler içeren task { } tür.

Genel olarak, görevleri kullanan .NET kitaplıklarıyla birlikte kullanıyorsanız ve zaman uyumsuz kod tailcalls veya örtük iptal belirteci yayma kullanmıyorsanız, yeni kodda kullanmayı düşünmelisiniz task {…}async {…} .

Zaman uyumsuzun temel kavramları

Aşağıdaki örnekte "zaman uyumsuz" programlamanın temel kavramlarını görebilirsiniz:

open System
open System.IO

// Perform an asynchronous read of a file using 'async'
let printTotalFileBytesUsingAsync (path: string) =
    async {
        let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
        let fileName = Path.GetFileName(path)
        printfn $"File {fileName} has %d{bytes.Length} bytes"
    }

[<EntryPoint>]
let main argv =
    printTotalFileBytesUsingAsync "path-to-file.txt"
    |> Async.RunSynchronously

    Console.Read() |> ignore
    0

Örnekte işlevi printTotalFileBytesUsingAsync türündedir string -> Async<unit>. işlevini çağırmak aslında zaman uyumsuz hesaplamayı yürütmez. Bunun yerine, zaman uyumsuz olarak yürütülecek işin belirtimi olarak davranan bir döndürürAsync<unit>. gövdesini çağırır Async.AwaitTask ve bu da sonucunu ReadAllBytesAsync uygun bir türe dönüştürür.

Bir diğer önemli satır da çağrısıdır Async.RunSynchronously. Bu, gerçekten bir F# zaman uyumsuz hesaplaması yürütmek istiyorsanız çağırmanız gereken Zaman Uyumsuz modül başlatma işlevlerinden biridir.

Bu, C#/Visual Basic programlama stilinde temel bir farktır async . F# dilinde zaman uyumsuz hesaplamalar Soğuk görevler olarak düşünülebilir. Açıkça yürütülmeye başlanmalıdır. Bu, C# veya Visual Basic'e kıyasla zaman uyumsuz çalışmayı birleştirmenize ve sıralamanıza olanak sağladığından bazı avantajları vardır.

Zaman uyumsuz hesaplamaları birleştirme

Hesaplamaları birleştirerek öncekini oluşturan bir örnek aşağıda verilmiştir:

open System
open System.IO

let printTotalFileBytes path =
    async {
        let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
        let fileName = Path.GetFileName(path)
        printfn $"File {fileName} has %d{bytes.Length} bytes"
    }

[<EntryPoint>]
let main argv =
    argv
    |> Seq.map printTotalFileBytes
    |> Async.Parallel
    |> Async.Ignore
    |> Async.RunSynchronously

    0

Gördüğünüz gibi işlevin main birkaç öğesi daha vardır. Kavramsal olarak aşağıdakileri yapar:

  1. komut satırı bağımsız değişkenlerini ile Seq.mapbir Async<unit> dizi hesaplamaya dönüştürün.
  2. Çalıştırıldığında hesaplamaları paralel olarak zamanlayan ve çalıştıran printTotalFileBytes bir Async<'T[]> oluşturun.
  3. Paralel hesaplamayı çalıştıracak ve sonucunu yoksayacak bir Async<unit> oluşturun (bu bir unit[]).
  4. tamamlanana kadar, ile Async.RunSynchronouslygenel oluşturulan hesaplamayı açıkça çalıştırın.

Bu program çalıştırıldığında, printTotalFileBytes her komut satırı bağımsız değişkeni için paralel olarak çalışır. Zaman uyumsuz hesaplamalar program akışından bağımsız olarak yürütülürken, bilgilerini yazdırdıkları ve yürütmeyi bitirdikleri tanımlı bir sıra yoktur. Hesaplamalar paralel olarak zamanlanır, ancak bunların yürütme sırası garanti değildir.

Zaman uyumsuz hesaplamaları sırala

Zaten çalışan bir görev yerine çalışmanın belirtimi olduğundan Async<'T> , daha karmaşık dönüştürmeleri kolayca gerçekleştirebilirsiniz. Aşağıda bir zaman uyumsuz hesaplama kümesini sıralayan bir örnek verilmiştir ve bunlar birer birer yürütülür.

let printTotalFileBytes path =
    async {
        let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
        let fileName = Path.GetFileName(path)
        printfn $"File {fileName} has %d{bytes.Length} bytes"
    }

[<EntryPoint>]
let main argv =
    argv
    |> Seq.map printTotalFileBytes
    |> Async.Sequential
    |> Async.Ignore
    |> Async.RunSynchronously
    |> ignore

Bu, öğelerini paralel olarak zamanlamak yerine öğelerinin argv sırasına göre yürütülecek şekilde zamanlanırprintTotalFileBytes. Önceki hesaplamanın yürütülmesi bitene kadar ardışık her işlem zamanlanmayacağından, hesaplamalar yürütülürken çakışma olmayacak şekilde sıralanır.

Önemli Zaman Uyumsuz modül işlevleri

F# dilinde zaman uyumsuz kod yazdığınızda genellikle sizin için hesaplamaların zamanlamasını işleyen bir çerçeveyle etkileşim kurarsınız. Ancak, her zaman böyle değildir, bu nedenle zaman uyumsuz çalışma zamanlamak için kullanılabilecek çeşitli işlevleri anlamak iyidir.

F# zaman uyumsuz hesaplamaları zaten yürütülmekte olan işin bir gösterimi yerine çalışmanın belirtimi olduğundan, açıkça bir başlangıç işleviyle başlatılmalıdır. Farklı bağlamlarda yararlı olan birçok Zaman Uyumsuz başlangıç yöntemi vardır. Aşağıdaki bölümde, daha yaygın başlangıç işlevlerinden bazıları açıklanmaktadır.

Async.StartChild

Zaman uyumsuz bir hesaplama içinde bir alt hesaplama başlatır. Bu, birden çok zaman uyumsuz hesaplamanın eşzamanlı olarak yürütülmesini sağlar. Alt hesaplama, üst hesaplamayla bir iptal belirteci paylaşır. Üst hesaplama iptal edilirse, alt hesaplama da iptal edilir.

İmza:

computation: Async<'T> * ?millisecondsTimeout: int -> Async<Async<'T>>

Kullanılması gereken durumlar:

  • Birden çok zaman uyumsuz hesaplamayı aynı anda değil aynı anda yürütmek istediğinizde, ancak bunların paralel olarak zamanlanmasını istemediğinizde.
  • Bir alt hesaplamanın ömrünü üst hesaplamanınkine bağlamak istediğinizde.

Dikkat etmek gerekenler:

  • ile birden çok hesaplama başlatmak, bunları paralel olarak zamanlamakla Async.StartChild aynı değildir. Hesaplamaları paralel olarak zamanlamak istiyorsanız kullanın Async.Parallel.
  • Bir üst hesaplamanın iptal edilmesi, başlattığı tüm alt hesaplamaların iptalini tetikler.

Async.StartImmediate

Geçerli işletim sistemi iş parçacığından hemen başlayarak zaman uyumsuz bir hesaplama çalıştırır. Hesaplama sırasında çağıran iş parçacığında bir şeyi güncelleştirmeniz gerekiyorsa bu yararlı olur. Örneğin, zaman uyumsuz bir hesaplamanın bir kullanıcı arabirimini güncelleştirmesi gerekiyorsa (ilerleme çubuğunu güncelleştirme gibi), Async.StartImmediate kullanılmalıdır.

İmza:

computation: Async<unit> * ?cancellationToken: CancellationToken -> unit

Kullanılması gereken durumlar:

  • Zaman uyumsuz hesaplamanın ortasındaki çağıran iş parçacığında bir şeyi güncelleştirmeniz gerektiğinde.

Dikkat etmek gerekenler:

  • Zaman uyumsuz hesaplamadaki kod, zamanlanan iş parçacığında çalışır. Bu iş parçacığı ui iş parçacığı gibi bir şekilde hassassa bu sorun olabilir. Bu gibi durumlarda, Async.StartImmediate kullanımı büyük olasılıkla uygun değildir.

Async.StartAsTask

İş parçacığı havuzunda bir hesaplama yürütür. Hesaplama sonlandırıldıktan (sonucu üretir, özel durum oluşturur veya iptal edilir) ilgili durumda tamamlanacak bir Task<TResult> döndürür. İptal belirteci sağlanmazsa varsayılan iptal belirteci kullanılır.

İmza:

computation: Async<'T> * ?taskCreationOptions: TaskCreationOptions * ?cancellationToken: CancellationToken -> Task<'T>

Kullanılması gereken durumlar:

  • Zaman uyumsuz bir hesaplamanın sonucunu göstermek için bir Task<TResult> veren bir .NET API'sine çağrı yapmanız gerektiğinde.

Dikkat etmek gerekenler:

  • Bu çağrı ek bir Task nesne ayırır ve bu da sık kullanılırsa ek yükü artırabilir.

Async.Parallel

Paralel olarak yürütülecek zaman uyumsuz hesaplamaların bir dizisini zamanlar ve bir sonuç dizisi sağlandıkları sırayla sonuç verir. Paralellik derecesi isteğe bağlı olarak parametresi belirtilerek maxDegreeOfParallelism ayarlanabilir/kısıtlanabilir.

İmza:

computations: seq<Async<'T>> * ?maxDegreeOfParallelism: int -> Async<'T[]>

Ne zaman kullanılır:

  • Aynı anda bir dizi hesaplama çalıştırmanız gerekiyorsa ve bunların yürütme sırasına hiç bağlı değilseniz.
  • Tümü tamamlanana kadar paralel olarak zamanlanmış hesaplama sonuçlarına ihtiyacınız yoksa.

Dikkat etmek gerekenler:

  • Sonuçta elde edilen değer dizisine yalnızca tüm hesaplamalar tamamlandıktan sonra erişebilirsiniz.
  • Hesaplamalar zamanlandıklarında çalıştırılır. Bu davranış, yürütme sıralarına güvenemeyeceğiniz anlamına gelir.

Async.Sequential

Geçirilme sırasına göre yürütülecek zaman uyumsuz hesaplamalar dizisi zamanlar. İlk hesaplama yürütülür, sonra bir sonraki, vb. Paralel olarak hiçbir hesaplama yürütülmeyecek.

İmza:

computations: seq<Async<'T>> -> Async<'T[]>

Ne zaman kullanılır:

  • Birden çok hesaplamayı sırayla yürütmeniz gerekiyorsa.

Dikkat etmek gerekenler:

  • Sonuçta elde edilen değer dizisine yalnızca tüm hesaplamalar tamamlandıktan sonra erişebilirsiniz.
  • Hesaplamalar, bu işleve geçirilme sırasına göre çalıştırılır ve bu da sonuçlar döndürülmeden önce daha fazla süre geçmesi anlamına gelebilir.

Async.AwaitTask

Verilenin Task<TResult> tamamlanmasını bekleyen ve sonucunu bir olarak döndüren zaman uyumsuz bir hesaplama döndürür Async<'T>

İmza:

task: Task<'T> -> Async<'T>

Kullanılması gereken durumlar:

  • F# zaman uyumsuz hesaplama içinde bir Task<TResult> döndüren bir .NET API'sini kullanırken.

Dikkat etmek gerekenler:

  • Özel durumlar Görev Paralel Kitaplığı kuralına göre sarmalanır AggregateException ; bu davranış F# zaman uyumsuz genel olarak özel durumları ortaya çıkarma biçiminden farklıdır.

Async.Catch

Belirli Async<'T>bir öğesini yürüten ve döndüren zaman uyumsuz bir Async<Choice<'T, exn>>hesaplama oluşturur. Verilen Async<'T> başarıyla tamamlanırsa, sonuç değeriyle bir Choice1Of2 döndürülür. Tamamlanmadan önce bir özel durum oluşturulursa, yükseltilmiş özel durumla birlikte bir Choice2of2 döndürülür. Kendisi birçok hesaplamadan oluşan zaman uyumsuz bir hesaplamada kullanılıyorsa ve bu hesaplamalardan biri bir özel durum oluşturuyorsa, kapsayan hesaplama tamamen durdurulur.

İmza:

computation: Async<'T> -> Async<Choice<'T, exn>>

Kullanılması gereken durumlar:

  • Bir özel durumla başarısız olabilecek zaman uyumsuz bir çalışma gerçekleştirirken ve çağıranda bu özel durumu işlemek istediğinizde.

Dikkat etmek gerekenler:

  • Birleştirilmiş veya sıralı zaman uyumsuz hesaplamalar kullanılırken, "iç" hesaplamalarından biri özel durum oluşturursa, kapsayan hesaplama tamamen durur.

Async.Ignore

Verilen hesaplamayı çalıştıran ancak sonucunu düşüren zaman uyumsuz bir hesaplama oluşturur.

İmza:

computation: Async<'T> -> Async<unit>

Kullanılması gereken durumlar:

  • Sonucu gerekmeyen zaman uyumsuz bir hesaplamanız olduğunda. Bu, zaman uyumsuz kod işlevine benzer ignore .

Dikkat etmek gerekenler:

  • kullanmak istediğiniz için veya gerektiren Async<unit>başka bir işlev kullanmanız Async.StartAsync.Ignore gerekiyorsa, sonucu atmanın sorun olup olmadığını göz önünde bulundurun. Sonuçları yalnızca tür imzasına sığacak şekilde atmaktan kaçının.

Async.RunSynchronously

Zaman uyumsuz bir hesaplama çalıştırır ve çağrılan iş parçacığında sonucunu bekler. Hesaplama bir özel durum oluşturursa bir özel durum yayılır. Bu çağrı engelleniyor.

İmza:

computation: Async<'T> * ?timeout: int * ?cancellationToken: CancellationToken -> 'T

Ne zaman kullanılır:

  • Gerekirse, yürütülebilir dosyanın giriş noktasında bir uygulamada yalnızca bir kez kullanın.
  • Performansı önemsemiyorsanız ve aynı anda bir dizi diğer zaman uyumsuz işlemi yürütmek istediğinizde.

Dikkat etmek gerekenler:

  • Çağırma Async.RunSynchronously , yürütme tamamlanana kadar çağıran iş parçacığını engeller.

Async.Start

İş parçacığı havuzunda döndüren unit zaman uyumsuz bir hesaplama başlatır. Tamamlanmasını beklemez ve/veya bir özel durum sonucunu gözlemler. ile Async.Start başlatılan iç içe hesaplamalar, bunları çağıran üst hesaplamadan bağımsız olarak başlatılır; yaşam süreleri hiçbir üst hesaplamaya bağlı değildir. Üst hesaplama iptal edilirse, hiçbir alt hesaplama iptal edilmez.

İmza:

computation: Async<unit> * ?cancellationToken: CancellationToken -> unit

Yalnızca şu durumlarda kullanın:

  • Bir sonuç vermeyen ve/veya bir hesaplamanın işlenmesini gerektirmeyen zaman uyumsuz bir hesaplamanız var.
  • Zaman uyumsuz bir hesaplamanın ne zaman tamamlanmasını bilmeniz gerekmez.
  • Zaman uyumsuz hesaplamanın hangi iş parçacığında çalıştırıldığı sizin için önemli değildir.
  • Yürütmeden kaynaklanan özel durumları bilmeniz veya bildirmeniz gerekmez.

Dikkat etmek gerekenler:

  • ile başlatılan Async.Start hesaplamalar tarafından tetiklenen özel durumlar çağırana yayılmaz. Çağrı yığını tamamen kaldırılacaktır.
  • ile Async.Start başlatılan herhangi bir çalışma (çağırma printfngibi), bir programın yürütülmesinin ana iş parçacığında etkinin olmasına neden olmaz.

.NET ile birlikte çalışma

Programlama kullanıyorsanızasync { }, zaman uyumsuz/await stili zaman uyumsuz programlama kullanan bir .NET kitaplığı veya C# kod tabanıyla birlikte çalışmanız gerekebilir. C# ve .NET kitaplıklarının çoğunluğu temel soyutlamaları olarak ve Task türlerini kullandığındanTask<TResult>, bu, F# zaman uyumsuz kodunuzu yazma yönteminizi değiştirebilir.

Seçeneklerden biri, .NET görevlerini doğrudan kullanarak task { }yazmaya geçmektir. Alternatif olarak, .NET zaman uyumsuz hesaplamasını beklemek için işlevini kullanabilirsiniz Async.AwaitTask :

let getValueFromLibrary param =
    async {
        let! value = DotNetLibrary.GetValueAsync param |> Async.AwaitTask
        return value
    }

.NET çağırana Async.StartAsTask zaman uyumsuz bir hesaplama geçirmek için işlevini kullanabilirsiniz:

let computationForCaller param =
    async {
        let! result = getAsyncResult param
        return result
    } |> Async.StartAsTask

kullanan Task API'lerle çalışmak için (bir değer döndürmeyen .NET zaman uyumsuz hesaplamaları), öğesini öğesine dönüştürecek Async<'T> ek bir Taskişlev eklemeniz gerekebilir:

module Async =
    // Async<unit> -> Task
    let startTaskFromAsyncUnit (comp: Async<unit>) =
        Async.StartAsTask comp :> Task

Giriş olarak kabul eden bir Async.AwaitTaskTask zaten var. Bu ve önceden tanımlanmış startTaskFromAsyncUnit işlevle, F# zaman uyumsuz bir hesaplamadan türleri başlatabilir ve bekleyebilirsiniz Task .

.NET görevlerini doğrudan F'de yazma#

F# dilinde görevleri doğrudan kullanarak task { }yazabilirsiniz, örneğin:

open System
open System.IO

/// Perform an asynchronous read of a file using 'task'
let printTotalFileBytesUsingTasks (path: string) =
    task {
        let! bytes = File.ReadAllBytesAsync(path)
        let fileName = Path.GetFileName(path)
        printfn $"File {fileName} has %d{bytes.Length} bytes"
    }

[<EntryPoint>]
let main argv =
    let task = printTotalFileBytesUsingTasks "path-to-file.txt"
    task.Wait()

    Console.Read() |> ignore
    0

Örnekte işlevi printTotalFileBytesUsingTasks türündedir string -> Task<unit>. İşlevi çağırmak görevi yürütmeye başlar. çağrısı task.Wait() görevin tamamlanmasını bekler.

Çok iş parçacıklı ilişki

Bu makalenin tamamında yazışmadan bahsedilse de, hatırlamanız gereken iki önemli şey vardır:

  1. Geçerli iş parçacığında açıkça başlatılmadığı sürece, zaman uyumsuz hesaplama ile iş parçacığı arasında benzim yoktur.
  2. F# dilinde zaman uyumsuz programlama, çok iş parçacığı oluşturma için bir soyutlama değildir.

Örneğin, bir hesaplama, işin niteliğine bağlı olarak çağıranın iş parçacığında çalışabilir. Hesaplama ayrıca iş parçacıkları arasında "atlayabilir", "bekleme" dönemleri arasında (örneğin, bir ağ çağrısının aktarımda olduğu durumlarda) yararlı işler yapmak için az bir süre ödünç alabilir.

F# geçerli iş parçacığında zaman uyumsuz hesaplama başlatmak için bazı yetenekler sağlasa da (veya açıkça geçerli iş parçacığında değil), zaman uyumsuz genellikle belirli bir iş parçacığı stratejisiyle ilişkilendirilmemiştir.

Ayrıca bkz.