読み込み中...

Swiftでの排他制御を初心者でもわかる15の実践的コード例で解説

Swiftの排他制御の使い方とサンプルコードをイラスト付きで解説するカバー画像 Swift
この記事は約26分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

この記事を読めば、Swiftでの排他制御を効果的に使うことができるようになります。

初心者の方でもステップバイステップで理解していただけるよう、基本から応用、実際のコード例まで詳しく解説しています。

Swiftのコーディングにおいて、排他制御は多くの場面で必要とされるテクニックの一つです。

それでは、Swiftとその中での排他制御の世界に一緒に深く潜り込んでみましょう。

●Swiftとは

Swiftは、Appleが開発したプログラミング言語で、iOSやmacOS、watchOSなどのAppleの各プラットフォーム向けのアプリケーション開発に利用されます。

近年、多くの開発者がObjective-CからSwiftへと移行しており、Swiftは現代のアプリ開発の主流となっています。

○Swiftの特徴

  • 高速性:Swiftは高速な実行性能を持ち、Objective-Cと比べても高いパフォーマンスを発揮します。
  • 安全性:Swiftの言語設計は、バグを減少させるためのさまざまな特性を持っています。例えば、nilの扱いや型の強制など、安全性を重視した設計がなされています。
  • モダンな文法:Swiftは読みやすく、書きやすい文法を持っています。これにより、コードが簡潔で理解しやすくなっています。

○Swiftでの排他制御の重要性

Swiftのプログラムは、複数のスレッドやタスクを同時に処理する場面が多々あります。

複数のスレッドが同時に同じデータにアクセスした場合、データの破損や意図しない動作を引き起こすことがあります。

このような問題を防ぐために、排他制御が必要となります。

Swiftでは、様々な方法で排他制御を実現することができ、それらの方法と実際の使用例を今後の項目で詳しく学んでいきましょう。

●排他制御の基本

排他制御とは、複数のプロセスやスレッドが同時に共有リソース(メモリやファイルなど)にアクセスすることを防ぐための手法のことを指します。

この制御を行うことで、データの破損やプログラムの不具合を防ぐことができます。

○排他制御とは?

排他制御は、その名の通り、一度アクセスしたリソースを他のプロセスやスレッドから「排他」することを意味します。

具体的には、あるスレッドがデータを読み書きしている間、他のスレッドはそのデータにアクセスすることができません。

この機能は、特にデータベースやファイルシステムなど、複数のスレッドやプロセスから同時にアクセスされる可能性のあるリソースに対して重要です。

○なぜ排他制御が必要か

ショッピングサイトのカートに商品を追加する際、複数のユーザーが同時にカートの更新を試みた場合、どのユーザーの更新を先に行うかが不明確となり、カートの内容が正しく反映されない可能性が高まります。

排他制御が適切に行われていないと、このようなデータの不整合が生じ、大きなトラブルの原因となります。

また、排他制御が不足している場合、次のような問題も生じる可能性があります。

  1. データの競合:複数のスレッドが同時に同じデータを書き込むことで、データが予期せず上書きされる。
  2. 不整合:一部のスレッドが古いデータを読み取り、その後に他のスレッドがデータを更新すると、古いデータを元に処理が進むため、不整合が生じる。
  3. パフォーマンスの低下:排他制御のためのロックが頻繁に行われると、システム全体のパフォーマンスが低下する可能性があります。

●Swiftでの排他制御の使い方

排他制御の概念を理解したところで、Swiftを使用した実践的な排他制御の手法について詳しく見ていきましょう。

○サンプルコード1:基本的な排他制御の実装

SwiftではDispatchSemaphoreを使って排他制御を実装することができます。

DispatchSemaphoreは、指定した数のリソースを同時にアクセスすることができるようになります。

一般的には、1つのリソースへのアクセスを許可する場合、セマフォの値を1に設定します。

import Dispatch

// セマフォの作成 (初期値は1)
let semaphore = DispatchSemaphore(value: 1)

// リソースへのアクセス開始
semaphore.wait() // リソースへのアクセスをブロック

// ここでリソースへの操作を行う
print("リソースを操作中")

