LoginSignup
0
0

Swift 24 スイフト構造化された同時性概要

Posted at

concurrencyは複数の作業を並列に実行するソフトウェア機能で定義できます。多いアフリ開発プロジェクトはある時点で同時処理をしようしかならないし、良い使用者経験を提供する為に必須的です。
今度の章にはスイフトプログラミング言語のStructeredconcurrency機能を見てみてそのような機能を使用してアプリプロジェクトでもマルチタスクサポートする方法を説明する

24.1 スレッド概要

threadは現代CPUの機能で全てのMultitaskingOperatingSystemで同時性をの基盤を提供します。現代CPUは複数のスレッドを実行することができますが、一度で並列で実行できる実際の数はCPUコーアの数で制限されます・CPUのモデルによって一般的に4から16コーア。CPUコーアよりより多くスレッドが必要な場合にOperatingSystemはスレッドスケジュりんぐをを実行して、これらのスレッドの実行を使用可能されるコーア間にどのように共有するかを決めます。

スレッドはメインプロセス内で実行されるミニプロセスと考えられることができますし、その目的はアフリのコード内で並列実行の形を可能される様にすることです。構造化された同時性がバックラウンドでスレッドを使用しますが全ての複雑性を処理するので自身が直接に相互作用するひつよはありません。

24.2 アフリのメインスレッド

アフリが初めて開始時ランタームシステームは普段アフリが基本的に実行される単一のスレッドを生成します。このスレッドをMainThreadだといいます。主要役目はUIレイアウトレンダリング、イベント処理及び使用者インタフェイスでビューと使用者相互昨業の面では使用者インタフェースを処理することです。
image.png
メインスレッドを使用して時間消耗的な作業を実行するアフリ内の方のコードは時間消耗的な作業が完了するまで全体アフリが止まる様に見えます。此れは、メインスレッドが他の作業を妨害されずに続くられるように実行する作業を別度のスレッドで初めて避けれます
image.png

24.3 completion handler

