ios 多线程和GCD

945 阅读10分钟

前言

对于多线程我觉得还是很重要的,实际开发中的使用以及面试中出现的频率都是非常高的。而多线程的使用大多数情况下会用到GCD,所以后面的线程探索就围绕GCD展开

1、线程的理解

首先理解几个名词

  • 进程:进程是指在系统中正在运行的一个应用程序。每个进程有独立的内存空间,互不影响。
  • 线程:线程是处理器调度的基本单位,线程必须依赖于进程,进程要想执行任务,必须得有线程,进程至少要有一条线程。
  • 多线程:iOS中的多线程同时执行的本质是CPU在多个任务直接进行快速的切换,由于CPU调度线程的时间足够快,就造成了多线程的“同时”执行的效果。其中切换的时间间隔就是时间片,每一时刻,一个线程都只能执行一个任务。单核CPU同一时间,CPU只能处理一条线程,即只有一条线程在工作。
  • 任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在 GCD 中是放在 block 中的
  • 队列:由一个或多个任务组成,一种特殊的线性表。当这些任务要开始执行时,系统会分别把他们分配到某个线程上去执行。队列和线程可以说是两个层级的概念。队列是为了方便使用和理解的抽象结构,而线程是系统级的进行运算调度的单位,他们是上下层级之间的关系。

2、多线程的意义

  • 优点:能适当提高程序的执行效率、能适当提高资源的利用率(CPU,内存),线程上的任务执行完成后,线程会自动销毁。
  • 缺点:如果开启大量的线程,会占用大量的内存空间,降低程序的性能,线程越多,CPU 在调用线程上的开销就越大。 如何开启一个新线程? 主要有四种方式:
  • pthread:底层的线程都是基于pthread创建的,偏向底层个人开发用的比较少。
    pthread_t  testhread=NULL;
    char* cString="test";
    int result= pthread_create(&testhread, NULL, testfunction, cString);
    if(result==0){
        NSLog(@"success");
    }else{
        NSLog(@"error");
    }
    
   void *testfunction(){
     NSLog(@"%s",__func__);
     return NULL;
   }
}
  • NSThread:phtread的上层封装,轻量级的线程操作,需要我们自己创建线程,调度任务,销毁线程。但是NSThread的线程之间的并发控制,是需要我们自己来控制,所以在处理并发上不如gcd简单方便。
//detach开启一个新线程 不用start
[NSThread detachNewThreadSelector:@selector(testFunc) toTarget:self withObject:nil];
//需要start
NSThread* newthread= [[NSThread alloc]initWithTarget:self selector:@selector(testFunc) object:nil];
[newthread start];
//后台线程也是对nsthread的封装 下面这两个函数也能进行线程之间通讯
[self performSelectorInBackground:@selector(go) withObject:@(50)];
//切换回主线程
[self performSelectorOnMainThread:@selector(go) withObject:nil waitUntilDone:YES];
  • GCD:他是苹果公司为多核的并行运算提出的解决方案,它在工作的时候会自动利用更多的处理器核心,不用关心线程代码的实现, 即不需要关心什么时候开起线程, 关闭线程, GCD会负责创建线程调度你的任务,基于这些优势,也是我们下面关注的重点。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    })
  • NSOperation:是对GCD的封装,面相对象的类。可以实现一些GCD中实现不了,或者实现比较复杂的功能。比如:设置最大并发数,设置线程间的依赖关系。NSOperation便于管理线程之间的关系。
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
 }];

3、队列与同步异步

  • 串行队列:每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
  • 并行队列:可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
  • 同步:同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行下一个任务。只能在当前线程中执行任务,不具备开启新线程的能力,所以会阻塞线程
  • 异步:异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。可以在新的线程中执行任务,具备开启新线程的能力不会阻塞线程

总结

异步开启新线程同步不会开启线程,只会在当前线程执行。队列本身跟线程没关系`,队列只关注任务,串行队列任务一个接着一个执行,并行队列可以多个任务同时执行。

文字描述不能让人真正的理解,下面上代码看一下。

同步不会开启新线程,异步会开启新线程

//串行队列
    dispatch_queue_t queue =dispatch_queue_create("concurrent",DISPATCH_QUEUE_SERIAL);
   //同步
    dispatch_sync(queue, ^{
                sleep(1);
                NSLog(@"串行同步task------%@", [NSThread currentThread]);
    });
    //异步
    dispatch_async(queue, ^{
                sleep(1);
                NSLog(@"串行异步task------%@", [NSThread currentThread]);
    });

    //并发队列
    dispatch_queue_t queue2 =dispatch_queue_create("concurrent",DISPATCH_QUEUE_CONCURRENT);
     dispatch_sync(queue2, ^{
                sleep(1);
                NSLog(@"并发同步task------%@", [NSThread currentThread]);

      });

    dispatch_async(queue2, ^{
               sleep(1);
               NSLog(@"并发异步task------%@", [NSThread currentThread]);
     });

输出:

串行同步task------<NSThread: 0x604000060a80>{number = 1, name = main}
并发同步task------<NSThread: 0x604000060a80>{number = 1, name = main}
串行异步task------<NSThread: 0x604000275e80>{number = 3, name = (null)}
并发异步task------<NSThread: 0x60400026e240>{number = 4, name = (null)}

分析:无论是串行队列还是并发队列,只要是sync同步任务,都会只在当前线程运行,当前线程是main主线程。只有async异步任务,才会开辟新的线程

串行队列每次只有一个任务被执行。让任务一个接着一个地执行

//串行队列
    dispatch_queue_t queue =dispatch_queue_create("concurrent",DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
                sleep(5);
            NSLog(@"task1------%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
            NSLog(@"task2------%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
            NSLog(@"task3------%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
            NSLog(@"task4------%@", [NSThread currentThread]);

    });

输出:

task1------<NSThread: 0x60400007c8c0>{number = 3, name = (null)}
task2------<NSThread: 0x60400007c8c0>{number = 3, name = (null)}
task3------<NSThread: 0x60400007c8c0>{number = 3, name = (null)}
task4------<NSThread: 0x60400007c8c0>{number = 3, name = (null)}

分析:串行队列会按照添加顺序挨个执行,哪怕是异步任务,都会等待前面一个任务执行完成才会执行,示例中task1睡眠了5秒才执行,但是task2必须等到task1执行完成才执行。这也就是为什么说串行队列里面的任务同步和异步任务的执行没有区别的原因。

并行队列可以多个任务同时执行

//并发队列
    dispatch_queue_t queue =dispatch_queue_create("concurrent",DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(queue, ^{
                sleep(5);
            NSLog(@"task1------%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
            NSLog(@"task2------%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
            NSLog(@"task3------%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
            NSLog(@"task4------%@", [NSThread currentThread]);
    });

输出:

task1------<NSThread: 0x60400007f740>{number = 1, name = main}
task2------<NSThread: 0x604000278240>{number = 3, name = (null)}
task4------<NSThread: 0x60000027eb80>{number = 5, name = (null)}
task3------<NSThread: 0x604000274380>{number = 4, name = (null)}

分析:在并发队列中task2、task3、task4执行顺序是无序的,执行的顺序取决于任务的复杂度和cpu的调度,任务是并发执行的。task1是同步任务,同步会阻塞当前线程的执行,所以task1会阻塞后面代码的执行,所以最先执行的是task1。

使用案例

比如有个卖票服务,总共有100张票,有两个窗口同时卖票,该如何控制卖票队列呢? 其中一种思路应该是每卖一张票都要看是否还有票可卖,那么查询是否还有票必须是在同步队列中的

_tickets = 100;
// 创建串行队列
    _queue = dispatch_queue_create("Cooci", DISPATCH_QUEUE_SERIAL);
    // 第一个线程卖票
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self saleTickes];
    });
     // 第二个线程卖票
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        [self saleTickes];
    });
    - (void)saleTickes {
    while (self.tickets > 0) {
        // 模拟延时
        [NSThread sleepForTimeInterval:1.0];
        // 使用串行队列,同步任务卖票
        dispatch_sync(_queue, ^{
            // 检查票数
            if (self.tickets > 0) {
                self.tickets--;
                NSLog(@"还剩 %zd %@", self.tickets, [NSThread currentThread]);
            } else {
                NSLog(@"没有票了");
            }
        });
    }
}

主队列和全局队列

  • 主队列:主线程队列,是一个串行队列,UI等函数都是在主队列中执行的,dispatch_get_main_queue
  • 全局队列:是一个全局并发队列,dispatch_get_global_queue。

上面我们说异步会开启新线程,但是在主队列中异步并不会开启新线程

dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"2--%@",[NSThread currentThread]);
    });

输出:

2--<NSThread: 0x60400007f580>{number = 1, name = main}

主线程死锁的问题是我们多线程开发中经常遇到的问题,下面上几个例子分析一下为什么会造成死锁?

主线程死锁

    NSLog(@"0");
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"1");
    });
    NSLog(@"2");

分析:sync是同步的,所以会阻塞2的执行,主队列串行队列,串行队列是挨个执行,所以1又需要等待先添加进主队中的方法执行完才会执行,2等待1执行,1又等待主队列执行完才执行,所以造成了死锁。

串行队列死锁

    dispatch_queue_t queue = dispatch_queue_create("test", NULL);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        }); 
        NSLog(@"4");
    });
    NSLog(@"5");

分析:首先这是一个串行队列,先执行1,dispatch_async异步方法不着急执行,然后执行5,再进入异步方法里执行2,dispatch_sync同步方法阻塞线程,4等待3执行完才执行,3是后加入到串行队列里的,所以3需要等待dispatch_async方法体执行完才会执行,造成死锁。这里还有一点需要注意,上面我们说串行队列同步和异步都是一样按照添加顺序执行,那为什么5在2的前面执行呢?注意是串行队里里的任务同步和异步没区别,5是在主队列里不是在test队列里。

那么如果把串行队列换成并发队列呢?

dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");

输出:

1
5
2
3
4

分析:这是一个并发队列,先执行1,dispatch_async异步不着急先执行5,进入异步代码块执行2,3是同步代码块会阻塞线程,4等待3的执行,由于是并发队列执行是没有顺序的,所以3不需要等待dispatch_async方法体执行完。

4、栅栏函数

栅栏函数今天我们只讲跟上面内容相关的两个dispatch_barrier_syncdispatch_barrier_async,栅栏函数会让程序先执行栅栏函数前面的,然后再执行栅栏函数,最后再执行栅栏函数后面的。下面举两个例子看下sync和async栅栏函数的区别

dispatch_barrier_async

  dispatch_async(queue1, ^{
        NSLog(@"1");
    });
    dispatch_async(queue1, ^{
        NSLog(@"2");
    });
    dispatch_barrier_async(queue1, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
    dispatch_async(queue1, ^{
        NSLog(@"5");
    });
    dispatch_async(queue1, ^{

        NSLog(@"6");

    });

输出:

1
4
2
3
5
6

分析:dispatch_barrier_async会让前面的先执行,即先执行1、2,然后再执行3,最后再执行5、6,由于async是异步的,所以不会阻塞4的执行,那么4、1、2是无序的,5和6也是无序的。

dispatch_barrier_sync

  dispatch_async(queue1, ^{
        NSLog(@"1");
    });
    dispatch_async(queue1, ^{
        NSLog(@"2");
    });
    dispatch_barrier_sync(queue1, ^{
        NSLog(@"3");
    });
    NSLog(@"4");
    dispatch_async(queue1, ^{
        NSLog(@"5");
    });
    dispatch_async(queue1, ^{

        NSLog(@"6");

    });

输出:

2
1
3
4
6
5

分析:dispatch_barrier_sync会让前面的先执行,即先执行1、2,然后再执行3,最后再执行5、6,由于sync是同步的,所以会阻塞4的执行,那么4肯定在3之后,1和2是无序的,5和6也是无序的。

栅栏函数线程安全使用案例

多线程并发操作同一片内存空间导致的线程不安全,比如属性的读写、或者数组的添加删除,可以使用栅栏函数阻塞线程的执行直到栅栏函数执行完。

// 多线程 操作marray
    dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<1000; i++) {
        dispatch_async(concurrentQueue, ^{
            NSString *imageName = [NSString stringWithFormat:@"%d.jpg", (i % 10)];
            NSURL *url = [[NSBundle mainBundle] URLForResource:imageName withExtension:nil];
            NSData *data = [NSData dataWithContentsOfURL:url];
            UIImage *image = [UIImage imageWithData:data];
            // self.mArray 多线程 地址永远是一个
            // self.mArray 0 - 1 - 2 变化
            // name = kc  getter - setter (retain release)
            // self.mArray 读 - 写  self.mArray = newaRRAY (1 2)
            // 多线程 同时  写  1: (1,2)  2: (1,2,3)  3: (1,2,4)
            // 同一时间对同一片内存空间进行操作 不安全 
            dispatch_barrier_async(concurrentQueue , ^{
                [self.mArray addObject:image];
            });
        });

    }

总结:

  • 主队列同步方法会造成死锁
  • 同步方法会阻塞线程的执行
  • 栅栏函数必须自定义并发队列。如果用的是串行队列或者系统提供的全局并发队列,这个栅栏函数的作用等同于一个同步函数的作用

5、 补充几个概念

5.1 任务的执行因素

  • cpu的调度
  • 执行任务的复杂度
  • 任务的优先级
  • 线程的状态

5.2 优先级反转

两种线程:IO密集型、CPU密集型

  • IO密集型,频繁等待的线程。更容易得到优先级提升。
  • CPU密集型,很少等待的线程。
  • IO密集型线程容易饿死。
  • cpu调度来提升等待线程的优先级

5.3 优先级的影响因素

  • 用户指定。
  • 等待的频繁度。
  • 长时间不执行。