一、线程概述
1.概念及作用
线程是可以在单个应用中并发执行多个代码路径的,多种技术之一。
从技术角度来看,一个线程就是一个需要管理执行代码的内核级和应用级数据结 构组合。
内核级结构协助调度线程事件,并抢占式调度一个线程到可用的内核之上。
应用级结构包括用于存储函数调用的调用堆栈和应用程序需要管理和操作线程属性 和状态的结构。
线程的作用:
2.线程替代选择
Operation objects,
Grand Central Dispatch (GCD),
Asynchronous functions,
Idle-time notifications, 相关类NSNotificationQueue
Timers,
Separate processes
3.线程状态
三个状态:运行(running)、就绪(ready)、阻塞(blocked)
CPU。线程持续在这三个状态之间切换,直到它最终退出或者进入中断状态。
4.线程同步
为了防止多个线程同时试图修改或者使用同一个资源,就需要处理线程同步的问题。
线程同步工具:锁,条件,原子操作等
5.线程间通信
线程间通信,可用的方法
Direct messaging
Global variables, shared memory, and objects
Conditions
Run loop sources
Ports and sockets
Message queues
Cocoa distributed objects
6.线程设计技巧
1)避免显示创建线程。可以使用替代方法,异步API,Operation Objects和GCD,这样很多事情不必在考虑。
2)保持线程合理的忙。线程会消耗系统的资源。
3)避免共享数据结构。
4)与界面相关的事件最好放在主线程中。
5)了解线程退出时的行为
6)处理异常。在线程中出现异常时,你需要简单地通知其他线程发生了什么。
在一些情况下,异常处理可能是自动创建的。比如,Objective-C 中的 @synchronized 包含了一个隐式的异常处理。
7)干净地中断线程。线程自然退出的最好方式是让它达到其主入口结束点。
由于中断线程,可能造成该线程开辟的内存,打开的文件等资源无法回收。所以中断时,要充分考虑这一点。
8)虽然应用程序开发人员控制应用程序是否执行多个线程,类库的开发者则无法这样控制。
当开发类库时,你必须假设调用应用程序是多线程,或者多线程之间可以随时切换。
二、线程管理
一个独立应用程序里面的所有线程共享相同的虚拟内存空间,并且具有和进程相同的访问权限。
1.线程成本
每个线程都需要分配一定的内核内存和应用程序内存空间的内存。
管理你的线程和协调其调度所需的核心数据结构存储在使用 Wired Memory 的内核里面。
你线程的堆栈空间和每个线程的数据都被存储在你应用程序的内存空间里面。
创建用户级线程所需的大致成本:
Kernel data structures: 1 KB左右
Stack space:512 KB(secondary threads) 8 MB (Mac OS X main thread) 1 MB (iOS main thread)
Creation time: 90 ms左右
2.线程的使用
你必须有一个函数或方法作为线程的主入口点,你必须使用一个可用的线程例程启动你的线程。
2.1创建线程
1)
[NSThread detachNewThreadSelector:@selector(threadMainMethod:) toTarget:self withObject:nil];
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadMainMethod:) object:nil];
[thread start];
注意:使用 initWithTarget:selector:object:方法的替代办法是子类化 NSThread,并重写它的 main 方法。
你可以使用你重写的该方法的版本来实现你线程的主体入口。更多信息,请参 阅 NSThread Class Reference 里面子类化的提示
这两种创建线程的技术都在你的应用程序里面新建了一个脱离的线程。
一个脱离的线程意味着当线程退出的时候线程的资源由系统自动回收。这也同样意味着之后不需要在其他线程里面显式的连接。
performSelector:onThread:withObject:waitUntilDone:是实现线程间通信的便捷方法。
注意:虽然在线程间的偶尔通信的时候使用该方法很好,但是你不能周期的或频繁的使用 performSelector:onThread:withObject:waitUntilDone:来实现线程间的通信。
3)使用POSIX线程
#include
#include
#include
void threadMainRoutine(void* data)
{
//Do some work here
}
void launchThread(void)
{
pthread_attr_t attr;
pthread_t thread;
int ret;
ret = pthread_attr_init(&attr);
assert(!ret);
ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
assert(!ret);
int threadError = pthread_create(&thread, &attr, &threadMainRoutine, NULL);
if (threadError != 0)
{
//error process
}
ret = pthread_attr_destroy(&attr);
assert(!ret);
}
[myObj performSelectorInBackground:@selector(doSomething) withObject:nil];
5)使用Mutiprocessing Services
在Cocoa中使用POSIX线程:
然而给定一个锁,你必须总是使用同样 的接口来创建和操纵该锁。
换言之,你不能使用 Cocoa 的 NSLock 对象来操纵一个你 使用 pthread_mutex_init 函数生成的互斥体,反之亦然。
2.2 配置线程属性
1)配置线程栈的大小
NSThread *thread = [[NSThread alloc] init];
[thread setStackSize:64];
应该在 start方法前使用
ret = pthread_attr_setstacksize(&attr, 64);
在创建pthread前设置
2)配置本地存储
每个线程都维护了一个键-值的字典,它可以在线程里面的任何地方被访问。
你可以使用该字典来保存一些信息,这些信息在整个线程的执行过程中都保持不变。
比如,你可以使用它来存储在你的整个线程过程中 Run loop 里面多次迭代的状态信息。
NSThread实例可以使用一下方法
NSMutableDictionary *dict = [thread threadDictionary];
大部分上层的线程技术都默认创建了脱离线程(Datached thread)。
大部分情况下,脱离线程(Detached thread)更受欢迎,因为它们允许系统在线程完成的时候立即释放它的数据结构。脱离线程同时不需要显示的和你的应用程序交互。意味着线程
检索的结果由你来决定。相比之下,系统不回收可连接线程(Joinable thread)的 资源直到另一个线程明确加入该线程,这个过程可能会阻止线程执行加入。
你可以认为可连接线程类似于子线程。虽然你作为独立线程运行,但是可连接线 程在它资源可以被系统回收之前必须被其他线程连接。可连接线程同时提供了一个显 示的方式来
把数据从一个正在退出的线程传递到其他线程。在它退出之前,可连接线 程可以传递一个数据指针或者其他返回值给 pthread_exit 函数。其他线程可以通过 pthread_join 函数来
拿到这些数据。
重要:在应用程序退出时,脱离线程可以立即被中断,而可连接线程则不可以。每个可连接 线程必须在进程被允许可以退出的时候被连接。所以当线程处于周期性工作而不允许被中
断的时 候,比如保存数据到硬盘,可连接线程是最佳选择。
4)设置线程的优先级
2.3 线程的主要入口
1)创建一个自动释放池
2)设置异常处理(防止出现异常,造成强退)
3)设置一个run loop
4)中断线程
如果你的应用程序需要在一个操作中间中断一个线程,你应该设计你的线程响应取消或退出的消息。
对于长时运行的操作,这意味着周期性停止工作来检查该消息是否到来。
如果该消息的确到来并要求线程退出,那么线程就有机会来执行任何清理和退出工作;否则,它返回继续工作和处理下一个数据块。
三、线程同步
涉及到线程安全时,一个好的设计是最好的保护。
避免共享资源,并尽量减少 线程间的相互作用,这样可以让它们减少互相的干扰。
但是一个完全无干扰的设计是不可能的。在线程必须交互的情况下,你需要使用同步工具,来确保当它们交互的时候是安全的。
1.线程同步工具
1.1 原子操作
#include
该头文件提供了原子操作的方法
1.2 内存屏障和 Volatile 变量
内存屏障(memory barrier)是一个使用来确保内存操作按照正确的顺序工作的非阻塞的同步工具。
内存屏障的作用就像一个栅栏,迫使处理器来完成位于障碍前面 的任何加载和存储操作,才允许它执行位于屏障之后的加载和存储操作。
内存屏障同样使用来确保一个线程(但对另外一个线程可见)的内存操作总是按照预定的顺序完成。
在变量之前加上关键字 volatile 可以强制编译器每次使用变量的时候都从内存里面加载。
谨慎地使用以上方法,因为会降低编译器可执行的优化。
1.3 锁
锁是最常用的同步工具。
可以使用锁来保护临界区,这些代码段在同一个时间只能允许被一个线程访问。
常用的锁有:Mutex [互斥锁],Recursive lock [递归锁],Read-write lock [读写锁],Distributed lock [分布锁],Spin lock [自旋锁],Double-checked lock [双重检查锁]
1.3.1 如何使用锁
1)POSIX互斥锁
pthread_mutex_t mutex;
2)NSLock
3)使用@synchronized指令
4)使用其他Cocoa锁
递归锁
NSRecursiveLock 类定义的锁可以在同一线程多次获得,而不会造成死锁。
一个递归锁会跟踪它被多少次成功获得了。每次成功的获得该锁都必须平衡调用锁住和解锁的操作。
只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获 得。
示例:
static NSRecursiveLock *lock = nil;
void initLock(void)
{
if (lock == nil)
{
lock = [[NSRecursiveLock alloc] init];
}
}
void lockFunction(int value)
{
[lock lock];
if (value != 0)
{
--value;
lockFunction(value);
}
[lock unlock];
}
void test()
{
lockFunction(6);
}
NSConditionLock 对象定义了一个互斥锁,可以使用特定值来锁住和解锁。
不要把该类型的锁和条件混淆了。它的行为和条件有点类似,但是它们的实现非常不同。
分布锁
1.4 条件
条件是信号量的另外一个形式,它允许在条件为真的时候线程间互相发送信号。
条件通常被使用来说明资源可用性,或用来确保任务以特定的顺序执行。
条件是一个特殊类型的锁,你可以使用它来同步操作必须处理的顺序。它们和互斥锁有微妙的不同。
当一个线程测试一个条件时,它会被阻塞直到条件为真。它会一直阻塞直到其他线程显式的修改信号量的状态。
条件和互斥锁(mutex lock)的区别在于多个线程被允许同时访问一个条件。条件更多是允许不同线程根据一些指定的标准通过的守门人。
由于微妙之处包含在操作系统实现上,条件锁被允许返回伪成功,即使实际上它们并没有被你的代码告知。
为了避免这些伪信号操作的问题,你应该总是在你的条件锁里面使用一个断言。该断言是一个更好的方法来确定是否安全让你的线程处理。
条件简单的让你的线程保持休眠直到断言被发送信号的线程设置了。
1.5 执行Selector例程
2.同步的成本和性能
锁和原子操作通常包含了内存屏障和内核级别同步的使用来确保代码正确被保护。
3.线程安全和信号量
在你应用程序里面实现信号量处理的第一条规则是避免假设任一线程处理信号量。
如果一个指定的线程想要处理给定的信号,你需要通过某些方法来通知该线程信号何时到达。
你不能只是假设该线程的一个信号处理例程的安装会导致信号被传递到同一线程里面。
4.线程安全的设计提示
1)完全避免同步
2)了解同步的限制
同步工具只有当它们被用在应用程序中的所有线程是一致时才是有效的。
3)注意对代码正确性的威胁
4)注意死锁或活锁
任何时候线程试图同时获得多于一个锁,都有可能引发潜在的死锁。
当两个线程竞争同一个资源的时候就可能发生活锁。
避免死锁和活锁的最好方法是同一个时间只拥有一个锁。如果你必须在同一时间获取多于一个锁,你应该确保其他线程没有做类似的事情。
5)正确使用volatile变量
通常情况下,互斥锁和其他同步机制是比 volatile 变量更好的方式来保护数据结构的完整性。
关键字 volatile 只是确保从内存加载变量而不是使用寄存器里面的变量。它不保证你代码访问变量是正确的。
不必同时使用锁和volatile关键字。