原文發(fā)表在個(gè)人博客iOS-線程同步詳解,轉(zhuǎn)載請注明出處。
本文對iOS系統(tǒng)上的線程的同步方式進(jìn)行了講解。
同步工具
線程同步工具可以幫助開發(fā)者在開發(fā)多線程應(yīng)用時(shí)耸别,盡可能避免線程間互相訪問導(dǎo)致各類問題。
Atomic Operations(原子操作)
Atomic Operations是一種基于基本數(shù)據(jù)類型的同步形式县钥,底層用匯編鎖來控制變量的變化秀姐,保證數(shù)據(jù)的正確性,好處在于不會block互相競爭的線程,且相比鎖耗時(shí)很少若贮。例如省有,一個(gè)整形計(jì)數(shù)器痒留,直接使用Atomic Operations,不需要通過鎖來控制計(jì)數(shù)器變化蠢沿。
舉例說明:
long total = 0;
void click(){
for(int i = 0; i < 1000; i++){
total++;
}
}
int main(int argc, const char * argv[]) {
clock_t start = clock();
vector<std::thread> threadGroup;
for(int i = 0; i < 100; ++i){
threadGroup.push_back(std::thread (click));
}
for (auto & th:threadGroup)
th.join();
clock_t finish = clock();
cout << "total:" << total << endl;
cout << "duration:" << (finish - start) << endl;
return 0;
}
上述代碼伸头,在沒有用到任何同步工具時(shí),運(yùn)行結(jié)果為:
total:99098
duration:6398
可見舷蟀,結(jié)果是不正確的恤磷。
如果使用Lock:
long total = 0;
mutex m;
void click(){
for(int i = 0; i < 1000; i++){
m.lock();
total++;
m.unlock();
}
}
運(yùn)行結(jié)果為:
total:100000
duration:486308
如果使用Atomic Operations:
atomic_long total(0);
void click(){
for(int i = 0; i < 1000; i++){
total++;
}
}
運(yùn)行結(jié)果為:
total:100000
duration:10141
可見,結(jié)果是正確的野宜,耗時(shí)相比Lock少非常多扫步。
Memory Barriers(內(nèi)存屏障)
為了達(dá)到最佳性能,編譯器通常會講匯編級別的指令進(jìn)行重新排序匈子,從而保持處理器的指令管道盡可能的滿河胎。作為優(yōu)化的一部分,編譯器可能會對內(nèi)存訪問的指令進(jìn)行重新排序(在它認(rèn)為不會影響數(shù)據(jù)的正確性的前提下)虎敦,然而游岳,這并不一定都是正確的,順序的變化可能導(dǎo)致一些變量的值得到不正確的結(jié)果其徙。
Memory Barriers是一種不會造成線程block的同步工具胚迫,它用于確保內(nèi)存操作的正確順序。Memory Barriers像一道屏障唾那,迫使處理器在其前面完成必須的加載或者存儲的操作晌区。Memory Barriers常被用于確保一個(gè)線程中可被其他線程訪問的內(nèi)存操作按照預(yù)期的順序執(zhí)行。具體參考Memory Barriers通贞。
在程序中應(yīng)用Memory Barriers只需要在指定地方調(diào)用:
OSMemoryBarrier();
舉例說明:
int x = 0;
int f = 0;
void A(){
while(f == 0);
OSMemoryBarrier();
cout << "x = " << x << endl;
}
void B(){
x = 42;
OSMemoryBarrier();
f = 1;
}
int main(int argc, const char * argv[]) {
vector<std::thread> threadGroup;
for(int i = 0; i < 2; ++i){
threadGroup.push_back(std::thread (i % 2 ? A : B));
}
for (auto & th:threadGroup)
th.join();
return 0;
}
上面的代碼中,如果去掉OSMemoryBarrier()恼五,可能會出現(xiàn)由于編譯器優(yōu)化昌罩,調(diào)整了指令順序,f=1放到了x=42的前面灾馒,而導(dǎo)致結(jié)果為:
x = 0
Volatile Variables(揮發(fā)變量)
Volatile Variables是另外一種針對變量的同步工具茎用。眾所周知,CPU訪問寄存器的速度比訪問內(nèi)存速度快很多睬罗,因此轨功,CPU有時(shí)候會將一些變量放置到寄存器中,而不是每次都從內(nèi)存中讀取(例如for循環(huán)中的i值)從而優(yōu)化代碼容达,但是可能會導(dǎo)致錯(cuò)誤古涧。
例如,一個(gè)線程在CPUA中被處理花盐,CPUA從內(nèi)存獲取變量F的值羡滑,此時(shí)菇爪,并沒有其他CPU用到變量F,所以CPUA將變量F存到寄存器中柒昏,方便下次使用凳宙,此時(shí),另一個(gè)線程在CPUB中被處理职祷,CPUB從內(nèi)存中獲取變量F的值氏涩,改變該值后,更新內(nèi)存中的F值有梆。但是是尖,由于CPUA每次都只會從寄存器中取F的值,而不會再次從內(nèi)存中取淳梦,所以析砸,CPUA處理后的結(jié)果就是不正確的。
對一個(gè)變量加上Volatile關(guān)鍵字可以迫使編譯器每次都重新從內(nèi)存中加載該變量爆袍,而不會從寄存器中加載首繁。當(dāng)一個(gè)變量的值可能隨時(shí)會被一個(gè)外部源改變時(shí),應(yīng)該將該變量聲明為Volatile陨囊。
舉例說明:
int x = 0;
int f = 0;
void A(){
while(f == 0);
cout << "x = " << x << endl;
}
void B(){
x = 42;
f = 1;
}
int main(int argc, const char * argv[]) {
vector<std::thread> threadGroup;
for(int i = 0; i < 2; ++i){
threadGroup.push_back(std::thread (i % 2 ? A : B));
}
for (auto & th:threadGroup)
th.join();
return 0;
}
上面的代碼在運(yùn)行時(shí)(開啟編譯器優(yōu)化)弦疮,概率出現(xiàn)線程A處于死循環(huán)中,即使線程B已經(jīng)改變了f的值蜘醋。
可以在變量f的定義前面加上Volatile胁塞,即可得到預(yù)期的結(jié)果。
x = 42
Locks(鎖)
Locks是一種最常用的同步工具压语。Locks可以對一段代碼進(jìn)行保護(hù)啸罢,保證同時(shí)只有一個(gè)線程在執(zhí)行該段代碼。
Locks的類型分為以下幾種胎食。
Lock | Description |
---|---|
Mutex(互斥) | 如果多個(gè)線程同時(shí)競爭一個(gè)Mutex Lock,只有一個(gè)將被允許訪問扰才,其他將被block。 |
Recursive(遞歸) | Recursive Lock也是一種Mutex Lock厕怜。它允許一個(gè)線程在釋放鎖前衩匣,多次執(zhí)行鎖內(nèi)代碼,其他將被block粥航。 |
Read-write(讀寫) | 多用于多讀少寫的數(shù)據(jù)琅捏,writer線程只有所有reader都釋放鎖時(shí),才能獲得鎖递雀,此時(shí)柄延,所有reader都等待鎖釋放。(POSIX線程才有) |
Distributed(分布) | 進(jìn)程級別的互斥鎖映之,distributed lock不會block一個(gè)進(jìn)程拦焚,而只會通知進(jìn)程該鎖無法獲取蜡坊。 |
Spin(旋轉(zhuǎn)) | Spin Lock將鎖的條件重復(fù)的變換,知道條件符合赎败。Spin Lock多被用于多處理器系統(tǒng)秕衙,當(dāng)需要等待一個(gè)鎖的時(shí)間盡可能短時(shí),切換鎖條件比block一個(gè)線程要更高效僵刮。iOS系統(tǒng)不支持該鎖据忘。 |
Double-checked(復(fù)核) | Double-checked Lock即在條件滿足,獲取鎖時(shí)搞糕,再對條件進(jìn)行一次判斷勇吊,多與單例模式結(jié)合。由于Double-checked Lock是不安全的窍仰,iOS系統(tǒng)并不支持該鎖汉规。 |
下面詳細(xì)介紹:
Mutex Lock
(1)POSIX Mutex Lock舉例:
pthread_mutex_t mutex;
void MyInitFunction()
{
pthread_mutex_init(&mutex, NULL);
}
void MyLockingFunction()
{
pthread_mutex_lock(&mutex);
// Do work.
pthread_mutex_unlock(&mutex);
}
(2)Cocoa NSLock舉例:
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];
}
}
NSLock除了lock, unlock方法外,還有tryLock和lockBeforeDate:方法驹吮。tryLock方法嘗試獲取鎖针史,但不會block線程,獲取失敗時(shí)碟狞,返回值為NO啄枕。lockBeforeDate:方法同樣不會block線程,在設(shè)定的時(shí)間里會嘗試獲取鎖族沃,獲取失敗時(shí)频祝,返回NO。
(3)@synchronized指令 舉例:
- (void)myMethod:(id)anObj
{
@synchronized(anObj)
{
// Everything between the braces is protected by the @synchronized directive.
}
}
@synchronized指令是一種非常方便的Mutex Lock脆淹,但是注意常空,如果指令包含模塊中的代碼拋出異常,@synchronized指令將會立即自動釋放鎖盖溺,所以需要在代碼中捕獲異常窟绷,或者使用NSLock。
(4)NSConditionLock
NSConditionLock也是一種Mutex Lock,它根據(jù)一個(gè)特定的整形值來作為條件獲取咐柜、釋放鎖。NSConditionLock常用于線程間需要特定順序進(jìn)行交互的攘残,例如生產(chǎn)者-消費(fèi)者拙友,生成者線程完成生成后,釋放鎖歼郭,消費(fèi)者線程此時(shí)獲得鎖遗契,開始消費(fèi)。
舉例說明:
#define NO_DATA 0
#define HAS_DATA 1
- (void)viewDidLoad {
[super viewDidLoad];
_condLock = [[NSConditionLock alloc] initWithCondition:0];
[[[NSThread alloc] initWithTarget:self selector:@selector(producer) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(consumer) object:nil] start];
}
- (void)producer
{
while(true){
[_condLock lockWhenCondition:NO_DATA];
NSLog(@"Produce..");
[_condLock unlockWithCondition:HAS_DATA];;
}
}
- (void)consumer
{
while(true){
[_condLock lockWhenCondition:HAS_DATA];
NSLog(@"Comsume..");
[_condLock unlockWithCondition:NO_DATA];
}
}
運(yùn)行結(jié)果:
Produce..
Comsume..
Produce..
Comsume..
...
Distributed Lock
Distributed Lock可以用于限制在不同主機(jī)上的多個(gè)應(yīng)用病曾,對共享資源的訪問限制牍蜂。Distributed Lock也是一種Mutex Lock漾根,使用文件系統(tǒng)的元素(文件/目錄)實(shí)現(xiàn)。
為了讓NSDistributedLock可用鲫竞,該鎖必須是對于所有應(yīng)用是可寫的辐怕。這意外著將其放在一個(gè)所有運(yùn)行該應(yīng)用的計(jì)算機(jī)都可以訪問的文件系統(tǒng)上。NSDistributedLock沒有l(wèi)ock方法从绘,而提供了tryLock方法寄疏,因?yàn)閘ock方法將會block當(dāng)前線程。
正常情況下僵井,調(diào)用unLock方法來釋放鎖陕截。在某種情況下,如果一個(gè)應(yīng)用在獲取到NSDistributedLock時(shí)批什,突然crash农曲,該應(yīng)用仍然持有該鎖,其他應(yīng)用將無法獲取驻债,此時(shí)需要用breakLock方法來強(qiáng)制獲取乳规。
舉例說明:
- (void)viewDidLoad {
[super viewDidLoad];
_distLock = [[NSDistributedLock alloc] initWithPath:@"/Users/YI/Desktop/test.html"];
[[[NSThread alloc] initWithTarget:self selector:@selector(A) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(B) object:nil] start];
}
- (void)A
{
while(true){
if([_distLock tryLock]){
NSLog(@"A Get Lock");
[_distLock unlock];
}
[NSThread sleepForTimeInterval:1.0];
}
}
- (void)B
{
while(true){
if([_distLock tryLock]){
NSLog(@"B Get Lock");
[_distLock unlock];
}
[NSThread sleepForTimeInterval:2.0];
}
}
用瀏覽器打開test.html,再運(yùn)行上述代碼却汉,則沒有任何輸出驯妄。因?yàn)榇藭r(shí)鎖被其他進(jìn)程占據(jù)。
加上breakLock方法:
_distLock = [[NSDistributedLock alloc] initWithPath:@"/Users/YI/Desktop/test.html"];
[_distLock breakLock];
運(yùn)行結(jié)果為:
A Get Lock
A Get Lock
B Get Lock
Recursive Lock
Recursive Lock是可以讓同一個(gè)線程多次獲取而不會導(dǎo)致死鎖的鎖合砂,Recursive Lock記錄了被獲取的次數(shù)青扔,每一次lock調(diào)用都必須有一次對應(yīng)的unlock調(diào)用,否則鎖將不會被釋放翩伪,其他線程無法獲取微猖。
Recursive Lock一般用于遞歸函數(shù)中,參考以下例子:
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
void MyRecursiveFunction(int value)
{
[theLock lock];
if (value != 0)
{
--value;
MyRecursiveFunction(value);
}
[theLock unlock];
}
MyRecursiveFunction(5);
如果沒有使用NSRecursiveLock缘屹,該線程將死鎖凛剥。
Double-checked Lock
volatile T* singleton = NULL;
T* GetInstance()
{
if(NULL == p)
{
lock();
if(NULL == singleton)
singleton = new T;
unlock();
}
return singleton;
}
如果沒有第二個(gè)if,有可能線程A執(zhí)行到lock()前,被block,此時(shí)線程B獲得鎖執(zhí)行完成轻姿,線程A被喚醒犁珠,又執(zhí)行了一次new語句。
Conditions(條件)
Conditions是一種特殊的lock,用于同步操作的順序互亮。與Mutex Lock不同的是犁享,一個(gè)等待Condition的線程保持block,直到另一個(gè)線程顯示對該Condition調(diào)用signal。
由于操作系統(tǒng)的原因豹休,Conditions可能會得到一些不正確的信號炊昆,為了避免這類問題,可以在使用Conditions時(shí),加入Predicate(斷言)凤巨。Predicate是一種有效地判斷是否讓一個(gè)線程處理信號的方式视乐。Conditions保持線程休眠,直到另一個(gè)線程調(diào)用signal敢茁,并設(shè)置了Predicate佑淀。
Cocoa Condition:
- (void)viewDidLoad {
[super viewDidLoad];
_cond = [NSCondition new];
[[[NSThread alloc] initWithTarget:self selector:@selector(A) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(B) object:nil] start];
}
static int timeToDoWork = 0;
- (void)A
{
[_cond lock];
while (timeToDoWork <= 0)
[_cond wait];
timeToDoWork--;
NSLog(@"Do work..");
[_cond unlock];
}
- (void)B
{
[_cond lock];
timeToDoWork++;
NSLog(@"Do work..");
[_cond signal];
[_cond unlock];
}
運(yùn)行結(jié)果為:
Add work..
Do work..
POSIX Condition:
pthread_mutex_t mutex;
pthread_cond_t condition;
Boolean ready_to_go = true;
void MyCondInitFunction()
{
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&condition, NULL);
}
void MyWaitOnConditionFunction()
{
pthread_mutex_lock(&mutex);
while(ready_to_go == false)
{
pthread_cond_wait(&condition, &mutex);
}
std::cout << "Do work.." << std::endl;
ready_to_go = false;
pthread_mutex_unlock(&mutex);
}
void SignalThreadUsingCondition()
{
pthread_mutex_lock(&mutex);
ready_to_go = true;
std::cout << "Add work.." << std::endl;
pthread_cond_signal(&condition);
pthread_mutex_unlock(&mutex);
}
POSIX Condition由Mutex和Condition結(jié)構(gòu)體兩部分組成,雖然兩者是獨(dú)立的卷要,但是在使用的時(shí)候渣聚,必須一一對應(yīng),否則將引發(fā)異常僧叉。