我是靠谱客的博主 清新哈密瓜,最近开发中收集的这篇文章主要介绍多线程程序设计(一),觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

当我们提到线程这个概念的时候,我们就不得不提到它的容器 --- 进程。那什么是进程呢?进程是一个正在运行的程序,它拥有自己的内存地址空间以及其向系统所申请的其它资源。内存地址空间中包含有文本区域( text region )、数据区域( data region )以及栈区域( stack region );所申请的资源包括该进程打开的文件以及套接字等系统资源。当进程被创建并在获得其运行所需要的所有资源过后,进程并不能自己开始执行。现在的进程就像一个植物人一样,由于大脑无法正常的工作,它除了躺在床上之外,是没有办法做其它任何工作的。与人类似,进程在没有获得系统中的 CPU 资源的时候,也只有乖乖的睡在那里。在系统中, CPU 资源是由操作系统中的调度程序进行分配的。调度程序一般采用时间片的方式来分配 CPU 资源,即让一个进程获得一定数量的时间(例如 10ms )片来执行它的代码,在该进程执行完该时间片后,调度程序将 CPU 资源交给另一个进程。这样在一个多道程序( multi programming )的系统中,用户就感觉系统中的程序好像在并行执行。所以在石器时代的操作系统中,进程是 CPU 调度的基本单位。

随着时间的发展,人们在使用以上的模型编写程序的时候遇到了以下的问题: 1. 一个进程大量的创建执行时间较短的进程,降低了系统的性能。例如早期的服务器程序对于每一个请求,都需要创建一个进程来对其进行处理。 2. 由于不同的进程在不同的地址空间中,进程间除了使用进程间通信( interprocess communication )的方式以外,没有其它办法对数据进行共享。为了解决以上两个问题,人们创造了线程模型。在线程模型中,进程变成了获取系统资源的最小单位,而线程变成了获取 CPU 资源的最小单位。所以当进程被创建的时候,进程必须自己创建一个线程(主线程),已让操作系统的调度程序可以将 CPU 时间片分配给该线程。由于线程是通过进程而不是通过系统获得资源,所以在一个进程中创建一个新的线程的时候,只需要向进程申请线程栈就可以了,而不需要再向系统申请其它资源。这样的设计大大的降低了系统的负担,所以线程也叫做轻量级进程。同时,由于多个线程可以共享同一个进程的地址空间以及其向系统申请的所有资源,所以在同一个进程中的多个线程之间的通信是非常容易的。

我们一般使用线程解决两类程序设计问题,第一类是通过使同一个进程中的线程并行运行,来提高进程的吞吐率和系统 CPU 的使用率。典型的情况是一个服务器程序,理论上它可以对每一个请求创建一个线程来对其进行处理,以提高系统的吞吐率。第二类是可以让进程中的线程异步( asynchronize )运行,以使程序不至于进入假死状态。试想用户点击了某个 GUI 程序中的一个按钮,点击该按钮会让程序向服务器上传一个文件,假设该文件很大,需要 30 分钟才能将文件上传到服务器上。如果我们在设计的时候在 GUI 的线程中上传该文件的话,我们就会发现在上传该文件的同时,我们的 GUI 程序就无法接受任何其它的操作了。这时,程序就进入了假死的状态,我们可怜的用户就只有坐在电脑前傻傻的看着电脑屏幕,而不知道发生了什么事。

下面就让我们来看一下,在 Windows 平台下面,如何来创建以及销毁一个线程。在 Windows 中,我们使用 API 函数 CreateThread 来创建一个线程,该函数的声明如下:

      HANDLE WINAPI CreateThread(

          __in_opt   LPSECURITY_ATTRIBUTES lpThreadAttributes,

          __in         SIZE_T dwStackSize,

          __in         LPTHREAD_START_ROUTINE lpStartAddress,

          __in_opt   LPVOID lpParameter,

          __in         DWORD dwCreationFlags,

          __out_opt LPDWORD lpThreadId

       );