Swiftはこれを使ってasynchronous codeを実装されました。ここではasynchronous operationsが開始され作業が完了したら呼び出される様に完了ハンドラーが割り振られます。反面、メインアフリコードは非同期作業がバッグラウンドで実行される間に継続的に実行されます。asynchronous operationsがが完了したら完了ハンドラーが呼び出されて結果を伝達されます。そうすると完了ハンドラーの本文で該当結果を処理します。
image.png
残念ながらcompletion handlerは作成及び理解が難しいアーラが発生しやすいコード構成をもたらすことがあります。またcompletion handlerは`asynchronous作業で発生するエラーを処理に適していないし一般的に大きくて複雑な重畳コードが作成されます。

24.4 ストラクト化されたasynchronous

此れはスイフト5.5で導入されてアフリ開発者が同時実行をより簡単に実行されます。言い直して、ストラクト化された同時性コードはロジク流れを理解するためにcompletion handlerをコードで再移動必要なくて上から下で読むことができます。また、ストラクト化されたasynchronousは非同期func(){ }で発生されるエラーをより易く処理することができます。
image.png

24.5 プロジェクト準備する

struct ContentView: View {
    var body: some View {
        Button(action:{
            doSomething()
        }){
            Text("Do something")
        }
    } //var body
    func doSomething(){
        
    }
    func takesTooLong(){
        
    }
} //struct ContentView

struct ContentView_Previews: PreviewProvider {
    static var previews: some View{
        ContentView()
    }
}

24.6 asynchronousコード

a・synchro・nous :
? not existing or happening at the same time.

 func doSomething(){
+        print("Start\(Date())")
+       takesTooLong()
+       print("End\(Date())")
    }
    func takesTooLong(){
+        sleep(5)
+        print("Async task completed at \(Date())")
    }
  • sleep()
    はfunctionが完了されるまでにメインスレッドをブロックする時間がかかる作業を実行る効果をシミュレーターします。
Start2023-11-14 06:14:43 +0000
Async task completed at 2023-11-14 06:14:48 +0000
End2023-11-14 06:14:48 +0000
Start2023-11-14 06:14:48 +0000

このタイムスタンプで主目するプインとは終了時間が始める時間後5秒だとということです。此れは doSomething()function内で呼ばれた後の全てのコードが呼び出しが返されるまで実行しなかったと知らせます。使用者には5秒間アフリが止まれる様に見えます。
これはasync/await 同時性ストラクトを実行して解決できます。

24.7 async/await 同時性紹介

ストラクトされた同時性の基本はasync/await組である。asyncキーワードfunc(){ }が呼び出されたスレッドに対して非同期的に実行されるを表示する為にfuncを宣言する時に使用者されます。従って二つの例題全てを次の様に宣言するしなければなりません

+ func doSomething()async{
        print("Start\(Date())")
        takesTooLong()
        print("End\(Date())")
    }
+ func takesTooLong()async{
        sleep(5)
        print("Async task completed at \(Date())")
    }

func(){ }をasyncで表示すればあるいくつかの目標を達成することができます。まず此れはfunc(){ }のコードが呼び出されるスレッドと他のスレッドで実行するべきを現れます。
またシステムが他の作業を実行する様に実行する間にfunc(){ }自体が一時停止されることをシステム知らせます。

async関数に対して主意することは、他のasync関数範囲内に限って呼び出すことです。今度の章の後方からご覧の通りTaskオブジェクトsyncコードasyncコード間の橋を提供するに使用されることができます。最後にasync関数他のasync関数呼び出す場合に、全ての下位作業も完了されるまでに上位関数を終了できません。
一番大事なことはfunc(){ }がasyncで宣言されるとawaitキーウードを通じて呼び出されることです。

24.8 sync関数でasync関数呼び出す

ストラクトされたsync性の規則はasync関数asyncコンテキスト内でだけで呼び出されることを現れます。プログラミングの進入点がsync関数なら、async関数がどうして呼び出せるかと疑問が生じます。その答えはsync関数内でTaskオブジェクトを使ってasync関数を始めることです。次の様に一つのasync関数を呼び出そうとするmain()というsync関数があると仮定しましましょう。

func main(){
doSomething()
}

此れはこのエラーを通知を表示します。

async' call in a function that does not support concurrency

いまできる唯一な選択肢はmain()をasync関数に作るとかストラクトされなかった作業でmain()関数を始めることです。main()をasync関数で宣言することが実行可能な選択肢では無いと仮定すると、この場合には

func main(){
+        Task{
+           await doSomething()
+        }
}

変更します。

24.9 awaitキーウード

前で明したように、awaitキーウードasync関数を呼び出す時に必要であり、一般的に他のasync関数範囲無いだけで使用されます。これなしてasync関数を呼び出するようにすると

Expression is 'async' but is not marked with 'await'

従って takesTooLong()関数を呼び出にはdoSomething()関数を変更が必要です。

 func doSomething()async{
        print("Start\(Date())")
+       await takesTooLong()
        print("End\(Date())")
    }

syncコンテキスト・ここではButtonビューのactionClosure・でasync doSomething()呼び出そうとするので、もう一度変更するべきです。この問題を解決するためにtaskオブジェクトを使ってdoSomething関数を始めるべきです。

start2023-11-14 08:57:53 +0000
Async task completed at 2023-11-14 08:57:58 +0000
End2023-11-14 08:57:58 +0000

ここでawaitキーウードが混乱するかもしれない、doSomething()関数は続行出来ずにtakesTooLong()関数が返却されるまで待ちするしか無いので、作業を呼び出したスレッドをまだ、阻止しているという印象を与える。実は作業は他のスレッドで行われましたがawaitキーウードは作業が完了されるまでに待ってとシステムに要求するすることです。そうなった理由は前で前述したようにスーパーasync関数は全てのサーブ関数が完了されるまでに完了し無いからです。その言葉はtakesTooLong関数次であるコードラーインが実行される前にasync takesTooLong()関数が返却される時まで待つすること以外には選択の余地がないことを意味します。次の節ではasync-let`バインディング表現式を使ってスーパー関数から待つことを後回しにする方法を説明します。だがそうする前にことコンテキストでawaitキーウードを使用するまたもう一つの効果を見る必要があります。

