iOS 多线程理解

一、iOS中常见的多线程方案

要解释多线程,需要先解释进程和线程之间的区别。线程才是程序真正的执行单元,一个进程可以有一个或者多个线程,线程没有单独的地址空间,一个线程崩溃整个进程会崩溃。而多线程是CPU快速的在多个线程之间进行切换,多线程的目的是为了同步完成多项任务,通过提高系统的资源利于率来提高系统的效率。多线程可以提高系统规定资源利用率,但是开启多线程需要花费时间和空间,过多开启多线程CPU频繁的在多个线程中调度会消耗大量的CPU资源。所以不要在系统中同时开启过多的子线程。



1、pthread: 一套通用的多线程API,跨平台可移植,使用难度大,C语言,程序猿管理,几乎不用
2、NSThread: 使用更加面向对象,简单易用,可直接操作线程对象 OC语言,程序猿管理,偶尔使用
3、GCD: 旨在替代NSThread等线程技术,充分利用设备的多核 C语言 ,自动管理,经常使用。GCD主要与block结合使用,代码简洁高效,执行效率稍微高点,只支持先进先出队列。
4、NSOperation: 基于GCD(底层是GCD)比GCD多了一些更简单实用的功能,使用更加面向对象,OC语言,自动管理,经常使用。NSOperationQueue可以建立各个NSOperation之间的依赖关系,支持KVO,可以监听operation是否正在执行,是否结束,是否取消。NSOperationQueue可以调整队列的执行顺序。

我们在实际开发过程中常用的多为GCD和NSoperation,下面我们着重讲解一下GCD相关的知识。

二、GCD

1、GCD通常使用两个函数来执行任务。

1)同步方式执行任务:

dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
其中queue是队列,block是任务
eg:

dispatch_queue_t queue =  dispatch_get_global_queue(0,0);
//在当前线程执行
dispatch_sync(queue,^{
     NSLog("@执行任务-%@",[NSThread currentThread]);
});

2)异步方式执行任务

dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
eg:

dispatch_queue_t queue =  dispatch_get_global_queue(0,0);
//在子线程执行任务
dispatch_async(queue,^{
     NSLog("@执行任务-%@",[NSThread currentThread]);
});

2、GCD的队列

GCD的队列可以分为两大类型:
1)并发队列(Concurrent Dispatch Queue):可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务),并发功能只有在异步函数下有效(dispatch_async)
eg:

//global类型一般是并发队列。
dispatch_queue_t queue =  dispatch_get_global_queue(0,0);
dispatch_async(queue,^{
     for(int i  = 0; i< 5; i++) {
         NSLog("@执行任务1-%@",[NSThread currentThread]);
      }
});
dispatch_async(queue,^{
     for(int i  = 0; i< 5; i++) {
         NSLog("@执行任务2-%@",[NSThread currentThread]);
      }
});

打印结果:任务1和任务2的打印信息交替显示

2)串行队列(Serial Dispatch Queue):让任务一个接着一个执行(一个任务执行完毕后再执行下一个任务)
eg:

dispatch_queue_t queue =  dispatch_get_global_queue(0,0);
dispatch_sync(queue,^{
     for(int i  = 0; i< 5; i++) {
         NSLog("@执行任务1-%@",[NSThread currentThread]);
      }
});
dispatch_sync(queue,^{
     for(int i  = 0; i< 5; i++) {
         NSLog("@执行任务2-%@",[NSThread currentThread]);
      }
});

打印结果:任务1执行完后再执行任务2
总结:

  1. dispatch_sync和dispatch_async用来控制是否需要开启新的线程
  2. 队列的类型决定了任务的执行方式(串行、并发)
    PS:dispatch_async只能代表它具备开启新线程的能力,并不是说使用了异步线程它就一定会开启新线程。
    eg:
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue,^{
         NSLog("@执行任务-%@",[NSThread currentThread]);
});
打印结果:在main线程,并没有开启子线程

各种队列的执行效果

**
总结:产生死锁需要以下两个条件都满足**

  • 使用sync函数
  • 往当前串行队列添加任务

