iOS - 關于線程同步

引用自多線程編程指南
應用程序里面多個線程的存在引發(fā)了多個執(zhí)行線程安全訪問資源的潛在問題好渠。兩個線程同時修改同一資源有可能以意想不到的方式互相干擾氛堕。比如,一個線程可能覆蓋其他線程改動的地方,或讓應用程序進入一個未知的潛在無效狀態(tài)吞滞。如果你幸運的話,受損的資源可能會導致明顯的性能問題或崩潰,這樣比較容易跟蹤并修復它掰读。然而如果你不走運,資源受損可能導致微妙的錯誤,這些錯誤不會立即顯現(xiàn)出來,而是很久之后才出現(xiàn),或者導致其他可能需要一個底層的編碼來顯著修復的錯誤亡容。
但涉及到線程安全時,一個好的設計是最好的保護品洛。避免共享資源,并盡量減少線程間的相互作用,這樣可以讓它們減少互相的干擾树姨。但是一個完全無干擾的設計是不可能的。在線程必須交互的情況下,你需要使用同步工具,來確保當它們交互的時候是安全的桥状。

一帽揪、同步工具

  • 原子操作
    原子操作是同步的一個簡單的形式,它處理簡單的數(shù)據類型。原子操作的優(yōu)勢是它們不妨礙競爭的線程岛宦。對于簡單的操作,比如遞增一個計數(shù)器,原子操作比使用鎖具有更高的性能優(yōu)勢台丛。
  • 內存屏障和 Volatile 變量
    為了達到最佳性能,編譯器通常會對匯編基本的指令進行重新排序來盡可能保持處理器的指令流水線耍缴。作為優(yōu)化的一部分,編譯器有可能對訪問主內存的指令,如果它認為這有可能產生不正確的數(shù)據時,將會對指令進行重新排序砾肺。不幸的是,靠編譯器檢測到所有可能內存依賴的操作幾乎總是不太可能的。如果看似獨立的變量實際上是相互影響,那么編譯器優(yōu)化有可能把這些變量更新位錯誤的順序,導致潛在不不正確結果防嗡。
    內存屏障(memory barrier) 是一個使用來確保內存操作按照正確的順序工作的非阻塞的同步工具变汪。內存屏障的作用就像一個柵欄,迫使處理器來完成位于障礙前面的任何加載和存儲操作,才允許它執(zhí)行位于屏障之后的加載和存儲操作。內存屏障同樣使用來確保一個線程(但對另外一個線程可見)的內存操作總是按照預定的順序完成蚁趁。如果在這些地方缺少內存屏障有可能讓其他線程看到看似不可能的結果(比如,內存屏障的維基百科條目)裙盾。為了使用一個內存屏障,你只要在你代碼里面需要的地方簡單的調用 OSMemoryBarrier 函數(shù)。
    Volatile 變量適用于獨立變量的另一個內存限制類型。編譯器優(yōu)化代碼通過加載這些變量的值進入寄存器番官。對于本地變量,這通常不會有什么問題庐完。但是如果一個變量對另外一個線程可見,那么這種優(yōu)化可能會阻止其他線程發(fā)現(xiàn)變量的任何變化。在變量之前加上關鍵字 volatile 可以強制編譯器每次使用變量的時候都從內存里面加載徘熔。如果一個變量的值隨時可能給編譯器無法檢測的外部源更改,那么你可以把該變量聲明為 volatile 變量门躯。
    因為內存屏障和 volatile 變量降低了編譯器可執(zhí)行的優(yōu)化,因此你應該謹慎使用它們,只在有需要的地方時候,以確保正確性。

  • 鎖是最常用的同步工具酷师。你可以是使用鎖來保護臨界區(qū)(critical section),這些代碼段在同一個時間只能允許被一個線程訪問讶凉。比如,一個臨界區(qū)可能會操作一個特定的數(shù)據結構,或使用了每次只能一個客戶端訪問的資源。
    下面列出了程序最常使用的鎖山孔。Mac OS X 和 iOS 提供了這些鎖里面大部分類型的實現(xiàn),但是并不是全部實現(xiàn)懂讯。對于不支持的鎖類型,說明列解析了為什么這些鎖不能直接在平臺上面實現(xiàn)的原因。
    • Mutex 互斥鎖:
      一個互斥鎖扮演了圍繞一個資源的保護的柵欄台颠『滞互斥鎖是一種讓資源同一時間只能通過一個線程訪問的信號量。如果一個互斥鎖正在使用串前,有其他線程嘗試去訪問它譬挚,那么線程會一直阻塞知道互斥鎖被原來的持有者釋放。如果多線程為了一個互斥鎖競爭酪呻,在同一時間只有一個線程被允許訪問减宣。
    • Recursive lock 遞歸鎖
      遞歸所是一個變種的互斥鎖。地貴所允許一個線程去在釋放之前訪問多次玩荠。其他的線程一直保持著阻塞狀態(tài)知道所的持有者釋放鎖的次數(shù)和創(chuàng)建時候一樣為止漆腌。遞歸鎖主要是在遞歸期間使用但是也可以被用在當多個方法都需要分別獲取鎖的時候來使用。
    • Read-write lock 讀寫鎖
      讀寫鎖也被稱為 shared-exclusive 鎖阶冈,這個類型的鎖通常用于大范圍的操作并且能夠顯著的提升性能如果受保護的數(shù)據結構經常被頻繁的讀取和偶爾的修改闷尿。在普通的操作期間,多個讀者可以同時訪問數(shù)據結構女坑。當一個線程想要去寫數(shù)據的時候填具,他會一直阻塞知道所有的讀者釋放這個鎖,到那時他才可以拿到這個鎖并且更新數(shù)據匆骗。當一個寫線程正在等待鎖的時候劳景,一個新的讀線程會組織色知道這個寫線程結束操作盟广。系統(tǒng)只支持 POSIX 線程使用讀寫鎖瓮钥。想知道更多的信息請查看這里 pthread
    • Distributed lock分布鎖
      一個分布鎖在程序級別提供了互斥的訪問烹吵。不像一個真正的互斥鎖肋拔,一個分布所不會阻塞一個程序或者在它運行的時候去保護它呀酸。它僅僅是當鎖忙的時候會報告之后讓程序來決定如何處理。
    • Spin lock自旋鎖
      自旋鎖會一直檢測它鎖的情況知道條件為真七咧。自旋鎖多被用在多處理器系統(tǒng)的期待等待鎖的時間比較小的時候跃惫。在這些情況下,更有效率的做法是去檢測而不是去包括上下文的切換和刷新線程的數(shù)據結構那樣去阻塞線程艾栋。因為自旋鎖的輪詢性質系統(tǒng)不提供任何的自旋鎖爆存。更多的信息可以參考內核編程指南。
    • Double-checked lock 雙重檢查鎖
      雙重檢查鎖定試圖采取一個鎖的開銷減少測試前鎖定標準鎖蝗砾。因為雙重檢查鎖定潛在的不安全,系統(tǒng)不提供顯式支持他們和他們的使用是先较。
      注意 :大部分鎖類型都合并了內存屏障來確保在進入臨界區(qū)之前它前面的加載和存儲指令都已經完成。
  • 條件
    條件是信號量的另外一個形式,它允許在條件為真的時候線程間互相發(fā)送信號悼粮。條件通常被使用來說明資源可用性,或用來確保任務以特定的順序執(zhí)行闲勺。當一個線程測試一個條件時,它會被阻塞直到條件為真。它會一直阻塞直到其他線程顯式的修改
    信號量的狀態(tài)扣猫。條件和互斥鎖(mutex lock)的區(qū)別在于多個線程被允許同時訪問一個條件菜循。條件更多是允許不同線程根據一些指定的標準通過的守門人。
    一個方式是你使用條件來管理掛起事件的池申尤。事件隊列可能使用條件變量來給等待線程發(fā)送信號,此時它們在事件隊列中的時候癌幕。如果一個事件到達時,隊列將給條件發(fā)送合適信號。如果一個線程已經處于等待,它會被喚醒,屆時它將會取出事件并處理它昧穿。如果兩個事件到達隊列的時間大致相同,隊列將會發(fā)送兩次信號喚醒兩個線程勺远。
  • 執(zhí)行 Selector 例程
    Cocoa 程序包含了一個在一個線程以同步的方式傳遞消息的方便方法。NSObject類聲明方法來在應用的一個活動線程上面執(zhí)行 selector 的方法时鸵。這些方法允許你的線程以異步的方式來傳遞消息,以確保它們在同一個線程上面執(zhí)行是同步的胶逢。比如,你可以通過執(zhí)行 selector 消息來把一個從你分布計算的結果傳遞給你的應用的主線程或其他目標線程。每個執(zhí)行 selector 的請求都會被放入一個目標線程的 run loop的隊列里面,然后請求會按照它們到達的順序被目標線程有序的處理饰潜。

