首页 iOS开发:多线程
文章
取消

iOS开发:多线程

在iOS开发中,多线程是一个非常重要的概念。它可以帮助我们提高应用程序的性能、响应速度和资源利用率。本篇博客将深入探讨多线程的原理、优缺点、实现方式、状态、锁、应用场景以及保活,帮助读者更好地理解iOS多线程编程。

多线程原理

多线程是指在一个进程内同时运行多个线程,每个线程都拥有独立的执行序列和上下文环境。与单线程相比,多线程可以在同一时间内完成更多的任务,从而使程序更具有响应性。在iOS中,多线程的实现依赖于操作系统的调度机制,由系统来负责线程的创建、调度和销毁。通常情况下,每个线程都有自己的栈空间和寄存器,它们之间共享进程的虚拟地址空间和全局变量。

多线程的优缺点

多线程的优点主要有:

  • 提高CPU利用率,提高程序的响应速度和处理能力;
  • 支持并发处理多任务,提高系统的并发度和稳定性;
  • 实现异步编程,避免阻塞主线程,提高用户体验;
  • 分离UI线程和耗时任务,保证UI的流畅性。

但是多线程也有一些缺点:

  • 线程切换开销大,需要占用额外的CPU资源;
  • 可能出现死锁、竞态条件等线程安全问题;
  • 需要考虑线程同步和协作问题,编写复杂度较高。

在使用多线程时,我们需要权衡多线程的优缺点,并选择适合当前场景的解决方案。

进程和线程的区别

进程和线程都是操作系统中的概念,但是它们有着明显的区别。

进程是一个独立的执行单元,拥有自己的内存空间、文件描述符、信号处理器等资源,进程之间不能直接共享数据,只能通过IPC机制来通信。每个进程都有自己的地址空间,因此进程之间的互相干扰很小,但是进程切换的开销比线程大。

线程是进程中的一个执行单元,线程共享进程的地址空间、文件描述符等资源,不同的线程可以直接操作共享的数据,因此线程之间的协作和同步更加方便。但是线程切换的开销比进程小,因为线程之间的数据隔离较弱,容易出现竞态条件等线程安全问题。

多线程的实现方式

在iOS中,多线程可以通过以下几种方式来实现:

NSThread

NSThread是Foundation框架中的一个类,它提供了线程对象的封装。通过创建NSThread对象,我们可以手动启动线程,并控制线程的生命周期。NSThread相对简单,适合一些简单的多线程场景。

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
26
27
28
29
//单独一个线程执行任务
//这种方式适合简单的并发任务,但需要手动管理线程的生命周期。
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
[thread start];

- (void)run:(id)object {
    // 执行任务代码
}


//多个线程执行相同任务
//这种方式可以同时启动多个线程执行相同的任务,提高执行效率。
for (int i = 0; i < 10; i++) {
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
    [thread start];
}


//多个线程执行不同任务
//这种方式可以同时启动多个线程执行不同的任务,提高执行效率。
for (int i = 0; i < 10; i++) {
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@(i)];
    [thread start];
}

- (void)run:(NSNumber *)number {
    int i = [number intValue];
    // 根据不同参数执行不同任务
}

GCD

GCD(Grand Central Dispatch)是苹果推荐的多线程解决方案之一。它是一个基于C语言的API,提供了高效、可扩展的异步执行机制。GCD将任务分成小块并分散到多个线程中执行,可以充分利用系统的资源。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//在主队列中异步执行任务
//这种方式适合在主线程中执行一些不耗时的任务,比如UI界面的更新操作。
dispatch_async(dispatch_get_main_queue(), ^{
    // 执行任务代码
});


//在全局队列中异步执行任务
//这种方式适合执行耗时的任务,在不阻塞主线程的情况下提高执行效率。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 执行任务代码
});


//串行队列中同步执行任务
//这种方式适合按照顺序执行多个任务,保证任务之间的同步性。
dispatch_queue_t queue = dispatch_queue_create("com.example.queue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
    // 执行任务代码
});