3、GCD中队列组的使用

思考1:如何用GCD实现下面功能:异步并发执行任务1、任务2 ,等任务1和任务2都执行完毕后再回到主线程执行任务3?
此时我们需要了解两个概念:
dispatch_group_async:实现监听一组任务是否完成,完成后得到通知执行其它的操作
dispatch_group_notify:监听group中任务的完成状态,当所有任务执行完成后追加任务到group并执行

    // 创建队列组
    dispatch_group_t group = dispatch_group_create();
    // 创建并发队列
    dispatch_queue_t queue = dispatch_queue_create("my_queue", DISPATCH_QUEUE_CONCURRENT);
    // 添加异步任务
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务1-%@", [NSThread currentThread]);
        }
    });
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务2-%@", [NSThread currentThread]);
        }
    });
     //等前面的任务执行完毕后,会自动执行这个任务
    dispatch_group_notify(group, queue, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            for (int i = 0; i < 5; i++) {
                NSLog(@"任务3-%@", [NSThread currentThread]);
            }
        });
    });

打印结果如下:先执行任务1和任务2,等这两个任务执行结束后再执行任务三


思考2:如何实现一个功能:在实际开发过程中,需要先调用接口1,等接口1调用完成后再调用接口2,等接口2调用完之后再调用接口3?
使用dispatch_barrier_async方法来实现。

 // 创建并发队列
    dispatch_queue_t queue = dispatch_queue_create("my_queue", DISPATCH_QUEUE_CONCURRENT);
    // 添加异步任务
    dispatch_barrier_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务1-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_barrier_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"任务2-%@", [NSThread currentThread]);
        }
    });
    
     //等前面的任务执行完毕后,会自动执行这个任务
    dispatch_barrier_async(queue, ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            for (int i = 0; i < 5; i++) {
                NSLog(@"任务3-%@", [NSThread currentThread]);
            }
        });
    });

四、多线程的安全隐患与解决方案

当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。


如上图所示,有一个整型变量值为17,此时有两条线程A和B同时对它进行读写操作。线程A读到变量值为17,线程B读到的值也为17。此时A对该值进行加1,使结果变成了18再写入到该变量。线程B对该变量进行加1,由于B读到的数值是17,所以加1之后结果也是18写入到该变量。最终两次线程操作之后的结果为18。
但是实际上,两个线程分别对该数值进行了加1操作,结果应该是19。这就是多线程操作下会引发对的全问题。
那么如何解决这种问题呢?
我们可以通过线程同步技术来解决该问题。同步,就是协同步调,按预定的先后次序进行线程操作。

五、iOS中的线程同步方案

常见的线程同步技术就是加锁

OSSpinLock

OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源。不过,此种加锁方法已经不再安全,可能会出现优先级反转问题而造成死锁。但是使用自旋锁是性能较高的一种方式,因为它就是处于一种忙等的状态。而其它锁是休眠机制,从休眠中唤醒是要耗费一定的性能的。 使用自旋锁需要导入头文件#import<libkern/OSAtomic.h>

//初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
//尝试加锁(如果需要等待就不加锁,直接返回false;如果不需要等待就加锁,返回true)
bool reslut = OSSpinLockTry(&lock);
//加锁
OSSpinLockLock(&lock);
//需要执行的代码
//解锁
OSSpinlockUnlock(&lock);

os_unfair_lock

os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持。从底层调用来看,等待os_unfair_locks锁的线程会处于休眠状态,并非忙等。它需要导入头文件#import<os/lock.h>

//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//尝试加锁
os_unfair_lock_trylock(&lock);
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);

pthread_mutex

mutex叫做“互斥锁”,等待锁的线程会处于休眠状态。它需要导入头文件#import<pthread.h>

   // 初始化锁的属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
    // 初始化锁
    //pthread_mutex_t mutex;
    pthread_mutex_init(mutex, &attr);
    //尝试加锁
    pthread_mutex_trylock(&mutex);
    //加锁
    pthread_mutex_lock(&mutex);
    //解锁
    pthread_mutex_unlock(&mutex);
    //销毁相关资源
    pthread_mutexattr_destroy(&attr);
    pthread_mutex_destory(&mutex);