二、同步的成本和性能

  • 同步幫助確保你代碼的正確性,但同時將會犧牲部分性能某筐。鎖和原子操作通常包含了內存屏障和內核級別同步的使用來確保代碼正確被保護。如果,發(fā)生鎖的爭奪,你的線程有可能進入阻塞,在體驗上會產生更大的遲延抄囚。
    以下數(shù)據列出了在無爭議情況下使用互斥鎖和原子操作的近似的相關成本幔托。這些測試的平均值是使用了上千的樣本分析出的結果。隨著線程創(chuàng)建時間的推移,互斥采集時間(即使在無爭議情況下)可能相差也很大,這依賴于進程的加載,計算機的處理速度和系統(tǒng)和程序現(xiàn)有可用的內存谬哀。
    • 互斥獲取時間約0.2微秒史煎,這是在一個無爭議的情況下鎖獲取時間。如果另一個線程持有的鎖恬偷,獲取時間可以更大喉磁。測定的數(shù)據分析生成的均值和中位數(shù)的值在互斥鎖在英特爾的iMac收購2 GHz酷睿雙核處理器,1 GB的運行Mac OS X v10.5RAM。
    • 原子微妙比較和交換孕暇,0.05微妙妖滔,這是比較時間在一個無爭議的情況下。數(shù)據分析測定均值和中位數(shù)的值操作,生成一個基于英特爾處理器的iMac 2 GHz酷睿雙核處理器,1 GB內存運行原子大約0.05微秒比較曲秉。
      當設計你的并發(fā)任務時,正確性是最重要的因素,但是也要考慮性能因素承二。代碼在多個線程下面正確執(zhí)行,但比相同代碼在當線程執(zhí)行慢,這是難以改善的妆够。如果你是改造已有的單線程應用,你應該始終給關鍵任務的性能設置測量基線神妹。當增加額外線程后,對相同的任務你應該采取新的測量方法并比較多線程和單線程情況下的性能狀況。在改變代碼之后,線程并沒有提高性能,你應該需要重新考慮具體的實現(xiàn)或同時使用線程腰鬼。

三熄赡、線程安全和信號量

當涉及到多線程應用程序時,沒有什么比處理信號量更令人恐懼和困惑的了。信號量是底層 BSD 機制,它可以用來傳遞信息給進程或以某種方式操縱它拧篮。一些應用程序使用信號量來檢測特定事件,比如子進程的消亡串绩。系統(tǒng)使用信號量來終止失控進程,和作為其他類型的通信消息。
使用信號量的問題并不是你要做什么,而是當你程序是多線程的時候它們的行為顷牌。在當線程應用程序里面,所有的信號量處理都在主線程進行窟蓝。在多線程應用程序里面,信號量被傳遞到恰好運行的線程,而不依賴于特定的硬件錯誤(比如非法指令)运挫。如果多個線程同時運行,信號量被傳遞到任何一個系統(tǒng)挑選的線程滑臊。換而言之,信號量可以傳遞給你應用的任何線程雇卷。
在你應用程序里面實現(xiàn)信號量處理的第一條規(guī)則是避免假設任一線程處理信號量。如果一個指定的線程想要處理給定的信號,你需要通過某些方法來通知該線程信號何時到達贮折。你不能只是假設該線程的一個信號處理例程的安裝會導致信號被傳遞到同一線程里面调榄。
關于更多信號量的信息和信號量處理例程的安裝信息,參見 signal 和 sigaction主頁。


