原文发表在个人博客iOS-线程同步详解,转载请注明出处。
本文对iOS系统上的线程的同步方式进行了讲解。
同步工具
线程同步工具可以帮助开发者在开发多线程应用时,尽可能避免线程间互相访问导致各类问题。
Atomic Operations(原子操作)
Atomic Operations是一种基于基本数据类型的同步形式,底层用汇编锁来控制变量的变化,保证数据的正确性,好处在于不会block互相竞争的线程,且相比锁耗时很少。例如,一个整形计数器,直接使用Atomic Operations,不需要通过锁来控制计数器变化。
举例说明:
long total = 0;
void click(){
for(int i = 0; i < 1000; i++){
total++;
}
}
int main(int argc, const char * argv[]) {
clock_t start = clock();
vector threadGroup;
for(int i = 0; i < 100; ++i){
threadGroup.push_back(std::thread (click));
}
for (auto & th:threadGroup)
th.join();
clock_t finish = clock();
cout << "total:" << total << endl;
cout << "duration:" << (finish - start) << endl;
return 0;
}
上述代码,在没有用到任何同步工具时,运行结果为:
total:99098
duration:6398
可见,结果是不正确的。
如果使用Lock:
long total = 0;
mutex m;
void click(){
for(int i = 0; i < 1000; i++){
m.lock();
total++;
m.unlock();
}
}
运行结果为:
total:100000
duration:486308
如果使用Atomic Operations:
atomic_long total(0);
void click(){
for(int i = 0; i < 1000; i++){
total++;
}
}
运行结果为:
total:100000
duration:10141
可见,结果是正确的,耗时相比Lock少非常多。
Memory Barriers(内存屏障)
为了达到最佳性能,编译器通常会讲汇编级别的指令进行重新排序,从而保持处理器的指令管道尽可能的满。作为优化的一部分,编译器可能会对内存访问的指令进行重新排序(在它认为不会影响数据的正确性的前提下),然而,这并不一定都是正确的,顺序的变化可能导致一些变量的值得到不正确的结果。
Memory Barriers是一种不会造成线程block的同步工具,它用于确保内存操作的正确顺序。Memory Barriers像一道屏障,迫使处理器在其前面完成必须的加载或者存储的操作。Memory Barriers常被用于确保一个线程中可被其他线程访问的内存操作按照预期的顺序执行。具体参考Memory Barriers。
在程序中应用Memory Barriers只需要在指定地方调用:
OSMemoryBarrier();
举例说明:
int x = 0;
int f = 0;
void A(){
while(f == 0);
OSMemoryBarrier();
cout << "x = " << x << endl;
}
void B(){
x = 42;
OSMemoryBarrier();
f = 1;
}
int main(int argc, const char * argv[]) {
vector threadGroup;
for(int i = 0; i < 2; ++i){
threadGroup.push_back(std::thread (i % 2 ? A : B));
}
for (auto & th:threadGroup)
th.join();
return 0;
}
上面的代码中,如果去掉OSMemoryBarrier(),可能会出现由于编译器优化,调整了指令顺序,f=1放到了x=42的前面,而导致结果为:
x = 0
Volatile Variables(挥发变量)
Volatile Variables是另外一种针对变量的同步工具。众所周知,CPU访问寄存器的速度比访问内存速度快很多,因此,CPU有时候会将一些变量放置到寄存器中,而不是每次都从内存中读取(例如for循环中的i值)从而优化代码,但是可能会导致错误。
例如,一个线程在CPUA中被处理,CPUA从内存获取变量F的值,此时,并没有其他CPU用到变量F,所以CPUA将变量F存到寄存器中,方便下次使用,此时,另一个线程在CPUB中被处理,CPUB从内存中获取变量F的值,改变该值后,更新内存中的F值。但是,由于CPUA每次都只会从寄存器中取F的值,而不会再次从内存中取,所以,CPUA处理后的结果就是不正确的。
对一个变量加上Volatile关键字可以迫使编译器每次都重新从内存中加载该变量,而不会从寄存器中加载。当一个变量的值可能随时会被一个外部源改变时,应该将该变量声明为Volatile。
举例说明:
int x = 0;
int f = 0;
void A(){
while(f == 0);
cout << "x = " << x << endl;
}
void B(){
x = 42;
f = 1;
}
int main(int argc, const char * argv[]) {
vector threadGroup;
for(int i = 0; i < 2; ++i){
threadGroup.push_back(std::thread (i % 2 ? A : B));
}
for (auto & th:threadGroup)
th.join();
return 0;
}
上面的代码在运行时(开启编译器优化),概率出现线程A处于死循环中,即使线程B已经改变了f的值。
可以在变量f的定义前面加上Volatile,即可得到预期的结果。
x = 42
Locks(锁)
Locks是一种最常用的同步工具。Locks可以对一段代码进行保护,保证同时只有一个线程在执行该段代码。
Locks的类型分为以下几种。
Lock | Description |
---|---|
Mutex(互斥) | 如果多个线程同时竞争一个Mutex Lock,只有一个将被允许访问,其他将被block。 |
Recursive(递归) | Recursive Lock也是一种Mutex Lock。它允许一个线程在释放锁前,多次执行锁内代码,其他将被block。 |
Read-write(读写) | 多用于多读少写的数据,writer线程只有所有reader都释放锁时,才能获得锁,此时,所有reader都等待锁释放。(POSIX线程才有) |
Distributed(分布) | 进程级别的互斥锁,distributed lock不会block一个进程,而只会通知进程该锁无法获取。 |
Spin(旋转) | Spin Lock将锁的条件重复的变换,知道条件符合。Spin Lock多被用于多处理器系统,当需要等待一个锁的时间尽可能短时,切换锁条件比block一个线程要更高效。iOS系统不支持该锁。 |
Double-checked(复核) | Double-checked Lock即在条件满足,获取锁时,再对条件进行一次判断,多与单例模式结合。由于Double-checked Lock是不安全的,iOS系统并不支持该锁。 |
下面详细介绍:
Mutex Lock
(1)POSIX Mutex Lock举例:
pthread_mutex_t mutex;
void MyInitFunction()
{
pthread_mutex_init(&mutex, NULL);
}
void MyLockingFunction()
{
pthread_mutex_lock(&mutex);
// Do work.
pthread_mutex_unlock(&mutex);
}
(2)Cocoa NSLock举例:
BOOL moreToDo = YES;
NSLock *theLock = [[NSLock alloc] init];
while (moreToDo) {
/* Do another increment of calculation */
/* until there’s no more to do. */
if ([theLock tryLock]) {
/* Update display used by all threads. */
[theLock unlock];
}
}
NSLock除了lock, unlock方法外,还有tryLock和lockBeforeDate:方法。tryLock方法尝试获取锁,但不会block线程,获取失败时,返回值为NO。lockBeforeDate:方法同样不会block线程,在设定的时间里会尝试获取锁,获取失败时,返回NO。
(3)@synchronized指令 举例:
- (void)myMethod:(id)anObj
{
@synchronized(anObj)
{
// Everything between the braces is protected by the @synchronized directive.
}
}
@synchronized指令是一种非常方便的Mutex Lock,但是注意,如果指令包含模块中的代码抛出异常,@synchronized指令将会立即自动释放锁,所以需要在代码中捕获异常,或者使用NSLock。
(4)NSConditionLock
NSConditionLock也是一种Mutex Lock,它根据一个特定的整形值来作为条件获取、释放锁。NSConditionLock常用于线程间需要特定顺序进行交互的,例如生产者-消费者,生成者线程完成生成后,释放锁,消费者线程此时获得锁,开始消费。
举例说明:
#define NO_DATA 0
#define HAS_DATA 1
- (void)viewDidLoad {
[super viewDidLoad];
_condLock = [[NSConditionLock alloc] initWithCondition:0];
[[[NSThread alloc] initWithTarget:self selector:@selector(producer) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(consumer) object:nil] start];
}
- (void)producer
{
while(true){
[_condLock lockWhenCondition:NO_DATA];
NSLog(@"Produce..");
[_condLock unlockWithCondition:HAS_DATA];;
}
}
- (void)consumer
{
while(true){
[_condLock lockWhenCondition:HAS_DATA];
NSLog(@"Comsume..");
[_condLock unlockWithCondition:NO_DATA];
}
}
运行结果:
Produce..
Comsume..
Produce..
Comsume..
...
Distributed Lock
Distributed Lock可以用于限制在不同主机上的多个应用,对共享资源的访问限制。Distributed Lock也是一种Mutex Lock,使用文件系统的元素(文件/目录)实现。
为了让NSDistributedLock可用,该锁必须是对于所有应用是可写的。这意外着将其放在一个所有运行该应用的计算机都可以访问的文件系统上。NSDistributedLock没有lock方法,而提供了tryLock方法,因为lock方法将会block当前线程。
正常情况下,调用unLock方法来释放锁。在某种情况下,如果一个应用在获取到NSDistributedLock时,突然crash,该应用仍然持有该锁,其他应用将无法获取,此时需要用breakLock方法来强制获取。
举例说明:
- (void)viewDidLoad {
[super viewDidLoad];
_distLock = [[NSDistributedLock alloc] initWithPath:@"/Users/YI/Desktop/test.html"];
[[[NSThread alloc] initWithTarget:self selector:@selector(A) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(B) object:nil] start];
}
- (void)A
{
while(true){
if([_distLock tryLock]){
NSLog(@"A Get Lock");
[_distLock unlock];
}
[NSThread sleepForTimeInterval:1.0];
}
}
- (void)B
{
while(true){
if([_distLock tryLock]){
NSLog(@"B Get Lock");
[_distLock unlock];
}
[NSThread sleepForTimeInterval:2.0];
}
}
用浏览器打开test.html,再运行上述代码,则没有任何输出。因为此时锁被其他进程占据。
加上breakLock方法:
_distLock = [[NSDistributedLock alloc] initWithPath:@"/Users/YI/Desktop/test.html"];
[_distLock breakLock];
运行结果为:
A Get Lock
A Get Lock
B Get Lock
Recursive Lock
Recursive Lock是可以让同一个线程多次获取而不会导致死锁的锁,Recursive Lock记录了被获取的次数,每一次lock调用都必须有一次对应的unlock调用,否则锁将不会被释放,其他线程无法获取。
Recursive Lock一般用于递归函数中,参考以下例子:
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];
void MyRecursiveFunction(int value)
{
[theLock lock];
if (value != 0)
{
--value;
MyRecursiveFunction(value);
}
[theLock unlock];
}
MyRecursiveFunction(5);
如果没有使用NSRecursiveLock,该线程将死锁。
Double-checked Lock
volatile T* singleton = NULL;
T* GetInstance()
{
if(NULL == p)
{
lock();
if(NULL == singleton)
singleton = new T;
unlock();
}
return singleton;
}
如果没有第二个if,有可能线程A执行到lock()前,被block,此时线程B获得锁执行完成,线程A被唤醒,又执行了一次new语句。
Conditions(条件)
Conditions是一种特殊的lock,用于同步操作的顺序。与Mutex Lock不同的是,一个等待Condition的线程保持block,直到另一个线程显示对该Condition调用signal。
由于操作系统的原因,Conditions可能会得到一些不正确的信号,为了避免这类问题,可以在使用Conditions时,加入Predicate(断言)。Predicate是一种有效地判断是否让一个线程处理信号的方式。Conditions保持线程休眠,直到另一个线程调用signal,并设置了Predicate。
Cocoa Condition:
- (void)viewDidLoad {
[super viewDidLoad];
_cond = [NSCondition new];
[[[NSThread alloc] initWithTarget:self selector:@selector(A) object:nil] start];
[[[NSThread alloc] initWithTarget:self selector:@selector(B) object:nil] start];
}
static int timeToDoWork = 0;
- (void)A
{
[_cond lock];
while (timeToDoWork <= 0)
[_cond wait];
timeToDoWork--;
NSLog(@"Do work..");
[_cond unlock];
}
- (void)B
{
[_cond lock];
timeToDoWork++;
NSLog(@"Do work..");
[_cond signal];
[_cond unlock];
}
运行结果为:
Add work..
Do work..
POSIX Condition:
pthread_mutex_t mutex;
pthread_cond_t condition;
Boolean ready_to_go = true;
void MyCondInitFunction()
{
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&condition, NULL);
}
void MyWaitOnConditionFunction()
{
pthread_mutex_lock(&mutex);
while(ready_to_go == false)
{
pthread_cond_wait(&condition, &mutex);
}
std::cout << "Do work.." << std::endl;
ready_to_go = false;
pthread_mutex_unlock(&mutex);
}
void SignalThreadUsingCondition()
{
pthread_mutex_lock(&mutex);
ready_to_go = true;
std::cout << "Add work.." << std::endl;
pthread_cond_signal(&condition);
pthread_mutex_unlock(&mutex);
}
POSIX Condition由Mutex和Condition结构体两部分组成,虽然两者是独立的,但是在使用的时候,必须一一对应,否则将引发异常。