IOS 开发多线程

前言

前言:了解多线程之前首先了解一些计算机的基本概念
计算机操作系统的基本概念
进程: 一个具有一定独立功能的程序关于某个数据集合的一次运行活动。可以理解成一个运行中的应用程序。
线程: 程序执行流的最小单元,线程是进程中的一个实体。
同步: 只能在当前线程按先后顺序依次执行,不开启新线程。
异步: 可以在当前线程开启多个新线程执行,可不按顺序执行。
队列: 装载线程任务的队形结构。
并发: 线程执行可以同时一起进行执行。
串行: 线程执行只能依次逐一先后有序的执行。

1.什么是多线程

多线程是一个比较轻量级的方法来实现单个应用程序内多个代码执行路径。在系 统级别内,程序并排执行,系统分配到每个程序的执行时间是基于该程序的所需时间 和其他程序的所需时间来决定的。然而在每个应程序的内部,存在一个或多个执行线 程,它同时或在一个几乎同时发生的方式里执行不同的任务。系统本身管理这些执行 的线程,调度它们在可用的内核上运行,并在需要让其他线程执行的时候抢先打断它 们。
从技术角度来看,一个线程就是一个需要管理执行代码的内核级和应用级数据结 构组合。内核级结构协助调度线程事件,并抢占式调度一个线程到可用的内核之上。 应用级结构包括用于存储函数调用的调用堆栈和应用程序需要管理和操作线程属性 和状态的结构。
摘取自<多线程编程指南>

IOS多线程概念图.png
2.iOS多线程对比

1.NSThread每个NSThread对象对应一个线程,真正最原始的线程。
1)优点:NSThread 轻量级最低,相对简单。
2)缺点:手动管理所有的线程活动,如生命周期、线程同步、睡眠等。

2.1.1NSThread三种实现开启线程方式
1.动态实例化
NSThread *thread = [[NSThread alloc] initWithTarget:self            selector:@selector(loadImageSource:) object:imgUrl];
thread.threadPriority = 1;// 设置线程的优先级(0.0 - 1.0,1.0最高级)
[thread start];
2.静态实例化
[NSThread detachNewThreadSelector:@selector(loadImageSource:) toTarget:self withObject:imgUrl];
3.隐式实例化
[self performSelectorInBackground:@selector(loadImageSource:) withObject:imgUrl];
//在指定线程上操作
[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:YES];
2.1.2使用场景1(开启子线程加载一张图片)

解决线程阻塞问题在资源下载过程中,由于网络原因有时候很难保证下载时间,如果不使用多线程可能用户完成一个下载操作需要长时间的等待,这个过程中无法进行其他操作。下面演示一个采用多线程下载图片的过程,在这个示例中点击按钮会启动一个线程去下载图片,下载完成后使用UIImageView将图片显示到界面中。可以看到用户点击完下载按钮后,不管图片是否下载完成都可以继续操作界面,不会造成阻塞。


@interface NSThreadViewController (){
    UIImageView *_imageView;
}

@end

@implementation NSThreadViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    [self layoutUI];
}

#pragma mark 界面布局
-(void)layoutUI{
    _imageView =[[UIImageView alloc]initWithFrame:[UIScreen mainScreen].bounds];
    _imageView.contentMode=UIViewContentModeScaleAspectFit;
    [self.view addSubview:_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];
}


#pragma mark 将图片显示到界面
-(void)updateImage:(NSData *)imageData{
    UIImage *image=[UIImage imageWithData:imageData];
    _imageView.image=image;
}

#pragma mark 请求图片数据
-(NSData *)requestData{
    //对于多线程操作建议把线程操作放到@autoreleasepool中
    @autoreleasepool {
        NSURL *url=[NSURL URLWithString:@"https://store.storeimages.cdn-apple.com/8750/as-images.apple.com/is/image/AppleInc/aos/published/images/s/eg/segment/hero/segment-hero-macbook-2017_GEO_CN?wid=400&hei=300&fmt=png-alpha&qlt=95&.v=1501280548817"];
        NSData *data=[NSData dataWithContentsOfURL:url];
        return data;
    }
}

#pragma mark 加载图片
-(void)loadImage{
    //请求数据
    NSData *data= [self requestData];
    /*将数据显示到UI控件,注意只能在主线程中更新UI,
     另外performSelectorOnMainThread方法是NSObject的分类方法,每个NSObject对象都有此方法,
     它调用的selector方法是当前调用控件的方法,例如使用UIImageView调用的时候selector就是UIImageView的方法
     Object:代表调用方法的参数,不过只能传递一个参数(如果有多个参数请使用对象进行封装)
     waitUntilDone:是否线程任务完成执行
     */
    [self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES];
}

