您的位置:首页 > 移动开发 > IOS开发

iOS多线程安全详解

2017-03-08 10:03 281 查看


转自:iOS多线程安全详解

里面有信号量,GCD栅栏函数的例子,感觉很直白易懂,mark下


一、概述

在多线程操作过程中,往往一个数据同时被多个线程读写,在这种情况下,如果没有相应的机制对数据进行保护,就很可能会发生数据污染的的问题,给程序造成各种难以重现的潜在bug。

多线程安全中相关术语及概念(假设操作的是数据库):

(1)脏读

指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中。这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据,依据脏数据所做的操作可能是不正确的。

(2)不可重复读

指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。

(3)幻觉读

指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。例如:

目前工资为5000的员工有10人,事务A读取所有工资为5000的人数为10人。此时,事务B插入一条工资也为5000的记录。这时,事务A再次读取工资为5000的员工,记录为11人。此时产生了幻读。

二、多线程的安全问题

线程不安全:就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。



线程安全:简单来说就是多个线程同时对共享资源进行访问时,采用了加锁机制,当一个线程访问共享资源,对该资源进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。



三、iOS多线程中的“锁”

1、互斥锁:@synchronized(id anObject)

1234567- (void)myMethod:(id)anObj{ @synchronized(anObj) { //do something here }}
2、atomicOC在定义属性时有nonatomic和atomic两种选择。atomic:原子属性,为setter方法加锁(默认就是atomic)。nonatomic:非原子属性,不会为setter方法加锁。atomic加锁原理:

1

2

3

4

5

6

7

8

@property
(assign,
atomic)
int
age;

-
(void)setAge:(int)age

{

@synchronized(self)
{

_age
=
age;

}

}

3、NSLock

NSLock对象实现了NSLocking protocol,包含几个方法:

lock——加锁

unlock——解锁

tryLock——尝试加锁,如果失败了,并不会阻塞线程,只是立即返回NO

lockBeforeDate:——在指定的date之前暂时阻塞线程(如果没有获取锁的话),如果到期还没有获取锁,则线程被唤醒,函数立即返回NO。

比如:

123456NSLock *theLock = [[NSLock alloc] init]; if ([theLock lock]) { //do something here [theLock unlock]; }
4、递归锁:NSRecursiveLock多次调用不会阻塞已获取该锁的线程。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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

void
recursiveLockTest(int
value)

{

[rcsLock
lock];

if
(value
!=
0)

{

--value;

recursiveLockTest(value);

}

[rcsLock
unlock];

}

recursiveLockTest(5);

上面如果直接使用NSLock就会造成死锁。NSRecursiveLock类定义的锁可以在同一线程多次lock,而不会造成死锁。递归锁会跟踪它被多少次lock。每次成功的lock都必须平衡调用unlock操作。只有所有的锁住和解锁操作都平衡的时候,锁才真正被释放给其他线程获得。

5、条件锁:NSConditionLock

有时一把只会lock和unlock的锁未必就能完全满足我们的使用。因为普通的锁只能关心锁与不锁,而不在乎用什么钥匙才能开锁,而我们在处理资源共享的时候,多数情况是只有满足一定条件的情况下才能打开这把锁:

1234567891011121314151617181920//主线程中NSConditionLock *theLock = [[NSConditionLock alloc] init]; //线程1dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ for (int i=0;i<=2;i++) { [theLock lock]; NSLog(@"thread1:%d",i); sleep(2); [theLock unlockWithCondition:i]; }}); //线程2dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [theLock lockWhenCondition:2]; NSLog(@"thread2"); [theLock unlock];});
在线程1中的加锁使用了lock,是不需要条件的,所以顺利的就锁住了。但在unlock的使用了一个整型的条件,它可以开启其它线程中正在等待这把钥匙的临界地,而线程2则需要一把被标识为2的钥匙,所以当线程1循环到最后一次的时候,才最终打开了线程2中的阻塞。但即便如此,NSConditionLock也跟其它的锁一样,是需要lock与unlock对应的,只是lock、lockWhenCondition:与unlock,unlockWithCondition:是可以随意组合的,当然这是与你的需求相关的。上述代码运行结果:

1

2

3

4

2017-03-04
22:21:29.031
LockDemo[87455:3031878]
thread1:0

2017-03-04
22:21:31.105
LockDemo[87455:3031878]
thread1:1

2017-03-04
22:21:33.175
LockDemo[87455:3031878]
thread1:2

2017-03-04
22:21:35.249
LockDemo[87455:3031879]
thread2

如果上面线程2的代码中将[theLock lockWhenCondition:2];改为[theLock lockWhenCondition:3];,则运行时就不会再打印出thread2了。

6、分布锁:NSDistributedLock

以上所有的锁都是在解决多线程之间的冲突,但如果遇上多个进程或多个程序之间需要构建互斥的情景该怎么办呢?这个时候我们就需要使用到NSDistributedLock了,从它的类名就知道这是一个分布式的Lock,NSDistributedLock的实现是通过文件系统的,所以使用它才可以有效的实现不同进程之间的互斥,但NSDistributedLock并非继承于NSLock,它没有lock方法,它只实现了tryLock,unlock,breakLock,所以如果需要lock的话,你就必须自己实现一个tryLock的轮询。

例如:

程序A:

12345678910dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ lock = [[NSDistributedLock alloc] initWithPath:@"/Users/lifengfeng/Desktop/locktest.txt"]; [lock breakLock]; [lock tryLock]; sleep(10); [lock unlock]; NSLog(@"appA: OK"); });
程序B:

1

2

3

4

5

6

7

8

9

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0),
^{

lock
=
[[NSDistributedLock
alloc]
initWithPath:@"/Users/lifengfeng/Desktop/locktest.txt"];

while
(![lock
tryLock])
{

NSLog(@"appB:
waiting");

sleep(1);

}

[lock
unlock];

NSLog(@"appB:
OK");

});

先运行程序A,然后立即运行程序B。根据打印你可以清楚的发现,当程序A刚运行的时候,程序B一直处于等待中。当大概10秒过后,程序B便打印出了appB:OK的输出,以上便实现了两上不同程序之间的互斥。/Users/lifengfeng/Desktop/locktest.txt是一个文件或文件夹的地址,如果该文件或文件夹不存在,那么在tryLock返回YES时,会自动创建该文件/文件夹。在结束的时候该文件/文件夹会被清除,所以在选择的该路径的时候,应该选择一个不存在的路径,以防止误删了文件。

7、GCD中信号量:dispatch_semaphore

假设现在系统有两个空闲资源可以被利用,但同一时间却有三个线程要进行访问,这种情况下,该如何处理呢?这里,我们就可以方便的利用信号量来解决这个问题。同样我们也可以用它来构建一把”锁”(从本质上讲,信号量与锁是有区别的,具体的请自行查阅资料)。

信号量:就是一种可用来控制访问资源的数量的标识。设定了一个信号量,在线程访问之前,加上信号量的处理,则可告知系统按照我们指定的信号量数量来执行多个线程。

在GCD中有三个函数是semaphore的操作:

dispatch_semaphore_create 创建一个semaphore

dispatch_semaphore_signal 发送一个信号

dispatch_semaphore_wait 等待信号

dispatch_semaphore_create函数有一个整形的参数,我们可以理解为信号的总量,dispatch_semaphore_signal是发送一个信号,自然会让信号总量+1,dispatch_semaphore_wait等待信号,当信号总量少于0的时候就会一直等待,否则就可以正常的执行,并让信号总量-1,根据这样的原理,我们便可以快速的创建一个并发控制来同步任务和有限资源访问控制。

具体使用如下:

123456789101112131415161718192021222324252627282930//crate的value表示,最多几个资源可访问 dispatch_semaphore_t semaphore = dispatch_semaphore_create(2); dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //任务1 dispatch_async(quene, ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"run task 1"); sleep(1); NSLog(@"complete task 1"); dispatch_semaphore_signal(semaphore); }); //任务2 dispatch_async(quene, ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"run task 2"); sleep(1); NSLog(@"complete task 2"); dispatch_semaphore_signal(semaphore); }); //任务3 dispatch_async(quene, ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@"run task 3"); sleep(1); NSLog(@"complete task 3"); dispatch_semaphore_signal(semaphore); });
运行结果:

1

2

3

4

5

6

2017-03-04
22:59:52.915
LockDemo[89228:3095043]
run
task
1

2017-03-04
22:59:52.915
LockDemo[89228:3095041]
run
task
2

2017-03-04
22:59:53.983
LockDemo[89228:3095043]
complete
task
1

2017-03-04
22:59:53.984
LockDemo[89228:3095040]
run
task
3

2017-03-04
22:59:53.992
LockDemo[89228:3095041]
complete
task
2

2017-03-04
22:59:55.029
LockDemo[89228:3095040]
complete
task
3

由于设定的信号值为2,先执行两个线程,等执行完一个,才会继续执行下一个,保证同一时间执行的线程数不超过2。

如果我们把信号值设为1

1dispatch_semaphore_t semaphore = dispatch_semaphore_create(1)
则运行结果为:

1

2

3

4

5

6

2017-03-04
23:01:56.468
LockDemo[89388:3100137]
run
task
1

2017-03-04
23:01:57.530
LockDemo[89388:3100137]
complete
task
1

2017-03-04
23:01:57.531
LockDemo[89388:3100135]
run
task
2

2017-03-04
23:01:58.531
LockDemo[89388:3100135]
complete
task
2

2017-03-04
23:01:58.532
LockDemo[89388:3100134]
run
task
3

2017-03-04
23:01:59.588
LockDemo[89388:3100134]
complete
task
3

8、GCD中“栅栏函数”:dispatch_barrier_async

dispatch_barrier_async函数的作用与barrier的意思相同,在进程管理中起到一个栅栏的作用,它等待所有位于barrier函数之前的操作执行完毕后执行,并且在barrier函数执行之后,barrier函数之后的操作才会得到执行,该函数需要同dispatch_queue_create函数生成的concurrent Dispatch Queue队列一起使用。

dispatch_barrier_async函数的作用:

(1)实现高效率的数据库访问和文件访问

(2)避免数据竞争

例如:

1234567891011121314151617181920//同dispatch_queue_create函数生成的concurrent Dispatch Queue队列一起使用 dispatch_queue_t queue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ NSLog(@"----1"); }); dispatch_async(queue, ^{ NSLog(@"----2"); }); dispatch_barrier_async(queue, ^{ NSLog(@"----barrier"); }); dispatch_async(queue, ^{ NSLog(@"----3"); }); dispatch_async(queue, ^{ NSLog(@"----4"); });
上述代码打印结果总是1 2 –> barrier –>3 4,即1、2总在barrier之前打印,3、4总在barrier之后打印,其中1、2 由于并行处理先后顺序不定,当然3、4也一样。 四、综合demo先看下面代码:

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

-
(void)viewDidLoad

{

[self
configData];

}

-
(void)configData

{

self.dataSource
=
[NSMutableArray
array];

for
(int
i
=
0;
i
<
100;
i++)
{

[self.dataSource
addObject:[NSString
stringWithFormat:@"Obj
- %i",
i]];

}

}

-
(IBAction)start:(id)sender

{

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0),
^{

for
(int
i
=
0;
i
<
self.dataSource.count;
i++)
{

[NSThread
sleepForTimeInterval:0.05];

NSLog(@"%@",
self.dataSource[i]);

}

});

}