四今穿、線程安全設計的技巧

同步工具是讓你代碼安全的有用方法,但是它們并非靈丹妙藥腮出。使用太多鎖和其他同步的類型原語和非多線程相比明顯會降低你應用的線程性能。在性能和安全之間尋找平衡是一門需要經驗的藝術洛二。以下各部分提供幫助你為你應用選擇合適的同步級別的技巧侣滩。

  • 完全避免同步
    對于你新的項目,甚至已有項目,設計你的代碼和數(shù)據結構來避免使用同步是一個很好的解決辦法君珠。雖然鎖和其他類型同步工具很有用,但是它們會影響任何應用的性能材部。而且如果整體設計導致特定資源的高競爭,你的線程可能需要等待更長時間。
    實現(xiàn)并發(fā)最好的方法是減少你并發(fā)任務之間的交互和相互依賴物臂。如果每個任務在它自己的數(shù)據集上面操作,那它不需要使用鎖來保護這些數(shù)據棵磷。甚至如果兩個任務共享一個普通數(shù)據集,你可以查看分區(qū)方法,它們設置或提供拷貝每一項任務的方法仪媒。當然,拷貝數(shù)據集本身也需要成本,所以在你做出決定前,你需要權衡這些成本和使用同步工具造成的成本那個更可以接受。
  • 了解同步的限制
    同步工具只有當它們被用在應用程序中的所有線程是一致時才是有效的偎巢。如果你創(chuàng)建了互斥鎖來限制特定資源的訪問,你所有線程都必須在試圖操縱資源前獲得同一互斥鎖艘狭。如果不這樣做導致破壞一個互斥鎖提供的保護,這是編程的錯誤巢音。
  • 注意對代碼正確性的威脅
    當你使用鎖和內存屏障時,你應該總是小心的把它們放在你代碼正確的地方。即
    使有條件的鎖(似乎很好放置)也可能會讓你產生一個虛假的安全感傲绣。以下一系列例
    子試圖通過指出看似無害的代碼的漏洞來舉例說明該問題秃诵。其基本前提是你有一個可變的數(shù)組,它包含一組不可變的對象集。假設你想要調用數(shù)組中第一個對象的方法毅往。你可能會做類似下面那樣的代碼:
NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[arrayLock unlock];
[anObject doSomething]

以上例子說明了想操作可變數(shù)組中的一個對象去做一些事情,當我拿到這個對象之后如果有其他線程進入到鎖中刪除了這個對象另凌,那么就會出現(xiàn)問題途茫,那么可能會像下面這樣修改代碼:

NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject doSomething];
[arrayLock unlock];

這樣做也是不好的娜扇,因為如果 dosomething 的執(zhí)行時間很長的話就會產生性能瓶頸枢析。所以最好的辦法是這樣:

NSLock* arrayLock = GetArrayLock();
NSMutableArray* myArray = GetSharedArray();
id anObject;
[arrayLock lock];
anObject = [myArray objectAtIndex:0];
[anObject retain];
[arrayLock unlock];
[anObject doSomething];
[anObject release];
  • 當心死鎖和活鎖
    任何時候線程試圖同時獲得多于一個鎖,都有可能引發(fā)潛在的死鎖醒叁。當兩個不同的線程分別保持一個鎖(而該鎖是另外一個線程需要的)又試圖獲得另外線程保持的鎖時就會發(fā)生死鎖。結果是每個線程都會進入持久性阻塞狀態(tài),因為它永遠不可能獲得另外那個鎖饮睬。
    一個活鎖和死鎖類似,當兩個線程競爭同一個資源的時候就可能發(fā)生活鎖。在發(fā)生活鎖的情況里,一個線程放棄它的第一個鎖并試圖獲得第二個鎖昼丑。一旦它獲得第二個鎖,它返回并試圖再次獲得一個鎖菩帝。線程就會被鎖起來,因為它花費所有的時間來釋放一個鎖,并試圖獲取其他鎖,而不做實際的工作酒繁。
    避免死鎖和活鎖的最好方法是同一個時間只擁有一個鎖州袒。如果你必須在同一時間獲取多于一個鎖,你應該確保其他線程沒有做類似的事情。
  • 正確使用 Volatile 變量
    如果你已經使用了一個互斥鎖來保護一個代碼段,不要自動假設你需要使用關鍵詞 volatile 來保護該代碼段的重要的變量夸研。一個互斥鎖包含了內存屏障來確保加載和存儲操作是按照正確順序的。在一個臨界區(qū)添加關鍵字 volatile 到變量上面會強制每次訪問該變量的時候都要從內存里面從加載姐扮。這兩種同步技巧的組合使用在一些特定區(qū)域是必須的,但是同樣會導致顯著的性能損失。如果單獨使用互斥鎖已經可以保護變量,那么忽略關鍵字 volatile缚俏。
    為了避免使用互斥鎖而不使用 volatile 變量同樣很重要惊搏。通常情況下,互斥鎖和其他同步機制是比 volatile 變量更好的方式來保護數(shù)據結構的完整性。關鍵字volatile 只是確保從內存加載變量而不是使用寄存器里面的變量忧换。它不保證你代碼訪問變量是正確的恬惯。

五、使用原子操作

非阻塞同步的方式是用來執(zhí)行某些類型的操作而避免擴展使用鎖包雀。盡管鎖是同步兩個線程的很好方式,獲取一個鎖是一個很昂貴的操作,即使在無競爭的狀態(tài)下宿崭。相比,許多原子操作花費很少的時間來完成操作也可以達到和鎖一樣的效果葡兑。
原子操作可以讓你在 32 位或 64 位的處理器上面執(zhí)行簡單的數(shù)學和邏輯的運算操作。這些操作依賴于特定的硬件設施(和可選的內存屏障)來保證給定的操作在影響內存再次訪問的時候已經完成。在多線程情況下,你應該總是使用原子操作,它和內存屏障組合使用來保證多個線程間正確的同步內存手负。


原子操作.png

六、使用鎖

鎖是線程編程同步工具的基礎迷守。鎖可以讓你很容易保護代碼中一大塊區(qū)域以便你可以確保代碼的正確性拗秘。Mac OS X 和 iOS 都位所有類型的應用程序提供了互斥鎖,而 Foundation 框架定義一些特殊情況下互斥鎖的額外變種。以下個部分顯式了如何使用這些鎖的類型。

  • 使用 POSIX 互斥鎖
    POSIX 互斥鎖在很多程序里面很容易使用。為了新建一個互斥鎖,你聲明并初始化一個 pthread_mutex_t 的結構游盲。為了鎖住和解鎖一個互斥鎖,你可以使用pthread_mutex_lock 和 pthread_mutex_unlock 函數(shù)欣范。列表 4-2 顯式了要初始化并使用一個 POSIX 線程的互斥鎖的基礎代碼噩死。當你用完一個鎖之后,只要簡單的調用pthread_mutex_destroy 來釋放該鎖的數(shù)據結構。
pthread_mutex_t mutex;
void MyInitFunction()
{
pthread_mutex_init(&mutex, NULL);
}
void MyLockingFunction()
{
pthread_mutex_lock(&mutex);
// Do work.
pthread_mutex_unlock(&mutex);
}

注意:上面的代碼只是簡單的顯式了使用一個 POSIX 線程互斥鎖的步驟泡嘴。你自己的代碼應該檢查這些函數(shù)返回的錯誤碼,并適當?shù)奶幚硭鼈儭?/em>

  • 使用NSLock類
    在 Cocoa 程序中 NSLock 中實現(xiàn)了一個簡單的互斥鎖。所有鎖(包括 NSLock)的接口實際上都是通過 NSLocking 協(xié)議定義的,它定義了 lock 和 unlock 方法屠列。你使用這些方法來獲取和釋放該鎖。
    除了標準的鎖行為,NSLock 類還增加了 tryLock 和 lockBeforeDate:方法仿畸。方法tryLock 試圖獲取一個鎖,但是如果鎖不可用的時候,它不會阻塞線程镀层。相反,它只是返回 NO痪枫。而 lockBeforeDate:方法試圖獲取一個鎖,但是如果鎖沒有在規(guī)定的時間內被獲得,它會讓線程從阻塞狀態(tài)變?yōu)榉亲枞麪顟B(tài)(或者返回 NO)。
    下面的例子顯式了你可以是 NSLock 對象來協(xié)助更新一個可視化顯式,它的數(shù)據結構被多個線程計算。如果線程沒有立即獲的鎖,它只是簡單的繼續(xù)計算直到它可以獲得鎖再更新顯式夏块。
BOOL moreToDo = YES;
NSLock *theLock = [[NSLock alloc] init];
...
while (moreToDo) {
/* Do another increment of calculation */
/* until there’s no more to do. */
if ([theLock tryLock]) {
/* Update display used by all threads. */
[theLock unlock];
}
}
  • 使用@synchronized指令(可以理解為互斥鎖)
    @synchronized 指令是在 Objective-C 代碼中創(chuàng)建一個互斥鎖非常方便的方法娶牌。@synchronized 指令做和其他互斥鎖一樣的工作(它防止不同的線程在同一時間獲取同一個鎖)累榜。然而在這種情況下,你不需要直接創(chuàng)建一個互斥鎖或鎖對象辨泳。相反,你只需要簡單的使用 Objective-C 對象作為鎖的令牌,如下面例子所示:
- (void)myMethod:(id)anObj
{
@synchronized(anObj)
{
// Everything between the braces is protected by the @synchronized directive.
}
}

創(chuàng)建給 @synchronized 指令的對象是一個用來區(qū)別保護塊的唯一標示符遇绞。如果你在兩個不同的線程里面執(zhí)行上述方法,每次在一個線程傳遞了一個不同的對象給anObj 參數(shù),那么每次都將會擁有它的鎖,并持續(xù)處理,中間不被其他線程阻塞。然而,如果你傳遞的是同一個對象,那么多個線程中的一個線程會首先獲得該鎖,而其他線程將會被阻塞直到第一個線程完成它的臨界區(qū)喷舀。
作為一種預防措施,@synchronized 塊隱式的添加一個異常處理例程來保護代碼硫麻。
該處理例程會在異常拋出的時候自動的釋放互斥鎖。這意味著為了使用@synchronized 指令,你必須在你的代碼中啟用異常處理樊卓。如果你不想讓隱式的異常處理例程帶來額外的開銷,你應該考慮使用鎖的類拿愧。

  • 使用其他 Cocoa 鎖
    • 使用 NSRecursiveLock 對象(遞歸鎖)
      NSRecursiveLock 類定義的鎖可以在同一線程多次獲得,而不會造成死鎖。一個遞歸鎖會跟蹤它被多少次成功獲得了碌尔。每次成功的獲得該鎖都必須平衡調用鎖住和解鎖的操作浇辜。只有所有的鎖住和解鎖操作都平衡的時候,鎖才真正被釋放給其他線程獲得。
      正如它名字所言,這種類型的鎖通常被用在一個遞歸函數(shù)里面來防止遞歸造成阻塞線程唾戚。你可以類似的在非遞歸的情況下使用他來調用函數(shù),這些函數(shù)的語義要求它們使用鎖柳洋。以下是一個簡單遞歸函數(shù),它在遞歸中獲取鎖。如果你不在該代碼里使用NSRecursiveLock 對象,當函數(shù)被再次調用的時候線程將會出現(xiàn)死鎖颈走。
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
void MyRecursiveFunction(int value)
{
[theLock lock];
if (value != 0)
{
--value;
MyRecursiveFunction(value);
}
    [theLock unlock];
}
MyRecursiveFunction(5);

