一、windows与linux线程
Windows对进程和线程的实现如同教科书一般标准,Windows内核有明确的线程和进程的概念。在Windows API中,可以使用明确的API:CreateProcess和CreateThread来创建进程和线程,并且有一系列的API来操纵它们。但对于Linux来说,线程并不是一个通用的概念。
Linux对多线程的支持颇为贫乏,事实上,在Linux内核中并不存在真正意义上的线程概念。Linux将所有的执行实体(无论是线程还是进程)都称为任务(Task),每一个任务概念上都类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。
二、多线程安全
多线程并发安全,可以使用信号量、锁与临界区等方式来在一般情况下保证多线程并发操作同一资源的问题,但是这里想要阐述的是通过这些方式在某些情况下依然是无法保证线程安全的,可以称为过度优化。
线程安全是一个非常烫手的山芋,因为即使合理运用了锁,也不一定能保证线程安全,这是因为落后的编译器无法满足日益增长的并发需求,很多看似无错的代码在优化和并发面前产生了麻烦,可以看下面的代码,
x = 0;
Thread1 Thread2
lock(); lock();
x++; x++;
unlock(); unlock();
上面的代码看着是没有任何问题的,对x的操作加锁,可以保护多线程并发对x的操作,x的结果会是2,但实际是这样吗??
呵呵。。。显然不是的,存在一种情况,编译器为了提高x的访问速度,将x放到某个寄存器里,我们都知道每个线程都有不同的寄存器,那么,下面的执行流程就会导致异常,
[Thread1]读取x的值到某个寄存器R[1](R[1]=0)。
[Thread1]R[1]++(由于之后可能还要访问x,因此Thread1暂时不将R[1]写回x)。
[Thread2]读取x的值到某个寄存器R[2](R[2]=0)。
[Thread2]R[2]++(R[2]=1)。
[Thread2]将R[2]写回至x(x=1)。
[Thread1](很久以后)将R[1]写回至x(x=1)。
可见,在这种情况下,即使正常加锁,也无法保证线程安全的。
还有一个例子,请看,
x = y = 0;
Thread1 Thread2
x = 1; y = 1;
r1 = y; r2 = x;
这样的代码能保证 r1=r2=1吗??猛的一看,没问题,必然肯定100%至少r1和r2有一个为1,可实际情况呢,呵呵,来分析一下喽,
事实上,r1=r2=0确实可能存在,CPU为了提高效率,可能会将上下两条互不相干的指令进行动态调度,导致Thread1中x=1,r1=y顺序调换,那么thread2同样也会存在这种情况,最终的结果可能r1和r2都为0,
那么可以使用volatile关键字试图阻止过度优化,volatile基本可以做到两件事情:
(1)阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回;
(2)阻止编译器调整操作volatile变量的指令顺序
还有一个和序有关的问题,看下面的代码,
volatile T* pInst = 0;
T* GetInstance()
{
if (pInst == NULL)
{
lock();
if (pInst == NULL)
pInst = new T;
unlock();
}
return pInst;
}
看着这段代码觉得应该逻辑什么的都是合理的,通过加锁保证pInst被创建出来,并返回给调用方永远指向一个对象,事实上呢,真的如此吗?我们结合new执行过程分析上述逻辑,
(1)申请内存;
(2)调用构造函数;
(3)将内存的地址赋值给pInst
这是整个过程,在这三步中,(2)(3)是可以调换顺序的,也就是说,完全有可能出现这样的情况:pInst的值已经不是NULL,但对象仍然没有构造完毕。这时候如果出现另外一个对GetInstance的并发调用,此时第一个if内的表达式pInst==NULL为false,所以这个调用会直接返回尚未构造完全的对象的地址(pInst)以提供给用户使用。那么程序这个时候会不会崩溃就取决于这个类的设计如何了。 有指令(barrier())可以阻止这种调换,但是还是强烈建议不要这么设计代码逻辑,为了并发安全,哈哈。
总结了这么多,只有一个结论,并发安全很重要一定要谨慎对待!!!对于上述存在情况,在实际开发过程中尽量不要存在这种使用方式。