🕑

DispatchQueueのlabelはあくまでただのlabel

2023/01/09に公開

めちゃくちゃアタリマエのことを書いておる。

概要

Foundation.DispatchQueueを初期化する際、指定するlabelはあくまでlabelであって、idではない。もし同一のlabelを指定して複数のDispatchQueueを用意してしまってもそれらはlabelが同一の別々のキューが生成される。同一のシリアルキューを使いたい場合、同一のDispatchQueueを保持する必要があり、labelで指定するのではない。

比較して理解したい

2つのコードを示して実験してみる

  • 単一labelでqueueを1つしか生成しない例
    • 意図通りなやり方
  • 単一labelでqueueを2つ生成する例
    • おそらく意図してることができていないやり方

実行環境について

実行環境はXcode 14.2のiOS Appからプロジェクトを作成してみます。

なぜ上記を明記したかというと、Playgroundで試すと結果が違うためです。

queueを一つしか生成しない例

まず普通の使い方を示す。これは正しい。

1つのDispatchQueue(DispatchQueue(label: label))を用意し、AとBの2つのクロージャを書く例を示す。Aのクロージャは終了を遅くしていて、先にキューに乗ってるのでBが待たされる。

let queue = DispatchQueue(label: "dummy") // ❗差分はここ

DispatchQueue.global().async {
    print("A: before")
    queue.async {
	print("A: begin")
        print("A: thread \(Thread.current)") // 実行スレッド
        Thread.sleep(forTimeInterval: 5) // stopを遅らせたい
	print("A: end") // ここがB:beginより先に終わってほしい
    }

    print("B: before")
    queue.async {
	print("B: begin")
	print("B: thread \(Thread.current)") // 実行スレッド
	print("B: end 🕑") // 最後に出力されるのはここであってほしい
    }
}

print("Hello, Xcode Project") // ここが先に実行されてることを示すため


出力結果は下記の通り。

Hello, Xcode Project
A: before
B: before
A: begin
A: thread <NSThread: 0x6000013f5240>{number = 6, name = (null)}
A: end
B: begin
B: thread <NSThread: 0x6000013f5240>{number = 6, name = (null)}
B: end 🕑
  • Hello ...が最初に出力されていて想定通り
  • A: beforeのあとにすぐB: beforeが続くのも想定通り
  • A: threadB: threadを出力してみると同じスレッドnumberが使われていて想定通り
  • A: endB: beginより先に終わっているのも想定通り
    • 1つのキューなので、Bは開始自体を送らされている

単一labelでqueueを2つ生成してしまう例

次に、想定とは別の本来意図していないであろう使い方をやってしまっている例。

import Foundation

let label = "dummy" // ❗差分はここ

DispatchQueue.global().async {
    print("A: before")
    DispatchQueue(label: label).async {
	print("A: begin")
	print("A: thread \(Thread.current)") // 実行スレッド
	Thread.sleep(forTimeInterval: 5) // stopを遅らせたい
	print("A: end 🕑") // ここは最後に出力される
    }

    print("B: before")
    DispatchQueue(label: label).async {
	print("B: begin")
	print("B: thread \(Thread.current)") // 実行スレッド
	print("B: end") // 最後に出力されるのはここであってほしいがそうならない
    }
}

print("Hello, Xcode Project") // ここが先に実行されてることを示すため

出力結果は下記の通り。

Hello, Xcode Project
A: before
B: before
B: begin
A: begin
A: thread <NSThread: 0x600002c0cf00>{number = 5, name = (null)}
B: thread <NSThread: 0x600002c07900>{number = 7, name = (null)}
B: end
A: end 🕑
  • A: threadB: threadを出力してみると違うスレッドのnumberなのがわかる

Aが終わる前にBが始まっている。つまり別々のシリアルキューを実行していて、本来やりたいことって単一のキューに乗せたいことだろうから結果は伴っていない。

データの競合させられるかを検証する

すでに自分で実験済みだった

https://gist.github.com/yimajo/104a13e3e063a9c8773eb9eac44eeb73

ドキュメントを見てみる

convenience init(
    label: String,
    qos: DispatchQoS = .unspecified,
    attributes: DispatchQueue.Attributes = [],
    autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit,
    target: DispatchQueue? = nil
)

https://developer.apple.com/documentation/dispatch/dispatchqueue/2300059-init

labelについて、分かった上で読んだらそんなに説明がわかりづらくはない。

A string label to attach to the queue to uniquely identify it in debugging tools such as Instruments, sample, stackshots, and crash reports.

Instrumentsなんかのデバッキングツール、サンプル、スタックショット(?)、クラッシュレポートに使われる。

Because applications, libraries, and frameworks can all create their own dispatch queues, a reverse-DNS naming style (com.example.myqueue) is recommended.

おすすめとしてはリバースDNSっぽい名前にしたらいいとある

(いや、そこまでしなくても。別アプリと混ざりようがないから)

追記

DNSについて

https://twitter.com/k_katsumi/status/1612416940601114624

labelのはなし

This parameter is optional and can be NULL.

このドキュメントのoptionlは明らかに間違いな気がする

感想として、もし自分がドキュメントを書くなら重要なのはlabelが重複してもそれは2つのキューができて困るということであり、idとして機能するわけじゃないということなんじゃないだろうか。なので、「labelはあくまで重複チェックされておらず、もし同一labelを付けて初期化を行うと、複数同じlabelがついたqueueが生成される」って書きたい。

Playgroudの実行結果のはなし

Playgroundで試してたら結果がおかしなことになっていたのを指摘していただきました。

https://twitter.com/yoshiyuki_n/status/1641734324797177856?s=20

Discussion