asyncCallを許可すること以外にもawaitキーウードはdoSomething()関数内で一時的な中断点を定義します。実行間にこの点に到達すると、doSomething()関数は一時的に止められることになり、実行中のスレッドに他の用途として使用されることを、システムに知らせます。これを通じてシステムは優先順位がもっと高い作業でリソースを割り合うことが出来ますし、後でdoSomething()関数を制御を↑ returnして実行を続くられる様にします。一時中断点を表示してdoSomething()関数はシステムが他の作業処理でリソースを一時割り当てられる様にして基本的にアプリの性能を良くします。システムの速度を考えると一時中断は1秒以上続けしなければアフリの全体性能に役立ちながら使用者目に留まらないことです。

例題コードでawaitキーウードを基本動作は実行を再開する前に呼び出された関数が↑ returnされる前まで待つすることです。しかしもっと一般的な要求事項はasync関数がバッグラウンドで実行される間に呼び出す関数の内でコードを継続実行することでする。此れはasync-letバインディングを使用して該当コードで後日まで持つことを引き摺れてできます。これを見せるために、まずtakesTooLong()関数が結果を・ここでは作業完了タイムスターンプ・を↑ returnされる様に修正しましょう。

+ func takesTooLong() async -> Date {
         sleep(5)
+        return Date()
}

次で、↑ returnされた結果をlet式を使用するだけどasyncキーウードで表示される変数で割り当うためにdoSomething()内呼び出すこと変更するべきです

func doSomething()async{
        print("Start\(Date())")
-       await takesTooLong()
+       async let result = takesTooLong()
        print("End\(Date())")
    }

これで、doSomething()関数内で結果値が↑ returnされた時前まで待機する場所を指定するだけでいいです。awaitキーウードを使ってresult変数で接近してこれを実行します。

func doSomething()async{
        print("Start\(Date())")
        async let result = takesTooLong()
+        print("After async-let \(Date())")
        //비동기 함수와 동시에 실행할 추가 코드가 여기에 온다
+        print("result = \(await result)")
        print("End\(Date())")
    }

結果値を出力時に、async takesTooLong()関数が結果値を↑ returnされるまで、実行を続けられないことをシステムに知らせる為にawaitを使用者しています。この時点で結果を使えるようになるまで実行が中止されます。しかし、async-letawait間の全てのコードはtakesTooLong()関数と同時に実行されます。

Start2023-11-15 05:23:01 +0000
After async-let 2023-11-15 05:23:01 +0000
result = 2023-11-15 05:23:06 +0000
End2023-11-15 05:23:06 +0000

after async-letメッセージにはresult = ↓ call↑ returnされるタイムスタンプより5秒早いタイムスタンプがあるのでtakesTooLong()が実行される間にコードが行われたことを確認できます。
GUEST_6045fc0c-6e10-4cd1-be45-91f61d61a595.jpeg

24.11 エラーハンドリング

ストラクト化された同時性のエラーハンドリングは throw/do/try/catch メカニズムを使用します。次の例題は既存のasync takesTooLong関数を修正してし遅延されたが特定範囲を超えるとエラーを発生します。

+enum DurationError: Error{
+        case tooLong
+        case tooShort
+    }
    
    func takesTooLong(delay: UInt32) async throws {
+        if delay < 5 {
+            throw DurationError.tooShort
+        } else if delay > 20 {
+            throw DurationError.tooLong
+        }
+       sleep(delay)
-       sleep(5)
+       print("Async task completed at \(Date())")
-       return Date()        
    }

これで関数が↓ callさればdo/try/catch 構文を使用してエラーを処理することができます。

func doSomething()async{
        print("Start\(Date())")
        do {
            try await takesTooLong(delay: 25)
        } catch DurationError.tooShort {
            print("Error: Duration too short")
        } catch DurationError.tooLong {
            print("Error : Duration too Long")
        } catch {
            print("UnknownError")
        }
        print("End\(Date())")
    }

実行された結果は次の様に出力されます。

Start2023-11-15 06:49:39 +0000
Error : Duration too Long
End2023-11-15 06:49:39 +0000