//并发队列中异步执行任务
//这种方式适合同时执行多个任务,提高执行效率。
dispatch_queue_t queue = dispatch_queue_create("com.example.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    // 执行任务代码
});


// 延迟提交任务到队列中
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
dispatch_after(delayTime, dispatch_get_main_queue(), ^{
    // 延迟执行任务
});


//等待多个任务完成后再执行其他任务
//这种方式可以等待多个任务完成后再执行其他任务,保证任务之间的同步性。
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 添加任务到group中
dispatch_group_async(group, queue, ^{
    // 执行任务1代码
});

dispatch_group_async(group, queue, ^{
    // 执行任务2代码
});

// 等待所有任务执行完毕
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

// 执行其他任务代码

NSOperationQueue

NSOperationQueue是基于GCD的高级抽象,它提供了一种更面向对象的方式来管理任务。通过创建NSOperation对象并添加到队列中,我们可以方便地控制任务的执行顺序和依赖关系,以及限制并发数等。NSOperationQueue相对于GCD来说更加灵活,适合一些复杂的多线程场景。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
//创建并发任务
//这种方式可以创建包含多个任务的并发任务执行,并发任务会自动管理线程的生命周期。
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    // 执行任务代码
}];

// 添加多个任务到operation中
[operation addExecutionBlock:^{
    // 执行任务1代码
}];

[operation addExecutionBlock:^{
    // 执行任务2代码
}];

// 创建队列并添加operation到队列中执行
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];


//设置任务之间的依赖关系
//这种方式可以设置任务之间的依赖关系,保证任务按照规定的顺序执行。
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

NSOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    // 执行任务1代码
}];

NSOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    // 执行任务2代码
}];

// 设置operation2在operation1完成后执行
[operation2 addDependency:operation1];

[queue addOperation:operation1];
[queue addOperation:operation2];

线程的状态

在多线程编程中,线程有以下几种状态:

  • 新建(New):当我们创建一个线程时,它处于新建状态。
  • 就绪(Runnable):当线程被创建后并不会立即执行,而是等待CPU资源分配。当CPU资源可用时,线程进入就绪状态。
  • 运行(Running):当线程获得CPU资源后,开始执行线程的代码,处于运行状态。
  • 阻塞(Blocked):当线程需要等待某些事件发生时(如I/O操作),将进入阻塞状态,直到事件完成。
  • 终止(Terminated):当线程执行结束或异常终止时,将进入终止状态。

多线程锁

在多线程编程中,由于线程之间共享同一进程的内存空间,可能会出现多个线程同时访问同一变量的情况。为了避免竞态条件等问题,我们需要使用锁机制来保护临界区的代码。常见的锁包括互斥锁、自旋锁、读写锁等。

互斥锁(Mutex)

互斥锁是最常见的一种锁机制。它采用二元信号量来实现对共享资源的互斥访问。当某个线程需要访问共享资源时,首先尝试获取互斥锁。如果锁已经被其他线程占用,则当前线程会被阻塞,直到其他线程释放锁。当当前线程获得锁后,可以安全地访问共享资源,并在访问完毕后释放锁。

1
2
3
4
5
6
7
8
9
10
11
// 创建互斥锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

// 上锁
pthread_mutex_lock(&mutex);

// 访问共享资源

// 解锁
pthread_mutex_unlock(&mutex);

自旋锁(Spinlock)

自旋锁是一种忙等待的锁机制。当某个线程需要访问共享资源时,如果锁已经被其他线程占用,则当前线程会一直循环检查锁是否可用,直到其他线程释放锁。自旋锁适用于临界区很小的情况下,因为长时间持有锁会浪费CPU资源。

1
2
3
4
5
6
7
8
9
10
// 创建自旋锁
os_unfair_lock spinlock = OS_UNFAIR_LOCK_INIT;

// 上锁
os_unfair_lock_lock(&spinlock);

// 访问共享资源

