对概念的理解是我们做任何事情的基础,因此我们从概念开始吧
程序执行顺序是按照串行执行的假设:
比如我们读诗词,默认从上到下
1.床 前 明 月 光,
2.疑 是 地 上 霜。
3.举 头 望 明 月,
4.低 头 思 故 乡。
而多线程以后,就很有可能变成
3.举 头 望 明 月,
1.床 前 明 月 光,
1.床 前 明 月 光,
4.低 头 思 故 乡。
一个列队中元素的数量必须小于或者等于存储元素的数组长度。如果这个列队只是在串行程序中使用,那么只要在所有共有方法的入口点和出口点保持这个不变性就足以保证程序的正确性。
一旦缺乏这种假设存在的前提条件,除非使用了某种特殊的方法,不然也许每行程序也许都要确保并发执行的时候不会出错,这将会让事情变得非常复杂。
可能一行代码被执行多次,导致你假设中的算法流程被破坏。
临界域是保证程序能串行执行的一种方式。
临界域(Critical Regiion)的概念:
信号量是规定了一定线程数可以执行的概念,这在实现对集合资源的保护时很有用。记住,信号量不属于某单一线程。
可以认为临界域是信号量的一个特例,规定了只有一个线程可以执行,也就是互斥。
信号量和临界域可以结合起来使用,以后会有例子说明。
常见的同义词诸如“加锁/释放”,“进入/退出","开始/结束"等,同义词虽多,可是表示的都是一个相同的概念。只要所有执行临界域代码的线程都按照一直的方式来访问数据,那么就可以避免发生数据间竞争的问题。
用伪代码表示就是:
EnterCriticalRegion();
DoSomeThings();
LeaveCriticalRegion();
有些临界域可以支持共享模式,例如read/write锁,这种方式能够使得多个线程并发的读取共享数据。
当然临界域这一概念的实现有很多种算法,我们先不讨论这个。因为操作系统对临界域这一概念有不少的既有实现。
不过相应的需求如下:
1.保证互斥性。
2.在临界域中的操作要进得来也要出得去。不能有线程因为死锁或者活锁问题导致无限期的停留在临界域中。
3.提供某种程度的公平性。
4.最好实现是低开销的。因为底层系统会频繁地使用临界域,进进出出。
可重入函数:
重入表示在一次函数执行的过程中,没有执行完时,又再次进入同一函数。冲入现象可能由同一线程造成,比如说递归;也有可能由多线程并发调用函数造成。
一个函数可重入表示这个函数的结果不会因为重入而产生变化,是稳定的。
粒度问题:
a++是用来说明多线程容易造成问题的最常用的例子。但是实现这种低级别的同步还是相对简单的,很多cpu内置提供支持。
但是更高级别的同步,比如说一个对象方法,包含很多个步骤,那么要保证其线程安全,就没那么简单。
在程序中通常包含一组子系统以及各种复合的数据结构,并且这些数据结构可能被多个线程并发访问。有两种方式来组织临界域:
粗粒度:通过只使用一个锁来保护子系统以及复合数据结构中的各个部分。优点:易于管理使用。缺点:伸缩性不强。
细粒度:对每一部分分别加锁。优点:高伸缩高并发。缺点:锁太多,难以合理划分和组织。误用时会产生很多问题。
线程:比喻为执行函数的虚拟处理器。
线程的状态:
1.假设线程没有状态的情况下,采用最简单也是最直观的“忙等待”(自旋)方式。下面这段程序中谓词(predicate ,也就是判断条件)保护了DoSomething的执行。
while(!p)
DoSomething();
直到p为true,也就是获得进入临界区的资格后,DoSomething才执行。否则就一直原地打转,不停的检查P的值。
但是忙等待依然消耗CPU的运行周期,直到它的时间片用完或者系统抢占把cpu资源分配到其他线程。在自旋过程中会阻止了p为true时其他线程的运行,也就是说已经进入临界区准备执行DoSomething的线程也必须等待该自旋线程释放CPU资源。这个执行DoSomething的线程估计会郁闷,心想终于轮到我执行了,CPU运行周期却被自旋线程用来不停对P求值了。
因为把CPU周期浪费在了不停检查p这个共享内存的状态上,所以忙等待在大部分情况下都不是一种好的做法。这种大量使用CPU时钟周期和执行内存访问的操作,会导致频繁的总线通信以及能源消耗(特别是对于移动设备)
所以我们需要其他的手段来表示线程等待,最好干脆就是操作系统帮我们封装好了。这样我们只要在对线程说"hold 住",该线程就变成等待状态,不再占用CPU运行周期。
系统内置对线程状态的支持:
Windows操作系统通过各种内核对象来提供真正的等待功能。
当线程等待时,它将进入等待状态(与运行状态相对应),这将触发上下文切换操作以将这个线程立即从这个处理器上移走,并且确保Windows线程调度器不会将它作为下一个将要运行的线程。这避免了CPU计算能力以及能源的 浪费,并允许系统中其他线程的执行。假设有系统函数Wait,这个函数可以使得线程进入等待状态,上面的忙等待代码就变成
if(!p)
Wati();
DoSomeThing();
现在,临界域中的线程不仅要使得P变为true,而且必须要考虑其他线程也可能处于等待状态。用一个WakeUp方法唤醒等待中的一个或多个线程
p = true;
WakeUP();
线程安全方面:
数据的状态
在面向对象的编程系统中,一个典型的对象由保存状态的字段和操作状态的方法组成,状态被破坏意味着会产生不可预计的后果。
1.共享状态
当状态被共享时,多个线程对状态的并发访问将会在时间上发生重叠;当这些线程在访问共享状态发生重叠时,那么彼此之间的操作将会相互干扰。
.NET框架的类型安全在一定程度上保证了私有状态,因为如果程序中能够生成一个指向进程地址空间中任意位置的指针,那么整个地址空间中的数据都是共享状态的。
共享状态具有可传递性。
new 出来的对象只有创建该对象的线程可以访问,所以是线程安全的,不过一但被共享状态所引用(如静态变量),那么就不再是线程安全的。
2.私有状态
方法栈,私有变量,参数
5.readonly一定程度上保障了数据的不可变性。虽然可以多次赋值。