
前言
說到多線程同步問題就不得不提多線程中的鎖機(jī)制,多線程操作過程中往往多個線程是并發(fā)執(zhí)行的,同一個資源可能被多個線程同時訪問,造成資源搶奪,這個過程中如果沒有鎖機(jī)制往往會造成重大問題。比如常見的車票的銷售問題。
線程同步
所謂線程同步就是為了防止多個線程搶奪同一個資源造成的數(shù)據(jù)安全問題,所采取的一種措施。主要的方法有以下幾種:
- 互斥鎖
使用@synchronized解決線程同步問題相比較NSLock要簡單一些,但是效率是眾多鎖中最差的。首先選擇一個對象作為同步對象(一般使用self),然后將”加鎖代碼”(爭奪資源的讀取、修改代碼)放到代碼塊中。 注意:鎖定1份代碼只用1把鎖,用多把鎖是無效的。使用互斥鎖,在同一個時間,只允許一條線程執(zhí)行鎖中的代碼.因?yàn)榛コ怄i的代價非常昂貴,所以鎖定的代碼范圍應(yīng)該盡可能小,只要鎖住資源讀寫部分的代碼即可。使用互斥鎖也會影響并發(fā)的目的。
@synchronized(self) {
//1.先檢查票數(shù)
int count = leftTicketsCount;
if (count>0) {
//暫停一段時間
[NSThread sleepForTimeInterval:0.002];
//2.票數(shù)-1
leftTicketsCount= count-1;
//獲取當(dāng)前線程
NSThread *current=[NSThread currentThread];
NSLog(@"%@--賣了一張票,還剩余%d張票", current.name, leftTicketsCount);
}
else {
//退出線程
[NSThread exit];
}
}
- 同步鎖NSLock
iOS中對于資源搶占的問題可以使用同步鎖NSLock來解決,使用時把需要加鎖的代碼(以后暫時稱這段代碼為”加鎖代碼“)放到NSLock的lock和unlock之間。

同步鎖時如果一個線程A已經(jīng)加鎖,線程B就無法進(jìn)入。那么B怎么知道是否資源已經(jīng)被其他線程鎖住呢?可以通過tryLock方法,此方法會返回一個BOOL型的值,如果為YES說明獲取鎖成功,否則失敗。
- 使用GCD解決資源搶占問題
在GCD中提供了一種信號機(jī)制,也可以解決資源搶占問題(和同步鎖的機(jī)制并不一樣)。GCD中信號量是dispatch_semaphore_t類型,支持信號通知和信號等待。每當(dāng)發(fā)送一個信號通知,則信號量+1;每當(dāng)發(fā)送一個等待信號時信號量-1,;如果信號量為0則信號會處于等待狀態(tài),直到信號量大于0開始執(zhí)行。根據(jù)這個原理我們可以初始化一個信號量變量,默認(rèn)信號量設(shè)置為1,每當(dāng)有線程進(jìn)入“加鎖代碼”之后就調(diào)用信號等待命令(此時信號量為0)開始等待,此時其他線程無法進(jìn)入,執(zhí)行完后發(fā)送信號通知(此時信號量為1),其他線程開始進(jìn)入執(zhí)行,如此一來就達(dá)到了線程同步目的。
dispatch_semaphore_t _semaphore;//定義一個信號量
#pragma mark 請求圖片數(shù)據(jù)
-(NSData *)requestData:(int )index{
NSData *data;
NSString *name;
# 信號等待
# 第二個參數(shù):等待時間
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
if (_imageNames.count>0) {
name=[_imageNames lastObject];
[_imageNames removeObject:name];
}
//信號通知
dispatch_semaphore_signal(_semaphore);
if(name){
NSURL *url=[NSURL URLWithString:name];
data=[NSData dataWithContentsOfURL:url];
}
return data;
}
- NSCondition 實(shí)現(xiàn)控制線程通信
NSCondition 的對象實(shí)際上作為一個鎖和一個線程檢查器:鎖主要為了當(dāng)檢測條件時保護(hù)數(shù)據(jù)源,執(zhí)行條件引發(fā)的任務(wù);線程檢查器主要是根據(jù)條件決定是否繼續(xù)運(yùn)行線程,即線程是否被阻塞。單純解決線程同步問題不是NSCondition設(shè)計的主要目的,NSCondition更重要的是解決線程之間的調(diào)度關(guān)系(當(dāng)然,這個過程中也必須先加鎖、解鎖)。NSCondition可以調(diào)用wati方法控制某個線程處于等待狀態(tài),直到其他線程調(diào)用signal(此方法喚醒一個線程,如果有多個線程在等待則任意喚醒一個)或者broadcast(此方法會喚醒所有等待線程)方法喚醒該線程才能繼續(xù)。
//初始化鎖對象
_condition=[[NSCondition alloc]init];
#pragma mark 創(chuàng)建圖片
-(void)createImageName{
[_condition lock];
//如果當(dāng)前已經(jīng)有圖片了則不再創(chuàng)建,線程處于等待狀態(tài)
if (_imageNames.count>0) {
NSLog(@"createImageName wait, current:%i",_currentIndex);
[_condition wait];
}else{
NSLog(@"createImageName work, current:%i",_currentIndex);
//生產(chǎn)者,每次生產(chǎn)1張圖片
[_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",_currentIndex++]];
//創(chuàng)建完圖片則發(fā)出信號喚醒其他等待線程
[_condition signal];
}
[_condition unlock];
}
iOS中的其他鎖
在iOS開發(fā)中,除了同步鎖有時候還會用到一些其他鎖類型,在此簡單介紹一下:
NSRecursiveLock:遞歸鎖,有時候“加鎖代碼”中存在遞歸調(diào)用,遞歸開始前加鎖,遞歸調(diào)用開始后會重復(fù)執(zhí)行此方法以至于反復(fù)執(zhí)行加鎖代碼最終造成死鎖,這個時候可以使用遞歸鎖來解決。使用遞歸鎖可以在一個線程中反復(fù)獲取鎖而不造成死鎖,這個過程中會記錄獲取鎖和釋放鎖的次數(shù),只有最后兩者平衡鎖才被最終釋放。
NSDistributedLock:分布鎖,它本身是一個互斥鎖,基于文件方式實(shí)現(xiàn)鎖機(jī)制,可以跨進(jìn)程訪問。
pthread_mutex_t:同步鎖,基于C語言的同步鎖機(jī)制,使用方法與其他同步鎖機(jī)制類似。
有一張圖片簡單的比較了各種鎖的加解鎖性能:

還有一種方式可以達(dá)到線程同步,那就是同步執(zhí)行
-
同步執(zhí)行 :我們可以使用多線程的知識,把多個線程都要執(zhí)行此段代碼添加到同一個串行隊列,這樣就實(shí)現(xiàn)了線程同步的概念。當(dāng)然這里可以使用 GCD 和 NSOperation 兩種方案,我都寫出來。
#GCD #需要一個全局變量queue,要讓所有線程的這個操作都加到一個queue中 dispatch_sync(queue, ^{ NSInteger ticket = lastTicket; [NSThread sleepForTimeInterval:0.1]; NSLog(@"%ld - %@",ticket, [NSThread currentThread]); ticket -= 1; lastTicket = ticket; }); #NSOperation & NSOperationQueue #1. 全局的 NSOperationQueue, 所有的操作添加到同一個queue中 # 2. 設(shè)置 queue 的 maxConcurrentOperationCount 為 1 #3. 如果后續(xù)操作需要Block中的結(jié)果,就需要調(diào)用每個操作的waitUntilFinished,阻塞當(dāng)前線程,一直等到當(dāng)前操作完成,才允許執(zhí)行后面的。waitUntilFinished 要在添加到隊列之后! NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ NSInteger ticket = lastTicket; [NSThread sleepForTimeInterval:1]; NSLog(@"%ld - %@",ticket, [NSThread currentThread]); ticket -= 1; lastTicket = ticket; }]; [queue addOperation:operation]; [operation waitUntilFinished]; #后續(xù)要做的事
PS:原子和非原子屬性
atomic 的本意是指屬性的存取方法是線程安全的,并不保證整個對象是線程安全的。比如setter函數(shù)里面改變兩個成員變量,如果你用nonatomic的話,getter可能會取到只更改了其中一個變量時候的狀態(tài),這樣取到的東西會有問題。
atomic:能夠?qū)崿F(xiàn)“單寫多讀”的數(shù)據(jù)保護(hù),同一時間只允許一個線程修改屬性值,但是允許多個線程同時讀取屬性值,在多線程讀取數(shù)據(jù)時,有可能出現(xiàn)“臟”數(shù)據(jù) - 讀取的數(shù)據(jù)可能會不正確。原子屬性是默認(rèn)屬性,atomic(原子屬性)在setter方法內(nèi)部加了一把自旋鎖如果不需要考慮線程安全,要指定 nonatomic。
關(guān)于atomic的實(shí)現(xiàn)最開始的方式如下,我們可以看到其實(shí)現(xiàn)原理也是通過加鎖實(shí)現(xiàn)的。
- (void)setCurrentImage:(UIImage *)currentImage
{
@synchronized(self) {
if (_currentImage != currentImage) {
[_currentImage release];
_currentImage = [currentImage retain];
// do something
}
}
}
- (UIImage *)currentImage
{
@synchronized(self) {
return _currentImage;
}
}
線程間通信
線程間通信用到的比較多的包括倆個方面: 其他線程向主線程的通信,其他倆個線程間的通信。
-
從其他線程回到主線程的方法
我們都知道在其他線程操作完成后必須到主線程更新UI。所以,介紹完所有的多線程方案后,我們來看看有哪些方法可以回到主線程。#NSThread [self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO]; #GCD dispatch_async(dispatch_get_main_queue(), ^{ }); #NSOperationQueue [[NSOperationQueue mainQueue] addOperationWithBlock:^{ }]; -
線程間通信
線程間通信和進(jìn)程間通信從本質(zhì)上講是相似的。線程間通信就是在進(jìn)程內(nèi)的兩個執(zhí)行流之間進(jìn)行數(shù)據(jù)的傳遞,就像兩條并行的河流之間挖出了一道單向流動長溝,使得一條河流中的水可以流入另一條河流,物質(zhì)得到了傳遞。
A. performSelect On The Thread
框架為我們提供了強(qiáng)制在某個線程中執(zhí)行方法的途徑,如果兩個非主線程的線程需要相互間通信,可以先將自己的當(dāng)前線程對象注冊到某個全局的對象中去,這樣相 互之間就可以獲取對方的線程對象,然后就可以使用下面的方法進(jìn)行線程間的通信了,由于主線程比較特殊,所以框架直接提供了在出線程執(zhí)行的方法。
#在主線程上執(zhí)行操作,例如給UIImageVIew設(shè)置圖片 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait //在指定線程上執(zhí)行操作 - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg waitUntilDone:(BOOL)wait #在分線程中下載完圖片后通知主線程更新 UI,通過如下方法,傳遞參數(shù)。 [self.imageView performSelector:@selector(setImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:NO];B.Mach Port
在蘋果的Thread Programming Guide的Run Pool一節(jié)的Configuring a Port-Based Input Source 這一段中就有使用Mach Port進(jìn)行線程間通信的例子。其實(shí)質(zhì)就是父線程創(chuàng)建一個NSMachPort對象,在創(chuàng)建子線程的時候以參數(shù)的方式將其傳遞給子線程,這樣子線程中就可以向這個傳過來的 NSMachPort對象發(fā)送消息,如果想讓父線程也可以向子線程發(fā)消息的話,那么子線程可以先向父線程發(fā)個特殊的消息,傳過來的是自己創(chuàng)建的另一個 NSMachPort對象,這樣父線程便持有了子線程創(chuàng)建的port對象了,可以向這個子線程的port對象發(fā)送消息了。當(dāng)然各自的port對象需要設(shè)置delegate以及schdule到自己所在線程的RunLoop中,這樣來了消息之后,處理port消息的delegate方法會被調(diào)用,你就可以自己處理消息了。下面是一處使用源碼:
#define kMsg1 100 #define kMsg2 101 - (void)viewDidLoad { [super viewDidLoad]; //1. 創(chuàng)建主線程的port // 子線程通過此端口發(fā)送消息給主線程 NSPort *myPort = [NSMachPort port]; //2. 設(shè)置port的代理回調(diào)對象 myPort.delegate = self; //3. 把port加入runloop,接收port消息 [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode]; NSLog(@"---myport %@", myPort); //4. 啟動次線程,并傳入主線程的port MyWorkerClass *work = [[MyWorkerClass alloc] init]; [NSThread detachNewThreadSelector:@selector(launchThreadWithPort:) toTarget:work withObject:myPort]; } - (void)handlePortMessage:(NSMessagePort*)message{ NSLog(@"接到子線程傳遞的消息!%@",message); //1. 消息id NSUInteger msgId = [[message valueForKeyPath:@"msgid"] integerValue]; //2. 當(dāng)前主線程的port NSPort *localPort = [message valueForKeyPath:@"localPort"]; //3. 接收到消息的port(來自其他線程) NSPort *remotePort = [message valueForKeyPath:@"remotePort"]; if (msgId == kMsg1) { //向子線的port發(fā)送消息 [remotePort sendBeforeDate:[NSDate date] msgid:kMsg2 components:nil from:localPort reserved:0]; } else if (msgId == kMsg2){ NSLog(@"操作2....\n"); } }
MyWorkerClass
#import "MyWorkerClass.h"
@interface MyWorkerClass() <NSMachPortDelegate> {
NSPort *remotePort;
NSPort *myPort;
}
@end
#define kMsg1 100
#define kMsg2 101
@implementation MyWorkerClass
- (void)launchThreadWithPort:(NSPort *)port {
@autoreleasepool {
//1. 保存主線程傳入的port
remotePort = port;
//2. 設(shè)置子線程名字
[[NSThread currentThread] setName:@"MyWorkerClassThread"];
//3. 開啟runloop
[[NSRunLoop currentRunLoop] run];
//4. 創(chuàng)建自己port
myPort = [NSPort port];
//5.
myPort.delegate = self;
//6. 將自己的port添加到runloop
//作用1、防止runloop執(zhí)行完畢之后推出
//作用2、接收主線程發(fā)送過來的port消息
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
//7. 完成向主線程port發(fā)送消息
[self sendPortMessage];
}
}
/**
* 完成向主線程發(fā)送port消息
*/
- (void)sendPortMessage {
NSMutableArray *array =[[NSMutableArray alloc]initWithArray:@[@"1",@"2"]];
//發(fā)送消息到主線程,操作1
[remotePort sendBeforeDate:[NSDate date]
msgid:kMsg1
components:array
from:myPort
reserved:0];
//發(fā)送消息到主線程,操作2
// [remotePort sendBeforeDate:[NSDate date]
// msgid:kMsg2
// components:nil
// from:myPort
// reserved:0];
}
#pragma mark - NSPortDelegate
/**
* 接收到主線程port消息
*/
- (void)handlePortMessage:(NSPortMessage *)message
{
NSLog(@"接收到父線程的消息...\n");
// unsigned int msgid = [message msgid];
// NSPort* distantPort = nil;
//
// if (msgid == kCheckinMessage)
// {
// distantPort = [message sendPort];
//
// }
// else if(msgid == kExitMessage)
// {
// CFRunLoopStop((__bridge CFRunLoopRef)[NSRunLoop currentRunLoop]);
// }
}
@end
另外Notification在多線程中的使用需要注意
Notification在多線程中只在同一個線程中POST和接收到消息,如果想實(shí)現(xiàn),在一個線程中發(fā)通知,在另一個線程中接收到事件,需要用到通知的 重定向技術(shù),這其中用到了進(jìn)程中的通信。了解更多看這里Notification與多線程。
本文參考文章:
IOS多線程開發(fā)其實(shí)很簡單
iOS線程通信和進(jìn)程通信的例子(NSMachPort和NSTask,NSPipe)
http://www.cnblogs.com/samyangldora/p/4631815.html