iOS多线程

1.Pthreads

真正跨平台的多线程技术,可以跨UNIX、Linux、windows平台。

  • 创建Pthreads线程

    • 如果要使用Pthreads,先导入头文件<pthread.h>
    • 创建
      pthread_create(pthread_t *restrict, const pthread_attr_t *attr,void *(*)(void *), void *restrict)
      • 第一个参数 : 保存线程ID的pthread_t,线程的代号(当做就是线程)
      • 第二个参数 : 线程的属性
      • 第三个参数 : 线程开启之后,用来执行的函数,传入函数指针,就是将来线程需要执行的函数
      • 第四个参数 : 给第三个参数的指向函数的指针 传递的参数
    • 函数返回值为int类型:0代表线程开启成功;其他代表开启失败
    • 线程开启之后,就会在子线程里面执行传入的函数
  • 其实pthread的功能相当强大,这里只是做一个最简单的了解,后期看情况是否要继续研究


补充点:函数指针

和block的指针类似,函数指针这么来表示:
  • 函数指针: (返回值类型)(* 变量名)(参数类型) 例如: (int)(* sum)(int,int)

    • 这个函数指针的变量名为sum,函数的返回值类型为int,两个参数都是int类型的
  • block指针:(返回值类型)(^变量名)(参数类型) 例如:(void)(^success)(int,int)

    • 这个block指针的变量名为success,函数无返回值,两个参数类型为int

2.NSThread

NS开头的直接就来到了Foundation框架,一个NSThread对象,就代表一条线程

  • 创建线程:

    1. **alloc + init **
      NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];

      • 此时线程上的任务不会立即执行,而是要启动线程:

      • 会返回创建的线程,可以设置线程的一些属性。

      • 系统会强引用该线程,直到线程死亡(任务执行完毕或强制关闭)。
        [thread start];//线程一旦启动,就会执行任务

    2. 直接开启新线程[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];

      • 这个方法会直接开启新线程,并执行任务

      • 无返回值,拿不到创建的线程

    3. 隐式开启线程[self performSelectorInBackground:@selector(run) withObject:nil];

      • 这个方法会直接开启新线程,并执行任务

      • 无返回值,拿不到创建的线程

  • 线程常用方法:

    • [NSThread currentThread] : 获取当前线程

    • [NSThread mainThread] : 获取主线程

    • [thread setName:name] : 设置线程的名称,方便调试

    • [thread name] : 获取线程的名称

  • 控制线程的状态

    • alloc + init : 创建线程,进入新建状态

    • start : 启动线程,进入准备就绪状态(等待CPU来调度)

    • CPU调度 : 进入运行状态

    • sleep : 进入阻塞状态

    • exit : 关闭线程,进入死亡状态

  • 线程状态示意图:

线程状态
  • 线程间通信常用方法

    • -(void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;

    • -(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;

    • 用上面的方法已经可以满足普通的多线程开发了。

  • 多线程数据访问问题:

不同线程,同一时刻访问同一块内存,可能导致数据出错。
解决办法,对可能会同时访问一块内存的代码加锁,同一时刻最多只能有一条线程访问这块内存。

互斥锁:对一段代码加锁之后,同一时刻,最多只能有一条线程执行加锁的代码。

  • 使用方法:@synchronized(锁对象) { 要锁住的代码 }

  • 注意点:

    1. 一定要是同一把锁,否则达不到上锁的的目的。
      1. 锁住尽量少的代码,互斥锁(上锁、解锁过程)非常耗资源。
  • 当多条线程想同时访问加锁的代码:(例如让三个线程同时执行一段加锁的代码)

    1. 当三个线程都开启之后,会陆续(虽然时间基本相同,但是还是有时间差的)来执行这段代码。
    2. 第一个线程来到之后,会开锁,进入锁住的代码,进入之后,就会解锁,防止其他线程进入。
    3. 当第一个线程执行完锁住的代码之后,就会走出加锁的代码,此时就会解锁。
    4. 之后,在锁外等候的第二个线程,就会进入加锁的代码,进入之后就会上锁。依次循环往复。
  • 关键点:
    当一个线程进入加锁的代码后,就会上锁,执行完毕之后就会解锁;当一个线程访问互斥锁锁住的代码,如果这段代码处于锁住的状态,这个线程就会等待,当这段代码解锁之后,马上进入代码,加上锁,执行代码。

互斥锁解决资源抢夺

补充点:自旋锁(automic)

  • iOS中属性默认是automic的,这种原子属性,相当于给set方法加上了自旋锁:
    • 使用自旋锁的时候,当上锁之后,等候的线程不会休眠,会一直循环,等候解锁;
  • 互斥锁:
    • 互斥锁,当加锁的资源已经被一条线程访问的时候,等候的线程会进入休眠状态。

3.GCD

GCD:Grand Central Dispatch,伟大的中枢调度器。使用GCD的时候要把自己置身于一个调度者的身份,而不是纠结线程的问题。就好比十字路口的交警,你不能只关注于一条路,而是调度所有的车辆在不同的道路上畅通行驶。

  • GCD中两个非常重要的概念:

    • 任务:要执行的操作
    • 队列:存放要执行的操作的地方
  • 将任务添加到队列中

    • GCD会自动将队列中的任务取出,放到对应的线程中执行
    • 任务的取出遵循FIFO原则:先进先出,后进后出
    • 注意:
      • 任务的取出是有顺序的
      • 只要将任务添加到队列,我们不用管线程的问题,系统会自己调度
  • 同步与异步 :是否会阻塞当前线程

    • 同步 :不具备开启新线程的能力,会等到当前的任务执行完毕,函数才返回
    • 异步 :具备开启新线程的能力,不会等到当前的任务执行完毕,函数就会返回
  • 串行与并行 :决定任务的执行方式

    • 串行 : 任务会一个接着一个的执行
    • 并行 : 任务会同时一起执行
  • 关于串行和并行:

    • 任务被添加至队列之后,GCD按照FIFO(先进先出)的原则取出队列中的任务,放到线程上执行,对于不同的情况,系统会选择是否创建子线程来执行任务。
      • 串行 : 从线程上取任务是FIFO的,而且要等一个任务执行完毕之后,才去取下一个任务;下一个执行完毕,再取下一个,依次循环,直到任务都执行完毕,队列被销毁。
      • 并行 : 从线程上取任务是FIFO的,但是把一个任务放到线程上之后,马上会去取另一个任务,知道队列上的任务都被放到线程上,如果此时系统的有能力开启多的线程,这些任务都会执行起来,至于哪个任务先执行完不一定。
  • 不同函数与队列的搭配方式下,线程开辟及任务执行方式:

不同的搭配方式
  • 简单的代码来说明:

     #pragma mark - 几种 函数 与 队列 的搭配方式
     /**
      *  异步函数 + 并行队列 ==>会创建一条或多条子线程,任务并行执行。
      */
     - (void)asyncConcurrent {
         /**
          *  创建一个队列
          *
          *  @param 第一个参数:队列的标示(方便我们调试)
          *  @param 第二个参数:创建的队列的类型(串行/并行)
          
             DISPATCH_QUEUE_CONCURRENT ==> 并行队列
             DISPATCH_QUEUE_SERIAL 《==》NULL ==> 串行队列
          
          *
          *  @return 返回创建好的队列
          */
     //  dispatch_queue_t queue = dispatch_queue_create("com.ljson.ljc", DISPATCH_QUEUE_CONCURRENT);
    
         /**
          *  获取一个全局的并行队列,这个队列已经由系统创建好
          *
          *  @param 第一个参数:队列的优先级/服务质量,传 0 代表使用默认的(具体可以查看头文件)
          *  @param 第二个参数:只作为占位,苹果目前没有用上,要求传 0
          *
          *  @return 返回一个全局队列
          */
         dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
         dispatch_async(queue, ^{
             NSLog(@"1-%@",[NSThread currentThread]);
         });
         dispatch_async(queue, ^{
             NSLog(@"2-%@",[NSThread currentThread]);
         });
         dispatch_async(queue, ^{
             NSLog(@"3-%@",[NSThread currentThread]);
         });
         dispatch_async(queue, ^{
             for (int i = 0; i < 9999; i ++) {
                 NSLog(@"%d-%@",i,[NSThread currentThread]);
             }
             NSLog(@"4-%@",[NSThread currentThread]);
         });
         /**
          这种情况下:
          会首先执行下面的代码,再执行任务(执行block中的代码)。
          在异步函数时:
          1.首先会执行当前的代码,而不会马上把任务(block中的代码)拿出来执行。
          2.在当前的代码执行完毕,就会来执行任务,至于需不需要开辟新的线程,还要看任务是放在什么队列当中执行:
             普通串行队列:开辟一条新的线程,所有任务在这个线程中串行执行。
             并行队列:可能会开辟多条线程(至于开多少条,有系统决定),并发执行这些任务。
             主队列:不会开辟新的线程,任务会在主队列当中串行执行。
          */
         NSLog(@"code over");
     }
     /**
      异步函数 + 串行队列 ==> 会创建一条子线程,任务会在新创建的子线程里面串行执行
      */
     - (void)asyncSerial {
         dispatch_queue_t queue = dispatch_queue_create("com.ljson.ljc",DISPATCH_QUEUE_SERIAL);
         dispatch_async(queue, ^{
             NSLog(@"1-%@",[NSThread currentThread]);
         });
         dispatch_async(queue, ^{
             NSLog(@"2-%@",[NSThread currentThread]);
         });
         dispatch_async(queue, ^{
             NSLog(@"3-%@",[NSThread currentThread]);
         });
         dispatch_async(queue, ^{
             for (int i = 0; i < 9999; i ++) {
                 NSLog(@"%d-%@",i,[NSThread currentThread]);
             }
             NSLog(@"4-%@",[NSThread currentThread]);
         });
         /**
          这种情况下,也会先执行下面的代码,再执行任务(block中的代码)
          道理同上
          */
         NSLog(@"code over");
     }
     /**
      同步函数 + 并行队列 ==> 不会创建子线程,任务会在当前线程串行执行。
      */
     - (void)syncConcurrent {
         dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
         dispatch_sync(queue, ^{
             NSLog(@"1-%@",[NSThread currentThread]);
         });
         dispatch_sync(queue, ^{
              NSLog(@"2-%@",[NSThread currentThread]);
         });
         dispatch_sync(queue, ^{
             NSLog(@"3-%@",[NSThread currentThread]);
         });
         dispatch_sync(queue, ^{
             for (int i = 0; i < 9999; i ++) {
                 NSLog(@"%d-%@",i,[NSThread currentThread]);
             }
             NSLog(@"4-%@",[NSThread currentThread]);
         });
         /**
          这里会在任务都执行完毕之后,再执行下面的代码
          在同步函数时:
          1、当前线程会被阻塞。
          2、立即执行任务
          3、在任务执行完毕之前,当前线程相当于阻塞住了(所以,在主线程中使用相当于没用)
          */
         NSLog(@"code over");
     }
     /**
      同步函数 + 串行队列 ==> 不会创建子线程,任务会在当前线程串行执行
      */
     - (void)syncSerial {
         dispatch_queue_t queue = dispatch_queue_create("com.ljson.ljc",DISPATCH_QUEUE_SERIAL);
         dispatch_sync(queue, ^{
             NSLog(@"1-%@",[NSThread currentThread]);
         });
         dispatch_sync(queue, ^{
             NSLog(@"2-%@",[NSThread currentThread]);
         });
         dispatch_sync(queue, ^{
             NSLog(@"3-%@",[NSThread currentThread]);
         });
         dispatch_sync(queue, ^{
             for (int i = 0; i < 9999; i ++) {
                 NSLog(@"%d-%@",i,[NSThread currentThread]);
             }
             NSLog(@"4-%@",[NSThread currentThread]);
         });
         /**
          会把任务依次执行完毕,才会执行下面的代码
          道理同上
          */
         NSLog(@"code over");
     }
     /**
      异步函数 + 主队列 ==> 不会创建子线程,任务会在主队列串行执行
      */
     - (void)asyncMainQueue {
         /**
          *  获取主队列。系统会自己创建主队列,主队列是串行队列
          *
          */
         dispatch_queue_t mainQueue = dispatch_get_main_queue();
         dispatch_async(mainQueue, ^{
             NSLog(@"1-%@",[NSThread currentThread]);
         });
         dispatch_async(mainQueue, ^{
             NSLog(@"2-%@",[NSThread currentThread]);
         });
         dispatch_async(mainQueue, ^{
             NSLog(@"3-%@",[NSThread currentThread]);
         });
         dispatch_async(mainQueue, ^{
             for (int i = 0; i < 9999; i ++) {
                 NSLog(@"%d-%@",i,[NSThread currentThread]);
             }
             NSLog(@"4-%@",[NSThread currentThread]);
         });
         /**
          这里会先执行下面的代码,再执行任务
          原因:
          这里是异步函数,不会阻塞当前线程。
          因为是主队列(主队列是串行队列),所以任务会在主线程上串行执行。
          */
         NSLog(@"code over");
     }
     /**
      同步函数 + 主队列 ==> 不会创建子线程,主线程会卡死
      */
     - (void)syncMainQueue {
         /**
          这里的主线程卡死。是由于调用的是同步函数,会阻塞当前的线程(当前的线程是主线程),所以把任务添加到主队列中之后,GCD会将主队列中的任务,取到主线程执行,但是此时主线程被阻塞,所以无法执行,导致主线程卡死。
          */
         dispatch_queue_t mainQueue = dispatch_get_main_queue();
         dispatch_sync(mainQueue, ^{
             NSLog(@"1-%@",[NSThread currentThread]);
         });
         dispatch_sync(mainQueue, ^{
             NSLog(@"2-%@",[NSThread currentThread]);
         });
         dispatch_sync(mainQueue, ^{
             NSLog(@"3-%@",[NSThread currentThread]);
         });
         dispatch_sync(mainQueue, ^{
         });
         NSLog(@"code over");
     }
    
  • 注意

    • 如果手动创建多条串行队列,这些队列将会并行,每个队列里面的任务会串行。
  • GCD 内存管理:

    • ARC环境,就像使用OC对象一样使用
    • MRC环境使用dispatch_retaindispatch_release 管理
内存管理
  • GCD dispatch_barrier:

    • 俗称 栅栏,顾名思义,就是将任务分隔开。
    • dispatch_barrier_async(,)dispatch_barrier_sync(,)两个函数。
    • 会让栅栏前面的任务执行完毕之后,才执行栅栏里面的任务;栅栏里面的任务执行完毕,才执行栅栏后面的任务。
  • GCD dispatch_group

    • 将任务包装在一个组里面,组里面的任务执行完毕之后,会调用dispatch_group_notify(, ,)函数。
    • 用这种方式,可以实现任务的依赖,但是不能跨组和队列。
  • GCD的其他常用函数
    • 延时执行:dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 2秒后执行这里的代码... });
    • 快速迭代:dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index){ // 执行10次代码,index顺序不确定 });
      • 注意,这里的快速迭代,会开启多条线程进行遍历,效率更高。
    • 一次性代码:static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 只执行1次的代码(这里面默认是线程安全的) });
      • + load+ inliazed 是不一样的,理论上,只要能拿到他们的类对象就可以执行他们。
        • +load方法在一个类加载的时候会调用。如果这个类有分类,那么先调用这个类的+load方法,再调用这个类的+ load方法。
        • + inliazed 会在子类和本类初始化类对象的时候调用。子类初始化类对象的时候,调用+inliazed方法的时候,会调用父类的+ inliazed方法。

