iOS-线程同步详解

原文发表在个人博客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<std::thread> 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<std::thread> 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<std::thread> 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结构体两部分组成,虽然两者是独立的,但是在使用的时候,必须一一对应,否则将引发异常。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容