参数 lpThreadAttributes 表示安全描述符,该描述符指示该函数创建的线程内核对象句柄能否被创建该线程的进程的子进程继承。填入NULL 表示不可被继承。

参数 dwStackSize 表示被创建线程的栈大小,单位为字节。在该参数填入 0 的时候,使用该进程所设定的默认值做为栈的大小。在 X86 架构的 CPU 上,该值的大小为 1MB ,可以通过 VS IDE 在工程的 Property->linker->system 中的 Stack Reserver Size 中填写该值。

参数 lpStartAddress 表示线程的入口点函数的指针,该函数就像 C/C++ 语言的 main 函数一样,当线程被创建的时候从该函数开始执行。该函数的声明如下:

DWORD WINAPI ThreadProc( __in  LPVOID lpParameter);

参数 lpParameter 表示用户在 CreateThread 函数中传递给线程的上下文。

该函数的返回值表示线程的退出代码,其它线程可以通过 GetExitCodeThread 函数来获取该线程的退出码。

参数 lpParameter 表示传递给线程的上下文参数。

参数 dwCreationFlags 表示创建线程后,线程所处的状态。如果使用值 CREATE_ SUSPENDED 填写该参数,则线程在创建完成后处于挂起状态;而如果使用值0 填写该参数,则线程处于运行状态。

参数 lpThreadId 表示该线程的在系统中全局唯一的 ID ,我们可以传入一个指针来获取它。如果传入 NULL 的话,则表示我们不需要获取该ID

该函数的返回值为线程内核对象的句柄。如果函数调用成功,则该值是一个不为NULL 的值,而如果失败,该值则为NULL

在知道了如何创建一个线程后,让我们再来看一下如何在Windows 中销毁我们创建的线程。在Windows 中,有四种方法可以销毁我们所创建的线程:

1. 让线程执行完用户的代码后,自己退出。例如在C/C++ 语言中使用return 退出在调用CreateThread 时设置的线程函数。
      2.
线程自己调用ExitThread 退出。

3. 其它的线程使用TerminateThread 终止正在执行中的线程。

4. 进程退出。

下面让我们具体的来看一下这四种销毁线程方法的区别:我们应该总是使用方式1 的方式来让线程销毁。因为只有该方式,能保证在线程在销毁时能够将申请的栈归还给系统,并保证线程中所构造的C++ 对象能够被正确的析构。方式2 能够保证线程销毁时可以将申请的栈归还给系统,但是使用方式2 的方式销毁线程的时候,我们在线程中所构造的C++ 对象并不能够被正确的析构,因为它们的析构函数并不会被自动的调用!!!假设我们在构造函数中申请了资源,并准备在析构函数中销毁我们在构造函数中申请的资源,这样的结果完全是灾难性的,因为构造函数没有被调用,所以我们所申请的资源,自然也不能正确的被销毁。在我们自己设计的程序中,我们绝不应该使用方式3 来销毁我们的线程,因为这样我们不仅不能够销毁我们在线程中所构造的对象,甚至我们还不能将线程所申请的栈归还给系统。只有在一种情况下,我们或许必须使用这种方式来终止线程。那就是当我们使用第三方提供给我们程序库,并且该库在从内存中被卸载出去时候,存在有线程仍然在我们程序中的情况下,我们或许不得不用 TerminateThread来杀死那些本不应该存在的线程囧。 至于方式4 ,它也会导致方式3 所说的问题,但是当进程被终止后,操作系统会回收分配给该进程的所有资源。

