第一章 win32基础
1, 什么是进程?
从 Win32 的角度来看,进程含有内存和资源。被进程拥有的内存,理论上可以高达 2GB。资源则包括核心对象(如 file handles 和线程)、USER 资源(如对话框和字符串)、GDI 资源(如 Device Context 和 brushes)。
进程就像一本活页笔记夹,你可以在其中的活页上写东西,也可以擦掉内容或甚至整页撕掉,活页笔记夹只是持有那些东西而已。同理,进程本身并不能够执行,它只是提供一个安置内存和线程的地方。
2,内存的分类
1)code:Code 是程序的可执行部分,一定是只读(read only)性质。这是 CPU 唯一允许执行的内存。可执行 Windows NT 的两种芯片:Intel 芯片和 RISC 芯片都有这项限制。
2)data:Data 是你的程序中的所有变量(不包括函数中的局部变量),可以区分为全局变量和静态变量两种。当然线程也可以使用 malloc()或 new 动态配置内存。
3)Stack: 是你调用函数时所用的堆栈空间,其中有局部变量。每个线程产生时配有一个堆栈。如果不需要,操作系统会将它动态扩充。
3,线程
线程在“任何时刻下的状态”被定义在进程的某块内存中,以及CPU 寄存器中
4,为什么不使用多个进程?
最重要的答案便是:线程价廉。线程启动比较快,退出比较快,对系统资源的冲击也比较小。而且,线程彼此分享了大部分核心对象(如 file handles)的拥有权。
例子:
1)handle的分享
2)服务器处理用户的请求
3)工作组数据服务器:
可能会有一些用户对它产生连接(connection)并持续数小时之久。这种情况下产生个别的进程以服务个别的用户,其额外负担(overhead)无足道哉。不幸的是,每个人还是使用同一个数据库,所以进程之间还是必须商议,看谁可以读一笔数据或写一笔数据,或改变一个索引。这和我们面对线程时遭遇的问题一样,只不过现在更复杂,因为要共享的是跨越进程的数据结构。如果使用线程,则程序的所有内存全由其线程共享。
5,多线程是怎么实现的?
1)同进程的线程:上下文切换
在一个抢先式多任务系统中,操作系统小心地确保每一个线程都有机会执行。它依赖硬件的协助以及许多的簿记工作。当硬件计时器认为某个线程已经执行够久了,就会发出一个中断(interrupt),于是 CPU 取得目前这个线程的当前状态,也就是把所有寄存器内容拷贝到堆栈之中,再把它从堆栈拷贝到一个 CONTEXT 结构(这样便储存了线程的状态)中,以备以后再用。
要切换不同的线程,操作系统应先切换该线程所隶属之进程的内存,然后恢复该线程放在 CONTEXT 结构中的寄存器值。这整个过程便称为context switch。
2)跨进程的线程如何使用Context Switch
如果第二个线程属于不同进程所有,则这两个进程没有办法共享任何内存。这种隔离策略可以保护进程免受其他人突如其来的伤害。最诡异的是两个进程以为它们在相同的地址上运行。两个进程的 0x1f000 指针事实上指向不同的实际内存。因此,不同进程间的线程如果要通讯,唯有依赖特别的设计,使之拥有共享内存(shared memory)。如果两个线程属于同一进程,它们将共享所有的内存。
6,Context Switch 的效率
一部双 CPU 系统可以同时执行同一进程(或不同进程)的两个线程。在一部 SMP 系统中,多个线程真的有可能被同时调用。图 1-3 显示了一个 SMP系统将 #3 线程排给 #1 CPU 去执行。 (图片来自《win32多线程程序设计》)
7, Race Conditions(竞争条件)
Context switching 是抢先式多任务的心脏。在一个合作型多任务系统 中,操作系统必须得到程序的允许才能够改变线程。但是在抢先式多任务系统中,控制权被强制移转,也因此两个线程之间的执行次序变得不可预 期。这不可预期性便造成了所谓的 race conditions(竞争条件)。
例子:给链表加一个节点
struct Node { struct Node *next; int data; }; struct List { struct Node *head; }; void AddHead(struct List *list, struct Node *node) { node->next = list->head; list->head = node; }
开始状态:
线程1:
目的:添加一个节点B
状态:调用addHead函数,第一段代码执行完后
线程2:
目的:添加一个节点C
状态:在线程1执行完addhead第一行代码后,进行上下文切换,执行addhead的第一行代码
继续执行线程1:
目的:添加一个节点B
状态:继续执行addhead剩下的代码,执行完毕
节点 C 于是被切断,或许就此成了一个内存泄漏(memory leak)
注意:或许这种情况的发生只有百万分之一的机率,或许不值得大惊小怪。然而 别忘了你的 CPU 是以每秒两千万到五千万个指令的速度在运转,对于一个常常 被使用的链表而言,race condition 的发生可能是一天多起。 (在实际的工程里面,这种情况很容易遇见,因此一个要注意多线程有竞争条件的情况)
写一个实际在公司遇见的情况:
template <typename TSample> struct RefSampleIH: TSample { ATL::CCriticalSection locker; volatile long iRefCount; RefSampleIH():iRefCount(1){} virtual ~RefSampleIH(){} virtual int AddRef() { tp_atom_add(&iRefCount, 1); return iRefCount; } virtual int Release() { tp_atom_add(&iRefCount, -1); const int refcount = iRefCount; if (refcount <= 0) delete this; return refcount; } };
注意Release()这个函数, 这段代码将会在两个线程中执行,能看出竞争条件在哪儿吗?
问题:第一行是一个原子操作,但是第二行第三行就不是了,原子的不彻底,那就会出现以下问题。
当两个线程公用一个RefSampleIH实例的时候(这种情况很常见)
1> 线程A: 进入Release()实例的refcount 减一 ,refcount = 1
2> 切换到线程B: 进入Release()实例的refcount 减一, refcount = 0
3> 切换到线程A: 判断refcount == 0,释放,正在delete
4> 切换到线程B: 判断refcount == -1,释放,delete完毕
5> 切换到线程A: delete抛出异常修改之后:
template <typename TSample> struct RefSampleIH: TSample { ATL::CCriticalSection locker; //添加了提个自动锁 volatile long iRefCount; RefSampleIH():iRefCount(1){} virtual ~RefSampleIH(){} virtual int AddRef() { CCritSecLock lock(locker); //添加了一个自动锁,初始化的时候会自动加锁,析构的时候会自 //动解锁,并且自动释放locker,避免内存泄漏(局部变量函 //数执行完之后,会自动调用其析构函数) tp_atom_add(&iRefCount, 1); return iRefCount; } virtual int Release() { CCritSecLock lock(locker); //和初始化函数的一样自动锁 tp_atom_add(&iRefCount, -1); const int refcount = iRefCount; if (refcount <= 0) delete this; return refcount; } };
认真看的小朋友可能会发现一个bug,这个自动锁虽然解决了竞争条件的问题,但会出现另一个问题,在delete this的时候,会将成员变量locker给delete掉,但是CCritSecLock 的析构函数会调用unlock(locker),并且delete掉它。
8,Atomic Operations(原子操作)
检查标记和设立标记的动作是如此地平常,所以被实现于许多 CPU 硬件中,名为 Test 和 Set 指令。实际上只有操作系统会使用 Test 和 Set 指令。操作系统提供比较高阶的机制,让你的程序用以取代 Test 和 Set。
上面代码的: tp_atom_add(&iRefCount, -1) 也是一个原子操作