// リソースへのアクセス終了
semaphore.signal() // リソースへのアクセスを開放

このコードではDispatchSemaphoreを使ってリソースへの同時アクセスを制限しています。

semaphore.wait()は、リソースへのアクセスをブロックし、semaphore.signal()はアクセスを開放する役割を持っています。

このコードを実行すると、”リソースを操作中”という文字が表示されます。

○サンプルコード2:複数のスレッドを持つアプリでの排他制御

多くのアプリケーションでは、複数のスレッドが動作しており、これらのスレッド間でのデータ競合を防ぐために、排他制御が必要になります。

ここでは、複数のスレッドで共有リソースにアクセスする場合のサンプルコードを紹介します。

import Dispatch

let semaphore = DispatchSemaphore(value: 1)
var sharedResource = 0

let globalQueue = DispatchQueue.global()

// スレッド1
globalQueue.async {
    semaphore.wait()
    sharedResource += 1
    print("スレッド1: \(sharedResource)")
    semaphore.signal()
}

// スレッド2
globalQueue.async {
    semaphore.wait()
    sharedResource += 2
    print("スレッド2: \(sharedResource)")
    semaphore.signal()
}

このコードの実行結果は、”スレッド1: 1″と”スレッド2: 3″または”スレッド2: 2″と”スレッド1: 3″のどちらかが表示されます。

この結果より、複数のスレッドでも共有リソースへのアクセスが正しく制御されていることがわかります。

○サンプルコード3:非同期タスクの排他制御

Swiftの非同期タスクは頻繁に利用されるが、これらのタスク間でのリソースの競合を避けるために排他制御は必須です。

特に、DispatchQueueを用いた非同期処理では、異なるスレッドからのリソースへのアクセスが重なる可能性があります。

非同期タスクにおける排他制御の一例として、非同期的にリソースへアクセスする際のセマフォの使用方法を見ていきましょう。

import Dispatch

let semaphore = DispatchSemaphore(value: 1)
var asyncResource = 0

let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)

concurrentQueue.async {
    for _ in 0..<100 {
        semaphore.wait()
        asyncResource += 1
        semaphore.signal()
    }
}

concurrentQueue.async {
    for _ in 0..<100 {
        semaphore.wait()
        asyncResource += 2
        semaphore.signal()
    }
}

DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
    print("非同期タスク完了後のリソースの値: \(asyncResource)")
}

このコードでは、2つの非同期タスクが同じリソースasyncResourceを更新しています。

セマフォを使用して、一度に1つのタスクだけがasyncResourceにアクセスできるようにしています。

asyncAfterメソッドを用いて、すべての非同期タスクが完了した後に、リソースの最終値を出力しています。

このコードを実行すると、”非同期タスク完了後のリソースの値: 300″と表示されます。

これは、最初の非同期タスクがリソースを100回1ずつ増加させ、次の非同期タスクがリソースを100回2ずつ増加させた結果です。

○サンプルコード4:データベースへのアクセス制御

データベースへのアクセスも競合が生じる可能性があるため、排他制御が必要です。

データベースの更新や読み取り時に他の操作が中断されないようにするための方法を見ていきましょう。

ここでは、データベースに対する排他制御のサンプルコードを紹介します。

import Dispatch

let dbSemaphore = DispatchSemaphore(value: 1)

func updateDatabase(data: String) {
    dbSemaphore.wait()
    // データベース更新の処理
    // ...
    print("データベースを更新: \(data)")
    dbSemaphore.signal()
}

func readDatabase() -> String {
    dbSemaphore.wait()
    // データベースからの読み取り処理
    // ...
    let result = "読み取りデータ"
    print(result)
    dbSemaphore.signal()
    return result
}

上記のコードは、updateDatabase関数とreadDatabase関数を用いてデータベースに対して書き込みと読み込みの操作を行っています。

セマフォを利用することで、これらの操作が同時に実行されないようにしています。

実際にコードを実行すると、データベースの更新や読み取りの際に、他の操作が中断されず、正しくデータベースの操作が行われることが確認できます。

●Swiftの排他制御の応用例

