Objective-Cにおけるnonatomic属性の落とし穴:スレッドセーフティの問題

Objective-Cにおいて、nonatomic属性を持つプロパティは、複数のスレッドから同時にアクセスされた場合にクラッシュを引き起こす可能性があります。これは、特にプロパティのsetterメソッドが複数のスレッドで同時に呼び出された場合に顕著になります。ARC環境下では、nonatomic属性のsetterメソッドは、オブジェクトの参照カウントを適切に管理しないため、二重解放(double release)が発生し、結果としてメモリ破損や不正なメモリアクセス(野指针)につながり、最終的にアプリケーションのクラッシュを招きます。

atomic属性を使用すると、setterおよびgetterメソッドに対する排他的なアクセスが保証されるため、このような問題は回避されます。これは、内部的に同期ロックメカニズムが用いられるためです。

以下に、nonatomic属性を持つプロパティへの並行アクセスが問題を引き起こす可能性を示すコード例を示します。

import UIKit

class ViewController: UIViewController {

    // nonatomic属性のプロパティ
    private var userName: String?

    override func viewDidLoad() {
        super.viewDidLoad()

        // 並列処理が可能なキューを作成
        let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)

        // 多数のスレッドから同時にプロパティへ値を設定
        for i in 0..<10000 {
            concurrentQueue.async {
                // 現在のスレッド情報をログに出力 (デバッグ用)
                // print("Current Thread: \(Thread.current)")
                self.userName = "User \(i)"
            }
        }
    }
}

このコードでは、userNameプロパティはnonatomicであり、concurrentQueueを使用して多数の非同期タスクを同時に実行しています。各タスクはuserNameプロパティに値を設定しようとしますが、複数のスレッドが同時にsetterメソッドを呼び出すと、参照カウントの競合が発生し、予期せぬ動作やクラッシュを引き起こす可能性があります。

問題を回避するためのアプローチ

このスレッドセーフティの問題を解決するには、いくつかの方法があります。

  1. シリアルキューの使用: すべての操作を単一のスレッドで順番に実行するシリアルキューを使用します。これにより、競合状態を防ぐことができます。
// シリアルキューを作成
let serialQueue = DispatchQueue(label: "com.example.serialQueue")

// 多数のスレッドから同時にプロパティへ値を設定
for i in 0..<10000 {
    serialQueue.async {
        self.userName = "User \(i)"
    }
}
  1. dispatch_syncの使用: 並列キューと組み合わせてdispatch_syncを使用すると、同期的に操作が実行され、各操作は完了するまで待機するため、競合を防ぐことができます。ただし、これはパフォーマンスに影響を与える可能性があります。
// 並列処理が可能なキューを作成
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)

// 多数のスレッドから同時にプロパティへ値を設定
for i in 0..<10000 {
    concurrentQueue.sync { // syncを使用
        self.userName = "User \(i)"
    }
}
  1. ロックメカニズムの導入: NSLockなどのロックを使用して、クリティカルセクション(プロパティへのアクセス部分)を保護します。これにより、一度に一つのスレッドのみがプロパティにアクセスできるようになります。
let lock = NSLock()
// 並列処理が可能なキューを作成
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)

// 多数のスレッドから同時にプロパティへ値を設定
for i in 0..<10000 {
    concurrentQueue.async {
        lock.lock() // ロックを取得
        self.userName = "User \(i)"
        lock.unlock() // ロックを解放
    }
}
  1. NSStringの内部表現への依存 (非推奨): 特定の条件(短い文字列、ASCII文字のみなど)下では、NSStringのインスタンスがNSTaggedPointerStringとして扱われることがあります。この場合、参照カウントの管理が行われず、retain/release操作が無視されるため、スレッドセーフになることがあります。しかし、これはNSStringの内部実装に依存するものであり、将来のOSバージョンやコンパイラの変更で動作しなくなる可能性が高いため、推奨される解決策ではありません。
// 短いASCII文字列の場合、NSTaggedPointerStringになる可能性
// this example is for illustration purposes and not recommended for production.
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)
for i in 0..<10000 {
    concurrentQueue.async {
        self.userName = "\(i)" // 短い数字文字列
    }
}
  1. 定数文字列の使用 (限定的): プログラム全体で同一の定数文字列リテラルを使用する場合、コンパイラはそれらを__NSCFConstantStringとして扱います。これらの定数文字列はプログラムの生存期間中、メモリ上に一つしか存在せず、参照カウントの管理も行われないため、スレッドセーフになります。ただし、これは動的に生成される文字列には適用されません。
// 定数文字列はスレッドセーフ
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)
for _ in 0..<10000 {
    concurrentQueue.async {
        self.userName = "Constant String Value" // 定数文字列
    }
}

一般的に、オブジェクトのプロパティが複数のスレッドからアクセスされる可能性がある場合は、常にスレッドセーフティを考慮し、適切な同期メカニズム(キュー、ロックなど)を適用することが重要です。

タグ: Objective-C Swift nonatomic atomic スレッドセーフティ

7月25日 00:43 投稿