pthread_mutex的锁有以下几种类型:
define PTHREAD_MUTEX_NORMAL 0:普通类型的锁,也就是默认的锁
define PTHREAD_MUTEX_ERRORCHECK 1:错误检查类型的锁
define PTHREAD_MUTEX_RECURSIVE 2:递归锁
PHTREAD_MUTEX_DEFAULT:默认锁
可以根据开发过程中的实际场景选择使用pthread_mutex锁的类型。

pthread_cond_t

条件锁,是pthread_mutex_t引申出来的锁。配合pthread_mutex_t来一起使用,可以用于线程的同步。亦或者是解决线程间的依赖关系。
它的使用场景有点特殊,在通常情况下,只要对一段代码加锁,必须要等到解锁后别的线程才能访问这段代码。但是条件它并不一定要等到锁被解开之后别的线程才能进行读写操作。
当线程进入wait之后mutex锁会放开,保证其他线程可以拿到mutex锁,直到收到signal信号或者broadcast之后才会唤醒当前线程,并且唤醒后再次对 mutex 进行加锁。

      //定义成员变量
      @property (assign, nonatomic) pthread_mutex_t mutex;
      @property (assign, nonatomic) pthread_cond_t cond;

        //初始化属性
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
        // 初始化锁
        pthread_mutex_init(&_mutex, &attr);
        // 销毁属性
        pthread_mutexattr_destroy(&attr);
        // 初始化条件
        pthread_cond_init(&_cond, NULL);

示例:在下面这两段代码中,add方法是往数组中添加元素,remove是从数组中删除元素。而在删除的时候我们需要对数组进行判断,如果数组元素个数为0,则不执行删除需要等到add线程执行添加元素后再进行删除操作。所以会在remove方法中添加一个条件。最终通过条件锁可以实现先添加元素再删除元素的操作。

// 线程1
// 删除数组中的元素
- (void)__remove  {
    pthread_mutex_lock(&_mutex);
    NSLog(@"__remove - begin");
    if (self.data.count == 0) {
        // 等待,它会先放开mutex锁,让add线程能用这把锁。等到有人唤醒 cond之后它就会继续执行后面的代码。
        pthread_cond_wait(&_cond, &_mutex);
    }
    [self.data removeLastObject];
    NSLog(@"删除了元素");
    pthread_mutex_unlock(&_mutex);
}

// 线程2
// 往数组中添加元素
- (void)__add  {
    pthread_mutex_lock(&_mutex);
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素"); 
    // 发送信号,这样remove中的等待条件就会接收到cond信号
    pthread_cond_signal(&_cond);
    // 广播
    //pthread_cond_broadcast(&_cond);
    pthread_mutex_unlock(&_mutex);
}

NSLock、NSRecursiveLock

NSLock是对mutex普通锁的封装,我们可以认为它就是pthread_mutex的类型为normal的这种锁。它遵循NSLocking协议,有lock和unlock两个方法。
NSRecursiveLock是对mutex递归锁对封装,它其实就是phtread_mutex对类型为recursived的这种锁。

NSCondition

NSCondition是对mutex和pthread_cond的封装,所以在使用的时候不用再使用NSLock去创建锁。它遵循了NSLocking协议,可以直接使用lock和unlock方法。

// 线程1 // 删除数组中的元素
- (void)__remove  {
    [self.condition lock];
    NSLog(@"__remove - begin");
    if (self.data.count == 0) {
        // 等待
        [self.condition wait];
    }
    [self.data removeLastObject];
    NSLog(@"删除了元素");
    [self.condition unlock];
}

// 线程2  // 往数组中添加元素
- (void)__add  {
    [self.condition lock];
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素"); 
    // 发送信号
    [self.condition signal];
    //[self.condtion broadcast];     // 广播
    [self.condition unlock];
}

PS:此处需要注意 [self.condition signal]和 [self.condition unlock]这两句代码的前后关系。
如上述代码中所示,signal语句在unlock之前。在先执行线程1的情况下,那么整段代码的流程大致为:

  • 先调用remove方法,加锁
  • 数组为空,等待
  • 调用add方法,加锁,添加元素,发送信号
  • remove中接收信号,重新加锁,发现锁已经被使用,继续等待
  • add中调用unlock解锁
  • remove中发现锁已经被解开,继续执行删除元素操作

如果代码中先调用unlock再调用signal

// 线程2  // 往数组中添加元素
- (void)__add  {
    [self.condition lock];
    [self.data addObject:@"Test"];
    NSLog(@"添加了元素"); 
    [self.condition unlock];
    [self.condition signal];
}

在先执行线程1的情况下,那么整段代码的流程大致为:

  • 先调用remove方法,加锁
  • 数组为空,等待
  • 调用add方法,加锁,添加元素,解锁,发送信号
  • remove中接收信号,重新加锁,发现锁没有被使用,继续执行删除元素操作

我们一般会碰到很多关于多线程的问题,在面试过程中我们会碰到很多跟多线程相关的问题。
1、你理解的多线程?

2、iOS的多线程方案有哪几种?你更倾向于哪一种?

3、你在项目中用过 GCD 吗?

4、GCD 的队列类型

5、说一下 OperationQueue 和 GCD 的区别,以及各自的优势

6、线程安全的处理手段有哪些?

7、OC你了解的锁有哪些?在你回答基础上进行二次提问;
追问一:自旋和互斥对比?
追问二:使用以上锁需要注意哪些?
追问三:用C/OC/C++,任选其一,实现自旋或互斥?口述即可!
8、下列代码的打印结果是?

dispatch_async(global_queue, ^{
    NSLog(@”1”);
    [self performSelector : @selector(printLog) withObject : nil afterDelay : 0 ];
    NSLog(@”3”);
});
- (void)printLog {  
    NSLog(@”2”);
}

打印结果是:1 3
原因:其实调用performSelector:withObject:afterDelay:这个方法时,它是往runloop里面添加了定时器。async异步会在子线程执行,而在子线程中是没有runloop的,所以test方法是无效的。
那么,怎样才能让该程序执行打印2的消息呢?

- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t global_queue = dispatch_get_global_queue(0, 0);
    dispatch_async(global_queue, ^{
        NSLog(@"1");
        [self performSelector : @selector(printLog) withObject : nil afterDelay : 0 ];
//启动runloop,让这个线程保活,代码写在此处的话打印结果就是1  2  3.如果该代码写在log3语句后面,则打印结果为1  3  2
      [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        NSLog(@"3");
    });
    
}
- (void)printLog {
    NSLog(@"2");
}

此时可以衍生出另一个问题,看如下代码打印结果是?

dispatch_async(global_queue, ^{
    NSLog(@”1”);
    [self performSelector : @selector(printLog) withObject : nil ];
    NSLog(@”3”);  
});
- (void)printLog {  
    NSLog(@”2”);
}

打印结果:1 2 3
原因:这里的performSelector方法没有afterDelay,它就是一个普通的objc_msgSend方法,这里的[self performSelector : @selector(printLog) withObject : nil ]等同于[self printLog]语句。

下列代码打印结果是?

- (void)viewDidLoad {
    NSLog(@”1”);
    [self performSelector : @selector(printLog) withObject : nil afterDelay:.0 ];
    NSLog(@”3”);
}
- (void)printLog {  
    NSLog(@”2”);
}

打印结果:1 3 2
原因:因为在主线程中执行的代码。该performSelector方法会在0秒后启动一个定时器,所以会先打印1和3,再在runloop中打印2。

9.跟死锁有关的面试题

// 问题1:以下代码是在主线程执行的,会不会产生死锁?
- (void)viewDidLoad {
      [super viewDidLoad];
      NSLog(@"1");
      dispatch_queue_t queue = dispatch_get_main_queue();
      dispatch_sync(queue, ^{
          NSLog(@"2");
      });
      NSLog(@"3");
}

解答:会,只能打印1。因为打印任务是在主线程执行的串行队列,使用dispatch_sync需要在当前主队列立即同步执行打印语句,使用同步函数往队列中添加任务会造成死锁。
解释:使用同步函数添加任务A到串行队列,说明要在当前串行队列立即执行任务A,任务A执行完后,才会执行A后面的代码。但是当前队列是串行,也就是说A必须等到当前串行队列中正在执行的任务B完成后才能执行,因此又必须先执行任务A,又要必须等到B执行完后才能执行下一个任务,所以会卡死,你等我,我等你,谁也无法执行。

// 问题2:以下代码是在主线程执行的,会不会产生死锁?
- (void)viewDidLoad {
      [super viewDidLoad];
      NSLog(@"1");
      dispatch_queue_t queue = dispatch_get_main_queue();
      dispatch_async(queue, ^{
          NSLog(@"2");
      });
      NSLog(@"3");
}

解答:不会产生死锁,打印1 3 2
解释:因为dispatch_async不要求立马在当前线程同步执行任务。它会等3打印之后再执行打印2语句。

// 问题3:以下代码会不会产生死锁?
- (void)viewDidLoad {
   [super viewDidLoad];
   NSLog(@"1");
   dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
   dispatch_async(queue, ^{
       NSLog(@"2");
       dispatch_sync(queue, ^{ 
           NSLog(@"3");
       });
       NSLog(@"4");
   });
   NSLog(@"5");
}

解答:会,打印1 5 2 然后崩溃。
解释:queue的类型是串行队列,dispatch_async会创建一个串行队列执行打印语句2,然后在这个串行队列中加入一个dispatch_sync语句,那情况跟问题1就类似,使用dispatch_sync在串行队列添加任务会造成死锁。

// 问题3:以下代码会不会产生死锁?
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1");
    dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);//串行队列
    dispatch_queue_t queue2 = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);并发队列   
        dispatch_async(queue, ^{
            NSLog(@"2");
            dispatch_sync(queue2, ^{ 
                NSLog(@"3");
            });
            NSLog(@"4");
        });
    NSLog(@"5");
}

解答:不会死锁。打印1 5 2 3 4。
解释:因为打印queue是异步串行队列,它会开启新线程执行打印,所以会世输出1 5 2,然后在新线程里面执行同步的并发任务queue2,它不会创建新线程会串行执行任务,所以会打印3之后再打印4。

10、下面代码打印结果是什么?

- (void)test {
    NSLog(@"2");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"1");
    }];
    [thread start];
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}

解答:打印1,然后程序崩溃。
原因:因为thread start时就会去执行thread里面的block语句,打印1之后线程结束了,此时再通过performSelector方法是没有这个线程的。
如果想要使得perform语句有效果,需要在thread线程里面调用runloop语句让这个线程一直存在。代码如下

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"1");
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }];
    [thread start];

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

推荐阅读更多精彩内容

  • 1.基本概念: 什么是进程: 1)进程是一个具有独立功能的程序关于某次数据集合的一次运行活动,他是操作系统分配资源...
    83c11ad52c96阅读 229评论 0 0
  • 本文用来介绍 iOS 多线程中 GCD 的相关知识以及使用方法。这大概是史上最详细、清晰的关于 GCD 的详细讲...
    花花世界的孤独行者阅读 494评论 0 1
  • 在这篇文章中,我将为你整理一下 iOS 开发中几种多线程方案,以及其使用方法和注意事项。当然也会给出几种多线程的案...
    张战威ican阅读 600评论 0 0
  • 学习多线程,转载两篇大神的帖子,留着以后回顾!第一篇:关于iOS多线程,你看我就够了 第二篇:GCD使用经验与技巧...
    John_LS阅读 604评论 0 3
  • 首先先说明此文是学习了李峰峰大牛的博客后所写,有兴趣的可以百度搜索一下李峰峰的博客。 一、线程和进程 1、线程 线...
    奇怪的她的他阅读 423评论 0 2