多线程编程之线程同步

各种锁的性能.png
线程安全是怎么产生的

每个线程都拥有它自己的执行堆栈,由内核调度独立的运行时间片。一个线程可 以和其他线程或其他进程通信,执行 I/O 操作,甚至执行任何你想要它完成的任务。 因为它们处于相同的进程空间,所以一个独立应用程序里面的所有线程共享相同的虚 拟内存空间,并且具有和进程相同的访问权限。线程编程的危害之一是在多个线程之间的资源争夺。如果多个线程在同一个时间 试图使用或者修改同一个资源,就会出现问题。缓解该问题的方法之一是消除共享资源。

同步工具

为了防止不同线程意外修改数据,你可以设计你的程序没有同步问题,或你也可 以使用同步工具。尽管完全避免出现同步问题相对更好一点,但是几乎总是无法实现。 以下个部分介绍了你可以使用的同步工具的基本类别。

1.原子操作(atomic

我们在声明一个变量的时候一般会使用nonatomic,这个就是非原子操作;原子操作是atomic
简单的加减使用原子操作具有更高的性能优势。注意是加减,不是增删!!
也就是说仅仅对于getter,setter是线程安全的,两个线程都去对变量赋值是安全的。对于比如NSMutableArray类型的增删操作不是线程安全的

2.内存屏障和 Volatile变量

因为内存屏障和 volatile变量降低了编译器可执行的优化,因此你应该谨慎使 用它们,只在有需要的地方时候,以确保正确性

3.锁Lock

锁是最常用的同步工具。你可以是使用锁来保护临界区(critical section),这 些代码段在同一个时间只能允许被一个线程访问。比如,一个临界区可能会操作一个 特定的数据结构,或使用了每次只能一个客户端访问的资源

3.1 信号量 Dispatch Semaphore

我的上一篇文章中有讲过一次性搞懂 GCD的所有用法

使用dispatch_semaphore_signal加1 dispatch_semaphore_wait减1,为0时等待的设置方式来达到线程同步的目的和同步锁一样能够解决资源抢占的问题。

  • dispatch_semaphore_create(long value)
  • dispatch_semaphore_signal(dispatch_semaphore_t _Nonnull dsema)
  • dispatch_semaphore_wait(dispatch_semaphore_t _Nonnull dsema, dispatch_time_t timeout)
dispatch_semaphore_t signal = dispatch_semaphore_create(1); //传入值必须 >=0, 若传入为0则阻塞线程并等待timeout,时间到后会执行其后的语句
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3.0f * NSEC_PER_SEC);

//线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"线程1 等待ing");
    dispatch_semaphore_wait(signal, overTime); //signal 值 -1
    NSLog(@"线程1");
    dispatch_semaphore_signal(signal); //signal 值 +1
    NSLog(@"线程1 发送信号");
    NSLog(@"--------------------------------------------------------");
});

//线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"线程2 等待ing");
    dispatch_semaphore_wait(signal, overTime);
    NSLog(@"线程2");
    dispatch_semaphore_signal(signal);
    NSLog(@"线程2 发送信号");
});

关于信号量,我们可以用停车来比喻:

停车场剩余4个车位,那么即使同时来了四辆车也能停的下。如果此时来了五辆车,那么就有一辆需要等待。
信号量的值(signal)就相当于剩余车位的数目,dispatch_semaphore_wait 函数就相当于来了一辆车,dispatch_semaphore_signal 就相当于走了一辆车。停车位的剩余数目在初始化的时候就已经指明了(dispatch_semaphore_create(long value)),调用一次dispatch_semaphore_signal,剩余的车位就增加一个;调用一次dispatch_semaphore_wait 剩余车位就减少一个;当剩余车位为 0 时,再来车(即调用 dispatch_semaphore_wait)就只能等待。有可能同时有几辆车等待一个停车位。有些车主没有耐心,给自己设定了一段等待时间,这段时间内等不到停车位就走了,如果等到了就开进去停车。而有些车主就像把车停在这,所以就一直等下去。

3.2 POSIX 互斥锁

POSIX互斥锁在程序里面很容易使用。

  • 导入头文件 #import <pthread.h>
  • 声明并 pthread_mutex_t mutex,
  • 初始化 pthread_mutex_init(&mutex, NULL)
  • pthread_mutex_lockpthread_mutex_unlock函数, 进行加锁解锁 * pthread_mutex_destroy释放该锁的数据结构。
#import <pthread.h>
@interface MYPOSIXViewController ()
{
    /** 声明pthread_mutex_t的结构 */
    pthread_mutex_t mutex; 
}
@end