注意:因為一個遞歸鎖不會被釋放直到所有鎖的調用平衡使用了解鎖操作,所以你必須仔細權衡是否決定使用鎖對性能的潛在影響膳灶。長時間持有一個鎖將會導致其他線程阻塞直到遞歸完成。如果你可以重寫你的代碼來消除遞歸或消除使用一個遞歸鎖,你可能會獲得更好的性能立由。
- 使用 NSConditionLock 對象(條件鎖)
NSConditionLock 對象定義了一個互斥鎖,可以使用特定值來鎖住和解鎖轧钓。不要把該類型的鎖和條件(參見“條件”部分)混淆了。它的行為和條件有點類似,但是它們的實現(xiàn)非常不同锐膜。
通常,當多線程需要以特定的順序來執(zhí)行任務的時候,你可以使用一個NSConditionLock 對象,比如當一個線程生產數(shù)據,而另外一個線程消費數(shù)據毕箍。生產者執(zhí)行時,消費者使用由你程序指定的條件來獲取鎖(條件本身是一個你定義的整形值)。當生產者完成時,它會解鎖該鎖并設置鎖的條件為合適的整形值來喚醒消費者線程,之后消費線程繼續(xù)處理數(shù)據道盏。
NSConditionLock 的鎖住和解鎖方法可以任意組合使用而柑。比如,你可以使用unlockWithCondition:和 lock 消息,或使用 lockWhenCondition:和 unlock 消息。當然,后面的組合可以解鎖一個鎖但是可能沒有釋放任何等待某特定條件值的線程荷逞。
下面的例子顯示了生產者-消費者問題如何使用條件鎖來處理媒咳。想象一個應用程序包含一個數(shù)據的隊列。一個生產者線程把數(shù)據添加到隊列,而消費者線程從隊列中取出數(shù)據种远。生產者不需要等待特定的條件,但是它必須等待鎖可用以便它可以安全的把數(shù)據添加到隊列涩澡。

id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
while(true)
{
[condLock lock];
/* Add data to the queue. */
[condLock unlockWithCondition:HAS_DATA];

因為初始化條件鎖的值為 NO_DATA,生產者線程在初始化的時候可以毫無問題的獲取該鎖。它會添加隊列數(shù)據,并把條件設置為 HAS_DATA坠敷。在隨后的迭代中,生產者線程可以把到達的數(shù)據添加到隊列,無論隊列是否為空或依然有數(shù)據妙同。唯一讓它進入阻塞的情況是當一個消費者線程充隊列取出數(shù)據的時候射富。
因為消費者線程必須要有數(shù)據來處理,它會使用一個特定的條件來等待隊列。當生產者把數(shù)據放入隊列時,消費者線程被喚醒并獲取它的鎖粥帚。它可以從隊列中取出數(shù)據,并更新隊列的狀態(tài)胰耗。下列代碼顯示了消費者線程處理循環(huán)的基本結構。

while (true)
{
[condLock lockWhenCondition:HAS_DATA];
/* Remove data from the queue. */
[condLock unlockWithCondition:(isEmpty ? NO_DATA : HAS_DATA)];
// Process the data locally.
-  使用 NSDistributedLock 對象

NSDistributedLock 類可以被多臺主機上的多個應用程序使用來限制對某些共享資源的訪問,比如一個文件芒涡。鎖本身是一個高效的互斥鎖,它使用文件系統(tǒng)項目來實現(xiàn),比如一個文件或目錄柴灯。對于一個可用的 NSDistributedLock 對象,鎖必須由所有使用它的程序寫入。這通常意味著把它放在文件系統(tǒng),該文件系統(tǒng)可以被所有運行在計算機上面的應用程序訪問拖陆。
不像其他類型的鎖,NSDistributedLock 并沒有實現(xiàn) NSLocking 協(xié)議,所有它沒有 lock 方法弛槐。一個 lock 方法將會阻塞線程的執(zhí)行,并要求系統(tǒng)以預定的速度輪詢鎖。以其在你的代碼中實現(xiàn)這種約束,NSDistributedLock 提供了一個 tryLock 方法,并讓你決定是否輪詢依啰。
因為它使用文件系統(tǒng)來實現(xiàn),一個 NSDistributedLock 對象不會被釋放除非它的擁有者顯式的釋放它乎串。如果你的程序在用戶一個分布鎖的時候崩潰了,其他客戶端無法訪問該受保護的資源。在這種情況下,你可以使用 breadLock 方法來打破現(xiàn)存的鎖以便你可以獲取它速警。但是通常應該避免打破鎖,除非你確定擁有進程已經死亡并不可能再釋放該鎖叹誉。
和其他類型的鎖一樣,當你使用 NSDistributedLock 對象時,你可以通過調用unlock 方法來釋放它。


七闷旧、使用條件

條件是一個特殊類型的鎖,你可以使用它來同步操作必須處理的順序长豁。它們和互斥鎖有微妙的不同。一個線程等待條件會一直處于阻塞狀態(tài)直到條件獲得其他線程顯式發(fā)出的信號忙灼。
由于微妙之處包含在操作系統(tǒng)實現(xiàn)上,條件鎖被允許返回偽成功,即使實際上它們并沒有被你的代碼告知匠襟。為了避免這些偽信號操作的問題,你應該總是在你的條件鎖里面使用一個斷言。該斷言是一個更好的方法來確定是否安全讓你的線程處理该园。條件簡單的讓你的線程保持休眠直到斷言被發(fā)送信號的線程設置了酸舍。

  • 使用NSCondition類
    NSCondition 類提供了和 POSIX 條件相同的語義,但是它把鎖和條件數(shù)據結構封裝在一個單一對象里面。結果是一個你可以像互斥鎖那樣使用的對象,然后等待特定條件里初。
    以下顯示了一個代碼片段,它展示了為等待一個 NSCondition 對象的事件序列啃勉。cocaoCondition 變量包含了一個 NSCondition 對象,而 timeToDoWork 變量是一個整形,它在其他線程里面發(fā)送條件信號時立即遞增。
[cocoaCondition lock];
while (timeToDoWork <= 0)
[cocoaCondition wait];
timeToDoWork--;
// Do real work here.
[cocoaCondition unlock];

以下代碼顯示了用于給 Cocoa 條件發(fā)送信號的代碼,并遞增他斷言變量双妨。你應該在給它發(fā)送信號前鎖住條件淮阐。

[cocoaCondition lock];
timeToDoWork++;
[cocoaCondition signal];
[cocoaCondition unlock];
- 使用 POSIX 條件

POSIX 線程條件鎖要求同時使用條件數(shù)據結構和一個互斥鎖。經管兩個鎖結構是分開的,互斥鎖在運行的時候和條件結構緊密聯(lián)系在一起刁品。多線程等待某一信號應該總是一起使用相同的互斥鎖和條件結構泣特。修改該成雙結構將會導致錯誤。
以下代碼顯示了基本初始化過程,條件和斷言的使用挑随。在初始化之后,條件和互斥鎖,使用 ready_to_go 變量作為斷言等待線程進入一個 while 循環(huán)群扶。僅當斷言被設置并且隨后的條件信號等待線程被喚醒和開始工作。

//創(chuàng)建互斥鎖
pthread_mutex_t mutex;
//創(chuàng)建條件
pthread_cond_t condition;
Boolean ready_to_go = true;
void MyCondInitFunction()
{
//初始化互斥鎖
pthread_mutex_init(&mutex);
//初始化條件
pthread_cond_init(&condition, NULL);
}
void MyWaitOnConditionFunction()
{
//鎖住互斥鎖
pthread_mutex_lock(&mutex);
//如果斷言已經被設置,那么就繞過 while 循環(huán)竞阐,如果沒有,線程會一直休眠知道斷言被設置暑劝。
while(ready_to_go == false)
{
pthread_cond_wait(&condition, &mutex);
}
// Do work. (The mutex should stay locked.)
// Reset the predicate and release the mutex.
ready_to_go = false;
pthread_mutex_unlock(&mutex);
}

信號線程負責設置斷言和發(fā)送信號給條件鎖骆莹。下面顯示了實現(xiàn)該行為的代碼。在該例子中,條件被互斥鎖內被發(fā)送信號來防止等待條件的線程間發(fā)生競爭條件担猛。

void SignalThreadUsingCondition()
{
//在這里,應該有其他線程的工作要做幕垦。
pthread_mutex_lock(&mutex);
ready_to_go = true;
//通知其他線程開始工作
pthread_cond_signal(&condition);
pthread_mutex_unlock(&mutex);
}

注意:上述代碼是顯示使用 POSIX 線程條件函數(shù)的簡單例子。你自己的代碼應該檢測這些函數(shù)返回錯誤碼并恰當?shù)奶幚硭鼈儭?/em>


八傅联、總結

  • Cocoa
    在 Cocoa 上面使用多線程的指南包括以下這些:
    • 不可改變的對象一般是線程安全的先改。一旦你創(chuàng)建了它們,你可以把這些對象在線
      程間安全的傳遞。另一方面,可變對象通常不是線程安全的蒸走。為了在多線程應用里面使用可變對象,應用必須適當?shù)耐匠鹉獭jP于更多信息,參閱”可變和不可變對比”。
    • 許多對象在多線程里面不安全的使用被視為是”線程不安全的”比驻。只要同一時間只有一個線程,那么許多這些對象可以被多個線程使用该溯。這種被稱為專門限制應用程序的主線程的對象通常被這樣調用。
    • 應用的主線程負責處理事件别惦。盡管 Application Kit 在其他線程被包含在事件路徑里面時還會繼續(xù)工作,但操作可能會被打亂順序狈茉。
    • 如果你想使用一個線程來繪畫一個視圖,把所有繪畫的代碼放在 NSView 的lockFocusIfCanDraw 和 unlockFocus 方法中間。為了在 Cocoa 里面使用 POSIX 線程,你必須首先把 Cocoa 變?yōu)槎嗑€程模式掸掸。關于
      更多信息,參閱“在 Cocoa 應用里面使用 POSIX 線程”部分氯庆。
      基礎框架(Fondation Framework)的線程安全
      有一種誤解,認為基礎框架(Foundation framework)是線程安全的,而Application Kit 是非線程安全的。不幸的是,這是一個總的概括,從而造成一點誤導扰付。每個框架都包含了線程安全部分和非線程安全部分堤撵。以下部分介紹 Foundationframework 里面的線程安全部分。
    • 線程安全的類和函數(shù)
      下面這些類和函數(shù)通常被認為是線程安全的悯周。你可以在多個線程里面使用它們的同一個實例,而無需獲取一個鎖粒督。


      1.png

      2.png
  • 非線程安全類
    以下這些類和函數(shù)通常被認為是非線程安全的。在大部分情況下,你可以在任何線程里面使用這些類,只要你在同一個時間只在一個線程里面使用它們禽翼。參考這些類對于的額外詳細信息的文檔屠橄。


    3.png

    4.png

    注意,盡管 NSSerializer,NSArchiver,NSCoder 和 NSEnumerator 對象本身是線程安全的,但是它們被放置這這里是因為當它們封裝的對象被使用的時候,更改這些對象數(shù)據是不安全的。比如,在歸檔情況下,修改被歸檔的對象是不安全的闰挡。對于一個枚舉,任何線程修改枚舉的集合都是不安全的锐墙。
    多線程編程指南

    • 只能用于主線程的類
      以下的類必須只能在應用的主線程類使用。
      • NSAppleScript
    • 可變 vs 不可變
      不可變對象通常是線程安全的长酗。一旦你創(chuàng)建了它們,你可以把它們安全的在線程間傳遞溪北。當前,在使用不可變對象時,你還應該記得正確使用引用計數(shù)。如果不適當?shù)尼尫帕艘粋€你沒有引用的對象,你在隨后有可能造成一個異常。
      可變對象通常是非線程安全的之拨。為了在多線程應用里面使用可變對象,應用應該使用鎖來同步訪問它們(關于更多信息,參見“原子操作”部分)茉继。通常情況下,集合類(比如,NSMutableArray,NSMutableDictionary) 是考慮多變時是非線程安全的。這意味著,如果一個或多個線程同時改變一個數(shù)組,將會發(fā)生問題蚀乔。你應該在線程讀取和寫入它們的地方使用鎖包圍著烁竭。
      即使一個方法要求返回一個不可變對象,你不應該簡單的假設返回的對象就是不可變的。依賴于方法的實現(xiàn),返回的對象有可能是可變的或著不可變的吉挣。比如,一個返回類型是 NSString 的方法有可能實際上由于它的實現(xiàn)返回了一個NSMutableString派撕。如果你想要確保對象是不可變的,你應該使用不可變的拷貝。
    • 可重入性
      可重入性是可以讓同一對象或者不同對象上一個操作“調用”其他操作成為可能睬魂。保持和釋放對象就是一個有可能被忽視的”調用”的例子终吼。
      以下列表列出了 Foundation framework 的部分顯式的可重入對象。所有其他類可能是或可能不是可重入的,或者它們將來有可能是可重入的氯哮。對于可重入性的一個完整的分析是不可能完成的,而且該列表將會是無窮盡的际跪。


      5.png
    • 類的初始化
      Objective-C 的運行時系統(tǒng)在類收到其他任何消息之前給它發(fā)送一個 initialize消息。這可以讓類有機會在它被使用前設置它的運行時環(huán)境蛙粘。在一個多線程應用里面,運行時保證僅有一個線程(該線程恰好發(fā)送第一條消息給類)執(zhí)行 initialized 方法,第二個線程阻塞直到第一個線程的 initialize 方法執(zhí)行完成垫卤。在此期間,第一個線程可以繼續(xù)調用其他類上的方法。該 initialize 方法不應該依賴于第二個線程對這個類的調用出牧。如果不是這樣的話,兩個線程將會造成死鎖穴肘。
    • 自動釋放池(Autorelease Pools)
      每個線程都維護它自己的 NSAutoreleasePool 的棧對象。Cocoa 希望在每個當前線程的棧里面有一個可用的自動釋放池舔痕。如果一個自動釋放池不可用,對象將不會給釋放,從而造成內存泄露评抚。對于 Application Kit 的主線程通常它會自動創(chuàng)建并消耗一個自動釋放池,但是輔助線程(和其他只有 Foundationd 的程序)在使用 Cocoa前必須自己手工創(chuàng)建。如果你的線程是長時間運行的,那么有可能潛在產生很多自動釋放的對象,你應該周期性的銷毀它們并創(chuàng)建自動釋放池(就像 Application Kit 對主線程那樣)伯复。否則,自動釋放對象將會積累并造成內存大量占用慨代。如果你的脫離線程沒有使用 Cocoa,你不需要創(chuàng)建一個自動釋放池。
    • Run Loops
      每個線程都有一個或多個 run loop啸如。然而每個 run loop 和每個線程都有它自己的輸入模式來決定 run loop 運行的釋放監(jiān)聽那些輸入源侍匙。輸入模式定義在一個 runloop 上面,不會影響定義在其他 run loop 的輸入模式,即使它們的名字相同。
      如果你的線程是基于 Application Kit 的話,主線程的 run loop 會自動運行,但是輔助線程(和只有 Foundation 的應用)必須自己啟動它們的 run loop叮雳。如果一個脫離線程沒有進入 run loop,那么線程在完成它們的方法執(zhí)行后會立即退出想暗。
      盡管外表顯式可能是線程安全的,但是 NSRunLoop 類是非線程安全的。你只能在擁有它們的線程里面調用它實例的方法帘不。
  • Application Kit 框架的線程安全
    • 非線程安全類
      以下這些類和函數(shù)通常是非線程安全的说莫。大部分情況下,你可以在任何線程使用這些類,只要你在同一時間只有一個線程使用它們。查看這些類的文檔來獲得更多的詳細信息寞焙。
      • NSGraphicsContext储狭。多信息,參見“NSGraphicsContext 限制”互婿。
      • NSImage.更多信息,參見“NSImage 限制”。
      • NSResponder辽狈。
      • NSWindow 和所有它的子類慈参。更多信息,參見“Window 限制
        只能用于主線程的類
        以下的類必須只能在應用的主線程使用。
      • NSCell 和所有它的子類稻艰。
      • NSView 和所有它的子類懂牧。更多信息,參見“NSView 限制”。
    • Window 限制
      你可以在輔助線程創(chuàng)建一個 window尊勿。Application Kit 確保和 window 相關的數(shù)據結構在主線程釋放來避免產生條件。在同時包含大量 windows 的應用中,window對象有可能會發(fā)生泄漏畜侦。
      你也可以在輔助線程創(chuàng)建 modal window元扔。在主線程運行 modal loop 時,Application Kit 阻塞輔助線程的調用。
    • 事件處理例程限制
      應用的主線程負責處理事件旋膳。主線程阻塞在 NSApplication 的 run 方法,通常該方法被包含在 main 函數(shù)里面澎语。在 Application Kit 繼續(xù)工作時,如果其他線程被包含在事件路徑,那么操作有可能打亂順序。比如,如果兩個不同的線程負責關鍵事件,那么關鍵事件有可能不是按照順序到達验懊。通過讓主線程來處理事件,事件可以被分配到輔助線程由它們處理擅羞。
      你可以在輔助線程里面使用 NSApplication 的 postEvent:atStart 方法傳遞一個事件給主線程的事件隊列。然而,順序不能保證和用戶輸入的事件順序相同义图。應用的主線程仍然輔助處理事件隊列的事件减俏。
    • 繪畫限制
      Application Kit 在使用它的繪畫函數(shù)和類時通常是線程安全的,包括NSBezierPath 和 NSString 類。關于使用這些類的詳細信息,在以下各部分介紹碱工。關于繪畫的額外信息和線程可以查看 Cocoa Drawing Guide娃承。
      • a) NSView 限制
        NSView 通常是線程安全的,包含幾個異常。你應該僅在應用的主線程里面執(zhí)行對NSView 的創(chuàng)建怕篷、銷毀历筝、調整大小、移動和其他操作廊谓。在其他輔助線程里面只要你把繪畫的代碼放在 lockFocusIfCanDraw 和 unlockFocus 方法之間也是線程安全的梳猪。
        如果應用的輔助線程想要告知主線程重繪視圖,一定不能在輔助線程直接調用display,setNeedsDisplay:,setNeedsDisplayInRect:,或 setViewsNeedDisplay:方法。相反,你應該給給主線程發(fā)生一個消息讓它調用這些方法,或者使用performSelectorOnMainThread:withObject:waitUntilDone:方法蒸痹。
        系統(tǒng)視圖的圖形狀態(tài)(gstates)是基于每個線程不同的春弥。使用圖形狀態(tài)可以在單線程的應用里面獲得更好的繪畫性能,但是現(xiàn)在已經不是這樣了。不正確使用圖形狀多
        態(tài)可能導致主線程的繪畫代碼更低效电抚。
      • b) NSGraphicsContext 限制 NSGraphicsContext 類代表了繪畫上下文,它由底層繪畫系統(tǒng)提供惕稻。每個NSGraphicsContext 實例都擁有它獨立的繪畫狀態(tài):坐標系統(tǒng)、裁剪蝙叛、當前字體等俺祠。該類的實例在主線程自動創(chuàng)建自己的 NSWindow 實例。如果你在任何輔助線程執(zhí)行繪畫操作,需要特定為該線程創(chuàng)建一個新的 NSGraphicsContext 實例。
        如果你在任何輔助線程執(zhí)行繪畫,你必須手工的刷新繪畫調用蜘渣。Cocoa 不會自動更新輔助線程繪畫的內容,所以你當你完成繪畫后需要調用 NSGraphicsContext 的flusGrahics 方法淌铐。如果你的應用程序只在主線程繪畫,你不需要刷新繪畫調用。
      • c) NSImage 限制線程可以創(chuàng)建 NSImage 對象,把它繪畫到圖片緩沖區(qū),還可以把它傳遞給主線程來繪畫蔫缸。底層的圖片緩存被所有線程共享腿准。關于圖片和如何緩存的更多信息,參閱Ccocoa Drawing Guide。
  • Core Foundation(核心框架)
    Core Foundation 是足夠線程安全的,如果你的程序注意一下的話,應該不會遇到任何線程競爭的問題拾碌。通常情況下是線程安全的,比如當你查詢(query)吐葱、引用(retain)、釋放(release)和傳遞(pass)不可變對象時校翔。甚至在多個線程查詢中央共享對象也是線程安全的弟跑。
    像 Cocoa 那樣,當涉及對象或它們內容突變時,Core Foundation 是非線程安全的。比如,正如你所期望的,無論修改一個可變數(shù)據或可變數(shù)組對象,還是修改一個可變數(shù)組里面的對象都是非線程安全的防症。其中一個原因是性能,這是在這種情況下的關鍵孟辑。此外,在該級別上實現(xiàn)完全線程安全是幾乎不可能的。例如,你不能排除從集合中引用(retain)一個對象產生的無法確定的結果蔫敲。該集合本身在被調用來引用(retain)它所包含的對象之前有可能已經被釋放了饲嗽。這些情況下,當你的對象被多個線程訪問或修改,你的代碼應該在相應的地方使
    用鎖來保護它們不要被同時訪問。例如,枚舉 Core Foundation 數(shù)組對象的代碼,在枚舉塊代碼周圍應該使用合適的鎖來保護它免遭其他線程修改奈嘿。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末貌虾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子指么,更是在濱河造成了極大的恐慌酝惧,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伯诬,死亡現(xiàn)場離奇詭異晚唇,居然都是意外死亡,警方通過查閱死者的電腦和手機盗似,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門哩陕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人赫舒,你說我怎么就攤上這事悍及。” “怎么了接癌?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵心赶,是天一觀的道長。 經常有香客問我缺猛,道長缨叫,這世上最難降的妖魔是什么椭符? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮耻姥,結果婚禮上销钝,老公的妹妹穿的比我還像新娘。我一直安慰自己琐簇,他們只是感情好蒸健,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著婉商,像睡著了一般似忧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上丈秩,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天橡娄,我揣著相機與錄音,去河邊找鬼癣籽。 笑死,一個胖子當著我的面吹牛滤祖,可吹牛的內容都是我干的筷狼。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼匠童,長吁一口氣:“原來是場噩夢啊……” “哼埂材!你這毒婦竟也來了?” 一聲冷哼從身側響起汤求,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤俏险,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扬绪,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體竖独,經...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年挤牛,在試婚紗的時候發(fā)現(xiàn)自己被綠了莹痢。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡墓赴,死狀恐怖竞膳,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情诫硕,我是刑警寧澤坦辟,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站章办,受9級特大地震影響锉走,放射性物質發(fā)生泄漏滨彻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一挠日、第九天 我趴在偏房一處隱蔽的房頂上張望疮绷。 院中可真熱鬧,春花似錦嚣潜、人聲如沸冬骚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽只冻。三九已至,卻和暖如春计技,著一層夾襖步出監(jiān)牢的瞬間喜德,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工垮媒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留舍悯,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓睡雇,卻偏偏與公主長得像萌衬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子它抱,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內容