预备知识:

1.进程与线程

进程:进程是指在系统中正在运行的一个应用程序。每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。(进程的查看可以在活动监视器去查看) 线程:1个进程要想执行任务,必须得有线程,每个程序至少要有一条线程。一个进程中所有的任务都是在线程中执行的。如果一个线程有多个任务需要处理,这就需要一一处理这些任务,这就是是线程的串行。

线程与进程的比较:

  • 线程是CPU调用(执行任务)的最小单位。
  • 进程是CPU分配资源和调度的单位。
  • 一个程序可以对应多个进程,一个进程中可以有多个线程,但至少要有一个线程。
  • 同一个进程内的线程共享进程的资源。
2.多线程

多线程:1个进程中可以开启多条线程,每条线程可以并行(同时)执行不同的任务。 串行:如果要在1个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务,也就是说,在同一时间内,1个线程只能执行1个任务(也就是说线程中同一时间段只能做一件事情)。



并行:多个线程都是执行,就是叫并行。


多线程原理(单个CPU的情况下): 同一时间,CPU只能处理1条线程,只有1条线程在工作(执行),多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换),如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象。 线程也不是开的越多越好,开得较多的话反而会降低效率,线程一般就3~5条最好。

3.主线程

主线程:一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”。

主线程的作用:

  • 显示\刷新UI界面
  • 处理UI事件(比如点击事件、滚动事件、拖拽事件等)

主线程的使用注意:

  • 将耗时操作尽量不放在主线程中执行。(文件上传等都是耗时操作)
  • 耗时操作会卡住主线程,会造成UI界面的卡顿,造成不好的用户体验。

由于在线程中是串行处理的,如果一个耗时操作放在主线程中执行,如果用户点击了按钮,而和UI相关的都是放在主线程的,所以只有当耗时操作完成后才会去执行按钮的点击事件,这样就会造成卡顿,用户体验不好。 耗时操作优化处理的方式:将耗时操作放在子线程(非主线程,后台线程)中进行处理。 由于子线程和主线程是同时执行的,这样耗时操作放在子线程中执行的,在点击按钮的时候,就不会造成卡顿的现象出现。

4.多线程的实现方案



  • pthread
- (IBAction)btnClickAction:(id)sender {
    // 创建线程对象
    pthread_t thread;
    
    // 创建线程
    
    /**
     创建线程

     @param _Nonnull#> 线程对象传递地址 description#>
     @param _Nullable#> 线程的属性(优先级等) description#>
     @param _Nonnull 指向函数的指针
     @return 函数需要传递的函数
     */
    pthread_create(&thread, nil, task, NULL); // 创建一条线程,如果要创建多个线程就可以将上面的代码copy下就可以了。
 
    pthread_t threadB;
    pthread_create(&threadB, nil, task, NULL); // 第二个线程
    
    pthread_equal(thread, threadB); // 判断两个线程是不是相等。
}


void * task(void *parameter) {
    NSLog(@"%@ ---- ", [NSThread currentThread]);
    // 可以将耗时操作放在这里执行。
    return NULL;
}

// 打印结果
2017-08-06 22:47:48.854 1-pthread[4855:328489] <NSThread: 0x608000266d40>{number = 3, name = (null)} ---- 
2017-08-06 22:47:48.854 1-pthread[4855:328490] <NSThread: 0x608000265b80>{number = 4, name = (null)} ---- 
复制代码
  • NSThread

创建线程的几个方法

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

- (void)creatNewThreadMethod {
    // 开启一条后台线程
    [self performSelectorInBackground:@selector(run:) withObject:@"开启后台子线程"];
}

- (void)creatDetachNewThreadMethod {
    // 创建分离子线程->会自动启动子线程
    [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];
}

- (void)creatNewThread {
    
    // 创建线程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
    // 启动线程,默认是暂停状态的
    [thread start];
    
}

- (void)run:(NSString *)param {
    NSLog(@"---run--%@", [NSThread currentThread]);
}
复制代码

设置线程的名字以及优先级等属性

// 创建多个线程->给每个线程命名以及优先级
- (void)creatMutilThread {
    NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
    thread1.name = @"thread 1";
    thread1.threadPriority = 0.6; // 设置优先级(0.0~1.0,默认的优先级是0.5)
    NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
    thread2.name = @"thread 2";
    thread2.threadPriority = 0.3;
    NSThread *thread3 = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
    thread3.name = @"thread 3";
    thread3.threadPriority = 1.0;
    [thread1 start];
    [thread2 start];
    [thread3 start];
}