24.12 Task理解する 

async⌥で実行される全ての作業はスイフトのTaskクラスのインスタンス内で実行されます。アフリは複数の作業を同時に実行されるし、そのような作業をHierachyで構成することができます。
今度の章のアフリでバートンが押されるとasync⌥バージョンのdoSomething()関数がTaskインスタンス内で実行されます。takesTooLong()関数が↓ callされるとシステムは関数コードが実行されるサーブ作業を生成します。作業HierachyTree側面でこのサーブ作業スーパー作業のdoSomething)のサーブです。サーブ作業内で全てのasync⌥関数↓ callは該当作業のサーブになるようなものです。

この作業Hierachy構造はストラクトされたsync性が構築される基盤を形成します。例えばサーブ作業はスーパーからプライオリティと同じ属性を継承してHierachyストラクトは全てのサーブ作業がが完了される時までスーパー作業が終了しないようにします。

24.13 ストラクト化されていないsync

此れはTaskオブジェクトを使って個別の作業を直接に作ることができます。すでに見たように、ストラクト化されない作業の一般的な用途はsync関数内でasync⌥関数↓ callすることです。

ストラクト化されてない作業はいつでも外部で取り消すことができますし持つと多い柔軟性には作業を作くて管理する為に、もっと多い作業を実行しなければならない側面で少しの付加的努力が必要です。

ストラクト化されてない作業は Taskイニシャライザー↓ callして実行されるコードが含まれたクロージャーを提供して生成されて初まります。

Task{
await doSomething()
}

また、その様な作業はactor context、プライオリティ、作業ローカル変数などと↓ callされるスーパーストラクトを継承します。作業を作るときに、作業で新しいプライオリティを指定することもできます。

Task(priority: .high) { 
await doSomething()
}

此れは他の作業と関連して、作業をどの様に予定を立ルでに対する暗示をシステムに提供します。一番高いものから一番低いものまで

  • .high / .userInitiatied
  • .low/ .utility
  • .medium
  • .background
    作業が直接に生成されるとTaskインタフェイスに対するプリファランスを↓ callします。此れは作業を取り消すするとか、作業範囲外部からでにキャンセルされたかどうかどうかを確認するに使用されることができます。
Task(priority: .high) { 
await doSomething()
}
.
.
if (!task.isCancelled) {
    task.cancel()
}

24.14 分離された作業

detached taskはストラクト化しれない同時性のまた他の形で↓ callするスーパーから属性をインヘリットしない点で、ストラクトしなかった同時性と違い点があるます。分離された作業はTask.detached()を↓ callされて生成します。

Task.detached{
    await doSomething()
}

分離された作業でもプライオリティ値が伝達されることがあるし、前で説明したものと同じ技術を使用してキャンセルの有無を確認できます。

let detachedTask = Task.detached(priority: .medium) { 
await doSomething()
}
.
.
if (!detachedTask.isCancelled) {
    detachedTask.cancel()
}

What’s the difference between a task and a detached task?

If you create a new task using the regular Task initializer, your work starts running immediately and inherits the priority of the caller, any task local values, and its actor context. On the other hand, detached tasks also start work immediately, but do not inherit the priority or other information from the caller.

24.15 Task管理

ストラクト{}された作業を使用されるか、またはストラクト化されない作業を使用すると関係なく、Taskクラスは作業範囲内で作業を管理する為に使用される一連のstaticメソッドを提供します。例えば作業は生成される時に割り当てたプライオリティをし区別する為にcurrentPriority属性を提供します。

Task{
    let priority = Task.currentPriority
    await doSomething
}

残念ですけど此れは読み取り専用属性で、実行中ている作業のプライオリティを変更する為に使用できません。isCancelledプロパティに接近して作業が取り消したのかを確認できます。

    if Task.isCancelled {
    //作業正理修行
    }

キャンスルを感じするまた他の選択肢は、作業が取り消した場合にはCancellationErrorエラーを発生させるcheckCancellation()メソッド↓ callする事です。

do{
    try Task.checkCancellation()
    } catch {
    //作業し正理修行
}

Task メソッドの cancel()↓ callしていつでも自身の作業をキャンセル事ができます。

Task.cancel()

最後に、作業コード内で実行を安全に一時中断することができる位置が有ればyield()メソッドを通じてシステムに宣言できます。

Task.yield()

24.16 Task グループ

今まで例題は一つまたは二つの作業(スーパー サーフ)を作る事でした。それぞれの場合には、私たちはコードを作成する前にどのくらいの作業が必要かを学びました。しかし動的な条件に従って、複数の作業を同時に生成して実行する必要がある状況がしばしば発生します。例えば、配列の格項目とかforルーフ 中て別度の作業を開始する必要がある場合があります。
スイフトはTaskグループを提供してその様な状況を解決します。

作業グループを使うと可変的な数の作業を生成してwithThrowingTaskGroup()またはwithTaskGroup()関数を使って実装される様にします。その後、作業を生成する為のルーフ構造が該当クロージャー内で定義してaddTask()関数を↓ callしてそれぞれの新しい作業を追加します。

次の様に二つの関数を修正してそれぞれのtakesTooLong()関数のインスタンスを実行する五つの作業で構成された作業グループを作りましょう

   func doSomething()async{
        await withTaskGroup(of: Void.self){group in
            for i in 1...5 {
                group.addTask {
                    let result = await takesTooLong()
                    print("Completed Task \(i) = \(result)")
                }
            }
        }
    }
    
    func takesTooLong() async -> Date{
        sleep(5)
        return Date()
    }

前のコードを実行すると次の様な結果が表示される前に作業が実行されている間5秒遅延が発生するだろう。

Completed Task 5 = 2023-11-16 05:28:36 +0000
Completed Task 3 = 2023-11-16 05:28:36 +0000
Completed Task 4 = 2023-11-16 05:28:36 +0000
Completed Task 2 = 2023-11-16 05:28:36 +0000
Completed Task 1 = 2023-11-16 05:28:36 +0000

作業は全て同時に実行されたことを現れる同時な完了タイムスタンプを表示されます。作業が開始された順序通りに完了していないという点もあります。同時性を使用して作業するときTaskが生成された順序通りに完了される保証がいない点も心に留めておくことが重要です。

addTask()関数意外にも次を含まれて作業のグループ内で接近できる幾つのメソッドと属性があるます。

  • cancelAll() - グループの全ての作業をキャンセルするメソッド ↓ call
  • isCancelled - 作業グループがすでにキャンセルされたかの有無を示すboolプロパティ
  • isEmpty - 作業グループ内で作業が残っているかを有無を示すboolプロパティ

24.17 データ競争避ける

前のTaskグループの例題でグループはTaskの結果を保存しないです。言い直して結果はTaskグループの範囲を逸脱せずTaskが終了されば維持もされないです。例えば、各作業に対する作業番号と結果タイムスタンプをスイフトのデックショナリオブジェクトで保存したいと仮定しましょう。同期コードで作業すると次のような方法を考えられることができます。

 func doSomething()async{
        var timeStamps: [Int: Date] = [:]
        await withTaskGroup(of: Void.self){group in
            for i in 1...5 {
                group.addTask {
                 timeStamps[i] = await takesTooLong()
                }
            }
        }
    }

残念だけとエラーを表示します。

Mutation of captured var 'timeStamps' in concurrently-executing code

ここで問題は同時にデータに接近する複数の作業があるしdata race条件が発生される危険があることです。これは複数の作業が同時に同一なデータに接近を試みて、この様な作業の中で一つ以上の作業が書き作業を実行する時に発生します。此れは一般的に診断が難しいデータ破損問題が発生します。

一つの選択肢はデータを保存するactorを作ることです。actor及びその様な特定問題を解決する為に使用者される方法は25章で紹介します。

他の方法には作業結果を順序的に↑ returnしてディクショナリー で追加される様に作業グループを調整することです。元々はwithTaskGroup()関数の↑ returnターフを次の様にVoid.selfにして作業グループを結果を↑ returnしないと宣言した。