Swiftの排他制御技術は基本的な部分だけでなく、高度なプログラム設計にも適用されます。

ここでは、Swiftでの高度な排他制御の実践的な応用例を、サンプルコードと共にご紹介いたします。

○サンプルコード5:高度な非同期処理での排他制御

非同期処理において、特定の処理の完了を待ちながら他のタスクを進行させるケースが考えられます。

この際、適切な排他制御が不可欠です。

import Dispatch

let group = DispatchGroup()
let queue = DispatchQueue(label: "com.example.myQueue")
var sharedResource = [Int]()

queue.async(group: group) {
    for i in 1...5 {
        sharedResource.append(i)
        sleep(1)
    }
}

queue.async(group: group) {
    for i in 6...10 {
        sharedResource.append(i)
        sleep(2)
    }
}

group.notify(queue: DispatchQueue.main) {
    print("非同期処理が完了し、リソースの内容は \(sharedResource) です。")
}

このコードは、2つの非同期タスクをDispatchGroupを用いてグループ化し、それぞれのタスクが完了した際にメインキューで通知を受け取ります。

2つの非同期タスクは、共有リソースsharedResourceを更新しています。

結果として、”非同期処理が完了し、リソースの内容は [1,2,3,4,5,6,7,8,9,10] です。”という出力が得られます。

○サンプルコード6:データ競合を避けるための排他制御

複数のスレッドやタスクが同時にデータにアクセスする際、データの整合性を保つための排他制御が必要です。

import Dispatch

let dataSemaphore = DispatchSemaphore(value: 1)
var importantData = 0
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)

concurrentQueue.async {
    for _ in 0..<100 {
        dataSemaphore.wait()
        importantData += 1
        dataSemaphore.signal()
    }
}

concurrentQueue.async {
    for _ in 0..<100 {
        dataSemaphore.wait()
        importantData -= 1
        dataSemaphore.signal()
    }
}

DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
    print("データの最終的な値は \(importantData) です。")
}

上記のコードでは、セマフォを使用してimportantDataへのアクセスを排他制御しています。

一つのタスクはデータを増やし、もう一つのタスクはデータを減少させています。

結果として、”データの最終的な値は 0 です。”という出力が得られることから、競合なく処理が行われたことが確認できます。

○サンプルコード7:ユーザーインターフェースの排他制御

ユーザーインターフェース(UI)はアプリケーションの顔とも言える部分であり、多数の操作や更新が行われる場所です。

そのため、UIの更新や操作に関する排他制御は特に重要となります。

誤った排他制御は、アプリケーションの動作に不具合やクラッシュを引き起こす可能性があります。

例えば、あるボタンのクリック時に非同期処理を開始し、その結果をテキストラベルに表示するシチュエーションを考えてみましょう。

この際、排他制御を正しく行わないと、ユーザーが何度もボタンをクリックすることで、表示結果が予期しないものになる可能性があります。

import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var resultLabel: UILabel!
    let semaphore = DispatchSemaphore(value: 1)

    @IBAction func fetchData(_ sender: UIButton) {
        DispatchQueue.global().async { [weak self] in
            self?.semaphore.wait()
            let data = self?.fetchFromServer()
            DispatchQueue.main.async {
                self?.resultLabel.text = data
                self?.semaphore.signal()
            }
        }
    }

    func fetchFromServer() -> String {
        // サーバからデータを取得するダミーの関数
        sleep(2)
        return "取得したデータ"
    }
}

このコードでは、fetchData関数が呼ばれるたびに非同期でサーバからデータを取得し、取得したデータをresultLabelに表示します。

セマフォを使用して排他制御を行い、一度データ取得が開始されると、次のデータ取得が完了するまで新たな取得はブロックされます。

この排他制御により、ユーザーがボタンを連続でクリックしても、ラベルの表示が乱れることはありません。

○サンプルコード8:動的なデータの更新に伴う排他制御

動的なデータの更新は、ユーザーの操作や外部データソースからの入力に応じて頻繁に発生します。

こうした更新を適切に処理するための排他制御も重要です。

import UIKit

