移动开发(IOS) – 多线程

来源:互联网 发布:淘宝权重值 编辑:程序博客网 时间:2024/06/05 18:35

移动开发(IOS) – 多线程

Bydocoder in 博客, 学习on 2014/07/04

thread

1.概念 

1.1.系统中的每一个进程都有自己独立的虚拟内存空间,而同一个进程中的多个线程则共用进程的内存空间。

1.2.每创建一个新的线程,都会消耗一定内存和CPU时间。

1.3.当多个线程对同一个资源出现争夺的时候需要注意线程安全问题。

1.4.多线程的优势:

1.4.1.充分发挥多核处理器优势,将不同线程任务分配给不同的处理器,真正进入“并行运算”状态。

1.4.2.将耗时、轮询或者并发需求高等任务分配到其他线程执行,并由主线程负责统一更新界面会使得应用程序更加流畅,用户体验更好。

1.4.3.当硬件处理器的数量增加,程序会运行更快,而无需做任何调整。

1.5.多线程的难点:

1.5.1.共享资源的“争夺。

1.5.2.多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了通过提高资源使用效率来提高系统的整体性能。

1.6.只有主线程有直接修改UI的能力。

1.7.内存管理对于多线程非常重要。

1.7.1.Objective-C 可以凭借 @autoreleasepool 使用内存资源,并需要时回收资源。

1.7.2.每个线程都需要有 @autoreleasepool ,否则可能会出现内存泄漏。

2.iOS的三种多线程技术

2.1.NSThread 每个 NSThread 对象对应一个线程,量级较轻。

2.1.1.优点:NSThread 比其他两个轻量级,使用简单。

2.1.2.缺点:需要自己管理线程的生命周期、线程同步、加锁、睡眠以及唤醒等。线程同步对数据的加锁会有一定的系统开销。

2.2.NSOperation/NSOperationQueue 面向对象的线程技术。

2.2.1.不需要关心线程管理,数据同步的事情,可以把精力放在自己需要执行的操作上。

2.2.2.NSOperation是面向对象的。

2.3.GCD(Grand Central Dispatch) 是基于 C 语言的框架,可以充分利用多核,是苹果推荐使用的多线程技术。

2.3.1.Grand Central Dispatch 是由苹果开发的一个多核编程的解决方案。iOS4.0+才能使用,是替代 NSThread, NSOperation的高效和强大的技术。

2.3.2.GCD 是基于 C 语言的。

以上这三种编程方式从上到下,抽象度层次是从低到高的,抽象度越高的使用越简单,也是Apple最推荐使用的。

3.NSObject的多线程方法

3.1.通常,由于线程管理相对比较繁琐,而很多耗时的任务又无法知道其准确的完成时间,因此可以使用 performSelectorInBackground 方法直接新建一个后台线程,并将选择器指定的任务在后台线程执行,而无需关心具体的 NSThread 对象:

1
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg

3.1.1.performSelectorInBackground 方法本身是在主线程中执行的,而选择器指定的方法是在后台线程中进行的。

3.1.2.使用 performSelectorInBackground 方法调用的任务可以更新 UI 界面。

3.2.如果要更新UI界面,可以在后台线程中调用 performSelectorOnMainThread 方法:

1
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;

尽管使用 performSelectorInBackground 方法调用的任务可以更新UI界面,但是在实际开发中,涉及到 UI 界面的更新操作,还是要使用 performSelectorOnMainThread 方法,以避免不必要的麻烦。

3.3.获取线程信息:

1
[NSThreadcurrentThread];

3.4.线程休眠(仅适用于开发调试时使用):

1
[NSThreadsleepForTimeInterval:1.0f];

3.5.特点:使用简单,量级轻;不能控制线程的执行顺序。

4.资源争夺

4.1.仅使用单例模式无法解决资源争夺问题。

4.2.使用同步锁 @synchronized 可以保证多个线程不会使用同一代码块,而且比 NSLock 具有更好的性能。