await withTaskGroup(of: Void.self) { group in

最初は、格TaskがTask番号(Int)とタイムスタンプ(Date)を含まれるTupleを↑ returnされる様に次の様に作業グループを設計することです。

  func doSomething()async{
        
        var timeStamps: [Int: Date] = [:]
        
        await withTaskGroup(of:(Int, Date).self){group in
            for i in 1...5 {
                group.addTask {
                 return(i, await takesTooLong())
                }
            }
        }
    }

次にはグループで↑ returnされた結果を処理する為に2番目のルーフを宣言するべきです。結果はasync⌥関数個別的に↑ returnされて、一度にすべての結果を処理するように簡単にループを作成できない。かく結果が↑ returnされる時まで持ちするべきです。その様な場合スイフトはfor-waitループを提供します。

24.18 for-await ループ

for-await 表現式を使用するとasync⌥的に↑ returnされる一連の値を一つずつループを回す事ができ、同時作業で↑ returnされる値の受信を待ちことができます。for-awaitを使用するための唯一な要求事項は一連のデータがAsyncSequenceプロトコル準拠することです。

例題では作業グルぷ範囲の中でaddTaskループ次にfor-awaitループを次のように追加するべきです。

   func doSomething()async{
        
        var timeStamps: [Int: Date] = [:]
        
        await withTaskGroup(of:(Int, Date).self){group in
            
            for i in 1...5 {
                group.addTask {
                 return(i, await takesTooLong())
                }
            }
+            for await (task, date) in group{
+                timeStamps[task]=date
+            }
        }//withTaskGroup
    }

各作業が↑ returnされるとfor-awaitループは結果tupleを受信してtimeStampsディクショナリーで保存します。これを確認する為に作業グループが終了された後でディクショナリー項目を出力する少しのコードを追加しましょう。

            for await (task, date) in group{
                timeStamps[task]=date
            }
        }//withTaskGroup
        
+        for (task, date) in timeStamps {
+            print("Task = \(task), Date = \(date)")
+        }

>>
Task = 5, Date = 2023-11-16 07:23:36 +0000
Task = 1, Date = 2023-11-16 07:23:36 +0000
Task = 4, Date = 2023-11-16 07:23:36 +0000
Task = 3, Date = 2023-11-16 07:23:36 +0000
Task = 2, Date = 2023-11-16 07:23:36 +0000

#24.19 async⌥プロパティ
async⌥関数ではなく、スイフトはクラスとストラクトタープ内でasync⌥プロパティもサポートします。次の例題にはasync⌥プロパティはgetterを明示的に宣言してasync表示で生成されます。現在には読む専用プロパティだけasync⌥になることができます。

struct MyStruct {
        var myResult: Date{
            get async {
                return await self.getTime()
            }
        }
        
        func getTime() async -> Date {
            sleep(5)
            return Date()
        }
    }
    
    func doSomething()async{
        
        let myStruct = myStruct()
        Task {
            let date = await myStruct.myResult
            print(date)
        }
    }

要約

現代CPUとOperatingSystemは同時に複数の作業を修行される様にコードが同時に行うように設計されました。此れは使用者インタフェイスをレンダリングする事と、使用者イベントで応答するし事を主にメインスレっとが担当して、他のスレッドにはその他の作業を実行することで可能になります。基本的にアフリの大部分のコードは他のスレッドで実行される様に特に設定しない限りメインスレッドでも実行されます。該当コードがあまりにも長い間メインスレッドを占有する作業を実行すると、作業が完了される時までアフリが止まったように見えます。これを避けるためにスイフトはストラクト化された同時性APIを提供します。これを使う時にメインスレッドを遮断するコードは別度のスレッドに行う様にasync⌥関数・asyncプロパティも支援・で配置されます。↓ callするコードはawaitキーウードを続く使用する前にasync⌥コードが完了される時までに待つようにさせるとか、async-letを使用して結果が必要な時まで継続行われる様に構成されることができます。

作業は個別的にまたは複数の作業のグループの形で実行されることができます。for-awaitループはasync⌥作業グループの結果を非同期的に処理する有用な方法を提供します。同時性で作業する時にはデータ競争を避ける事が大事で、普段はスイフトのActorを使って解決できます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0