补充:单例

单例: 程序运行过程中,一个类始终只有一个实例对象。从创建好之后,程序死亡,才会让这个实例对象死亡。
一次性代码,经常是用在创建单例对象的时候,保证只分配一次内存。

  • 实现单例的方案

    • 保证只分配一次内存
      • 调用alloc方法的时候,内部会调用allocWithZone方法,所以控制好allocWithZone方法的内存开辟操作就能控制alloc
      • copymutableCopy 同样要控制,直接返回调用者就好(因为copymutableCopy是对象方法,所以如果第一次内存分配控制好了,这里直接返回self
  • 具体实现代码

      //保存单例对象的静态全局变量
      static id _instance;
      + (instancetype)sharedTools {
          return [[self alloc]init];
      }
      //在调用alloc方法之后,最终会调用allocWithZone方法
      + (instancetype)allocWithZone:(struct _NSZone *)zone {
          //保证分配内存的代码只执行一次
          static dispatch_once_t onceToken;
          dispatch_once(&onceToken, ^{
              _instance = [super allocWithZone:zone];
          });
          return _instance;
      }
      //这是个对象方法,既然有对象而且是单例,那么调用者就是这个单例对象了,那就返回调用的对象就行
      - (id)copyWithZone:(NSZone *)zone {
          return self;
      }
      //这是个对象方法,既然有对象而且是单例,那么调用者就是这个单例对象了,那就返回调用的对象就行
      - (id)mutableCopyWithZone:(NSZone *)zone {
          return self;
      }
      #if __has_feature(objc_arc)
      //如果是ARC环境
      #else
      //如果不是ARC环境
    
      //既然是单例对象,总不能被人给销毁了吧,一旦销毁了,分配内存的代码已经执行过了,就再也不能创建对象了。所以覆盖掉release操作
      - (oneway void)release {
      }
      //这是个对象方法,既然有对象而且是单例,那么调用者就是这个单例对象了,那就返回调用的对象就行
      - (instancetype)retain {
          return self;
      }
      //为了便于识别,这里返回 MAXFLOAT ,别的程序员看到这个数据,就能意识到这是单例了。纯属装逼……
      - (NSUInteger)retainCount {
          return MAXFLOAT;
      }
      #endif
    
  • 注意

    • 单例不能继承,由于保存单例的是静态全局变量,所以如果有子类继承的话,拿到的将是同一个对象,访问的是同一块内存。
    • 不同的单例,最好直接继承自NSObject,而不要继承自实现单例的类。
    • 为了便于创建单例,可以把上面的代码,抽成宏,方便以后使用。

4.NSOperation

是苹果用OC对GCD的封装,更加的面向对象。把任务创建好,添加到队列即可,系统会自己分配线程,让任务执行。

  • NSOperation和NSOperationQueue实现多线程的具体步骤

    1. 先将需要执行的操作封装到一个NSOperation对象中

    2. 然后将NSOperation对象添加到NSOperationQueue

    3. 系统会自动将NSOperationQueue中的NSOperation取出来

    4. 将取出的NSOperation封装的操作放到一条新线程中执行

  • 队列 NSOperationQueue

    • mainQueue :主队列
      • 获取方式:[NSOperationQueue mainQueue]
      • 任务添加到主队列之后,只会被分配到主线程来执行,所以任务一定会是串行
    • 自己创建的队列:
      • 获取方式 :alloc + init 创建
      • 任务添加到这个自己创建的队列,不会被分派到主线程来执行,所以一定会在子线程执行。至于开多少条线程来执行任务,要根据任务的数量以及队列的maxConcurrentOperationCount来决定。
      • maxConcurrentOperationCount:最大并发数:
        • 给自己创建的队列设置最大并发数,能够控制系统同时最多开启的线程数
          • 设置为1,任务会在子线程里面串行执行。(因为对已一条线程而言,任务只会在上面串行执行)
          • 设置为大于1,任务会并发在多条子线程上执行
          • 设置为0,任务不会执行
  • 任务:NSOperation

NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类

  • NSOperation的子类:

    • NSInvocationOperation,设置targetselector,任务是:要执行某个对象的某个方法

      • 调用它的start方法之后,这个任务就会被添加到当前线程执行
      • 添加到队列之后,就会在新的线程执行
    • NSBlockOperation 类方法创建:[NSBlockOperation blockOperationWithBlock:^{ }]

      • 添加到队列之后,就会执行任务
    • 自定义Operation继承自NSOperation

      • 把要做的操作放到自定义Operationmain方法里面即可
      • 即实现- (void)main方法
      • 创建自定义的Operation对象,添加到队列中,就会执行 main方法里面的任务
  • 队列的操作:

    • 取消任务:

      • 取消队列的所有任务 - (void)cancelAllOperations;
      • 也可以调用NSOperation的- (void)cancel方法取消单个操作
    • 注意:

      • 取消操作的时候,只会取消还没有执行的操作,已经在执行的操作会执行完毕
      • 所以,在自定义Operation的时候,每开始一个耗时操作,就检测一下,当前的操作是否已经取消([self isCancelled]),取消了就直接return,不要执行。
      • 操作一旦取消了,就不能恢复
    • 暂停和恢复队列

      • -(void)setSuspended:(BOOL)b; // YES代表暂停队列,NO代表恢复队列
      • -(BOOL)isSuspended; //判断是否被暂停了
    • 注意:

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

推荐阅读更多精彩内容

  • 一、前言 上一篇文章iOS多线程浅汇-原理篇中整理了一些有关多线程的基本概念。本篇博文介绍的是iOS中常用的几个多...
    nuclear阅读 2,032评论 6 18
  • 在这篇文章中,我将为你整理一下 iOS 开发中几种多线程方案,以及其使用方法和注意事项。当然也会给出几种多线程的案...
    张战威ican阅读 596评论 0 0
  • 欢迎大家指出文章中需要改正或者需要补充的地方,我会及时更新,非常感谢。 一. 多线程基础 1. 进程 进程是指在系...
    xx_cc阅读 7,168评论 11 70
  • 01 老屋很老了,从我出生起它就在那里。听奶奶说它是我太爷爷盖起来的,一直留到了现在。 斑驳的青瓦早已被连年的雨水...
    知子已知阅读 168评论 0 1
  • 我偶有几次做梦,梦中又漫步在月夜下的老哈河边。微风轻拂着我的脸颊,抚摸着我的头发。它掠过平静的河...
    老哈河阅读 370评论 0 0