// 解锁
os_unfair_lock_unlock(&spinlock);

读写锁(RWLock)

读写锁是一种特殊的锁机制,它允许多个线程同时读取共享资源,但只允许一个线程进行写操作。这样可以提高读取性能,同时保证数据的一致性。当某个线程需要访问共享资源时,如果没有其他线程正在进行写操作,则当前线程可以获得读锁;否则,当前线程会被阻塞,直到写操作完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建读写锁
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);

// 上读锁
pthread_rwlock_rdlock(&rwlock);

// 访问共享资源

// 解读锁
pthread_rwlock_unlock(&rwlock);

// 上写锁
pthread_rwlock_wrlock(&rwlock);

// 修改共享资源

// 解写锁
pthread_rwlock_unlock(&rwlock);

多线程应用场景

在iOS开发中,多线程可以应用于以下几个场景:

  • 数据下载:当我们需要从网络上下载大量数据时,可以将下载任务放到后台线程中执行,防止卡顿界面。
  • 图片处理:当我们需要对图片进行裁剪、缩放等操作时,可以将耗时的任务放到后台线程中执行,防止影响用户体验。
  • 数据库操作:当我们需要对数据库进行读写操作时,可以将操作放到后台线程中执行,避免阻塞主线程。
  • 音视频播放:当我们需要播放音视频时,可以将播放任务放到后台线程中执行,避免影响其他UI操作。
  • 定时任务:当我们需要定时执行某些任务时,可以使用NSTimer或GCD定时器来实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在一个社交应用中,当用户浏览朋友圈时,可能需要同时加载多张图片,我们可以将图片加载任务放到后台队列中执行
// 创建一个后台队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

// 在后台队列中执行图片加载任务
dispatch_async(queue, ^{
    NSURL *url = [NSURL URLWithString:@"http://example.com/image.jpg"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];
    
    // 将图片显示到 UI 上需要回到主线程
    dispatch_async(dispatch_get_main_queue(), ^{
        self.imageView.image = image;
    });
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 在一个电商应用中,当用户下单时,需要进行多项操作,包括生成订单、更新商品库存、发送邮件等
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

NSBlockOperation *createOrderOp = [NSBlockOperation blockOperationWithBlock:^{
    // 生成订单
}];

NSBlockOperation *updateStockOp = [NSBlockOperation blockOperationWithBlock:^{
    // 更新商品库存
}];

NSBlockOperation *sendEmailOp = [NSBlockOperation blockOperationWithBlock:^{
    // 发送邮件
}];

[updateStockOp addDependency:createOrderOp];
[sendEmailOp addDependency:updateStockOp];

[queue addOperations:@[createOrderOp, updateStockOp, sendEmailOp] waitUntilFinished:YES];

多线程保活

在iOS中,如果一个线程没有被引用,系统可能会将其回收,导致线程退出。为了避免这种情况,我们需要对线程进行保活。常见的保活方式包括:

  • 使用RunLoop:在线程中启动RunLoop,并添加至少一个输入源(如定时器)或者观察者(如CFRunLoopObserverRef),使得RunLoop处于运行状态,从而保持线程不退出。
  • 使用信号量:在线程中创建信号量,并在任务完成后调用信号量的signal方法,通知等待的线程继续执行。
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
26
27
28
// 使用RunLoop保活线程
- (void)startThread {
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
    [self.thread start];
}

- (void)threadMain {
    @autoreleasepool {
        // 添加输入源
        [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        // 进入RunLoop
        [[NSRunLoop currentRunLoop] run];
    }
}

// 使用信号量保活线程
- (void)startThread {
    self.semaphore = dispatch_semaphore_create(0);
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMain) object:nil];
    [self.thread start];
}

- (void)threadMain {
    // 执行任务
    [self doSomething];
    // 释放信号量
    dispatch_semaphore_signal(self.semaphore);
}
本文由作者按照 CC BY 4.0 进行授权

iOS开发:KVC

iOS开发:RunLoop