理解线程安全

  1. 线程安全问题的来源
  2. 如何保证线程安全
  3. 原子性
  4. 不可变性

线程安全问题的来源

线程安全是编程中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

可以看到这一切的诱因就是因为共享变量。如果我们的线程执行过程中,没有相互影响,就不会出现问题。

当我们的线程访问共享变量时,我们无法预测操作系统是否将为我们的线程选择一个正确的顺序。这就尴尬了!

对多个线程对资源的访问,我们称之为 竞争。

由于两个或者多个进程竞争使用不能被同时访问的资源,使得这些进程有可能因为时间上推进的先后原因而出现问题,这叫做竞争条件(Race Condition)。

竞争条件分为两类

  1. Mutex 不能被多个进程同时使用的资源
  2. Synchronization 两个或多个进程彼此指针存在内在的制约关系

消费者生产者问题

因为插入和取出项目都涉及更新共享变量,所以我们必须保证对缓冲区的访问是互斥的。但是只保证互斥访问是不够的,我们还需要调度对缓冲区的访问。如果缓冲区是满的,那么生产者必须等待直到有一个槽位变为可用。与之相似,如果缓冲区是空的,那么消费者必须等待直到有一个项目变为可用。

读者-写者问题

读者写者问题是互斥问题的一个概括。一组并发的线程要访问一个共享对象。写者必须拥有对对象独占的访问。

如何保证线程安全

在我们需要访问共享变量的情况下,我们需要保证线程执行顺序的正确性。或者我们尽量避免使用共享变量。

进度图
如果我们用 纵横坐标分别表示两个进程执行的指令顺序,那么操作共享变量的指令会构成一个二维的不安全区,当两条线程的执行轨迹会同时访问不安全区时,我们就认为执行是不安全的。(局限性:无法描述多处理器并发执行)

我们可以选择安全的轨迹线,或者使用信号量实现互斥。

思考一下,锁做了什么?

lock是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的满足.
对于竞争条件中的 Mutex 我们可以使用互斥锁处理。Synchronization 可以使用条件锁。
使用lock是可以保证线程安全的,但不能保证线程的执行顺序。

原子性

原子性(Atomic),一个事务包含多个操作,这些操作要么全部执行,要么全都不执行。

OC 中一个很经典的面试题是, property 设置 atomic 能保证线程安全吗?
很多人都回答可以,这是压根没理解线程安全的体现。
原子性不能保证线程安全
原子性可以保证写操作一小块代码段是互斥的,但是并不能保证线程安全。

设置atomic之后,只是保证了 属性的赋值操作是互斥的,可惜只是该属性..
不能保证我们整个代码的线程安全。

考虑一段代码.

1
2
3
4
5
6
self.a = 0;//1
if (self.a == 0) {
print("safe");
} else {
print("unsafe");
}

当 self.a = 0; 执行完毕后

处理机调度,切换到另一个线程执行

1
self.b = 0;

再次切换为原先的线程
此时,结果是
unsafe.
我们原先的值被其他线程篡改了。并不能保证线程安全。

会想一下 那经典的 进度图,原子性无法阻止多个线程访问不安全区。

加锁之后,就保证 代码块 不会被多个进程访问,保证了线程安全。

1
2
3
4
5
6
7
8
lock();
self.a = 0;//1
if (self.a == 0) {
print("safe");
} else {
print("unsafe");
}
unlock();

不可变性

不可变性,这个跟线程安全关系大吗?
显然,我们对共享变量的访问,会导致线程安全问题是因为我们对其进行了写操作。不可变可以避免代码编写中因为疏忽导致的问题。真正处理线程安全的时候,你遇到的,会是可变的共享变量!
所以,这个对线程安全问题,没有用处。

理解会随着经验和知识的积累 改变
Keep Update

script>