-
(IBAction)removeAllObjs:(id)sender

{

[self.dataSource
removeAllObjects];

}

用户在点击start按钮后,会在一个全局的queue里面对构造的数据进行遍历,为了模拟实际场景中网络请求的时延,每次循环让当前线程休息0.05s。这样在遍历的过程中,如果用户点击了移除按钮,此时
self.dataSource[i]
执行时,因为数组已经被清空了,会报数组越界的错误。

解决办法:

(1)使用@synchronized修复

123456789101112131415161718- (IBAction)start:(id)sender{dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @synchronized(self.dataSource) { for (int i = 0; i < self.dataSource.count; i++) { [NSThread sleepForTimeInterval:0.05]; NSLog(@"%@",self.dataSource[i]); } } });} - (IBAction)removeAllObjs:(id)sender{ @synchronized(self.dataSource) { [self.dataSource removeAllObjects]; }}
(2)使用NSLock修复

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

//声明一个全局变量

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

-
(IBAction)start:(id)sender

{

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,
0),
^{

[rLock
lock];

for
(int
i
=
0;
i
<
self.dataSource.count;
i++)
{

[NSThread
sleepForTimeInterval:0.05];

NSLog(@"%@",
self.dataSource[i]);

}

[rLock
unlock];

});

}

-
(IBAction)removeAllObjs:(id)sender