#pragma mark 多线程下载图片
-(void)loadImageWithMultiThread{
    //方法1:使用对象方法
    //创建一个线程,第一个参数是请求的操作,第二个参数是操作方法的参数
    //    NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
    //    //启动一个线程,注意启动一个线程并非就一定立即执行,而是处于就绪状态,当系统调度时才真正执行
    //    [thread start];
    
    //方法2:使用类方法
    [NSThread detachNewThreadSelector:@selector(loadImage) toTarget:self withObject:nil];
}
picture.gif
2.1.3使用场景2(开启子线程加载多张图片)

#import "NSThreadTwoViewController.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
@interface NSThreadTwoViewController (){
    NSMutableArray *_imageViews;
}


@end

@implementation NSThreadTwoViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];

    [self layoutUI];
}

#pragma mark 界面布局
-(void)layoutUI{
    //创建多个图片控件用于显示图片
    _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), 100+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];
}

#pragma mark 将图片显示到界面
-(void)updateImage:(KCImageData *)imageData{
    UIImage *image=[UIImage imageWithData:imageData.data];
    UIImageView *imageView= _imageViews[imageData.index];
    imageView.image=image;
}

#pragma mark 请求图片数据
-(NSData *)requestData:(NSInteger )index{
    //对于多线程操作建议把线程操作放到@autoreleasepool中
    @autoreleasepool {
        NSURL *url=[NSURL URLWithString:@"https://store.storeimages.cdn-apple.com/8750/as-images.apple.com/is/image/AppleInc/aos/published/images/s/eg/segment/hero/segment-hero-macbook-2017_GEO_CN?wid=400&hei=300&fmt=png-alpha&qlt=95&.v=1501280548817"];
        NSData *data=[NSData dataWithContentsOfURL:url];
        return data;
    }
}

#pragma mark 加载图片
-(void)loadImage:(NSNumber *)index{
    //    NSLog(@"%i",i);
    //currentThread方法可以取得当前操作线程
    NSLog(@"current thread:%@",[NSThread currentThread]);
    
    NSInteger i=[index integerValue];
    
    //    NSLog(@"%i",i);//未必按顺序输出
    
    NSData *data= [self requestData:i];
    
    KCImageData *imageData=[[KCImageData alloc]init];
    imageData.index= i;
    imageData.data=data;
    [self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES];
}

#pragma mark 多线程下载图片
-(void)loadImageWithMultiThread{
    //创建多个线程用于填充图片
    for (int i=0; i<ROW_COUNT*COLUMN_COUNT; ++i) {
        //        [NSThread detachNewThreadSelector:@selector(loadImage:) toTarget:self withObject:[NSNumber numberWithInt:i]];
        NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]];
        thread.name=[NSString stringWithFormat:@"myThread%i",i];//设置线程名称
        [thread start];
    }
}

效果图.gif

从上面的运行效果大家不难发现,图片并未按顺序加载,原因有两个:第一,每个线程的实际执行顺序并不一定按顺序执行(虽然是按顺序启动);第二,每个线程执行时实际网络状况很可能不一致。当然网络问题无法改变,只能尽可能让网速更快,但是可以改变线程的优先级,让15个线程优先执行某个线程。线程优先级范围为0~1,值越大优先级越高,每个线程的优先级默认为0.5。修改图片下载方法如下,改变最后一张图片加载的优先级,这样可以提高它被优先加载的几率,但是它也未必就第一个加载。因为首先其他线程是先启动的,其次网络状况我们没办法修改。

线程状态

线程状态分为isExecuting(正在执行)、isFinished(已经完成)、isCancellled(已经取消)三种。其中取消状态程序可以干预设置,只要调用线程的cancel方法即可。但是需要注意在主线程中仅仅能设置线程状态,并不能真正停止当前线程,如果要终止线程必须在线程中调用exist方法,这是一个静态方法,调用该方法可以退出当前线程。
假设在图片加载过程中点击停止按钮让没有完成的线程停止加载,具体实现如下:

#import "NSthreadThreeViewController.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 NSthreadThreeViewController (){
    NSMutableArray *_imageViews;
    NSMutableArray *_imageNames;
    NSMutableArray *_threads;
}

@end

