多线程初识

2017-07-19  本文已影响0人  褪而未变

线程概述

有些程序是一条直线,起点到终点;有些程序是一个圆,不断循环,直到将它切断
一个运行着的程序就是一个进程或者叫做一个任务,一个进程至少包含一个线程,线程就是程序的执行流。Mac和iOS中的程序启动,创建好一个进程的同时, 一个线程便开始运行,这个线程叫主线程。主线程在程序中的地位和其他线程不同,它是其他线程最终的父线程,且所有界面的显示操作即AppKit或 UIKit的操作必须在主线程进行。
系统中的每一个进程都有自己独立的虚拟内存空间,而同一个进程中的多个线程则共用进程的内存空间。每创建一个新的线程,都需要一些内存(如每个线程有自己的Stack空间)和消耗一定的CPU时间。另外当多个线程对同一个资源出现争夺的时候需要注意线程安全问题
多线程的实现原理:虽然在同一时刻,CPU只能处理1条线程,但是CPU可以快速地在多条线程之间调度(切换),造成了多线程并发执行的假象。

多线程的优点

能适当提高程序的执行效率。
能适当提高资源利用率(CPU、内存利用率)。

多线程的缺点

创建线程是需要成本的:iOS下主要成本包括:在栈空间的子线程512KB、主线程1MB,创建线程大约需要90毫秒的创建时间。
线程越多,CPU在调度线程上的开销就越大。
线程越多,程序设计就越复杂:因为要考虑到线程之间的通信,多线程的数据共享。

下面开始撸代码:
------------------------------------------------------------------华丽的分割线

1.耗时操作的问题演示

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self longOperation];
}
- (void)longOperation
{
    NSLog(@"start");

    NSTimeInterval start = CACurrentMediaTime();

    for (int i = 0; i < 10000000; i++) {

        // 存储在栈区
        //  int num = 10;

        // 存储在常量区
        //  NSString *str1 = @"hello";

        // 存储在堆区
        //  NSString *str2 = [NSString stringWithFormat:@"hello_%d",i];

        // I/O操作 : 把数据从内存输出到外接设备,或者由外接设备输入到内存;
        NSLog(@"%d",i);
    }

    NSLog(@"over %f", CACurrentMediaTime() - start);
}

结论

  1. 空的for循环不耗时
  2. 操作内存的栈区速度很快;栈区存储空间地址是连续的;
  3. 操作内存的常量区速度很快;内存空间只开辟一次;
  4. 操作内存的堆区速度相对栈区和常量区要慢些;堆区内存空间不连续,需要寻址;
  5. I/O操作是很耗时的; (把数据从内存输出到外接设备,或者由外接设备输入到内存)
  6. 耗时操作对UI交互的影响 : 卡死了主屏幕,直到耗时操作执行完,屏幕的交互才能正常进行;
  7. 解决耗时操作卡顿UI的办法 : 多线程技术;
  8. 学习多线程的目的 : 把耗时操作放在后台执行,不让耗时操作卡顿UI;

2.解决耗时操作卡顿UI的办法

使用多线程技术 : 解决屏幕卡死的问题

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//    [self longOperation];

    // 使用多线程技术
    [self performSelectorInBackground:@selector(longOperation) withObject:nil];
}

3.多线程基本概念

同步 & 异步

进程 & 线程

多线程

4.多线程执行原理

5.多线程优缺点

优点

缺点

6.主线程

7.多线程的实现方案

(二)创建线程三种方式

1.准备新线程执行的方法

- (void)demo:(id)obj
{
    NSLog(@"传入参数 => %@",obj);
    NSLog(@"hello %@",[NSThread currentThread]);
}

2.对象方法创建

- (void)threadDemo1
{
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(demo:) object:@"alloc"];
    // 手动启动线程
    [thread start];
}

3.类方法创建

- (void)threadDemo2
{
    [NSThread detachNewThreadSelector:@selector(demo:) toTarget:self withObject:@"detach"];
}

4.NSObject(NSThreadPerformAdditions) 的分类创建

- (void)threadDemo3
{
    [self performSelectorInBackground:@selector(demo:) withObject:@"perform"];
}

5.总结

(三)target和selector的关系

1.target和selector的关系分析

2.代码演练

准备Person对象

@interface Person : NSObject

/// 人名
@property (nonatomic,copy) NSString *name;
/// 创建人的构造方法
+ (instancetype)personWithDict:(NSDictionary *)dict;
/// 人有个方法
- (void)personDemo:(id)obj;

@end

@implementation Person

+ (instancetype)personWithName:(NSString *)name
{
    Person *person = [[Person alloc] init];
    person.name = name;
    return person;
}

- (void)personDemo:(id)obj
{
    NSLog(@"创建的人名 => %@",self.name);
    NSLog(@"hello %@",[NSThread currentThread]);
}

@end

控制器中的使用

定义属性

@interface ViewController ()
@property (nonatomic,strong) Person *person;
@end