4.3.为了保证属性安全,被争夺资源的属性应该设置为原子属性 atomic。

5.NSThread

5.1.创建线程方法:

1
2
3
4
5
6
7
8
/*
 * 参数说明:
 * selector:线程执行的方法,只能有一个参数,不能有返回值
 * target:selector消息发送的对象
 * argument:传输给target的唯一参数,也可以是nil
 */
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;
- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument;

5.1.1.detachNewThreadSelector 方法会直接启动线程方法。

5.1.2.initWithTarget 需要调用 start 方法才能够启动线程方法。

1
2
3
4
5
6
//类方法
[NSThreaddetachNewThreadSelector:@selector(threadSaleTicketWithName:) toTarget:selfwithObject:@"thread-1"];
//init方法
NSThread*thread= [[NSThreadalloc]initWithTarget:selfselector:@selector(threadSaleTicketWithName:) object:@"thread-2"];
// 启动线程
[threadstart];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)threadSaleTicketWithName:(NSString*)name
{
     // 使用 NSThread 时,线程调用的方法千万要使用 @autoreleasepool
     @autoreleasepool{
        while(YES) {
            @synchronized(self) {
                if([Ticket sharedTicket].tickets > 0) {
                    [Ticket sharedTicket].tickets--;
                    NSString*str = [NSStringstringWithFormat:@"剩余票数 %d 线程名称 %@", [Ticket sharedTicket].tickets, name];
                    // 更新UI
                    [selfperformSelectorOnMainThread:@selector(appendContent:) withObject:str waitUntilDone:YES];
                }else {
                    break;
                }
            }
            // 模拟休息
            if([name isEqualToString:@"thread-1"]) {
                [NSThreadsleepForTimeInterval:1.0f];
            }else {
                [NSThreadsleepForTimeInterval:0.1f];
            }
        }
    }
}

6.NSOperation  &  NSOperationQueue

6.1.NSOperation 的两个子类:NSInvocationOperation, NSBlockOperation。

6.2.工作原理:

6.2.1.用 NSOperation 封装要执行的操作。

6.2.2.将创建好的 NSOperation 对象放 NSOperationQueue 中。

6.2.3.启动 OperationQueue 开始新的线程执行队列中的操作。

6.3.注意事项:

6.3.1.使用 NSBlockOperation 更加简单直接。

6.3.2.定义完操作后,将添加到操作队列中,即可启动异步操作,否则操作任务仍然在主线程中执行。

6.3.3.使用多线程时通常需要控制线程的并发数,因为线程会消耗系统资源,同时运行的线程过多,系统会变慢。

6.3.4.使用以下方法可以控制并发的线程数量:

1
- (void)setMaxConcurrentOperationCount:(NSInteger)cnt;

6.3.5.使用 addDependency 可以建立操作之间的依赖关系,设定操作的执行顺序。

6.4.更新界面时使用 [[NSOperationQueue mainQueue]addOperationWithBlock: 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//队列可以设置同时并发线程的数量
[self.queue setMaxConcurrentOperationCount:2];
 
NSBlockOperation*op1 = [NSBlockOperationblockOperationWithBlock:^{
    NSLog(@"下载 %@", [NSThread currentThread]);
}];
NSBlockOperation*op2 = [NSBlockOperationblockOperationWithBlock:^{
    NSLog(@"美化 %@", [NSThread currentThread]);
}];
NSBlockOperation*op3 = [NSBlockOperationblockOperationWithBlock:^{
    NSLog(@"更新 %@", [NSThread currentThread]);
    //在主线程更新 UI
    [[NSOperationQueuemainQueue]addOperationWithBlock:^{
         NSLog(@"更新 UI %@", [NSThread currentThread]);
    }];
}];
// Dependency依赖
// 依赖关系可以多重依赖
// 不要建立循环依赖
[op2 addDependency:op1];
[op3 addDependency:op2];
 
[self.queue addOperation:op3];
[self.queue addOperation:op1];
[self.queue addOperation:op2];

7.GCD

7.1.GCD 是基于 C 语言的框架。

7.2.工作原理:

7.2.1.让程序平行排队的特定任务,根据可用的处理资源,安排它们在任何可用的处理器上执行任务。

7.2.2.要执行的任务可以是一个函数或者一个 block 。

7.2.3.底层是通过线程实现的,不过程序员可以不必关注实现的细节。

7.2.4.GCD 中的 FIFO 队列称为 dispatch queue,可以保证先进来的任务先得到执行。

7.2.5.dispatch_notify 可以实现监听一组任务是否完成,完成后得到通知。

7.3.GCD 队列:

7.3.1.全局队列:所有添加到主队列中的任务都是并发执行的。(可能会开启多条线程)

1
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

7.3.2.串行队列:所有添加到串行队列中的任务都是顺序执行的。(只可能会开启一条线程)

1
2
3
//创建串行队列,串行队列不能够获取。
//队列名称可以随意,不过不要使用@
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);

 

