MFC 线程编程

原文地址

线程简介

我们知道一般情况程序中的代码都是按顺序从头开始一行一行的执行以最后.中间不能出现同时执行的情况.比如一段代码调用两个函数

FunOne();

FunTwo();

只要当函数FunOne中的代码执行完才返回来执行FunTwo.假如逻辑上是有先后顺序那还真只能这样按顺序执行下来.不过有假如FunOne与FunTwo没有逻辑先后顺序,是相互独立的.比如两个函数分别处理两不同的文件one.text与two.txt.

这种情形就可以用到线程,弄两个线程去执行这两函数.这样两函数同时执行,提高了效率(如果单核的CPU可能没有真正的并行效果不明显,那多核CPU执行多线程那是能够真正达到并行执行,效果很明显的).

实际上可以这样简单的理解线程,它是CPU的调度单位.而一个线程是对应一个函数.所以别把一个线程想得太复杂,就只是执行个函数而已.只不过执行的时候是并行执行罢了.如果只是简单的几个线程不涉及使用共同的资源,没其他啥关联.就完全跟简单的执行一个函数类似.只是如果多个线程间关系复杂就会涉及到啥同步问题,那样就有很多复杂的细节性问题.

线程与函数

线程函数必须是全局函数,或者是类的静态成员函数,因为非静态成员函数有this指针,而在进程中无法访问此指针。

但是静态成员函数只能访问静态成员,解决此问题途径:

  1. 就是在调用静态成员函数时将this指针作为参数传入,通过该指针访问非静态成员。

  2. 不将线程函数定义为类的静态成员函数,而是定义为类的友元函数,这样函数线程也可以有类成员函数相同的权限。

最简单示例
线程分工作线程与界面线程.这里就以工作线程为例

1.先来看个MFC中的创建线程的简单例子.

UINT ThreadFun(LPVOID pParam){ //线程要调用的函数

MessageBox(NULL,_T(“i am called by a thread.”), _T(“thread func”),MB_OK);

}

::AfxBeginThread(ThreadFun, NULL); //这就是创建一个线程并执行了,调用上面的函数弹出一个对话框.

2.示例分析
上面的线程是简单的不能再简单了吧.下面从两个来分析下.

a.首先是被调用的函数有啥讲究不? 当然有,被线程用到的函数格式必须是统一的,返回类型必须是UINT,函数只能有一个参数LPVOID.其中UINT就是个无符号的整形,LPVOID是void*,所以这个参数表示可以传任何类型的指针过来的.

b.函数AfxBeginThread的分析.

这个函数还有返回值CWinThread*的,如果你只是简单的创建一个线程并执行,就不用管了.但如果想要对创建的线程做其他操作就必须这样写.

CWinThread* pThread = ::AfxBeginThread(ThreadFun, NULL); //接下来做啥就直接调用pThead就行.

另外函数AfxBeginThread的参数有很多个,但很多都有默认值.下面是完整的参数

CWinThread* AfxBeginThread(

     AFX_THREADPROC pfnThreadProc, //一个函数指针
     LPVOID pParam, //void*类型的指针,可以传任何种类指针过来.
     int nPriority = THREAD_PRIORITY_NORMAL, //线程优先级
     UNT nStackSize = 0, //分配堆栈大小
     DWORD dwCreateFlags = 0, //表示线程创建后是立即执行还是等会执行
     LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL //线程安全属性指针
     );//用于创建工作者线程

上面的参数我们用的最多的是3个,其他一盘都默认值.

AFX_THREADPROC pfnThreadProc //函数指针肯定是必须要指定的,不然线程执行哪个函数去啊

LPVOID pParam //这是传给上面指定函数的参数.如果被调用的函数需要啥参数就只能在这里指定了.

DWORD dwCreateFlags //默认值为0表示创建线程后立即执行.如果是CREATE_SUSPEND则表示创建好后先挂起.必须通过ResumeThread来执行.

稍复杂点的例子

扩充下上面的例子,给函数传入参数,并且休眠和挂起线程.

UINT ThreadFun(LPVOID pParam){ //线程要调用的函数

int* pNum = (int*)pParam; //假如会传入一个整形指针参数

MessageBox(NULL,_T(“i am called by a thread.”), _T(“thread func”),MB_OK);

}

CWinThread* pThread; //定义一个线程指针

void CreateThread(){//创建一个线程并挂起

int* pNum = new int(88); //传入的参数

pThread = ::AfxBeginThread(ThreadFun, pNum,THREAD_PRIORITY_NORMAL,0,CREATE_SUSPENDED);

}

void StartThread(){ //运行线程

pThread->ResumeThread();

}

线程休眠与挂起的区别
上面示例中先创建一个线程,并让它挂起.(suspend),被挂起的线程只有通过ResumeThread才能开始执行.

而休眠则一般是这样使用.

UINT ThreadFun(LPVOID pParam){ //线程要调用的函数

::Sleep(1000); //表示函数执行到这里先休息1000微秒,也就是1秒.然后再接着执行下面的语句.

MessageBox(NULL,_T(“i am called by a thread.”), _T(“thread func”),MB_OK);

}

所以休眠一般是会在线程调用的那个函数中指定.休眠在指定的时间后会自动再次执行,相当于暂停一断时间然后又自动活过来了,不用像挂起还必须得显式去启动才行.

内核对象 进程
内核对象
这里的对象不不是指一个类的实例化,不过实际上也可以类似的等同.因为内核对象是内核分配的一个内存块,这种内存块就是一个结构体(struct).应用程序若需要访问内核对象需要通过一些API函数,不能直接访问(基于安全的考虑).内核对象的拥有者是内核,所以何时释放对象的内存是由内核决定的.我们使用内核对象时一般是通过一个句柄支间接的使用,于是每有一个句柄与对象关联则对象的引用计数加1,当系统发现内核对象的引用计数为0时则释放内核对象内存.(看起来是不是有点像智能指针的用法了啊?)

进程与内核对象
每个进程在初始化时被分配一个句柄表,表中保存进程能访问的所有内核对象的句柄(进程是不能直接访问内核对象,只能先在找到句柄表中的句柄,然后再使用内核对象.)

当然进程还能通过CreateObject来创建一些内核对象,然后不使用时使用CloseHandle来关闭内核对象.

如果某个进程创建内核对象时指定SECURITY_ATTRIBUTES中的bInheritHandle为TRUE,创建子进程时(CreateProcess也设bInheritHandle为TRUE)则子进程也能拥有那个内核对象的访问权限(此时子进程的句柄表会复制该内核对象句柄过来,内核对象引用计数加1).当然如果父进程在创建了子进程之后再生成一些内核对象,则子进程是不会继承那访问权限的.

除了通过继承可以获得某个内核对象的访问权限外还可以通过同名共享(不过需要内核对象支持这种共享方式,不是所有种类的内核对象支持).当然通过CreateObject来创建好一个名为test1的内核对象后,此时如果有另外的进程再创建一个名为test1的内核对象那不会真的创建,而只是返回之前已创建好的test的句柄(看起来有点像是单例模式的应用啊)

另外还可以通过复制内核对象的句柄,通过DuplicateHandle,当然了前提是进程要有对那个句柄的访问权限先.(在句柄表中有)

线程与进程
进程只是个容器,不会执行任何操作.它里面有很多线程(至少必须有一个主线程).进程内的所有线程共享进程的内核对象.

当一个进程中止时所以线程自然中止.

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页