懒加载Person

@implementation ViewController

- (Person *)person
{
    if (_person==nil) {
        _person = [Person personWithName:@"zhangjie"];
    }
    return _person;
}

新的实例化方法

// 崩溃
[self performSelectorInBackground:@selector(personDemo:) withObject:@"perform"];
// 正确的调用方式
[self.person performSelectorInBackground:@selector(personDemo:) withObject:@"perform"];
// 崩溃
[NSThread detachNewThreadSelector:@selector(personDemo:) toTarget:self withObject:@"detach"];

// 正确的调用方式
[NSThread detachNewThreadSelector:@selector(personDemo:) toTarget:self.person withObject:@"detach"];
// 崩溃
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(personDemo:) object:@"alloc"];
// 手动开启线程
[thread start];
// 正确的调用方式
NSThread *thread = [[NSThread alloc] initWithTarget:self.person selector:@selector(personDemo:) object:@"alloc"];
// 手动开启线程
[thread start];

(四)线程状态-生命周期

线程生命周期的控制

NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadDemo) object:nil];
[thread start];

代码演练

创建线程对象和就绪状态

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 新建状态
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadDemo) object:nil];
    // 就绪状态 : 将线程放进"可调度线程池",等待被CPU调度.
    [thread start];
}

新线程执行的方法

- (void)threadDemo
{
    // 提示 : 能执行到这里说明线程是运行状态
    NSLog(@"%@",[NSThread currentThread]);

    // 使当前线程休眠2秒钟 : 休眠指定时长
    [NSThread sleepForTimeInterval:2.0];

    NSLog(@"第一次睡醒");

    // 使当前线程休眠到指定日期
    [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:2.0]];

    NSLog(@"第二次睡醒");

    // 使当前线程退出 : 当前线程一旦退出,后续的所有代码都不会执行
    // 注意 : 该方法不能在主线程使用,会使主线程退出
    [NSThread exit];

    NSLog(@"没戏了?");
}

关于exit的结论

线程的取消 (在线程执行的方法的外部取消)

- (void)cancel NS_AVAILABLE(10_5, 2_0);
[thread cancel]
if ([NSThread currentThread].isCancelled) {
    NSLog(@"该线程已经被取消");
    return;
}

(五)线程属性

1.常用属性

关于优先级和服务质量

  • 多线程的目的:是将耗时的操作放在后台,不阻塞主线程和用户的交互!
  • 多线程开发的原则:简单
  • 在开发时,最好不要修改优先级,不要相信 用户交互 服务质量
  • 内核调度算法在决定该运行哪个线程时,会把线程的优先级作为考量因素
* 较高优先级的线程会比较低优先级的线程具有更多的运行机会
* 较高优先级不保证你的线程具体执行的时间,只是相比较低优先级的线程,更有可能被调度器选择执行而已

2.代码演示

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"主线程栈区空间大小 == %tu KB 是否是主线程 %zd",[NSThread currentThread].stackSize / 1024,[NSThread currentThread].isMainThread);

    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];

    // 给线程起名字
    thread1.name = @"download A";

    // 设置线程优先级
    thread1.threadPriority = 1.0;

    // 线程就绪
    [thread1 start];

    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
    thread2.name = @"download B";
    thread2.threadPriority = 0;
    [thread2 start];
}

- (void)demo
{
    NSLog(@"子线程栈区空间大小 == %tu KB 是否是主线程 %zd",[NSThread currentThread].stackSize / 1024,[NSThread currentThread].isMainThread);

    for (int i = 0; i < 10; i++) {
        NSLog(@"%@",[NSThread currentThread]);
    }
}

3.补充

(六)线程安全-资源共享

1.多线程操作共享资源的问题

开发提示

代码实现卖票逻辑

@interface ViewController ()

/// 总票数(共享的资源)
@property (nonatomic,assign) int tickets;

@end
- (void)viewDidLoad {
    [super viewDidLoad];

    // 设置余票数
    self.tickets = 20;
}
-  (void)saleTickets
{
    // while 循环保证每个窗口都可以单独把所有的票卖完
    while (YES) {

        // 判断是否有票
        if (self.tickets>0) {

            // 模拟网络延迟 : 放大出错时的效果,没有实际意义
            [NSThread sleepForTimeInterval:1.0];

            // 有票就卖一张
            self.tickets--;
            // 卖完一张票就提示用户余票数
            NSLog(@"剩余票数 => %zd %@",self.tickets,[NSThread currentThread]);
        } else {
            // 没有就提示用户
            NSLog(@"没票了");
            // 此处要结束循环,不然会死循环
            break;
        }
    }
}

单线程

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 在主线程中卖票
    [self saleTickets];
}

多线程

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 在主线程中卖票
    // [self saleTickets];

    // 售票口 A
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
    thread1.name = @"售票口 A";
    [thread1 start];

    // 售票口 B
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
    thread2.name = @"售票口 B";
    [thread2 start];
}

