Aracılığıyla paylaş


F# dilinde 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ı aynı anda ilerletirken duyarlı bir kullanıcı arayüzünü veya ana iş parçacığını korumak.

Arka plan çalışması genellikle birden çok iş parçacığının kullanımını içerse de, zaman uyumsuzluk ve çok iş parçacıklı kavramlarını ayrı ayrı ele almak ö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.

Asenkroni tanımı

Önceki nokta - zaman uyumsuzluğun birden çok iş parçacığının kullanımından bağımsız olduğu konusu - 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.
  • Paralellik; birden çok hesaplama veya tek bir hesaplamanın birkaç bölümü tam olarak aynı anda çalıştırıldığında.
  • Asenkroni; bir veya daha fazla hesaplama ana program akışının dışında ayrı olarak yürütüldüğünde.

Üçü de ortogonal kavramlardır, ancak özellikle birlikte kullanıldığında kolayca birleştirilebilir. Örneğin, birden çok eşzamanlı hesaplamayı paralel olarak gerçekleştirmeniz gerekebilir. Bu ilişki, paralellik ve asenkronun birbirini ima ettiği anlamına gelmez.

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

  • "a", "değil" anlamına gelir.
  • "synchronous", "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. Hepsi bu kadar! 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. Asenkron bir hesaplamanın ne zaman veya nasıl gerçekleştirileceğine dair az sayıda 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 üzerine kuruludur: eşzamanlı olmayan hesaplamalar ve görevler.

  • Başlatıldığında bir görev oluşturmak için birleştirilebilir zaman uyumsuz hesaplamayı temsil eden Async<'T> türü, async { } içerir.
  • Task<'T> türü, yürütülen bir .NET görevini temsil eden task { } ifadeler içerir.

Genel olarak, görevleri kullanan .NET kitaplıklarıyla birlikte çalışıyorsanız ve zaman uyumsuz kodun tailcall'ları veya örtük iptal belirteci yayılmasına güvenmiyorsanız, yeni kodda task {…}'i async {…}'e tercih etmeyi düşünmelisiniz.

Asenkronun 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>. Bir işlevi çağırmak aslında zaman uyumsuz hesaplamayı gerçekleştirmez. Bunun yerine, zaman uyumsuz olarak yürütülecek işin Async<unit> olarak davranan bir döndürür. Gövdesinde Async.AwaitTask çağrılır ve bu, ReadAllBytesAsync sonucunu uygun bir türe dönüştürür.

Diğer bir önemli satır ise Async.RunSynchronously çağrısıdır. Bu, gerçekten bir F# zaman uyumsuz hesaplamayı yürütmek istiyorsanız çağırmanız gereken Async modülünün başlatıcı 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 bir şekilde başlatılmalıdır ki gerçekten yürütülsün. 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 bir Async<unit> hesaplama dizisine, Seq.map ile dönüştürün.
  2. Çalıştırıldığında hesaplamaları paralel olarak zamanlayan ve çalıştıran Async<'T[]> bir printTotalFileBytes oluşturun.
  3. Paralel hesaplamayı çalıştıracak ve sonucunu göz ardı edecek bir Async<unit> oluşturun (bu bir unit[]).
  4. Genel olarak oluşturulan hesaplamayı Async.RunSynchronously ile açıkça çalıştırın ve tamamlanana kadar bekleyin.

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 değil, çalışmanın belirtimi olduğu için Async<'T> kullanarak daha karmaşık dönüşümleri kolayca gerçekleştirebilirsiniz. Zaman uyumsuz hesaplamaların sırayla yürütülmesini sağlayan bir örnek aşağıda verilmiştir.

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 printTotalFileBytes sırasına göre yürütülecek şekilde zamanlanırargv. Ö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 Asenkron 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 eşzamansız çalışma zamanlamak için kullanılabilecek çeşitli işlevleri anlamanın faydası vardır.

F# eşzamanlı olmayan hesaplamaları, zaten yürütülmekte olan işin bir temsili yerine çalışmanın bir tanımı olduğundan, açıkça bir başlangıç işleviyle başlatılmalıdır. Birçok farklı bağlamda yararlı olan Asenkron başlangıç metotları 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. Çocuk hesaplama, üst hesaplamayla iptal belirtecini paylaşır. Ana hesaplama iptal edilirse, alt hesaplama da iptal edilir.

İmza:

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

Ne zaman kullanılır:

  • 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.
  • Ebeveyn hesaplamanın iptal edilmesi, başlattığı tüm alt hesaplamaları iptal eder.

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

Ne zaman kullanılır:

  • Zaman uyumsuz bir hesaplama sırasında çağıran iş parçacığında bir şeyi güncellemeniz gerektiğinde.