@implementation NSthreadThreeViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];

    [self layoutUI];
}
#pragma mark 界面布局
-(void)layoutUI{
    //创建多个图片空间用于显示图片
    _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), 100+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 *buttonStart=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    buttonStart.frame=CGRectMake(50, 500, 100, 25);
    [buttonStart setTitle:@"加载图片" forState:UIControlStateNormal];
    [buttonStart addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:buttonStart];
    
    //停止按钮
    UIButton *buttonStop=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    buttonStop.frame=CGRectMake(160, 500, 100, 25);
    [buttonStop setTitle:@"停止加载" forState:UIControlStateNormal];
    [buttonStop addTarget:self action:@selector(stopLoadImage) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:buttonStop];
    
    //创建图片链接
    _imageNames=[NSMutableArray array];
     for (int i=0; i<ROW_COUNT*COLUMN_COUNT; i++) {
        [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];
    }
     }
     
#pragma mark 将图片显示到界面
     -(void)updateImage:(KCImageData *)imageData{
         UIImage *image=[UIImage imageWithData:imageData.data];
         UIImageView *imageView= _imageViews[imageData.index];
         imageView.image=image;
     }
     
#pragma mark 请求图片数据
     -(NSData *)requestData:(NSInteger )index{
         //对于多线程操作建议把线程操作放到@autoreleasepool中
         @autoreleasepool {
             NSURL *url=[NSURL URLWithString:_imageNames[index]];
             NSData *data=[NSData dataWithContentsOfURL:url];
             
             return data;
         }
     }
     
#pragma mark 加载图片
     -(void)loadImage:(NSNumber *)index{
         NSInteger i=[index integerValue];
         
         NSData *data= [self requestData:i];
         
         
         NSThread *currentThread=[NSThread currentThread];
         
         //    如果当前线程处于取消状态,则退出当前线程
         if (currentThread.isCancelled) {
             NSLog(@"thread(%@) will be cancelled!",currentThread);
             [NSThread exit];//取消当前线程
         }
         
         KCImageData *imageData=[[KCImageData alloc]init];
         imageData.index=i;
         imageData.data=data;
         [self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES];
     }
     
#pragma mark 多线程下载图片
     -(void)loadImageWithMultiThread{
         int count=ROW_COUNT*COLUMN_COUNT;
         _threads=[NSMutableArray arrayWithCapacity:count];
         
         //创建多个线程用于填充图片
         for (int i=0; i<count; ++i) {
             NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]];
             thread.name=[NSString stringWithFormat:@"myThread%i",i];//设置线程名称
             [_threads addObject:thread];
         }
         //循环启动线程
         for (int i=0; i<count; ++i) {
             NSThread *thread= _threads[i];
             [thread start];
         }
     }
     
#pragma mark 停止加载图片
     -(void)stopLoadImage{
         for (int i=0; i<ROW_COUNT*COLUMN_COUNT; i++) {
             NSThread *thread= _threads[i];
             //判断线程是否完成,如果没有完成则设置为取消状态
             //注意设置为取消状态仅仅是改变了线程状态而言,并不能终止线程
             if (!thread.isFinished) {
                 [thread cancel];
                 
             }
         }
     }

实现效果如图

效果图.gif

2.NSOperation
自带线程管理的抽象类。
1)优点:自带线程周期管理,操作上可更注重自己逻辑。
2)缺点:面向对象的抽象类,只能实现它或者使用它定义好的两个子类:NSInvocationOperation 和 NSBlockOperation。

主要的实现方式:结合NSOperation和NSOperationQueue实现多线程编程。
实例化NSOperation的子类,绑定执行的操作。
创建NSOperationQueue队列,将NSOperation实例添加进来。
系统会自动将NSOperationQueue队列中检测取出和执行NSOperation的操作。

①.NSInvocationOperation创建线程。
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(loadImageSource:) object:imgUrl];
//[invocationOperation start];//直接会在当前线程主线程执行
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperation:invocationOperation];
②.NSBlockOperation创建线程
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
    [self loadImageSource:imgUrl];
}];
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue addOperation:blockOperation];

2.2 NSInvocationOperation使用场景

首先使用NSInvocationOperation进行一张图片的加载演示,整个过程就是:创建一个操作,在这个操作中指定调用方法和参数,然后加入到操作队列。其他代码基本不用修改,直接修加载图片方法如下:

-(void)loadImageWithMultiThread{
    /*创建一个调用操作
     object:调用方法参数
    */
    NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
    //创建完NSInvocationOperation对象并不会调用,它由一个start方法启动操作,但是注意如果直接调用start方法,则此操作会在主线程中调用,一般不会这么操作,而是添加到NSOperationQueue中
//    [invocationOperation start];
    
    //创建操作队列
    NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
    //注意添加到操作队后,队列会开启一个线程执行此操作
    [operationQueue addOperation:invocationOperation];
}
2.2.2 NSBlockOperation使用场景:下面采用NSBlockOperation创建多个线程加载图片。

#import "NSOperationViewController.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

@interface NSOperationViewController (){
    NSMutableArray *_imageViews;
    NSMutableArray *_imageNames;
}

@end

@implementation NSOperationViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    [self layoutUI];
}
#pragma mark 界面布局
-(void)layoutUI{
    //创建多个图片控件用于显示图片
    _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), 100+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];
    
    //创建图片链接
    _imageNames=[NSMutableArray array];
    for (int i=0; i<ROW_COUNT*COLUMN_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:(NSInteger)index{
    UIImage *image=[UIImage imageWithData:data];
    UIImageView *imageView= _imageViews[index];
    imageView.image=image;
}

#pragma mark 请求图片数据
-(NSData *)requestData:(NSInteger )index{
    //对于多线程操作建议把线程操作放到@autoreleasepool中
    @autoreleasepool {
        NSURL *url=[NSURL URLWithString:_imageNames[index]];
        NSData *data=[NSData dataWithContentsOfURL:url];
        
        return data;
    }
}

#pragma mark 加载图片
-(void)loadImage:(NSNumber *)index{
    NSInteger i=[index integerValue];
    
    //请求数据
    NSData *data= [self requestData:i];
    NSLog(@"%@",[NSThread currentThread]);
    //更新UI界面,此处调用了主线程队列的方法(mainQueue是UI主线程)
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        [self updateImageWithData:data andIndex:i];
    }];
}



#pragma mark 多线程下载图片
//-(void)loadImageWithMultiThread{
//    int count=ROW_COUNT*COLUMN_COUNT;
//    //创建操作队列
//    NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
//    operationQueue.maxConcurrentOperationCount=5;//设置最大并发线程数
//    //创建多个线程用于填充图片
//    for (int i=0; i<count; ++i) {
//        //方法1:创建操作块添加到队列
//        //        //创建多线程操作
//        //        NSBlockOperation *blockOperation=[NSBlockOperation blockOperationWithBlock:^{
//        //            [self loadImage:[NSNumber numberWithInt:i]];
//        //        }];
//        //        //创建操作队列
//        //
//        //        [operationQueue addOperation:blockOperation];
//        
//        //方法2:直接使用操队列添加操作
//        [operationQueue addOperationWithBlock:^{
//            [self loadImage:[NSNumber numberWithInt:i]];
//        }];
//        
//    }
//}

-(void)loadImageWithMultiThread{//设置优先加载最后一张照片
    int count=ROW_COUNT*COLUMN_COUNT;
    //创建操作队列
    NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
    operationQueue.maxConcurrentOperationCount=5;//设置最大并发线程数
    
    NSBlockOperation *lastBlockOperation=[NSBlockOperation blockOperationWithBlock:^{
        [self loadImage:[NSNumber numberWithInt:(count-1)]];
    }];
    //创建多个线程用于填充图片
    for (int i=0; i<count-1; ++i) {
        //方法1:创建操作块添加到队列
        //创建多线程操作
        NSBlockOperation *blockOperation=[NSBlockOperation blockOperationWithBlock:^{
            [self loadImage:[NSNumber numberWithInt:i]];
        }];
        //设置依赖操作为最后一张图片加载操作
        [blockOperation addDependency:lastBlockOperation];
        
        [operationQueue addOperation:blockOperation];
        
    }
    //将最后一个图片的加载操作加入线程队列
    [operationQueue addOperation:lastBlockOperation];
}

效果图.gif
线程执行顺序

前面使用NSThread很难控制线程的执行顺序,但是使用NSOperation就容易多了,每个NSOperation可以设置依赖线程。假设操作A依赖于操作B,线程操作队列在启动线程时就会首先执行B操作,然后执行A。对于前面优先加载最后一张图的需求,只要设置前面的线程操作的依赖线程为最后一个操作即可。修改图片加载方法如下:

-(void)loadImageWithMultiThread{
    int count=ROW_COUNT*COLUMN_COUNT;
    //创建操作队列
    NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
    operationQueue.maxConcurrentOperationCount=5;//设置最大并发线程数
    
    NSBlockOperation *lastBlockOperation=[NSBlockOperation blockOperationWithBlock:^{
        [self loadImage:[NSNumber numberWithInt:(count-1)]];
    }];
    //创建多个线程用于填充图片
    for (int i=0; i<count-1; ++i) {
        //方法1:创建操作块添加到队列
        //创建多线程操作
        NSBlockOperation *blockOperation=[NSBlockOperation blockOperationWithBlock:^{
            [self loadImage:[NSNumber numberWithInt:i]];
        }];
        //设置依赖操作为最后一张图片加载操作
        [blockOperation addDependency:lastBlockOperation];
        
        [operationQueue addOperation:blockOperation];
        
    }
    //将最后一个图片的加载操作加入线程队列
    [operationQueue addOperation:lastBlockOperation];
}

3.GCD
Grand Central Dispatch (GCD)是Apple开发的一个多核编程的解决方法。
1)优点:最高效,避开并发陷阱。
2)缺点:基于C实现。

通过 GCD,开发者不用再直接跟线程打交道了,只需要向队列中添加代码块即可,GCD 在后端管理着一个线程池。GCD 不仅决定着你的代码块将在哪个线程被执行,它还根据可用的系统资源对这些线程进行管理。这样可以将开发者从线程管理的工作中解放出来,通过集中的管理线程,来缓解大量线程被创建的问题。

GCD 带来的另一个重要改变是,作为开发者可以将工作考虑为一个队列,而不是一堆线程,这种并行的抽象模型更容易掌握和使用。

GCD 公开有 5 个不同的队列:运行在主线程中的 main queue,3 个不同优先级的后台队列,以及一个优先级更低的后台队列(用于 I/O)。 另外,开发者可以创建自定义队列:串行或者并行队列。自定义队列非常强大,在自定义队列中被调度的所有 block 最终都将被放入到系统的全局队列中和线程池中。

GCD.png

Serial Diapatch Queue 串行队列

当任务相互依赖,具有明显的先后顺序的时候,使用串行队列是一个不错的选择 创建一个串行队列:

dispatch_queue_t serialDispatchQueue=dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_SERIAL); 

第一个参数为队列名,第二个参数为队列类型,当然,第二个参数如果写NULL,创建出来的也是一个串行队列。然后我们在异步线程来执行这个队列:

dispatch_async(serialDispatchQueue, ^{  
    NSLog(@"1");  
});  
    
dispatch_async(serialDispatchQueue, ^{  
    sleep(2);  
    NSLog(@"2");  
});  
    
dispatch_async(serialDispatchQueue, ^{  
    sleep(1);  
    NSLog(@"3");  
}); 

为了能更好的理解,我给每个异步线程都添加了一个log,看一下日志平台的log:

2016-03-07 10:17:13.907 GCD[2195:61497] 1
2016-03-07 10:17:15.911 GCD[2195:61497] 2
2016-03-07 10:17:16.912 GCD[2195:61497] 3

串行队列使用场景
使用串行队列时首先要创建一个串行队列,然后调用异步调用方法,在此方法中传入串行队列和线程操作即可自动执行。下面使用线程队列演示图片的加载过程,你会发现多张图片会按顺序加载,因为当前队列中只有一个线程。

#import "GCDViewController.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
@interface GCDViewController (){
    NSMutableArray *_imageViews;
    NSMutableArray *_imageNames;
}


@end

@implementation GCDViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    [self layoutUI];
    
   
}
#pragma mark 界面布局
-(void)layoutUI{
    //创建多个图片控件用于显示图片
    _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), 100+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];
    
    //创建图片链接
    _imageNames=[NSMutableArray array];
    for (int i=0; i<ROW_COUNT*COLUMN_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:(NSInteger )index{
    UIImage *image=[UIImage imageWithData:data];
    UIImageView *imageView= _imageViews[index];
    imageView.image=image;
}

#pragma mark 请求图片数据
-(NSData *)requestData:(NSInteger )index{
    NSURL *url=[NSURL URLWithString:_imageNames[index]];
    NSData *data=[NSData dataWithContentsOfURL:url];
    
    return data;
}

#pragma mark 加载图片
-(void)loadImage:(NSNumber *)index{
    
    //如果在串行队列中会发现当前线程打印变化完全一样,因为他们在一个线程中
    NSLog(@"thread is :%@",[NSThread currentThread]);
    
    NSInteger i=[index integerValue];
    //请求数据
    NSData *data= [self requestData:i];
    //更新UI界面,此处调用了GCD主线程队列的方法
    dispatch_queue_t mainQueue= dispatch_get_main_queue();
    
    dispatch_async(mainQueue, ^{
        [self updateImageWithData:data andIndex:i];
    });
}

#pragma mark 多线程下载图片
-(void)loadImageWithMultiThread{
    int count=ROW_COUNT*COLUMN_COUNT;
    
    /*创建一个串行队列
     第一个参数:队列名称
     第二个参数:队列类型
     */
    dispatch_queue_t serialQueue=dispatch_queue_create("myThreadQueue1", DISPATCH_QUEUE_SERIAL);//注意queue对象不是指针类型
    //创建多个线程用于填充图片
    for (int i=0; i<count; ++i) {
        //异步执行队列任务   任务按照顺序执行 并且会开启子线程去执行
        dispatch_async(serialQueue, ^{
            [self loadImage:[NSNumber numberWithInt:i]];
        });
        
    }
    //非ARC环境请释放
    //    dispatch_release(seriQueue);
}
效果图.gif
Concurrent Diapatch Queue 并发队列

与串行队列刚好相反,他不会存在任务间的相互依赖。

创建一个并发队列:

dispatch_queue_t concurrentDispatchQueue=dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);