资源抢夺结果

出错原因分析

2.解决多线程操作共享资源的问题

添加互斥锁

- (void)saleTickets
{
    // while 循环保证每个窗口都可以单独把所有的票卖完
    while (YES) {

        // 添加互斥锁
        @synchronized(self) {
            // 判断是否有票
            if (self.tickets>0) {

                // 模拟网络延迟 : 放大出错时的效果,没有实际意义
                [NSThread sleepForTimeInterval:1.0];

                // 有票就卖一张
                self.tickets--;
                // 卖完一张票就提示用户余票数
                NSLog(@"剩余票数 => %zd",self.tickets);
            } else {
                // 没有就提示用户
                NSLog(@"没票了");
                // 此处要结束循环,不然会死循环
                break;
            }
        }
    }
}

互斥锁小结

(七)原子属性

1.原子属性相关概念

2.模拟原子属性

/// 非原子属性
@property (nonatomic,strong) NSObject *obj1;
/// 原子属性:内部有"自旋锁"
@property (atomic,strong) NSObject *obj2;
/// 用于模拟原子属性
@property (atomic,strong) NSObject *obj3;

3.模拟原子属性

// 合成指令
@synthesize obj3 = _obj3;

/// obj3的setter方法
- (void)setObj3:(NSObject *)obj3
{
    // 使用互斥锁替代看不见的自旋锁
    @synchronized(self) {
        _obj3 = obj3;
    }
}

/// obj3的getter方法
- (NSObject *)obj3
{
    return _obj3;
}

4.性能测试

/// 测试"非原子属性","互斥锁","自旋锁"的性能
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSInteger largeNum = 1000*1000;

    NSLog(@"非原子属性");
    CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < largeNum; i++) {
        self.obj1 = [[NSObject alloc] init];
    }
    NSLog(@"非原子属性 => %f",CFAbsoluteTimeGetCurrent()-start);

    NSLog(@"原子属性");
    start = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < largeNum; i++) {
        self.obj2 = [[NSObject alloc] init];
    }
    NSLog(@"原子属性 => %f",CFAbsoluteTimeGetCurrent()-start);

    NSLog(@"模拟原子属性");
    start = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < largeNum; i++) {
        self.obj3 = [[NSObject alloc] init];
    }
    NSLog(@"模拟原子属性 => %f",CFAbsoluteTimeGetCurrent()-start);
}

测试结果

5.互斥锁和自旋锁对比

共同点

不同点

6.开发建议

(八)NSThread线程间通信

1.ATS

使用http地址时Xcode会认为不够安全从而保存,为解决此问题需要在info文件的Xml文件内添加下列代码
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>

2.代码实现

定义属性

@interface ViewController ()

/// 滚动视图
@property (nonatomic,strong) UIScrollView *scrollView;
/// 图片视图
@property (nonatomic,weak) UIImageView *imageView;

@end

loadView 方法复习

加载视图层次

- (void)loadView
{
    // 创建滚动视图
    self.scrollView = [[UIScrollView alloc] initWithFrame:[UIScreen mainScreen].bounds];
    // 将滚动视图设置成根视图
    self.view = self.scrollView;
    self.scrollView.backgroundColor = [UIColor redColor];

    // 创建图片视图
    UIImageView *imageView = [[UIImageView alloc] init];
    [self.view addSubview:imageView];
    self.imageView = imageView;
}

异步下载图片

- (void)viewDidLoad {
    [super viewDidLoad];

    // 主线程中下载图片
    // [self downloadImageData];

    // 开启新线程异步下载图片
    [self performSelectorInBackground:@selector(downloadImageData) withObject:nil];
}

下载图片主方法

- (void)downloadImageData
{
    // 图片资源地址
    NSURL *url = [NSURL URLWithString:@"http://h.hiphotos.baidu.com/image/pic/item/c995d143ad4bd1130c0ee8e55eafa40f4afb0521.jpg"];
    // 所有的网络数据都是以二进制的形式传输的,所以用NSData来接受
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];

    // 回到主线程更新UI
    // waitUntilDone:是否等待主线程中的`updateUIwWithImage`方法执行结束再执行"下一行代码",一般设置成NO,不用等待
    [self performSelectorOnMainThread:@selector(updateUIwWithImage:) withObject:image waitUntilDone:NO];

    // 测试 waitUntilDone:
    NSLog(@"下一行代码");
}

刷新UI

- (void)updateUIwWithImage:(UIImage *)imgae
{
    NSLog(@"updateUIwWithImage");

    // 设置图片视图
    self.imageView.image = image;
    // 设置图片视图的大小跟图片一般大
    [self.imageView sizeToFit];

    // 设置滚动视图的滚动:滚动范围跟图片一样大
    [self.scrollView setContentSize:image.size];
}

线程间通信

所以一个线程的数据可以直接提供给其他线程使用,叫做线程间通信;

上一篇下一篇

猜你喜欢

热点阅读