class DynamicUpdateViewController: UIViewController {
    @IBOutlet weak var dynamicLabel: UILabel!
    var data = [Int]()
    let dataLock = NSLock()

    @IBAction func updateData(_ sender: UIButton) {
        DispatchQueue.global().async { [weak self] in
            guard let self = self else { return }
            self.dataLock.lock()
            self.data.append(Int.random(in: 1...100))
            let latestData = self.data.last
            self.dataLock.unlock()

            DispatchQueue.main.async {
                self.dynamicLabel.text = "最新のデータ: \(latestData ?? 0)"
            }
        }
    }
}

上記のコードでは、updateData関数が呼ばれるたびに、ランダムな整数をデータ配列に追加します。この操作をdataLockで排他制御しています。

排他制御の結果、ユーザーがボタンを連続でクリックしても、データ配列の更新とラベルの表示が正しく連携され、データの整合性が保たれます。

○サンプルコード9:高速化のための排他制御

アプリケーションの高速化はユーザー体験を向上させる重要な要素です。

特に、多くのデータを処理する場合や、複数のスレッドを活用する場合には、効果的な排他制御を行うことで、パフォーマンスを最大限に引き出すことができます。

しかし、排他制御を不適切に実装すると、逆にパフォーマンスが低下することもあります。

ここでは、Swiftでの高速化を意識した排他制御の方法をサンプルコードを交えて紹介します。

多くのデータを扱う場合、一つ一つのデータに対してロックを取得するのではなく、まとめて一度にロックを取得することで、オーバーヘッドを削減できます。

ここでは、一度に複数のデータを処理する際の排他制御の例を紹介します。

import Foundation

class FastProcessing {
    var data = [Int](repeating: 0, count: 1000)
    let lock = NSLock()

    func updateAllData() {
        lock.lock()
        for i in 0..<data.count {
            data[i] = i
        }
        lock.unlock()
    }
}

このコードでは、一つのNSLockを使用して、data配列の全ての要素を一度に更新しています。

この方法は、データの一貫性を保つために必要なロックの回数を減少させ、高速な更新を実現します。

次に、このコードを実行すると、data配列の全ての要素がそのインデックスの値に更新されます。

例えば、data[5]の値は5に、data[999]の値は999に更新されることになります。

○サンプルコード10:安全なメモリアクセスのための排他制御

Swiftでは、メモリ安全性を確保するために、同時に同じメモリにアクセスすることを制限します。

しかし、非同期処理や複数スレッドを使用する場合、この制限を守るためには適切な排他制御が不可欠です。

下記のサンプルコードは、複数のスレッドから同時にアクセスされる可能性のあるデータを、排他制御を用いて安全に更新する方法を表しています。

import Foundation

class SafeMemoryAccess {
    var sharedData: Int = 0
    let dataLock = NSLock()

    func safelyUpdateSharedData() {
        dataLock.lock()
        sharedData += 1
        dataLock.unlock()
    }
}

このコードのsafelyUpdateSharedData関数は、sharedDataの値を1増加させる機能を持っていますが、その前後でdataLockを用いて排他制御を行っています。

この制御のおかげで、複数のスレッドが同時にsharedDataを更新しようとしても、一度に一つのスレッドだけが更新を行い、メモリの不整合を防ぐことができます。

このコードを実行すると、sharedDataは正確にスレッド数だけ増加します。

例えば、10のスレッドがsafelyUpdateSharedData関数を同時に呼び出した場合、sharedDataの値は10増加することになります。

●注意点と対処法

Swiftにおける排他制御の実装は、効果的なマルチスレッド処理やデータの一貫性を保つために不可欠です。

しかしながら、正しく実装されていない排他制御は、さまざまな問題を引き起こす可能性があります。

○正しく排他制御を行わないと起こる問題

排他制御の目的は、複数のスレッドが同時にデータにアクセスした際の競合やデータの破損を防ぐことです。

正しく実装されていない場合、次のような問題が生じる可能性があります。

  1. データの破損:複数のスレッドが同時にデータを変更すると、予期しない結果が生じることがあります。
  2. デッドロック:2つ以上のスレッドが互いに必要なリソースの解放を待ってしまい、プログラムが停止してしまう現象です。
  3. パフォーマンスの低下:不適切なロックの取得や解放により、プログラムの実行速度が遅くなることがあります。

○デッドロックを回避するためのテクニック

デッドロックはマルチスレッドプログラムの大きな課題の一つです。

下記のサンプルコードは、2つのリソースを取得する際のデッドロックのリスクを表しています。

import Foundation

let lockA = NSLock()
let lockB = NSLock()

func thread1Function() {
    lockA.lock()
    sleep(1) // 一時停止
    lockB.lock()
    // 処理内容
    lockB.unlock()
    lockA.unlock()
}

func thread2Function() {
    lockB.lock()
    sleep(1) // 一時停止
    lockA.lock()
    // 処理内容
    lockA.unlock()
    lockB.unlock()
}

このコードのthread1FunctionlockAを取得した後、lockBを取得しようとします。

一方、thread2FunctionlockBを取得した後、lockAを取得しようとします。

この2つの関数がほぼ同時に実行されると、デッドロックが発生する可能性があります。

デッドロックを回避するための一つの方法は、常にロックを一定の順序で取得することです。

すなわち、lockAを取得した後にのみlockBを取得するといった具体的な順序を定めることで、デッドロックのリスクを低減できます。

○パフォーマンス低下を避けるための工夫

排他制御を適切に実装することで、パフォーマンスの低下を回避することができます。

ロックの取得や解放は、一定のオーバーヘッドが伴うため、不要なロックは避けるよう心がけることが大切です。

また、より精緻なロックの設計、例えば「読み取り専用のロック」と「書き込み専用のロック」を分けることで、読み取り処理の同時実行数を増やすなどの工夫が可能です。

例として、Swiftで提供されているNSRecursiveLockNSConditionなどの高度なロック機構を利用することで、より柔軟な排他制御を実現することができます。

●カスタマイズ方法

Swiftの排他制御をより効果的に使用するためのカスタマイズ方法について解説します。

カスタマイズすることで、アプリケーションの要件や状況に合わせて、最適な排他制御を実現することができます。

○サンプルコード11:カスタムロックの作成

Swiftでは、NSLockingプロトコルを採用することで、独自のロックメカニズムを実装することができます。

下記のサンプルコードは、独自のロックを作成する一例です。

import Foundation

class CustomLock: NSLocking {
    private var internalLock = NSLock()

    func lock() {
        internalLock.lock()
        // ここに独自の処理を追加
    }

    func unlock() {
        // ここに独自の処理を追加
        internalLock.unlock()
    }
}

このコードでは、CustomLockクラスはNSLockingプロトコルに従い、lockunlockメソッドを提供しています。

内部でNSLockを用いて基本的なロック機能を実現しつつ、必要に応じて独自の処理を追加することができます。

○サンプルコード12:複数のロックを同時に取得する方法

複数のリソースにアクセスする必要がある場合、複数のロックを同時に取得する必要があります。

しかし、これはデッドロックのリスクを増加させる可能性があります。

そのリスクを低減するための方法として、常に一定の順序でロックを取得することが推奨されます。

下記のサンプルコードは、複数のロックを一定の順序で取得する方法を表しています。

import Foundation

let lockA = NSLock()
let lockB = NSLock()

func accessResources() {
    // ロックをID順やアドレス順など、一定の順序で取得
    if lockA < lockB {
        lockA.lock()
        lockB.lock()
    } else {
        lockB.lock()
        lockA.lock()
    }

    // リソースへのアクセス処理

    lockA.unlock()
    lockB.unlock()
}

この方法により、複数のスレッドが異なる順序でロックを取得しようとすることを防ぎ、デッドロックのリスクを低減します。

○サンプルコード13:条件付きのロック制御

条件付きのロック制御は、特定の条件が満たされた場合にのみロックを取得する方法です。

これは、リソースへのアクセスを効果的に制御する際に役立ちます。

Swiftでは、NSConditionを使用して条件付きのロック制御を実装することができます。

import Foundation

class ConditionLockExample {
    private var data: [Int] = []
    private let condition = NSCondition()

    // データ追加関数
    func addData(value: Int) {
        condition.lock()
        data.append(value)
        condition.signal()
        condition.unlock()
    }

    // データ取得関数
    func fetchData() -> Int? {
        condition.lock()
        while data.isEmpty {
            condition.wait()
        }
        let value = data.removeFirst()
        condition.unlock()
        return value
    }
}

このコードでは、dataという整数のリストを管理しています。

addData関数を使用してデータを追加し、fetchData関数を使用してデータを取得します。

データが空の場合、fetchData関数はcondition.wait()を使用して、データが追加されるまで待機します。

データが追加されると、addData関数がcondition.signal()を使用して待機中のスレッドを通知し、データを取得することができます。

○サンプルコード14:ロックのタイムアウト設定

ロックの取得を試みる際に、無限に待機するのではなく、指定した時間だけ待機した後にタイムアウトする方法もあります。

これは、デッドロックを回避するための一つの方法として利用されます。

SwiftのNSLockクラスには、tryメソッドを使用してロックの取得を試み、失敗した場合にはタイムアウトとして処理を進めることができます。

import Foundation

let lock = NSLock()

DispatchQueue.global().async {
    if lock.try() {
        // ロック取得に成功した場合の処理
        // ...
        lock.unlock()
    } else {
        // ロック取得に失敗した場合の処理
        // ...
    }
}

このコードを実行すると、lock.try()はロックの取得を試みます。

ロックが取得できた場合は、ロック取得に成功した場合の処理が実行されます。

ロックが取得できなかった場合は、ロック取得に失敗した場合の処理が実行されます。

この方法を使用することで、ロックの取得を無限に待つことなく、効率的に処理を進めることができます。

○サンプルコード15:排他制御の最適化テクニック

排他制御の最適化は、アプリケーションのパフォーマンスを向上させるための重要なステップです。

例えば、読み取り専用の操作と書き込みの操作が混在している場合、読み取り専用の操作のためのロックを軽減することで、同時に多くの読み取り操作を許可することができます。

Swiftでは、NSRecursiveLockNSReadersWritersLockなど、様々なロックメカニズムが提供されています。

これを利用して、読み取りと書き込みのバランスを取りながら、排他制御を最適化することができます。

例として、読み取りと書き込みのバランスを取った排他制御の一例を紹介します。

import Foundation

class ReadersWritersLockExample {
    private var data: Int = 0
    private let rwLock = NSRecursiveLock()

    // データの読み取り関数
    func readData() -> Int {
        rwLock.lock()
        let value = data
        rwLock.unlock()
        return value
    }

    // データの書き込み関数
    func writeData(newValue: Int) {
        rwLock.lock()
        data = newValue
        rwLock.unlock()
    }
}

このコードでは、読み取りと書き込みの操作が混在していますが、NSRecursiveLockを使用して、複数の読み取り操作を同時に許可しつつ、書き込み操作が行われる際には他のすべての操作をブロックしています。

これにより、読み取り専用の操作の効率を向上させつつ、データの整合性を保つことができます。

まとめ

Swiftにおける排他制御は、アプリケーションの安全性と効率性を確保するための重要な要素です。

この記事では、Swiftでの排他制御の基本的な使い方から、応用的な技術、注意点、最適化のテクニックについて詳細に解説しました。

排他制御の実装は、多数のスレッドやタスクが同時に動作する現代のアプリケーションにおいて、データの整合性を保ちながら高いパフォーマンスを出すためには欠かせないスキルとなっています。

特に、条件付きのロック制御やロックのタイムアウト設定など、高度なテクニックを駆使することで、さらに効果的な排他制御を実現することが可能です。

初心者の方にもわかりやすく、Swiftでの排他制御の深い部分までを解説したこの記事を参考に、Swiftのアプリケーション開発における排他制御の知識を深め、より高品質なコードを書く手助けとしていただければ幸いです。

今後もSwiftやその他のプログラミング言語に関するさまざまなテーマでの情報をお届けしてまいりますので、ぜひお楽しみに!