比较2个队列的创建,我们发现只有第二个参数从DISPATCH_QUEUE_SERIAL变成了对应的DISPATCH_QUEUE_CONCURRENT,其他完全一样。

用同一段代码,换一种队列我们来比较一下效果:

dispatch_async(concurrentDispatchQueue, ^{
    NSLog(@"1");
});
dispatch_async(concurrentDispatchQueue, ^{
    sleep(2);
    NSLog(@"2");
});
dispatch_async(concurrentDispatchQueue, ^{
    sleep(1);
    NSLog(@"3");
});
输出的log:
2016-03-07 10:42:38.289 GCD[2260:72557] 1
2016-03-07 10:42:39.291 GCD[2260:72559] 3
2016-03-07 10:42:40.293 GCD[2260:72556] 2

结论:我们发现,log的输出在3个不同编号的线程中进行,而且相互不依赖,不阻塞。并且任务的执行顺序是无序的。

修改串行队列加载图片部分的代码

-(void)loadImageWithMultiThread{
    int count=ROW_COUNT*COLUMN_COUNT;
    
    /*取得全局队列
     第一个参数:线程优先级
     第二个参数:标记参数,目前没有用,一般传入0
    */
    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]];
        });
    }
}

运行效果如下


效果图.gif
Global Queue & Main Queue

这是系统为我们准备的2个队列:

Global Queue其实就是系统创建的Concurrent Diapatch Queue
Main Queue 其实就是系统创建的位于主线程的Serial Diapatch Queue
通常情况我们会把这2个队列放在一起使用,也是我们最常用的开异步线程-执行异步任务-回主线程的一种方式

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"异步线程");
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"异步主线程");
    });
});

通过上面的代码我们发现了2个有意思的点:
dispatch_get_global_queue存在优先级,没错,他一共有4个优先级:

#define DISPATCH_QUEUE_PRIORITY_HIGH 2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    NSLog(@"4");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
    NSLog(@"3");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"2");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    NSLog(@"1");
});

在指定优先级之后,同一个队列会按照这个优先级执行,打印的顺序为1、2、3、4,当然这不是串行队列,所以不存在绝对回调先后。(设置优先级就是说可以优先执行某个任务但是这个任务会不会比其他任务先执行完不能确定)

异步主线程
在日常工作中,除了在其他线程返回主线程的时候需要用这个方法,还有一些时候我们在主线程中直接调用异步主线程,这是利用dispatch_async的特性:block中的任务会放在主线程本次runloop之后返回。这样,有些存在先后顺序的问题就可以得到解决了。

说完了队列,我们再说说GCD提供的一些操作队列的方法

dispatch_set_target_queue

刚刚我们说了系统的Global Queue是可以指定优先级的,那我们如何给自己创建的队列执行优先级呢?这里我们就可以用到dispatch_set_target_queue这个方法:

dispatch_queue_t serialDispatchQueue=dispatch_queue_create("com.test.queue", NULL);
dispatch_queue_t dispatchgetglobalqueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
dispatch_set_target_queue(serialDispatchQueue, dispatchgetglobalqueue);
dispatch_async(serialDispatchQueue, ^{
    NSLog(@"我优先级低,先让让");
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"我优先级高,我先block");
});

我把自己创建的队列塞到了系统提供的global_queue队列中,我们可以理解为:我们自己创建的queue其实是位于global_queue中执行,所以改变global_queue的优先级,也就改变了我们自己所创建的queue的优先级。所以我们常用这种方式来管理子队列。

dispatch_after

这个是最常用的,用来延迟执行的GCD方法,因为在主线程中我们不能用sleep来延迟方法的调用,所以用它是最合适的,我们做一个简单的例子:

NSLog(@"小破孩-波波1");
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    NSLog(@"小破孩-波波2");
});

输出的结果:

2016-03-07 11:25:06.019 GCD[2443:95722] 小破孩-波波1
2016-03-07 11:25:08.019 GCD[2443:95722] 小破孩-波波2
我们看到他就是在主线程,就是刚好延迟了2秒,当然,我说这个2秒并不是绝对的,为什么这么说?还记得我之前在介绍dispatch_async这个特性的时候提到的吗?他的block中方法的执行会放在主线程runloop之后,所以,如果此时runloop周期较长的时候,可能会有一些时差产生。
dispatch_group

当我们需要监听一个并发队列中,所有任务都完成了,就可以用到这个group,因为并发队列你并不知道哪一个是最后执行的,所以以单独一个任务是无法监听到这个点的,如果把这些单任务都放到同一个group,那么,我们就能通过dispatch_group_notify方法知道什么时候这些任务全部执行完成了。

dispatch_queue_t queue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group=dispatch_group_create();
dispatch_group_async(group, queue, ^{NSLog(@"0");});
dispatch_group_async(group, queue, ^{NSLog(@"1");});
dispatch_group_async(group, queue, ^{NSLog(@"2");});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"down");});

在例子中,我把3个log分别放在并发队列中,通过把这个并发队列任务统一加入group中,group每次runloop的时候都会调用一个方法dispatch_group_wait(group, DISPATCH_TIME_NOW),用来检查group中的任务是否已经完成,如果已经完成了,那么会执行dispatch_group_notify的block,输出’down’看一下运行结果:

2016-03-07 14:21:58.647 GCD[9424:156388] 2
2016-03-07 14:21:58.647 GCD[9424:156382] 0
2016-03-07 14:21:58.647 GCD[9424:156385] 1
2016-03-07 14:21:58.650 GCD[9424:156324] down

此方法的作用是在并发队列中,完成在它之前提交到队列中的任务后打断,单独执行其block,并在执行完成之后才能继续执行在他之后提交到队列中的任务:

dispatch_queue_t concurrentDispatchQueue=dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentDispatchQueue, ^{NSLog(@"0");});
dispatch_async(concurrentDispatchQueue, ^{NSLog(@"1");});
dispatch_async(concurrentDispatchQueue, ^{NSLog(@"2");});
dispatch_async(concurrentDispatchQueue, ^{NSLog(@"3");});
dispatch_barrier_async(concurrentDispatchQueue, ^{sleep(1); NSLog(@"4");});
dispatch_async(concurrentDispatchQueue, ^{NSLog(@"5");});
dispatch_async(concurrentDispatchQueue, ^{NSLog(@"6");});
dispatch_async(concurrentDispatchQueue, ^{NSLog(@"7");});
dispatch_async(concurrentDispatchQueue, ^{NSLog(@"8");});

输出的结果为:

2016-03-07 14:45:32.410 GCD[10079:169655] 1
2016-03-07 14:45:32.410 GCD[10079:169658] 2
2016-03-07 14:45:32.410 GCD[10079:169656] 0
2016-03-07 14:45:32.410 GCD[10079:169661] 3
2016-03-07 14:45:33.414 GCD[10079:169661] 4
2016-03-07 14:45:33.415 GCD[10079:169661] 5
2016-03-07 14:45:33.415 GCD[10079:169658] 6
2016-03-07 14:45:33.415 GCD[10079:169655] 8
2016-03-07 14:45:33.415 GCD[10079:169662] 7

总结:4之后的任务在我线程sleep之后才执行,这其实就起到了一个线程锁的作用,在多个线程同时操作一个对象的时候,读可以放在并发进行,当写的时候,我们就可以用dispatch_barrier_async方法,效果杠杠的。

dispatch_sync

dispatch_sync 会在当前线程执行队列,并且阻塞当前线程中之后运行的代码,所以,同步线程非常有可能导致死锁现象,我们这边就举一个死锁的例子,直接在主线程调用以下代码:

dispatch_sync(dispatch_get_main_queue(), ^{
    NSLog(@"有没有同步主线程?");
});

根据FIFO(先进先出)的原则,block里面的代码应该在主线程此次runloop后执行,但是由于他是同步队列,所有他之后的代码会等待其执行完成后才能继续执行,2者相互等待,所以就出现了死锁。

我们再举一个比较特殊的例子:

dispatch_queue_t queue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(queue, ^{sleep(1);NSLog(@"1");});
dispatch_sync(queue, ^{sleep(1);NSLog(@"2");});
dispatch_sync(queue, ^{sleep(1);NSLog(@"3");});
NSLog(@"4");