Dikkat etmek gerekenler:

  • Zaman uyumsuz hesaplamadaki kod, denk geldiği herhangi bir iş parçacığında çalışır. Bu, kullanıcı arabirimi iş parçacığı gibi bir iş parçacığı bir şekilde hassassa 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ığında (sonucu ürettiğinde, istisna oluşturduğunda veya iptal edildiğinde) ilgili durumda tamamlanmış 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>

Ne zaman kullanılır:

  • 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 asenkron hesaplamalardan oluşan bir dizi planlanır ve temin edildikleri sıraya göre bir sonuçlar dizisi 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.
  • Eğer 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, planlandıkları zaman gerçekleştirilecektir. Bu davranış, onların yürütme sıralarına güvenemeyeceğiniz anlamına gelir.

Async.Sequential

Geçildikleri sırayla yürütülecek zaman uyumsuz hesaplamalar dizisini zamanlayın. İ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

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

İmza:

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

Ne zaman kullanılır:

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

Dikkat etmek gerekenler:

  • Özel durumlar, Görev Paralel Kitaplığı'nın kuralına göre AggregateException biçiminde sarmalanır; bu davranış, F# eşzamanlı olmayan işlemlerinin genelde özel durumları nasıl ortaya çıkardığından farklıdır.

Async.Catch

Belirli bir Async<'T> yürütüp bir Async<Choice<'T, exn>> döndüren zaman uyumsuz bir hesaplama oluşturur. Verilen Async<'T> başarıyla tamamlanırsa, sonuç değeriyle bir Choice1Of2 döndürülür. Tamamlanmadan önce bir istisna fırlatılırsa, oluşan istisna ile birlikte bir Choice2of2 döndürülür. Zaman uyumsuz bir hesaplama birçok alt hesaplamadan oluşuyorsa ve bu alt hesaplamalardan biri bir özel durum ortaya çıkarıyorsa, tüm kapsayıcı hesaplama durdurulacaktır.

İmza:

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

Ne zaman kullanılır:

  • 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ı asenkron hesaplamalar kullanılırken, "iç" hesaplamalardan biri bir istisna oluşturursa, kapsayan hesaplama tamamen durduracaktır.

Async.Ignore

Verilen hesaplamayı çalıştıran ancak sonucunu yok sayan asenkron bir hesaplama oluşturur.

İmza:

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

Ne zaman kullanılır:

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

Dikkat etmek gerekenler:

  • Eğer Async.Ignore veya Async.Start gerektiren başka bir işlevi kullanmak istediğiniz için Async<unit> kullanmanız gerekiyorsa, sonucu atmanın sorun olup olmadığını göz önünde bulundurun. Sonuçları yalnızca tür imzasına sığacak şekilde göz ardı etmekten 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şturduğunda bir istisna 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:

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

Async.Start

Zaman uyumsuz bir hesaplama başlatır ve unit iş parçacığı havuzunda geri döndürür. Tamamlanmasını beklemez veya bir özel durum sonucunu göz ardı eder. 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:

  • Async.Start ile başlatılan hesaplamalar tarafından tetiklenen özel durumlar çağırana aktarılmaz. Çağrı yığını tamamen kaldırılacaktır.
  • printfn ile başlatılan herhangi bir görev (örneğin Async.Start çağırma), programın ana iş parçacığında bir etkiye neden olmaz.

.NET ile birlikte çalışma

Eğer async { } programlama kullanıyorsanız, async/await stili asenkron programlama kullanan bir .NET kütüphanesi veya C# kod tabanıyla birlikte çalışmanız gerekebilir. C# ve çoğu .NET kütüphanesi temel soyutlamaları olarak Task<TResult> ve Task türlerini kullandığından, bu durum F# asenkron kodunuzu yazma şeklinizi 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 Async.AwaitTask işlevini kullanabilirsiniz.

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

Eşzamansız bir hesaplamayı .NET arayana geçirmek için Async.StartAsTask işlevini kullanabilirsiniz.

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

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

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

Zaten bir giriş olarak Async.AwaitTask kabul eden bir Task var. Bu ve önceden tanımlanmış startTaskFromAsyncUnit işleviyle F# zaman uyumsuz bir hesaplamadan Task türlerini başlatabilir ve bekleyebilirsiniz.

.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>. Görevi yürütmeye başlamak için işlevi çağırmak. çağrısı task.Wait() görevin tamamlanmasını bekler.

Çoklu iş parçacığı ile ilişki

Bu makale boyunca threading'den bahsedilse de, hatırlamanız gereken iki önemli şey vardır:

  1. Geçerli iş parçacığında açıkça başlatılmadıkça, zaman uyumsuz hesaplama ile iş parçacığı arasında bir ilişki yoktur.
  2. F# dilindeki zaman uyumsuz programlama, çoklu iş parçacıkları için bir soyutlama değildir.

Örneğin, bir hesaplama, işin niteliğine bağlı olarak kendisini çağıran iş parçacığında çalışabilir. Hesaplama ayrıca iş parçacıkları arasında "atlayabilir" ve "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 kısa bir süreliğine onları ö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 bakınız