@implementation MYPOSIXViewController
- (void)dealloc{
    /** 释放该锁的数据结构 */
    pthread_mutex_destroy(&mutex);  
}
- (void)viewDidLoad {
    [super viewDidLoad];
   /** 初始化 */
    pthread_mutex_init(&mutex, NULL);
}

- (void)getIamgeName:(NSMutableArray *)imageNames{
    NSString *imageName;
    /**  加锁 */
    pthread_mutex_lock(&mutex);
    if (imageNames.count>0) {
        imageName = [imageNames firstObject];
        [imageNames removeObjectAtIndex:0];
    }
    /** 解锁 */
    pthread_mutex_unlock(&mutex);
}
3.3 NSLock互斥锁

在 Cocoa 程序中 NSLock 中实现了一个简单的互斥锁。所有锁(包括 NSLock)的 接口实际上都是通过NSLocking协议定义的,它定义了 lockunlock方法。你使用 这些方法来获取和释放该锁。
除了标准的锁行为,NSLock 类还增加了tryLocklockBeforeDate:方法。方法 tryLock 试图获取一个锁,但是如果锁不可用的时候,它不会阻塞线程。相反,它只 是返回 NO。而 lockBeforeDate:方法试图获取一个锁,但是如果锁没有在规定的时间 内被获得,它会让线程从阻塞状态变为非阻塞状态(或者返回 NO)。

NSLock 头文件中只有如下方法,成员变量

- (void)lock;
- (void)unlock;
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name 
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];
}
3.4 NSCondition条件锁

NSCondition同样实现了NSLocking协议,所以它和NSLock一样,也有NSLocking协议的lockunlock方法,可以当做NSLock来使用解决线程同步问题,用法完全一样。同时,NSCondition提供更高级的用法。waitsignal,和条件信号量类似。

@interface TestViewController ()
/*
 创建一个数组盛放生产的数据,创建一个线程锁
*/
@property (nonatomic, strong) NSCondition *condition;
@property (nonatomic, strong) NSMutableArray *products;

 @end

 @implementation TestViewController
#pragma mark - event reponse
/*
 拖拽一个点击事件,创建两个线程
*/
- (IBAction)coditionTest:(id)sender {
    NSLog(@"condiction 开始");
    [NSThread detachNewThreadSelector:@selector(createConsumenr) toTarget:self withObject:nil];
    [NSThread detachNewThreadSelector:@selector(createProducter) toTarget:self withObject:nil];
}
#pragma mark - provate methods
 - (void)createConsumenr{
     [self.condition lock];
     while(self.products.count == 0){
         NSLog(@"等待产品");
         [_condition wait];
     }
     [self.products removeObject:0];
     NSLog(@"消费产品");
     [_condition unlock];
 }

 - (void)createProducter{
     [self.condition lock];
     [self.products addObject:[[NSObject alloc] init]];
     NSLog(@"生产了一个产品");
     [_condition signal];
     [_condition unlock];
 }

 #pragma mark - getters and setters
 - (NSMutableArray *)products {
     if(_products == nil){
         _products = [[NSMutableArray alloc] initWithCapacity:0];
     }
     return _products;
 }

 - (NSCondition *)condition {
     if(_condition == nil){
         _condition = [[NSCondition alloc] init];
     }
     return _condition;
 }
 @end
3.5 NSRecursiveLock递归锁

递归锁可以被同一线程多次请求,而不会引起死锁,这主要是用在循环或递归操作中。

NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];

void MyRecursiveFunction(int value){
  [theLock lock];
  if (value != 0){
    --value;
    MyRecursiveFunction(value);
  }

  MyRecursiveFunction(5);
3.6 @synchronized

写法最简单 @synchronized( xx ), 也是性能消耗最高的锁

- (void)getIamgeName:(int)index{
    NSString *imageName;
    @synchronized(self) {
        if (imageNames.count>0) {
            imageName = [imageNames lastObject];
            [imageNames removeObject:imageName];
        }
    }
}
3.7 OSSpinLock 被公认为不安全,不做描述

线程安全总结

  • 不可改变的对象一般是线程安全的。一旦你创建了它们,你可以把这些对象在线程间安全的传递。另一方面,可变对象通常不是线程安全的。为了在多线程应用 里面使用可变对象,应用必须适当的同步。
  • 许多对象在多线程里面不安全的使用被视为是”线程不安全的”。只要同一时间 只有一个线程,那么许多这些对象可以被多个线程使用。这种被称为专门限制应 用程序的主线程的对象通常被这样调用。
  • 应用的主线程负责处理事件。尽管ApplicationKit在其他线程被包含在事件路 径里面时还会继续工作,但操作可能会被打乱顺序。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容