在线程被销毁过后,我们还需要正确关闭掉在前面使用CreateThread创建的线程内核对象。在Windows中,我们使用CloseHandle关闭系统中的任何内核对象,例如进程、线程以及信号量和互斥量等。但为了正确关闭线程内核对象,我们需要使用WaitForSingleObject和CloseHandle配合才行。我们使用 WaitForSingleObject等待线程执行完毕,在线程执行完毕,并从线程入口点函数退出过后,我们才应该使用CloseHandle函数来关闭线程内核对象。下面让我们来看一下WaitForSingleObject函数。

    DWORD WINAPI WaitForSingleObject(

        __in HANDLE hHandle,

        __in DWORD dwMilliseconds

    );

    该函数的作用是等待内核对象hHandle处于有信号(Signal)状态,当内核对象无信号的时候,

调用该函数的线程进入阻塞状态,直到该内核状态有状态为止,或者设置的等待时间

dwMilliseconds 超时 ,调用线程才重新处于准备状态,等待调度程序调度该线程执行。

参数 hHandle表示内核对象的句柄,对于线程内核对象来说,当线程执行完毕后,该内核对象就处于有

信号状态

        参数 dwMilliseconds表示等待超时时间,单位为毫秒。当超过超时时间之后,即使内核对象仍然处于 无信号状态,调用线程仍从阻塞状态进入准备状态。我们可以使用 值INFINITE表示无限等待 。

返回值 WAIT_OBJECT_0表示内核对象处于有信号状态;返回值 WAIT_TIMEOUT表示超时;返回值

WAIT_ABANDONED 表示该内核对象被其它线程或者进程关闭,此时调用WaitForSingleObject函数的

线程进入准备状态;返回值 WAIT_FAILED表示有错误发生。

       下面就让我们来看一个具体的示例,来看如何创建,并且正确的销毁一个线程以及其内核对象。

       如前面所讲,在Windows 平台下面,我们应该使用API 函数CreateThread 来创建一个线程。但是当我们使用C/C++ 来编写Windows 程序的时候,情况会发生一些变化,我们不应该在C/C++ 程序中使用CreateThread 函数来创建线程,而是应该使用CRT 函数_beginthreadex 来创建线程。其原因是C 运行库出现的时间比线程在操作系统中出现的时间要早了很多,而C 运行库中有很多全局变量(errno) 和需要使用函数内部静态变量的库函数(strerror 以及strtok) 。这些变量和函数在多线程环境中都是不安全。考虑在一个线程中调用了一个CRT 函数,并且该函数由于某些原因而产生了错误。因为产生了错误,该函数会设置全局变量errno ,以向用户说明产生了错误。与此同时,另一个线程中也调用了一个CRT 函数,并且该函数被成功的调用,此时,因为没有任何错误发生,该函数会将errno 设置为0 。为了解决以上的情况,微软在CRT 中加入了函数_beginthreadex ,当使用这个函数创建线程的时候,该函数会使用线程局部存储(TLS )来创建一个结构体_ptiddata ,并用该结构体来包裹CRT 所需要的全部全局变量以及静态变量,以使CRT 函数能够在多线程程序中正确的使用。

_beginthreadex 函数的声明如下:

uintptr_t _beginthreadex(

    void *security,

    unsigned stack_size,

    unsigned ( __stdcall *start_address )( void * ),

    void *arglist,

    unsigned initflag,

    unsigned *thrdaddr

 );

从函数的参数上来说,_beginthreadex 函数和CreateThread 函数完全一样,只是参数的类型由Windows 类型变成了C/C++ 类型。

下面让我们来看一个使用_beginthreadex 创建线程的示例,实际上该示例和使用CreateThread 创建线程的示例是一样的,只是使用_beginthreadex 函数代替CreateThread 函数创建了线程。

      

      好了,关于如何在Windows下创建线程和销毁线程的内容我们就讲在这里,下次我会为大家讲解如何进行线程之间的同步。

最后

以上就是清新哈密瓜为你收集整理的多线程程序设计(一)的全部内容,希望文章能够帮你解决多线程程序设计(一)所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(59)

评论列表共有 0 条评论

立即
投稿
返回
顶部