其打印结果为:

2016-03-07 17:15:48.124 GCD[14198:272683] 1
2016-03-07 17:15:49.125 GCD[14198:272683] 2
2016-03-07 17:15:50.126 GCD[14198:272683] 3
2016-03-07 17:15:50.126 GCD[14198:272683] 4

从线程编号中我们发现,同步方法没有去开新的线程,而是在当前线程中执行队列,会有人问,上文说dispatch_get_global_queue不是并发队列,并发队列不是应该会在开启多个线程吗?这个前提是用异步方法。GCD其实是弱化了线程的管理,强化了队列管理,这使我们理解变得比较形象。

dispatch_apply

这个方法用于无序查找,在一个数组中,我们能开启多个线程来查找所需要的值,我这边也举个例子:

NSArray *array=[[NSArray alloc]initWithObjects:@"0",@"1",@"2",@"3",@"4",@"5",@"6", nil];
dispatch_queue_t queue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply([array count], queue, ^(size_t index) {
    NSLog(@"%zu=%@",index,[array objectAtIndex:index]);
});
NSLog(@"阻塞");

输出结果:

2016-03-07 17:36:50.726 GCD[14318:291701] 1=1
2016-03-07 17:36:50.726 GCD[14318:291705] 0=0
2016-03-07 17:36:50.726 GCD[14318:291783] 3=3
2016-03-07 17:36:50.726 GCD[14318:291782] 2=2
2016-03-07 17:36:50.726 GCD[14318:291784] 5=5
2016-03-07 17:36:50.726 GCD[14318:291627] 4=4
2016-03-07 17:36:50.726 GCD[14318:291785] 6=6
2016-03-07 17:36:50.727 GCD[14318:291627] 阻塞
通过输出log,我们发现这个方法虽然会开启多个线程来遍历这个数组,但是在遍历完成之前会阻塞主线程。
dispatch_suspend & dispatch_resume

队列挂起和恢复,这个没什么好说的,直接上代码:

dispatch_queue_t concurrentDispatchQueue=dispatch_queue_create("com.test.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentDispatchQueue, ^{serialDispatchQueue
    for (int i=0; i<100; i++)
    {
        NSLog(@"%i",i);
        if (i==50)
        {
            NSLog(@"-----------------------------------");
            dispatch_suspend(concurrentDispatchQueue);
            sleep(3);
            dispatch_async(dispatch_get_main_queue(), ^{
                dispatch_resume(concurrentDispatchQueue);
            });
        }
    }
});

我们甚至可以在不同的线程对这个队列进行挂起和恢复,因为GCD是对队列的管理。

Semaphore

我们可以通过设置信号量的大小,来解决并发过多导致资源吃紧的情况,以单核CPU做并发为例,一个CPU永远只能干一件事情,那如何同时处理多个事件呢,聪明的内核工程师让CPU干第一件事情,一定时间后停下来,存取进度,干第二件事情以此类推,所以如果开启非常多的线程,单核CPU会变得非常吃力,即使多核CPU,核心数也是有限的,所以合理分配线程,变得至关重要,那么如何发挥多核CPU的性能呢?如果让一个核心模拟传很多线程,经常干一半放下干另一件事情,那效率也会变低,所以我们要合理安排,将单一任务或者一组相关任务并发至全局队列中运算或者将多个不相关的任务或者关联不紧密的任务并发至用户队列中运算,所以用好信号量,合理分配CPU资源,程序也能得到优化,当日常使用中,信号量也许我们只起到了一个计数的作用,真的有点大材小用。

dispatch_semaphore_t semaphore = dispatch_semaphore_create(10);//为了让一次输出10个,初始信号量为10
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for (int i = 0; i <100; i++)
{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);//每进来1次,信号量-1;进来10次后就一直hold住,直到信号量大于0;
    dispatch_async(queue, ^{
        NSLog(@"%i",i);
        sleep(2);
        dispatch_semaphore_signal(semaphore);//由于这里只是log,所以处理速度非常快,我就模拟2秒后信号量+1;
    });
} 
dispatch_once

这个函数一般是用来做一个真的单例,也是非常常用的,在这里就举一个单例的例子吧:

static SingletonTimer * instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    instance = [[SingletonTimer alloc] init];
});

return instance;

参考资料如下:
http://pingguohe.net/2016/03/07/GCD-is-so-easy.html
iOS开发系列--并行开发其实很容易
http://blog.jobbole.com/69019/
https://segmentfault.com/a/1190000006612189

文章中对应的demo链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容