- (void)run:(NSString *)param {
    for (int i = 0; i < 10; i++) {
        NSLog(@"---run--%@", [NSThread currentThread].name);
    }
}
// 打印结果
2017-08-06 23:11:51.487 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.487 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.487 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.487 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.488 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.487 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.488 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.488 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.488 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.488 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.488 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.488 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.490 2-NSThread基本使用[5136:356390] ---run--thread 3
2017-08-06 23:11:51.489 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.492 2-NSThread基本使用[5136:356388] ---run--thread 1
2017-08-06 23:11:51.493 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.495 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.496 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.496 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.496 2-NSThread基本使用[5136:356389] ---run--thread 2
2017-08-06 23:11:51.497 2-NSThread基本使用[5136:356389] ---run--thread 2
打印次数越多,打印的数字就越趋于准确的值。
复制代码
  • 线程状态



线程的控制状态等操作:

// 启动
- (void)start;
// 阻塞(暂停)线程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 强制停止线程
+ (void)exit; // 一旦线程停止(死亡)了,就不能再次开启任务
// 和break不一致,表示任务执行完毕后才退出的。
复制代码
5.多线程安全隐患

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


以上的图展示存取钱是同时处理的,造成了最终的钱变少了,造成了数据的不当处理。 处理的方案 苹果的文档说明的解决方案就是加上一个互斥锁,在访问数据的时候加上互斥锁,只要次个线程才能访问处理,在访问结束的时候打开把互斥锁打开,然后另一个线程进行访问,同样加上互斥锁,循环如此,这样就解决了数据安全的隐患。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.totalCount = 100;
    self.threadA = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.threadB = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.threadC = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.threadA.name = @"售票员A";
    self.threadB.name = @"售票员B";
    self.threadC.name = @"售票员C";
    [self.threadA start];
    [self.threadB start];
    [self.threadC start];
}

- (void)saleTicket {
    while (1) {
        // 锁必须是全局唯一的。
        // 1.注意点,不能随便能乱加,加锁的位置要注意
        // 2.加锁的前提条件,--多线程共享同一个资源
        // 3.注意加锁是需要代价的--需要耗费性能的
        // 4.加锁的结果是造成线程同步。->ABC ABC ABC的循环执行任务
        @synchronized (self) {
            NSInteger count = self.totalCount;
            if (count > 0) {
                self.totalCount = count - 1;
                // 卖出去一张票,还剩下多少张
                NSLog(@"%@卖出去一张票, 还剩下%ld张", [NSThread currentThread].name, self.totalCount);
            } else {
                NSLog(@"没票了");
                break;
            }
        }
    }  
}
复制代码
6.原子和非原子性

在oc中定义属性时有nonatomic和atomic两种选择; atomic:原子属性,为setter方法加锁(默认就是atomic),意味着线程是安全的。 nonatomic:非原子属性,不会为setter方法加锁。

7.线程间的通信

有的时候,同一个进程跑了多个线程,有的子线程的输出的结果是另一个子线程的输入,这就需要两个子线程中进行一种通信。所以线程往往不是孤立存在的,多个线程之间需要经常进行通信。 线程之间的通信的方法:

// 回到主线程
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
// 回到线程对象,可以回到主线程也可以回到子线程
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
复制代码

例子:下载图片并展示 如果不做线程的处理,

- (void)wjDownloadImageCountTime {
    NSDate *start = [NSDate date]; // 获得当前时间
    NSURL *url = [NSURL URLWithString:@"http://pic1.win4000.com/wallpaper/d/573d37cc14e3c.jpg"];
    NSData *imageData = [NSData dataWithContentsOfURL:url];
    UIImage *img = [UIImage imageWithData:imageData];
    self.imageView.image = [img imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
    NSDate *end = [NSDate date];
    NSLog(@"count time is: %f", [end timeIntervalSinceDate:start]);
}
复制代码

以上能够完成图片的下载,图片的展示也能成功,但是一旦UI上的图片比较多的话,就会阻塞主线程,所以不采取。 优化的处理

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 放在子线程去下载图片
    [NSThread detachNewThreadSelector:@selector(wjDownloadImage) toTarget:self withObject:nil];

}

- (void)wjDownloadImage {
    // http://img4.imgtn.bdimg.com/it/u=816246739,294523191&fm=214&gp=0.jpg
    // 1.下载图片的url
    NSURL *url = [NSURL URLWithString:@"http://pic1.win4000.com/wallpaper/d/573d37cc14e3c.jpg"];
    // 2.下载图片到本地->二进制数据 -> 耗时操作
    NSData *imageData = [NSData dataWithContentsOfURL:url];
    // 3.将二进制数据转为图片
    UIImage *img = [UIImage imageWithData:imageData];
    // 4.显示UI->回到主线程去刷新UI界面
    [self performSelectorOnMainThread:@selector(wjShowImage:) withObject:img waitUntilDone:YES];
//  简便方法,就不需要写下面的那个方法了。
//    [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:img waitUntilDone:YES];
}

- (void)wjShowImage:(UIImage *)img {
    self.imageView.image = img;
}
复制代码