{

[rLock
lock];

[self.dataSource
removeAllObjects];

[rLock
unlock];

}

(3)使用dispatch_semaphore_signal修复

12345678910111213141516171819202122//声明全局变量dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); - (IBAction)start:(id)sender{dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); for (int i = 0; i < self.dataSource.count; i++) { [NSThread sleepForTimeInterval:0.05]; NSLog(@"%@",self.dataSource[i]); } dispatch_semaphore_signal(semaphore); }); } - (IBAction)removeAllObjs:(id)sender{ dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); [self.dataSource removeAllObjects]; dispatch_semaphore_signal(semaphore);}
(4)使用dispatch_barrier_async修复

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

//声明全局变量

dispatch_queue_t
concurrentQueue
=
dispatch_queue_create("com.threadsafe.sing",
DISPATCH_QUEUE_CONCURRENT);

-
(IBAction)start:(id)sender

{

dispatch_async(concurrentQueue,
^{

for
(int
i
=
0;
i
<
self.dataSource.count;
i++)
{

[NSThread
sleepForTimeInterval:0.05];

NSLog(@"%@",
self.dataSource[i]);

}

});

}

-
(IBAction)removeAllObjs:(id)sender

{

dispatch_barrier_async(concurrentQueue,
^{

[self.dataSource
removeAllObjects];

});

}

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: