多线程简介


多线程简介

iOS系统 中,每一个应用都是一个进程。具体了解Runloop底层原理:https://www.jianshu.com/p/9cb4edc0670d,除了Runloop底层原理还介绍了线程间的通讯等。

进程与线程

  • 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行
  • 进程要想执行任务,必须得有线程,进程至少要有一条线程
  • 程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程
  • 进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存。

进程与线程的关系

  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
  • 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
  • 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
  • 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • 线程是处理器调度的基本单位,但是进程不是。

多线程的意义

多线程的原理其实就是CPU在单位时间片里快速在各个线程之间切换。一般情况下无论多核还是单核,我们的线程运行总是 “并发” 的,这时候我们所说的”并发”是一种模拟出来的状态,CPU在单位时间片里快速在各个线程之间切换,每个线程执行一小段时间,让多个线程看起来就像在同时运行。只有当cpu数量大于等于线程数量,这个时候是真正并发,可以多个线程同时执行计算。

优点
  • 能适当提高程序的执行效率。
  • 能适当提高资源的利用率(CPU、内存)。
  • 线程上的任务执行完成后,线程会自动销毁。
    缺点
  • 开启线程需要占用一定的内存空间(默认情况下,每一个线程占用512KB)。
  • 如果开启大量的线程,会占用大量的内存空间,降低程序的性能。
  • 线程越多,CPU在调度线程上的开销就越大。
  • 程序设计更加复杂,比如线程间的通讯、多线程的数据共享。

线程的生命周期

线程在创建后,start开始进入Runnable就绪的状态,这时会执行很多初始化的操作;接下进入running状态,CPU会调度当前线程,如果线程池中有当前线程会直接执行,在时间片的影响后CPU再次调度其他线程,直到当前线程任务执行完毕或强制退出,或者堵塞(调用sleep、等待同步锁或者从可调度线程池中移除)结束再次回到Runnable状态。

多线程相关补充

多线程技术方案

image.png

线程池

线程池可以使线程得到复用,所谓线程复用就是线程在执行完一个任务后并不被销毁,该线程可以继续执行其他的任务。在线程池大小小于核心线程池大小的时候,如果小于则直接创建线程执行任务。如果超出核心线程池大小则依赖队列,此时线程池会判断当前队列是不是已经满了,如果没有满,则提交任务到工作队列中,等待线程池调度执行任务。如果满了,并且当前工作队列所依赖的线程没有执行工作,那么则可以利用当前线程执行任务,如果此时线程都在工作,接下来会交给饱和策略。饱和策略一般默认都是中止策略,调用者可以捕获到该异常;还有抛弃策略,会悄悄抛弃该任务,不过一般会抛弃最旧的任务或者优先级比较低的任务等;还有调用者运行策略,实现了一种机制,这个机制不会抛弃任务也不会抛出异常,而是将任务回退到调用者,来达到降低新任务的流量。还有等待策略,也就是需要排队等候执行。

线程安全—锁

在开发高性能程序的时候几乎都会用到多线程,但是用到多线程也会碰到一些安全问题。比如多个线程同时对一块内存发生读和写的操作,或者程序执行的顺序会被打乱,可能造成提前释放一个变量,造成计算结果错误等,所以我们经常会用到锁。我们用锁来保证代码操作的原子性,让多线程对同一个数据或者资源进行访问同步。

锁的分类

这里简单说下锁,根据锁的状态、锁的特性和锁的设计等分为:

  • 公平锁/非公平锁
  • 可重入锁—又名递归锁,一定程度上避免死锁。
  • 独享锁/共享锁
  • 互斥锁/读写锁
  • 乐观锁/悲观锁
  • 分段锁
  • 偏向锁/轻量级锁/重量级锁
  • 自旋锁
atomic与nonatomic

说到原子性就会想到属性关键字中atomic和nonatomic。设置atomic之后,默认生成的getter和setter方法执行是原子的,它只保证了自身的读/写操作,却不能说是线程安全。

  • nonatomic 非原子属性
  • atomic 原子属性(线程安全),针对多线程设计的,默认值
  • 保证同一时间只有一个线程能够写入(但是同一个时间多个线程都可以取值)
  • atomic 本身就有一把锁(自旋锁)
  • 单写多读:单个线程写入,多个线程可以读取

atomic:线程安全,需要消耗大量的资源
nonatomic:非线程安全,适合内存小的移动设备
我们可以用读写锁来解决,例如:

// 在 OC 中,如果同时重写 了 setter & getter 方法,系统不再提供 _成员变量,需要使用合成指令
// @synthesize name 取个别名:_name
@synthesize name = _name;
- (NSString *)name {
    return _name;
}
- (void)setName:(NSString *)name {
    /**
     * 增加一把锁,就能够保证一条线程在同一时间写入!
     */
    @synchronized (self) {
        _name = name;
    }
}

  • 互斥锁小结

    • 保证锁内的代码,同一时间,只有一条线程能够执行!
    • 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差!
  • 互斥锁参数

    • 能够加锁的任意 NSObject 对象
    • 注意:锁对象一定要保证所有的线程都能够访问
    • 如果代码中只有一个地方需要加锁,大多都使用 self,这样可以避免单独再创建一个锁对象

关于线程安全问题,多线程安全比多线程性能更重要,建议用@synchronizedNSLock,可保证可读性和安全性。

线程的创建

/**
 线程创建的方式
 */
- (void)creatThreadMethod{
    
    NSLog(@"%@", [NSThread currentThread]);
    
    //A: 1:开辟线程
    NSThread *t = [[NSThread alloc] initWithTarget:self.p selector:@selector(study:) object:@3];
    // 2. 启动线程
    [t start];
    t.name = @"学习线程";
    
    // detach 分离,不需要启动,直接分离出新的线程执行
    [NSThread detachNewThreadSelector:@selector(study:) toTarget:self.p withObject:@5];
    
    //NSObject (NSThreadPerformAdditions)的分类
    //C : `隐式`的多线程调用方法,没有thread,也没有 start
    self.p = [[Person alloc] init];
    [self.p performSelectorInBackground:@selector(study:) withObject:@10];
    

    // GCD
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self study;];
    });
    
    // NSOperation
    [[[NSOperationQueue alloc] init] addOperationWithBlock:^{
        [self threadTest];
    }];
    NSLog(@"%@", [NSThread currentThread]);
}

Person.m文件实现

- (void)study:(id)time{
    for (int i = 0; i<[time intValue]; i++) {
        NSLog(@"%@ 开始学习了 %d分钟",[NSThread currentThread],i);
    }
}

或者

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//0: pthread
        
    /**
     pthread_create 创建线程
     参数:
     1. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 `_t / Ref` 结尾
     同时不需要 `*`
     2. 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,0 C 使用的)
     3. 线程要执行的`函数地址`
     void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
     (*): 函数名
     (void *): 参数类型,void *
     4. 传递给第三个参数(函数)的`参数`
     
     返回值:C 语言框架中非常常见
     int
     0          创建线程成功!成功只有一种可能
     非 0       创建线程失败的错误码,失败有多种可能!
     */
    // pthread
    pthread_t threadId = NULL;
    //c字符串
    char *cString = "HelloCode";
    // OC prethread -- 跨平台
    // 锁
    int result = pthread_create(&threadId, NULL, pthreadTest, cString);
    if (result == 0) {
        NSLog(@"成功");
    } else {
        NSLog(@"失败");
    }
    // GCD
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self threadTest];
    });
    
    // NSOperation
    [[[NSOperationQueue alloc] init] addOperationWithBlock:^{
        [self threadTest];
    }];
}

- (void)threadTest{
    NSLog(@"begin");
    NSInteger count = 1000 * 100;
    for (NSInteger i = 0; i < count; i++) {
        // 栈区
        NSInteger num = i;
        // 常量区
        NSString *name = @"zhang";
        // 堆区
        NSString *myName = [NSString stringWithFormat:@"%@ - %zd", name, num];
        NSLog(@"%@", myName);
    }
    NSLog(@"over");
}

void *pthreadTest(void *para){
    // 接 C 语言的字符串
    //    NSLog(@"===> %@ %s", [NSThread currentThread], para);
    // __bridge 将 C 语言的类型桥接到 OC 的类型
    NSString *name = (__bridge NSString *)(para);
    
    NSLog(@"===>%@ %@", [NSThread currentThread], name);
    
    return NULL;
}

TIP:C与OC的桥接

  • __bridge只做类型转换,但是不修改对象(内存)管理权;
  • __bridge_retained(也可以使用CFBridgingRetain)将Objective-C的对象转换为Core Foundation的对象,同时将对象(内存)的管理权交给我们,> 后- 续需要使用CFRelease或者相关方法来释放对象;
  • __bridge_transfer(也可以使用CFBridgingRelease)将Core Foundation的对象转换为Objective-C的对象,同时将对象(内存)的管理权交给ARC。

该文章为记录本人的学习路程,也希望能够帮助大家,知识共享,共同成长,共同进步!!!


文章作者: Vincent
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Vincent !
  目录