7.3.3.主队列:所有添加到主队列中的任务都是在主线程中执行的。

1
dispatch_get_main_queue();

7.4.GCD任务的执行方式

7.4.1.异步操作:dispatch_async 在其他线程执行任务,会开启新的线程,异步方法无法确定任务的执行顺序。

7.4.2.同步操作:dispatch_sync 在当前在当前线程执行任务,不开启新的线程。同步操作与队列无关,同步方法会依次执行,能够决定任务的执行顺序,更新界面 UI 时,最好使用同步方法。

7.5.GCD 的优点:

7.5.1.充分利用多核。

7.5.2.所有的多线程代码集中在一起,便于维护。

7.5.3.GCD 中无需使用 @autoreleasepool。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. 获取全局队列
//优先级:DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
for(UIImageView *imageView in self.imageViewSet) {
    // 2. 在全局队列上异步调用方法,加载并更新图像
    dispatch_async(queue, ^{
        NSLog(@"GCD- %@", [NSThread currentThread]);
        NSIntegernum = arc4random_uniform(17) + 1;
        NSString*imageName = [NSStringstringWithFormat:@"NatGeo%02d.png", num];
        // 通常此处的image是从网络上获取的
        UIImage *image = [UIImage imageNamed:imageName];
        // 3. 在在主线程队列中,调用同步步方法设置UI
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"更新图片- %@", [NSThread currentThread]);
            [imageView setImage:image];
        });
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建全局队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// GCD中可以将一组相关联的操作,定义到一个群组中
// 定义到群组中之后,当所有线程完成时,可以获得通知
// 1) 定义群组
dispatch_group_t group = dispatch_group_create();
// 2) 定义群组的异步任务
dispatch_group_async(group, queue, ^{
    [selfgcdSaleTicketWithName:@"gcd-1"];
});
dispatch_group_async(group, queue, ^{
    [selfgcdSaleTicketWithName:@"gcd-2"];
});
dispatch_group_async(group, queue, ^{
    [selfgcdSaleTicketWithName:@"gcd-3"];
});
// 3) 群组任务完成通知
dispatch_group_notify(group, queue, ^{
    NSLog(@"卖完了");
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (void)gcdSaleTicketWithName:(NSString*)name
{
    while(YES) {
        // 同步锁synchronized要锁的范围,对被抢夺资源修改/读取的代码部分
        @synchronized(self) {
            if([Ticket sharedTicket].tickets > 0) {
                [Ticket sharedTicket].tickets--;
                // 提示内容
                NSString*str = [NSStringstringWithFormat:@"剩余票数 %d, 线程名称 %@", [Ticket sharedTicket].tickets, name];
                // 更新界面
                dispatch_sync(dispatch_get_main_queue(), ^{
                    [selfappendContent:str];
                });
            }else {
                break;
            }
        }
        // 模拟线程休眠
        if([name isEqualToString:@"gcd-1"]) {
            [NSThreadsleepForTimeInterval:1.0f];
        }else {
            [NSThreadsleepForTimeInterval:0.2f];
        }
    }
}

0 0