Swift 5.8 日本語化計画 : Swift 5.8
メモリの安全性
デフォルトでは、Swift は安全でない動作がコード内で起こらないようにします。たとえば、Swift では、変数は使用される前に初期化され、メモリの割り当てが解除された後はアクセスされず、配列インデックスの範囲外エラーがチェックされます。
Swift はまた、メモリ内の場所を変更するコードに、そのメモリへの排他的アクセスを要求することで、同じメモリ領域への複数のアクセスが競合しないようにします。Swift は自動的にメモリを管理するため、ほとんどの場合、メモリへのアクセスについて考える必要はありません。ただし、競合が発生する可能性がある場所を理解することが重要で、そのため、メモリへのアクセスが競合するコードを書くことを避けることができます。コードに競合が含まれていると、コンパイル時または実行時エラーが発生します。
メモリへのアクセス競合の理解
変数の値を設定したり、関数に引数を渡したりなどの時に、コードにメモリへのアクセスが発生します。たとえば、以下のコードには、読み取りアクセスと書き込みアクセスの両方が含まれています。
- // A write access to the memory where one is stored.
- var one = 1
- // A read access from the memory where one is stored.
- print("We're number \(one)!")
メモリへの競合するアクセスは、コードのさまざまな部分が同時にメモリ内の同じ位置にアクセスしようとしているときに発生する可能性があります。同時にメモリ内の位置に複数のアクセスがあると、予期しない動作や矛盾する動作が発生する可能性があります。Swift では、数行のコードにまたがる値を変更する方法があり、独自の変更の途中で値にアクセスしようとする可能性を生じます。
紙に書かれた予算をどのように更新するかについて考えることで、同様の問題を見ることができます。予算を更新するのは、2 段階のプロセスがあります。まず、項目の名前と価格を追加し、それから合計金額を変更して現在リストにある項目を反映させます。更新の前後に、下の図に示すように、予算からの情報を読み込んで正解を得ることができます。
予算に項目を追加している間は、合計金額が新しく追加された項目を反映するように更新されていないため、一時的に無効な状態になります。項目を追加するプロセス中に合計金額を読み出すと、間違った情報になります。
この例ではまた、競合するメモリアクセスを修正するときに遭遇する可能性のある問題も示しています。異なる回答を生成する競合を修正する複数の方法があり、どの答えが正しいか常には明らかではありません。この例では、元の合計金額または更新された合計金額のどちらを希望しているかによって、$5 または $320 のいずれかが正解になります。競合するアクセスを修正する前に、それが意図されていたものを判断しなければなりません。
単一のスレッド内からのメモリへのアクセスが競合する場合、Swift は必ずコンパイル時または実行時にエラーが発生します。マルチスレッドコードの場合は、Thread Sanitizer を使用してスレッド間の競合するアクセスを検出します。
メモリアクセスの特徴
競合するアクセスのコンテキストで考慮すべきメモリアクセスの 3 つの特徴、すなわちアクセスが読取りまたは書込みかどうか、アクセスの持続時間、およびアクセスされるメモリ内の位置があります。具体的には、以下のすべての条件を満たす 2 つのアクセスがある場合、競合が発生します。
- 少なくとも 1 つは書き込みアクセスか nonatomic アクセスです。
- それらはメモリ内の同じ位置にアクセスします。
- それらのアクセス期間は重複します。
読み取りアクセスと書き込みアクセスの違いは、通常明らかです。書き込みアクセスはメモリ内の位置を変更しますが、読み取りアクセスは変更しません。メモリ内の位置は、例えば変数、定数、またはプロパティなど、何がアクセスされているかを参照します。メモリアクセスの期間は、瞬時または長期のいずれかです。
C での atomic 操作のみを使用する場合、操作は atomic です。それ以外の場合は nonatomic です。これらの関数のリストについては、stdatomic(3) のマニュアルページを参照してください。
アクセスが開始した後、終了する前に他のコードを実行できない場合、アクセスは 瞬間的 です。その性質上、2 つの瞬時アクセスは同時に発生することはできません。ほとんどのメモリアクセスは瞬間的です。例えば、以下のコードリストのすべての読み取りアクセスと書き込みアクセスは瞬間的です。
- func oneMore(than number: Int) -> Int {
- return number + 1
- }
- var myNumber = 1
- myNumber = oneMore(than: myNumber)
- print(myNumber)
- // Prints "2"
しかし、長期 アクセスと呼ばれる、他のコードの実行にまたがる、メモリにアクセスする、いくつかの方法があります。瞬間的アクセスと長期アクセスの違いは、長期アクセスが開始した後、終了する前に、オーバーラップ と呼ばれる他のコードを実行する可能性があることです。長期アクセスは、他の長期アクセスや瞬間的アクセスと重複する可能性があります。
オーバーラップするアクセスは、関数とメソッドでの in-out パラメータを使用するコード、または構造体の変異メソッドのコードで主に表れます。長期アクセスを使用する特定の種類の Swift コードについては、以下のセクションで説明します。
In-Out パラメータへのアクセスの競合
関数には、すべての in-out パラメータへの長期書き込みアクセスがあります。in-out パラメータに対する書き込みアクセスは、すべての in-out でないパラメータが評価された後に始まり、その関数への呼び出しの全期間にわたって続きます。複数の in-out パラメータがある場合、書き込みアクセスはパラメータが表示されるのと同じ順序で開始します。
この長期書き込みアクセスの結果の 1 つは、有効範囲ルールとアクセス制御によって許可されていても、元の変数が in-out で渡されていてもアクセスできないことです。元の変数にアクセスすると競合が発生します。例えば:
- var stepSize = 1
- func increment(_ number: inout Int) {
- number += stepSize
- }
- increment(&stepSize)
- // Error: conflicting accesses to stepSize
上記のコードでは、stepSize はグローバル変数であり、通常は increment(_:) 内からアクセス可能です。しかし、stepSize への読み取りアクセスは number への書き込みアクセスと重複します。以下の図に示すように、 number と stepSize は両方ともメモリ内の同じ位置を参照します。読み取りアクセスと書き込みアクセスは同じメモリを参照し、オーバーラップして競合します。
この競合を解決する 1 つの方法は、stepSize の明示的なコピーを作成することです。
- // Make an explicit copy.
- var copyOfStepSize = stepSize
- increment(©OfStepSize)
- // Update the original.
- stepSize = copyOfStepSize
- // stepSize is now 2
increment(_:) を呼び出す前に stepSize のコピーを作成すると、copyOfStepSize の値が現在のステップサイズで増分されていることが明らかです。書き込みアクセスが開始する前に読み取りアクセスが終了するため、競合は発生しません。
in-out パラメータへの長期書き込みアクセスのもう 1 つの結果は、同じ関数の複数の in-out パラメータの引数として 1 つの変数を渡すと競合が発生することです。例えば:
- func balance(_ x: inout Int, _ y: inout Int) {
- let sum = x + y
- x = sum / 2
- y = sum - x
- }
- var playerOneScore = 42
- var playerTwoScore = 30
- balance(&playerOneScore, &playerTwoScore) // OK
- balance(&playerOneScore, &playerOneScore)
- // Error: Conflicting accesses to playerOneScore
上記の balance(_:_:) 関数は、2 つのパラメータを変更して、合計値をそれらの間で均等に分割します。 playerOneScore と playerTwoScore を引数として呼び出しても競合は発生しません。2 つの書き込みアクセスが同時に重複していますが、メモリ内の異なる位置にアクセスします。対照的に、playerOneScore を両方のパラメータの値として渡すと、メモリ内の同じ位置への 2 つの書き込みアクセスを同時に実行しようとするため、競合が発生します。
メソッド内の Self へのアクセスの競合
構造体の変異メソッドは、メソッド呼び出しの期間中、self への書き込みアクセス権を持ちます。例えば、各プレイヤーがダメージを受けたときに減少する Health (健康) 量と、特殊能力を使用するときに減少する Energy (エネルギー) 量を持つゲームを考えてみましょう。
- struct Player {
- var name: String
- var health: Int
- var energy: Int
- static let maxHealth = 10
- mutating func restoreHealth() {
- health = Player.maxHealth
- }
- }
上記の restoreHealth() メソッドでは、self への書き込みアクセスはメソッドの先頭で開始され、メソッドが戻るまで続きます。この場合、Player インスタンスのプロパティへのアクセスが重複しうる restoreHealth() 内には他のコードはありません。以下の shareHealth(with:) メソッドは、in-out パラメータとして別の Player インスタンスを取り、アクセスが重複する可能性があります。
- extension Player {
- mutating func shareHealth(with teammate: inout Player) {
- balance(&teammate.health, &health)
- }
- }
- var oscar = Player(name: "Oscar", health: 10, energy: 10)
- var maria = Player(name: "Maria", health: 5, energy: 10)
- oscar.shareHealth(with: &maria)     // OK
上記の例では、オスカーのプレーヤー (player) がマリアのプレーヤー (player) と health を共有するために shareHealth(with:) メソッドを呼び出すことで競合が発生することはありません。oscar は変異メソッド内の self の値であり、maria は in-out パラメータとして渡されたので、同じ期間、maria への書き込みアクセスがあるため、メソッド呼び出し中に oscar への書き込みアクセスがあります。下記の図に示すように、それらはメモリ内の異なる位置にアクセスします。2 つの書き込みアクセスが時間的に重複したとしても、競合しません。
しかし、もしあなたが shareHealth(with:) への引数として oscar を渡すと、競合が起こります:
- oscar.shareHealth(with: &oscar)
- // Error: conflicting accesses to oscar
メソッドの期間中、変異メソッドは self への書き込みアクセスを必要とし、in-out パラメータは同じ期間中、 teammate への書き込みアクセスを必要とします。このメソッド内では、self と teammate の両方がメモリ内の同じ場所を参照します (下図参照)。2 つの書き込みアクセスは同じメモリを参照し、重複して競合します。
プロパティへのアクセスの競合
構造体、タプル、列挙型などの型は、構造体のプロパティやタプルの要素など、個々の構成要素値で構成されます。これらは値型であるため、値の一部を変更すると値全体が変更され、つまり、プロパティの 1 つに対する読み取りまたは書き込みアクセスでは、全体の値に対する読み取りまたは書き込みアクセスが必要です。たとえば、タプルの要素への書き込みアクセスを重複させると、競合が発生します。
- var playerInformation = (health: 10, energy: 20)
- balance(&playerInformation.health, &playerInformation.energy)
- // Error: conflicting access to properties of playerInformation
上記の例では、タプルの要素に対して balance(_:_:) を呼び出すと、playerInformation への書き込みアクセスが重複しているため、競合が発生します。playerInformation.health と playerInformation.energy は、in-out パラメータとして渡され、つまり、balance(_:_:) は、関数呼び出しの間、それらへの書き込みアクセスが必要です。どちらの場合も、タプル要素への書き込みアクセスは、タプル全体への書き込みアクセスを必要とします。これは、重複している期間で playerInformation への 2 つの書き込みアクセスがあり、競合の原因となる事を意味します。
以下のコードは、グローバル変数に格納されている構造体のプロパティへの書き込みアクセスが重複している場合に、同じエラーが表示されることを示しています。
- var holly = Player(name: "Holly", health: 10, energy: 10)
- balance(&holly.health, &holly.energy)     // Error
実際、構造体のプロパティへのほとんどのアクセスは安全に重複し合う事が出来ます。たとえば、上記の例の holly 変数がグローバル変数ではなくローカル変数に変更された場合、コンパイラは構造体の格納されたプロパティへの重複したアクセスが安全であることを証明できます。
- func someFunction() {
- var oscar = Player(name: "Oscar", health: 10, energy: 10)
- balance(&oscar.health, &oscar.energy)    // OK
- }
上記の例では、オスカーの健康 (health) とエネルギー (energy) は balance(_:_:) への 2 つの in-out パラメータとして渡されます。コンパイラは、2 つの格納されたプロパティがどのようにも相互作用しないため、メモリの安全性が保持されることを証明できます。
メモリの安全性を維持するために、構造体のプロパティへの重複したアクセスに対する制限は常には必要ではありません。メモリの安全性は望ましい保証ですが、排他的なアクセスはメモリの安全性よりも厳しい要件です。つまり、メモリへの排他的アクセスに違反していてもメモリの安全性が保持されるコードもあると言う事です。Swift は、コンパイラがメモリへの非排他的アクセスがまだ安全であることを証明できる場合、このメモリに安全なコードを許可します。具体的には、以下の条件が適用される場合、構造体のプロパティへの重複アクセスが安全であることを証明できます。
- インスタンスの格納されたプロパティにのみアクセスし、計算されたプロパティやクラスプロパティにはアクセスしません。
- 構造体は、グローバル変数ではなくローカル変数の値です。
- 構造体は任意のクロージャではキャプチャされないか、エスケープされていないクロージャによってのみキャプチャされるかのいずれかです。
コンパイラはアクセスが安全であることを証明できない場合は、アクセスを許可しません。
前:自動参照カウント 次:アクセス制御
トップへ
トップへ
トップへ
トップへ