4、線程同步
說到多線程就不得不提多線程中的鎖機制,多線程操作過程中往往多個線程是并發(fā)執(zhí)行的,同一個資源可能被多個線程同時訪問,造成資源搶奪,這個過程中如果沒有鎖機制往往會造成重大問題。舉例來說,每年春節(jié)都是一票難求,在12306買票的過程中,成百上千的票瞬間就消失了。不妨假設(shè)某輛車有1千張票,同時有幾萬人在搶這列車的車票,順利的話前面的人都能買到票。但是如果現(xiàn)在只剩下一張票了,而同時還有幾千人在購買這張票,雖然在進入購票環(huán)節(jié)的時候會判斷當(dāng)前票數(shù),但是當(dāng)前已經(jīng)有100個線程進入購票的環(huán)節(jié),每個線程處理完票數(shù)都會減1,100個線程執(zhí)行完當(dāng)前票數(shù)為-99,遇到這種情況很明顯是不允許的。
要解決資源搶奪問題在iOS中有常用的有兩種方法:一種是使用NSLock同步鎖,另一種是使用@synchronized代碼塊。兩種方法實現(xiàn)原理是類似的,只是在處理上@synchronized代碼塊使用起來更加簡單(C#中也有類似的處理機制synchronized和lock)。
這里不妨還拿圖片加載來舉例,假設(shè)現(xiàn)在有9張圖片,但是有15個線程都準(zhǔn)備加載這9張圖片,約定不能重復(fù)加載同一張圖片,這樣就形成了一個資源搶奪的情況。在下面的程序中將創(chuàng)建9張圖片,每次讀取照片鏈接時首先判斷當(dāng)前鏈接數(shù)是否大于1,用完一個則立即移除,最多只有9個。在使用同步方法之前先來看一下錯誤的寫法:
//
// 線程同步
// MultiThread
//
// Created by Kenshin Cui on 14-3-22.
// Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//
#import "KCMainViewController.h"
#import "KCImageData.h"
#define ROW_COUNT 5
#define COLUMN_COUNT 3
#define ROW_HEIGHT 100
#define ROW_WIDTH ROW_HEIGHT
#define CELL_SPACING 10
#define IMAGE_COUNT 9
@interface KCMainViewController (){
NSMutableArray *_imageViews;
NSMutableArray *_imageNames;
}
@end
@implementation KCMainViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}
#pragma mark 界面布局
-(void)layoutUI{
//創(chuàng)建多個圖片控件用于顯示圖片
_imageViews=[NSMutableArray array];
for (int r=0; r<ROW_COUNT; r++) {
for (int c=0; c<COLUMN_COUNT; c++) {
UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)];
imageView.contentMode=UIViewContentModeScaleAspectFit;
// imageView.backgroundColor=[UIColor redColor];
[self.view addSubview:imageView];
[_imageViews addObject:imageView];
}
}
UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame=CGRectMake(50, 500, 220, 25);
[button setTitle:@"加載圖片" forState:UIControlStateNormal];
//添加方法
[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
//創(chuàng)建圖片鏈接
_imageNames=[NSMutableArray array];
for (int i=0; i<IMAGE_COUNT; i++) {
[_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];
}
}
#pragma mark 將圖片顯示到界面
-(void)updateImageWithData:(NSData *)data andIndex:(int )index{
UIImage *image=[UIImage imageWithData:data];
UIImageView *imageView= _imageViews[index];
imageView.image=image;
}
#pragma mark 請求圖片數(shù)據(jù)
-(NSData *)requestData:(int )index{
NSData *data;
NSString *name;
if (_imageNames.count>0) {
name=[_imageNames lastObject];
[_imageNames removeObject:name];
}
if(name){
NSURL *url=[NSURL URLWithString:name];
data=[NSData dataWithContentsOfURL:url];
}
return data;
}
#pragma mark 加載圖片
-(void)loadImage:(NSNumber *)index{
int i=[index integerValue];
//請求數(shù)據(jù)
NSData *data= [self requestData:i];
//更新UI界面,此處調(diào)用了GCD主線程隊列的方法
dispatch_queue_t mainQueue= dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
[self updateImageWithData:data andIndex:i];
});
}
#pragma mark 多線程下載圖片
-(void)loadImageWithMultiThread{
int count=ROW_COUNT*COLUMN_COUNT;
dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//創(chuàng)建多個線程用于填充圖片
for (int i=0; i<count; ++i) {
//異步執(zhí)行隊列任務(wù)
dispatch_async(globalQueue, ^{
[self loadImage:[NSNumber numberWithInt:i]];
});
}
}
@end
首先在_imageNames中存儲了9個鏈接用于下載圖片,然后在requestData:方法中每次只需先判斷_imageNames的個數(shù),如果大于一就讀取一個鏈接加載圖片,隨即把用過的鏈接刪除,一切貌似都沒有問題。此時運行程序,會發(fā)現(xiàn)多于9張而并不是出現(xiàn)了9張不同的圖片。
上面這個結(jié)果不一定每次都出現(xiàn),關(guān)鍵要看從_imageNames讀取鏈接、刪除鏈接的速度,如果足夠快可能不會有任何問題,但是如果速度稍慢就會出現(xiàn)上面的情況,很明顯上面情況并不滿足前面的需求。
分析這個問題造成的原因主:當(dāng)一個線程A已經(jīng)開始獲取圖片鏈接,獲取完之后還沒有來得及從_imageNames中刪除,另一個線程B已經(jīng)進入相應(yīng)代碼中,由于每次讀取的都是_imageNames的最后一個元素,因此后面的線程其實和前面線程取得的是同一個圖片鏈接這樣就造成圖中看到的情況。要解決這個問題,只要保證線程A進入相應(yīng)代碼之后B無法進入,只有等待A完成相關(guān)操作之后B才能進入即可。下面分別使用NSLock和@synchronized對代碼進行修改。
NSLock
iOS中對于資源搶占的問題可以使用同步鎖NSLock來解決,使用時把需要加鎖的代碼(以后暫時稱這段代碼為”加鎖代碼“)放到NSLock的lock和unlock之間,一個線程A進入加鎖代碼之后由于已經(jīng)加鎖,另一個線程B就無法訪問,只有等待前一個線程A執(zhí)行完加鎖代碼后解鎖,B線程才能訪問加鎖代碼。需要注意的是lock和unlock之間的”加鎖代碼“應(yīng)該是搶占資源的讀取和修改代碼,不要將過多的其他操作代碼放到里面,否則一個線程執(zhí)行的時候另一個線程就一直在等待,就無法發(fā)揮多線程的作用了。
另外,在上面的代碼中”搶占資源“_imageNames定義成了成員變量,這么做是不明智的,應(yīng)該定義為“原子屬性”。對于被搶占資源來說將其定義為原子屬性是一個很好的習(xí)慣,因為有時候很難保證同一個資源不在別處讀取和修改。nonatomic屬性讀取的是內(nèi)存數(shù)據(jù)(寄存器計算好的結(jié)果),而atomic就保證直接讀取寄存器的數(shù)據(jù),這樣一來就不會出現(xiàn)一個線程正在修改數(shù)據(jù),而另一個線程讀取了修改之前(存儲在內(nèi)存中)的數(shù)據(jù),永遠(yuǎn)保證同時只有一個線程在訪問一個屬性。
下面的代碼演示了如何使用NSLock進行線程同步:
KCMainViewController.h
//
// KCMainViewController.h
// MultiThread
//
// Created by Kenshin Cui on 14-3-22.
// Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface KCMainViewController : UIViewController
@property (atomic,strong) NSMutableArray *imageNames;
@end
KCMainViewController.m
//
// 線程同步
// MultiThread
//
// Created by Kenshin Cui on 14-3-22.
// Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//
#import "KCMainViewController.h"
#import "KCImageData.h"
#define ROW_COUNT 5
#define COLUMN_COUNT 3
#define ROW_HEIGHT 100
#define ROW_WIDTH ROW_HEIGHT
#define CELL_SPACING 10
#define IMAGE_COUNT 9
@interface KCMainViewController (){
NSMutableArray *_imageViews;
NSLock *_lock;
}
@end
@implementation KCMainViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}
#pragma mark 界面布局
-(void)layoutUI{
//創(chuàng)建多個圖片控件用于顯示圖片
_imageViews=[NSMutableArray array];
for (int r=0; r<ROW_COUNT; r++) {
for (int c=0; c<COLUMN_COUNT; c++) {
UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)];
imageView.contentMode=UIViewContentModeScaleAspectFit;
[self.view addSubview:imageView];
[_imageViews addObject:imageView];
}
}
UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame=CGRectMake(50, 500, 220, 25);
[button setTitle:@"加載圖片" forState:UIControlStateNormal];
//添加方法
[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
//創(chuàng)建圖片鏈接
_imageNames=[NSMutableArray array];
for (int i=0; i<IMAGE_COUNT; i++) {
[_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];
}
//初始化鎖對象
_lock=[[NSLock alloc]init];
}
#pragma mark 將圖片顯示到界面
-(void)updateImageWithData:(NSData *)data andIndex:(int )index{
UIImage *image=[UIImage imageWithData:data];
UIImageView *imageView= _imageViews[index];
imageView.image=image;
}
#pragma mark 請求圖片數(shù)據(jù)
-(NSData *)requestData:(int )index{
NSData *data;
NSString *name;
//加鎖
[_lock lock];
if (_imageNames.count>0) {
name=[_imageNames lastObject];
[_imageNames removeObject:name];
}
//使用完解鎖
[_lock unlock];
if(name){
NSURL *url=[NSURL URLWithString:name];
data=[NSData dataWithContentsOfURL:url];
}
return data;
}
#pragma mark 加載圖片
-(void)loadImage:(NSNumber *)index{
int i=[index integerValue];
//請求數(shù)據(jù)
NSData *data= [self requestData:i];
//更新UI界面,此處調(diào)用了GCD主線程隊列的方法
dispatch_queue_t mainQueue= dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
[self updateImageWithData:data andIndex:i];
});
}
#pragma mark 多線程下載圖片
-(void)loadImageWithMultiThread{
int count=ROW_COUNT*COLUMN_COUNT;
dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//創(chuàng)建多個線程用于填充圖片
for (int i=0; i<count; ++i) {
//異步執(zhí)行隊列任務(wù)
dispatch_async(globalQueue, ^{
[self loadImage:[NSNumber numberWithInt:i]];
});
}
}
@end
前面也說過使用同步鎖時如果一個線程A已經(jīng)加鎖,線程B就無法進入。那么B怎么知道是否資源已經(jīng)被其他線程鎖住呢?可以通過tryLock方法,此方法會返回一個BOOL型的值,如果為YES說明獲取鎖成功,否則失敗。另外還有一個lockBeforeData:方法指定在某個時間內(nèi)獲取鎖,同樣返回一個BOOL值,如果在這個時間內(nèi)加鎖成功則返回YES,失敗則返回NO。
@synchronized代碼塊
使用@synchronized解決線程同步問題相比較NSLock要簡單一些,日常開發(fā)中也更推薦使用此方法。首先選擇一個對象作為同步對象(一般使用self),然后將”加鎖代碼”(爭奪資源的讀取、修改代碼)放到代碼塊中。@synchronized中的代碼執(zhí)行時先檢查同步對象是否被另一個線程占用,如果占用該線程就會處于等待狀態(tài),直到同步對象被釋放。下面的代碼演示了如何使用@synchronized進行線程同步:
-(NSData *)requestData:(int )index{
NSData *data;
NSString *name;
//線程同步
@synchronized(self){
if (_imageNames.count>0) {
name=[_imageNames lastObject];
[NSThread sleepForTimeInterval:0.001f];
[_imageNames removeObject:name];
}
}
if(name){
NSURL *url=[NSURL URLWithString:name];
data=[NSData dataWithContentsOfURL:url];
}
return data;
}
擴展--使用GCD解決資源搶占問題
在GCD中提供了一種信號機制,也可以解決資源搶占問題(和同步鎖的機制并不一樣)。GCD中信號量是dispatch_semaphore_t類型,支持信號通知和信號等待。每當(dāng)發(fā)送一個信號通知,則信號量+1;每當(dāng)發(fā)送一個等待信號時信號量-1,;如果信號量為0則信號會處于等待狀態(tài),直到信號量大于0開始執(zhí)行。根據(jù)這個原理我們可以初始化一個信號量變量,默認(rèn)信號量設(shè)置為1,每當(dāng)有線程進入“加鎖代碼”之后就調(diào)用信號等待命令(此時信號量為0)開始等待,此時其他線程無法進入,執(zhí)行完后發(fā)送信號通知(此時信號量為1),其他線程開始進入執(zhí)行,如此一來就達到了線程同步目的。
//
// GCD實現(xiàn)多線程--消息信號
// MultiThread
//
// Created by Kenshin Cui on 14-3-22.
// Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//
#import "KCMainViewController.h"
#import "KCImageData.h"
#define ROW_COUNT 5
#define COLUMN_COUNT 3
#define ROW_HEIGHT 100
#define ROW_WIDTH ROW_HEIGHT
#define CELL_SPACING 10
#define IMAGE_COUNT 9
@interface KCMainViewController (){
NSMutableArray *_imageViews;
NSLock *_lock;
dispatch_semaphore_t _semaphore;//定義一個信號量
}
@end
@implementation KCMainViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}
#pragma mark 界面布局
-(void)layoutUI{
//創(chuàng)建多個圖片控件用于顯示圖片
_imageViews=[NSMutableArray array];
for (int r=0; r<ROW_COUNT; r++) {
for (int c=0; c<COLUMN_COUNT; c++) {
UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)];
imageView.contentMode=UIViewContentModeScaleAspectFit;
[self.view addSubview:imageView];
[_imageViews addObject:imageView];
}
}
UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
button.frame=CGRectMake(50, 500, 220, 25);
[button setTitle:@"加載圖片" forState:UIControlStateNormal];
//添加方法
[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
//創(chuàng)建圖片鏈接
_imageNames=[NSMutableArray array];
for (int i=0; i<IMAGE_COUNT; i++) {
[_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];
}
/*初始化信號量
參數(shù)是信號量初始值
*/
_semaphore=dispatch_semaphore_create(1);
}
#pragma mark 將圖片顯示到界面
-(void)updateImageWithData:(NSData *)data andIndex:(int )index{
UIImage *image=[UIImage imageWithData:data];
UIImageView *imageView= _imageViews[index];
imageView.image=image;
}
#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;
}
#pragma mark 加載圖片
-(void)loadImage:(NSNumber *)index{
int i=[index integerValue];
//請求數(shù)據(jù)
NSData *data= [self requestData:i];
//更新UI界面,此處調(diào)用了GCD主線程隊列的方法
dispatch_queue_t mainQueue= dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
[self updateImageWithData:data andIndex:i];
});
}
#pragma mark 多線程下載圖片
-(void)loadImageWithMultiThread{
int count=ROW_COUNT*COLUMN_COUNT;
// dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//這里創(chuàng)建一個并發(fā)隊列(使用全局并發(fā)隊列也可以)
dispatch_queue_t queue=dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
for (int i=0; i<count; i++) {
dispatch_async(queue, ^{
[self loadImage:[NSNumber numberWithInt:i]];
});
}
}
@end
運行效果與前面使用同步鎖是一樣的。
擴展--控制線程通信
由于線程的調(diào)度是透明的,程序有時候很難對它進行有效的控制,為了解決這個問題iOS提供了NSCondition來控制線程通信(同前面GCD的信號機制類似)。NSCondition實現(xiàn)了NSLocking協(xié)議,所以它本身也有l(wèi)ock和unlock方法,因此也可以將它作為NSLock解決線程同步問題,此時使用方法跟NSLock沒有區(qū)別,只要在線程開始時加鎖,取得資源后釋放鎖即可,這部分內(nèi)容比較簡單在此不再演示。當(dāng)然,單純解決線程同步問題不是NSCondition設(shè)計的主要目的,NSCondition更重要的是解決線程之間的調(diào)度關(guān)系(當(dāng)然,這個過程中也必須先加鎖、解鎖)。NSCondition可以調(diào)用wati方法控制某個線程處于等待狀態(tài),直到其他線程調(diào)用signal(此方法喚醒一個線程,如果有多個線程在等待則任意喚醒一個)或者broadcast(此方法會喚醒所有等待線程)方法喚醒該線程才能繼續(xù)。
假設(shè)當(dāng)前imageNames沒有任何圖片,而整個界面能夠加載15張圖片(每張都不能重復(fù)),現(xiàn)在創(chuàng)建15個線程分別從imageNames中取圖片加載到界面中。由于imageNames中沒有任何圖片,那么15個線程都處于等待狀態(tài),只有當(dāng)調(diào)用圖片創(chuàng)建方法往imageNames中添加圖片后(每次創(chuàng)建一個)并且喚醒其他線程(這里只喚醒一個線程)才能繼續(xù)執(zhí)行加載圖片。如此,每次創(chuàng)建一個圖片就會喚醒一個線程去加載,這個過程其實就是一個典型的生產(chǎn)者-消費者模式。下面通過NSCondition實現(xiàn)這個流程的控制:
KCMainViewController.h
//
// KCMainViewController.h
// MultiThread
//
// Created by Kenshin Cui on 14-3-22.
// Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface KCMainViewController : UIViewController
#pragma mark 圖片資源存儲容器
@property (atomic,strong) NSMutableArray *imageNames;
#pragma mark 當(dāng)前加載的圖片索引(圖片鏈接地址連續(xù))
@property (atomic,assign) int currentIndex;
@end
KCMainViewController.m
//
// 線程控制
// MultiThread
//
// Created by Kenshin Cui on 14-3-22.
// Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//
#import "KCMainViewController.h"
#import "KCImageData.h"
#define ROW_COUNT 5
#define COLUMN_COUNT 3
#define ROW_HEIGHT 100
#define ROW_WIDTH ROW_HEIGHT
#define CELL_SPACING 10
#define IMAGE_COUNT 9
@interface KCMainViewController (){
NSMutableArray *_imageViews;
NSCondition *_condition;
}
@end
@implementation KCMainViewController
#pragma mark - 事件
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}
#pragma mark - 內(nèi)部私有方法
#pragma mark 界面布局
-(void)layoutUI{
//創(chuàng)建多個圖片控件用于顯示圖片
_imageViews=[NSMutableArray array];
for (int r=0; r<ROW_COUNT; r++) {
for (int c=0; c<COLUMN_COUNT; c++) {
UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)];
imageView.contentMode=UIViewContentModeScaleAspectFit;
[self.view addSubview:imageView];
[_imageViews addObject:imageView];
}
}
UIButton *btnLoad=[UIButton buttonWithType:UIButtonTypeRoundedRect];
btnLoad.frame=CGRectMake(50, 500, 100, 25);
[btnLoad setTitle:@"加載圖片" forState:UIControlStateNormal];
[btnLoad addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btnLoad];
UIButton *btnCreate=[UIButton buttonWithType:UIButtonTypeRoundedRect];
btnCreate.frame=CGRectMake(160, 500, 100, 25);
[btnCreate setTitle:@"創(chuàng)建圖片" forState:UIControlStateNormal];
[btnCreate addTarget:self action:@selector(createImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btnCreate];
//創(chuàng)建圖片鏈接
_imageNames=[NSMutableArray array];
//初始化鎖對象
_condition=[[NSCondition alloc]init];
_currentIndex=0;
}
#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];
}
#pragma mark 加載圖片并將圖片顯示到界面
-(void)loadAnUpdateImageWithIndex:(int )index{
//請求數(shù)據(jù)
NSData *data= [self requestData:index];
//更新UI界面,此處調(diào)用了GCD主線程隊列的方法
dispatch_queue_t mainQueue= dispatch_get_main_queue();
dispatch_sync(mainQueue, ^{
UIImage *image=[UIImage imageWithData:data];
UIImageView *imageView= _imageViews[index];
imageView.image=image;
});
}
#pragma mark 請求圖片數(shù)據(jù)
-(NSData *)requestData:(int )index{
NSData *data;
NSString *name;
name=[_imageNames lastObject];
[_imageNames removeObject:name];
if(name){
NSURL *url=[NSURL URLWithString:name];
data=[NSData dataWithContentsOfURL:url];
}
return data;
}
#pragma mark 加載圖片
-(void)loadImage:(NSNumber *)index{
int i=(int)[index integerValue];
//加鎖
[_condition lock];
//如果當(dāng)前有圖片資源則加載,否則等待
if (_imageNames.count>0) {
NSLog(@"loadImage work,index is %i",i);
[self loadAnUpdateImageWithIndex:i];
[_condition broadcast];
}else{
NSLog(@"loadImage wait,index is %i",i);
NSLog(@"%@",[NSThread currentThread]);
//線程等待
[_condition wait];
NSLog(@"loadImage resore,index is %i",i);
//一旦創(chuàng)建完圖片立即加載
[self loadAnUpdateImageWithIndex:i];
}
//解鎖
[_condition unlock];
}
#pragma mark - UI調(diào)用方法
#pragma mark 異步創(chuàng)建一張圖片鏈接
-(void)createImageWithMultiThread{
dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//創(chuàng)建圖片鏈接
dispatch_async(globalQueue, ^{
[self createImageName];
});
}
#pragma mark 多線程下載圖片
-(void)loadImageWithMultiThread{
int count=ROW_COUNT*COLUMN_COUNT;
dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i=0; i<count; ++i) {
//加載圖片
dispatch_async(globalQueue, ^{
[self loadImage:[NSNumber numberWithInt:i]];
});
}
}
@end
在上面的代碼中l(wèi)oadImage:方法是消費者,當(dāng)在界面中點擊“加載圖片”后就創(chuàng)建了15個消費者線程。在這個過程中每個線程進入圖片加載方法之后都會先加鎖,加鎖之后其他進程是無法進入“加鎖代碼”的。但是第一個線程進入“加鎖代碼”后去加載圖片卻發(fā)現(xiàn)當(dāng)前并沒有任何圖片,因此它只能等待。一旦調(diào)用了NSCondition的wait方法后其他線程就可以繼續(xù)進入“加鎖代碼”(注意,這一點和前面說的NSLock、@synchronized等是不同的,使用NSLock、@synchronized等進行加鎖后無論什么情況下,只要沒有解鎖其他線程就無法進入“加鎖代碼”),同時第一個線程處于等待隊列中(此時并未解鎖)。第二個線程進來之后同第一線程一樣,發(fā)現(xiàn)沒有圖片就進入等待狀態(tài),然后第三個線程進入。。。如此反復(fù),直到第十五個線程也處于等待。此時點擊“創(chuàng)建圖片”后會執(zhí)行createImageName方法,這是一個生產(chǎn)者,它會創(chuàng)建一個圖片鏈接放到imageNames中,然后通過調(diào)用NSCondition的signal方法就會在條件等待隊列中選擇一個線程(該線程會任意選取,假設(shè)為線程A)開啟,那么此時這個線程就會繼續(xù)執(zhí)行。在上面代碼中,wati方法之后會繼續(xù)執(zhí)行圖片加載方法,那么此時線程A啟動之后繼續(xù)執(zhí)行圖片加載方法,當(dāng)然此時可以成功加載圖片。加載完圖片之后線程A就會釋放鎖,整個線程任務(wù)完成。此時再次點擊”創(chuàng)建圖片“按鈕重復(fù)前面的步驟加載其他圖片。
為了說明上面的過程,這里以一個流程圖的進行說明,流程圖藍(lán)色部分代表15個加載圖片的線程,綠色部分表示創(chuàng)建圖片資源線程。
iOS中的其他鎖
在iOS開發(fā)中,除了同步鎖有時候還會用到一些其他鎖類型,在此簡單介紹一下:
NSRecursiveLock :遞歸鎖,有時候“加鎖代碼”中存在遞歸調(diào)用,遞歸開始前加鎖,遞歸調(diào)用開始后會重復(fù)執(zhí)行此方法以至于反復(fù)執(zhí)行加鎖代碼最終造成死鎖,這個時候可以使用遞歸鎖來解決。使用遞歸鎖可以在一個線程中反復(fù)獲取鎖而不造成死鎖,這個過程中會記錄獲取鎖和釋放鎖的次數(shù),只有最后兩者平衡鎖才被最終釋放。
NSDistributedLock:分布鎖,它本身是一個互斥鎖,基于文件方式實現(xiàn)鎖機制,可以跨進程訪問。
pthread_mutex_t:同步鎖,基于C語言的同步鎖機制,使用方法與其他同步鎖機制類似。
提示:在開發(fā)過程中除非必須用鎖,否則應(yīng)該盡可能不使用鎖,因為多線程開發(fā)本身就是為了提高程序執(zhí)行順序,而同步鎖本身就只能一個進程執(zhí)行,這樣不免降低執(zhí)行效率。
5、總結(jié)
1、無論使用哪種方法進行多線程開發(fā),每個線程啟動后并不一定立即執(zhí)行相應(yīng)的操作,具體什么時候由系統(tǒng)調(diào)度(CPU空閑時就會執(zhí)行)。
2、更新UI應(yīng)該在主線程(UI線程)中進行,并且推薦使用同步調(diào)用,常用的方法如下:
-(void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait或者-(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL) wait;方法傳遞主線程[NSThread mainThread]) [NSOperationQueue mainQueue] addOperationWithBlock: dispatch_sync(dispatch_get_main_queue(), ^{})
3、NSThread適合輕量級多線程開發(fā),控制線程順序比較難,同時線程總數(shù)無法控制(每次創(chuàng)建并不能重用之前的線程,只能創(chuàng)建一個新的線程)。
4、對于簡單的多線程開發(fā)建議使用NSObject的擴展方法完成,而不必使用NSThread。
5、可以使用NSThread的currentThread方法取得當(dāng)前線程,使用 sleepForTimeInterval:方法讓當(dāng)前線程休眠。
6、NSOperation進行多線程開發(fā)可以控制線程總數(shù)及線程依賴關(guān)系。
7、創(chuàng)建一個NSOperation不應(yīng)該直接調(diào)用start方法(如果直接start則會在主線程中調(diào)用)而是應(yīng)該放到NSOperationQueue中啟動。
8、相比NSInvocationOperation推薦使用NSBlockOperation,代碼簡單,同時由于閉包性使它沒有傳參問題。
9、NSOperation是對GCD面向?qū)ο蟮腛bjC封裝,但是相比GCD基于C語言開發(fā),效率卻更高,建議如果任務(wù)之間有依賴關(guān)系或者想要監(jiān)聽任務(wù)完成狀態(tài)的情況下優(yōu)先選擇NSOperation否則使用GCD。
10、在GCD中串行隊列中的任務(wù)被安排到一個單一線程執(zhí)行(不是主線程),可以方便地控制執(zhí)行順序;并發(fā)隊列在多個線程中執(zhí)行(前提是使用異步方法),順序控制相對復(fù)雜,但是更高效。
11、在GDC中一個操作是多線程執(zhí)行還是單線程執(zhí)行取決于當(dāng)前隊列類型和執(zhí)行方法,只有隊列類型為并行隊列并且使用異步方法執(zhí)行時才能在多個線程中執(zhí)行(如果是并行隊列使用同步方法調(diào)用則會在主線程中執(zhí)行)。
12、相比使用NSLock,@synchronized更加簡單,推薦使用后者。