Win32多线程程序设计学习(第一章)

第一章 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) 也是一个原子操作

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值