iOS 多线程

为什么要学习多线程

当在主线程执行耗时操作的时候,界面会卡死,阻塞用户交互,影响用户体验。怎么解决这个问题呢?就要使用多线程技术

多线程概念

  • 同步
  • 异步
  • 进程
  • 线程

多线程原理

  • (单核 CPU)统一时间,CPU 只能处理 1 个线程,只有 1 个线程在执行
  • 多线程同时执行其实是 CPU 快速的在多个线程之间切换
  • CPU 调度的时间足够快,就造成了多线程的 ”同时“ 执行
  • 如果线程数非常多,CPU 会在 n 个线程之间切换,消耗大量的 CPU 资源,每个线程被调度的次数降低,线程的执行效率降低

多线程的优缺点

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

主线程

  • 一个程序运行后,默认会开启 1 个线程,称为 主线程UI 线程
  • 主线程 一般用来刷新 UI 界面,处理 UI 事件(比如:点击、滚动、拖拽等事件)
  • 主线程 使用注意
    • 别将耗时的操作放到主线程中
    • 耗时操作会卡住主线程,严重影响用户体验

多线程的技术方案

技术方案 简介 语言 线程生命周期 使用频率
pthread 1. 一套通用的多线程 API
2. 适用于 Uinix\Linux\Windows 等系统
3. 跨平台\可移植
4.使用难度大
C 手动管理 几乎不用
NSThread 1. 使用更加面向对象
2. 简单易用,可直接操作线程对象
OC 手动管理 偶尔使用
GCD 1. 旨在替代 NSThread 等线程技术
2. 充分利用设备的多核
C 自动管理 经常使用
NSOperation 1. 基于 GCD (底层是 GCD
2. 比 GCD 多了一些更简单实用的功能
3. 使用更加面向对象
OC 自动管理 经常使用

pthread 详解

  • pthread 概述
    • pthreadPOSIX 多线程开发框架,是跨平台的 C 语言框架,需要自己管理线程的创建销毁等操作
    • pthread_t 用于标识一个线程,不能单纯看成整数,通过头文件可以看到是 _opaque_pthread_t 类型的结构体指针
    • pthread_attr_t 线程的属性,通过头文件可以看到是一个 _opaque_pthread_attr_t 类型结构体
  • pthread API 介绍
    • int pthread_create(pthread_t _Nullable * _Nonnull __restrict, const pthread_attr_t * _Nullable __restrict,void * _Nullable (* _Nonnull)(void * _Nullable),void * _Nullable __restrict);
      • 创建一个线程,创建成功返回 0,失败返回对应错误码
      • 第一个参数为指向线程标识符的指针
      • 第二个参数用来设置线程属性,我们可以初始化一个 pthread_attr_t 变量指定优先级等属性,一般可以传入 NULL,采用缺省值
    • int pthread_join(pthread_t , void * _Nullable * _Nullable);
      • 等待某个线程执行完毕,这个函数是一个线程阻塞的函数,将一直阻塞到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回
      • 第一个参数为被等待的线程标识符
      • 第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值
    • int pthread_detach(pthread_t);
      • 将该子线程的状态设置为 detached,则该线程运行结束后会自动释放所有资源
      • pthread_detachpthread_join 回收线程资源的区别:
        • pthread_join 会同步等待子线程任务结束后回收其资源,如果该子线程没有运行结束,父线程会被阻塞,在有些情况下我们并不希望如此,就可以用 pthread_detach
        • pthread_detach 不会阻塞调用线程
      • 调用方式:父线程调用: pthread_detach(thread2),也可以在子线程中调用: pthread_detach(pthread_self())pthread_self() 获取当前线程ID
    • int pthread_kill(pthread_t, int);
      • 该函数可以用于向指定的线程发送信号
      • 如果线程内不对信号进行处理,则调用默认的处理程式,如 SIGQUIT 信号会退出终止线程,SIGKILL会杀死线程等等,可以调用signal(SIGQUIT, sig_process_routine); 来自定义信号的处理程序
      • 第一个参数表示线程的标识符
      • 第二个参数表示传递的signal参数,一般都是大于0的,这时系统默认或者自定义的都是有相应的处理程序。常用信号量宏可以在这里查看**#import <signal.h>** ,signal0 时,是一个被保留的信号,一般用这个保留的信号测试线程是否存在
      • pthread_kill 返回值如下:
        • 0:调用成功
        • ESRCH:线程不存在
        • EINVAL:信号不合法
        • 测试线程是否存在/终止的方法

线程的状态

graph LR
A[New] -->|Start| B[Runnable]
B --> C{Running}
C --> B
C --> D[Dead]
C --> E[Blocked] 
E -->|Sleep| B

多线程访问共享资源的问题

  • 共享资源
    • 1 块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
    • 比如多个线程访问同一个对象、同一个变量、同一个文件
  • 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题

互斥锁

原子属性

互斥锁和自旋锁的区别