标签(空格分隔): Windows multithread programming 多线程 并发 编程


背景知识

在开始学习多线程编程之前,先来学习下进程线程

进程

进程是指具有一定独立功能的程序在某个数据集合上的一次运行活动,是系统进行资源分配和调度运行的一个基本单位。简单地说,晋城市程序在计算机上的一次执行活动,当你启动了一个程序,你就启动了一个进程,退出一个程序,也就结束了一个进程。

打开windows任务管理器-->详细信息,可以看到Windows系统下有很多进程在运行。

注意:

程序并不等于进程。程序只是一组指令的有序集合,它本身没有任何运行的含义,只是一个静态实体。进程是程序在某个数据集上的执行,是一个动态实体。

程序只有被装入内存后才能运行,程序一旦进入到内存就成为进城了,因此,进程的创建过程也就是程序从外存储器(硬盘或者网卡)被加载到内存的过程。进程因创建而产生,因调度而运行,因等待资源或事件而处于等待状态,因完成任务而销毁,它反映了一个程序在一定的数据集上运行的全部动态过程。

进程在其存在过程中,由于多个进程的并发执行,受到CPU、外部设备等资源的制约,使得它们的状态不断发生变化。进程的基本状态有三种:就绪状态、运行状态、阻塞状态。三种状态可以相互转化。

  • 就绪状态:进程获得除了CPU之外的一切运行所需的资源,等待获得CPU,一旦获得CPU即可立即运行。(如数据已经准备好,或者接收到有新数据,需要CPU来处理)
  • 运行状态:进程获得了包括CPU在内的一切资源,正在CPU上运行。(正在运行指令,处理数据)
  • 阻塞状态:正在CPU上运行的进程,由于某种原因,不再具备运行的条件

    而暂时停止运行。(比如需要等待I/O完成、当前进程的CPU时间片耗尽、等待其他进程发来消息、等待用户完成输入等)。

进程调度:当就绪进程的数目多于CPU的数目时,需要按照一定的算法动态地将CPU分配给就绪进程队列中的某一个进程,并使之运行,这就是所谓的进程调度。

当分配给某个进程的运行时间(时间片)用完了时,进程就会有运行状态回到就绪状态。运行中的进程如果需要执行I/O操作,比如从键盘输入数据,就会进入到阻塞状态等待I/O操作完成,I/O操作完成后,就会转入就绪状态等待下一次调度。

进程调度的关键是进程调度算法。进程调度算法解决两个问题:一是当CPU空闲时,选择哪个就绪进程运行;二是进程占有CPU后,它能运行多长时间。后一个问题也称为调度方式。调度方式有两种:不可抢占(或不可剥夺)方式和可抢占(或可剥夺)方式。

不可抢占方式是指一旦某个就绪进程获得CPU后,只要不是进程主动放弃,将会一直运行下去,直到运行结束,期间CPU不可剥夺。可抢占方式是指:当一个进程正在运行时,系统可根据某种原则,剥夺其CPU的使用并分配给其他进程,剥夺原则包括优先权、短进程优先、时间片等。

进程调度算法种类较多,但概括起来最基本的算法主要有静态优先级,动态优先级和时间片轮转等。实际系统中采用的调度算法一般是多种算法结合和改进,Windows系统采用的是“抢占式多任务”就是一种时间片和优先级相结合的调度方式。系统为每个进程分配一定的CPU时间,当程序的运行超过规定时间后,系统就会中断该进程并把CPU的控制权转交给优先级较高的进程,如果无更高级别的进程,则转交给其他相同优先级的进程。


线程

线程是为了在进程内部实现并发性而引入的概念。

进程内部的并发行是指:在同一个进程内部可以同时进行多项工作,而线程就是完成其中某一项工作的单一指令序列。一般情况下,同一进程中的多个线程各自完成不同的工作,比如一个线程负责通过网络收发数据,另一个线程完成所需的计算工作,第三个线程来执行文件输入输出,当其中一个由于某种原因阻塞后,比如通过网络收发数据的线程等待对方发送数据,另外的线程仍然能执行而不会被阻塞。

一个进程内的所有线程使用同一个地址空间,即各线程是在同一地址空间运行的,各线程自己并不独立拥有系统资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。解决同意进程的各线程之间如何共享内存、如何通信等问题是多线程编程中的难点。由于线程之间的相互制约,以及程序功能的需求,线程在运行中也会呈现出间断性,因此一个线程在其生命期内有两种存在状态--运行状态和阻塞(挂起)状态。有很多原因可导致线程在这两种状态之间进行切换。线程的状态相互转换常见的如Sleep()函数的调用, Suspend(),等待锁可用,等待I/O操作,Resume(),I/O操作完成,

线程仅仅简单地借用了进程切换的概念,它把进程间的切换变成了同一个进程内的几个函数间的切换。同一个进程中函数间的切换相对于进程切换来说所需的开销要小得多,它只需要保存少数几个寄存器、一个堆栈指针以及程序计数器等少量内容。在进程内创建、终止线程比操作系统创建、终止进程也要快。

有多个线程的程序称为多线程程序。Windows系统支持多线程程序,允许程序中存在多个线程。事实上,任何一个Windows中的应用程序都至少有一个线程,即主线程,其他线程都是主线程的子孙线程。


进程与线程的区别

进程与线程的主要区别在于,多进程中每个进程都有自己的地址空间(address space),而多线程则共享同一进程的地址空间;进程是除CPU以外的资源分配的基本单位,线程主要是执行和调度(CPU运行时间分配)的基本单位。

线程是进程内部的一个执行单元。每一个进程至少有一个主执行线程,它无须用户去主动创建,是系统自动创建的。用户根据需要在应用程序中创建其他线程,多个线程并发地运行于同一个进程中。

一个进程中的所有线程都在该进程的虚拟地址空间中,共同使用该虚拟地址空间中的全局变量和系统资源,所以线程间的通信非常方便。

系统创建好进程后,实际上就启动了执行该进程的主执行线程。在VC++程序中,主线程的启动点是以函数形式(即main或WinMain函数)提供给Windows系统。主线程终止了,进程也将随之终止,而不管其他线程是否执行完毕。

多线程可以实现并行处理,避免了某项任务长时间占用CPU时间。当线程数目多于计算机的CPU数目时,为了运行所有这些线程,操作系统为每个独立线程安排一些CPU时间,操作系统以轮换方式向线程提供时间片,这就造成了一种假象:好像这些线程都在同时运行。

尽管比进程间的切换要好得多,但是线程间的切换仍会消耗很多的CPU资源,在一定程度上,也会降低系统的性能。


Windows多线程编程

多线程给应用开发带来了许多好处,但并非在任何情况下都要使用多线程,一定要根据应用程序的具体情况来综合考虑。一般来说,以下情况下可以考虑使用多线程:

  • 应用程序中的各任务相对独立
  • 某些任务耗时较多
  • 各任务需要有不同的优先级

在VC++程序设计中,有多种方法在程序中实现多线程

  1. Win32SDK函数
  2. 使用C/C++运行时库函数
  3. 使用MFC类库
使用Win32 SDK函数实现多线程
1.创建线程

在程序中创建一个线程需要以下两个步骤:

  1. 编写线程函数

    所有线程必须从一个指定的函数开始执行,该函数就是所谓的线程函数。线程函数必须具有类似下面所示的函数原型:
  1. DWORD ThreadFunc(LPVOID lpvThreadParam);

ThreadFunc是新创建的线程函数的名字,可以由编程者任意指定,但必须符合VC++标识符的命名规范。该函数仅有一个LPVOID类型的参数,LPVOID的类型定义如下:

  1. typedef void * LPVOID;

它既可以是一个DWORD类型的整数,也可以是一个指向一个缓冲区的void类型的指针。函数返回一个DWORD类型的值。

一般来说,C++的类成员函数不能作为线程函数。这是因为在类中定义的成员函数,编译器会给其加上this指针。但如果需要线程函数像类的成员函数那样能访问类的所有成员,可采用两种方法。第一种方法是将该成员函数声明为static类型,但static成员函数只能访问static成员,不能访问类中的非静态成员,解决此问题的一种途径是可以在调用类静态成员函数(线程函数)时,将this指针作为参数传入,并在该线程函数中用强制类型转换将this转换成指向该类的指针,通过该指针访问非静态成员。第二种是不定义类成员函数为线程函数,而将线程函数定义为类的友元函数,这样线程函数也可以有泪成员函数同等的权限。

  1. 创建一个线程

    进程的主线程是操作系统在创建进程时自动生成的,但如果要让一个线程创建一个新的线程,则必须调用线程创建函数。Win32 SDK提供的线程创建函数是CreateThread()。

函数原型

  1. HANDLE CreateThread(
  2. LPSECURITY_ATTRIBUTES lpThreadAttributes,
  3. DWORD dwStackSize,
  4. LPTHREAD_START_ROUTINE lpStartAddress,
  5. LPVOID lpParameter,
  6. DWORD dwCreationFlags,
  7. LPDWORD lpThreadId
  8. );

函数参数

  • lpThreadAttributes:指向一个SECURITY_ATTRIBUTES结构的指针,该结构决定了线程的安全属性,一般置为NULL
  • dwStackSize:指定线程的堆栈深度,一般设置为0
  • lpStartAddress:线程起始地址,通常为线程函数名
  • LPTHREAD_START_ROUTINE类型定义:
  1. typedef unsigned long (__stdcall *LPTHREAD_START_ROUTINE) (void* lpThreadParameter);
  • lpParameter: 线程函数的参数
  • dwCreationFlags: 控制线程创建的附加标志。该参数为0,则线程在被创建后立即开始执行;如果该参数为CREATE_SUSPENDED, 则创建线程后盖线程处于挂起状态,直至函数ResumeThread被调用
  • lpThreadID: 该参数返回所创建线程的ID

返回值

  • 该函数在其调用进程的进程空间里创建一个新的线程,并返回已建线程的句柄,如果创建成功则返回线程的句柄,否则返回NULL。

    注意:使用同一个线程函数可以创建多个各自独立工作的线程。

编写如下示例程序1.

  1. // TestProject.cpp : 定义控制台应用程序的入口点。
  2. #include "stdafx.h"
  3. #include <stdio.h>
  4. #include <Windows.h>
  5. #define N 100000
  6. int ThreadF0(LPVOID lpParam)
  7. {
  8. long *a = (long*)lpParam;
  9. for (int i = 0; i < N; ++i) {
  10. //InterlockedExchangeAdd(a, 1);
  11. (*a) +=1;
  12. //Sleep(3);
  13. //printf("0: %d\n", *a);
  14. }
  15. printf("0: %d\n", *a);
  16. return 0;
  17. }
  18. int ThreadF2(LPVOID lpParam)
  19. {
  20. long *b = (long*)lpParam;
  21. for (int i = 0; i < N; ++i) {
  22. (*b) += 1;
  23. //InterlockedExchangeAdd(b, 1);
  24. //Sleep(2);
  25. //printf(" 1: %d\n", *b);
  26. }
  27. printf("2: %d\n", *b);
  28. return 0;
  29. }
  30. int main(int argc, char *argv[])
  31. {
  32. int t = 0;
  33. HANDLE htd0, htd1, htd2;
  34. DWORD thrdID0, thrdID2;
  35. htd0 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadF0, (void*)&t, 0, &thrdID0);
  36. htd1 = 0;
  37. //htd1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadF1, (void*)&t, 0, &thrdID1);
  38. htd2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadF2, (void*)&t, 0, &thrdID2);
  39. Sleep(1000);
  40. printf("t:%d\n", t);
  41. printf("hello, world\n");
  42. return 0;
  43. }

输出如下:

  1. 0: 100000
  2. 2: 120107
  3. t:120107
  4. hello, world
  5. 请按任意键继续. . .

需要特别说明的是:

这里的循环次数N不能设置得太小,因为现在CPU运行速度很快,如果设置得太小是无法(理论上也可以看到,只是出现的概率很低)看到示例1中线程切换引起的异常的。

如果没有线程切换,t最终的值应该是200000,但是这里线程0和线程2切换,彼此相互影响了,使得t最后没有达到200000.

另,sleep会引起线程之间的主动切换。所以,系统会每次在运算(加1之前或者加1之后)后,如果你在线程中使用了sleep遇到sleep就切换线程。

所以需要在正在加1的过程中切换线程,才能看到这样不做控制是有问题的。

而要在正在加的过程中切换线程,只能由系统自动切,不能通过主动调用sleep来实现。

在简单的加法操作中,可以使用InterlockedExchangeAdd函数来进行运算,这样就不会怕线程切换了。

复杂的多步操作用锁、临界区等线程同步的操作。

而操作系统的进程(或者线程)间通信,可以用事件对象(Windows系统)结合WaitForSingleObject()函数,socket,信号,管道,消息队列,共享内存等方式

另外两个小的线程例子参见:

此外,Windows操作系统还提供了Sleep()函数

  1. VOID Sleep(DWORD dwMilliseconds);

Sleep()函数是一个Windows API函数,其功能使线程阻塞dwMilliseconds毫秒。使用Sleep()函数需要包含头文件"windows.h"

函数参数:

dwMilliseconds:指定线程阻塞的时间长度,时间的单位是毫秒(ms)。如果参数取值为0,执行该函数也将使线程阻塞转而执行其他同优先级的线程,如果不存在其他同优先级的线程,线程将立刻恢复执行。如果取值为常量INFINITE,则线程将被无限期阻塞。

线程函数的参数传递

由CreateThread函数原型可以看出,创建线程时,可以给线程传递一个void指针类型的参数,该参数为CreateThread()函数的第四个参数。

当需要将一个整型数据作为线程函数的参数传递给线程时,可将该整型数据强制转换为LPVOID类型,作为它的实参传递给线程函数。

当需要向线程传递一个字符串时,则创建线程时的实参传递既可以使用字符数组,也可以使用std::string类。使用字符数组时,实参可直接使用字符数组名或指向字符串的char *类型的指针。使用std::string类型时,可将指向std::string对象的指针强制转换为LPVOID。

如果需要向线程传送多个数值时,由于线程函数的参数只有一个,所以需要先将它们封装在一个结构体变量中,然后将该变量的指针作为参数传递给线程函数。

示例代码见:

https://github.com/xiaoliuzi/netlib_demo/tree/master/learn_multi_thread


参考:

《Windows网络编程基础教程》 杨传栋 张焕远 编著 清华大学出版社

Windows多线程编程入门的更多相关文章

  1. Windows多线程编程入门笔记

    每次处理并行任务时,如果要等待用户输入或依赖外部(如与灿亨控制器响应),就应该为类似的操作单独创建一个线程,这样我们的程序才不会挂起无响应. 静态库和动态库 静态库是指在程序运行前就编译完成的库,如# ...

  2. windows多线程编程星球(一)

    以前在学校的时候,多线程这一部分是属于那种充满好奇但是又感觉很难掌握的部分.原因嘛我觉得是这玩意儿和编程语言无关,主要和操作系统的有关,所以这部分内容主要出现在讲原理的操作系统书的某一章,看完原理是懂 ...

  3. 转载自~浮云比翼:Step by Step:Linux C多线程编程入门(基本API及多线程的同步与互斥)

    Step by Step:Linux C多线程编程入门(基本API及多线程的同步与互斥)   介绍:什么是线程,线程的优点是什么 线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可 ...

  4. 微博,and java 多线程编程 入门到精通 将cpu 的那个 张振华

    http://down.51cto.com/data/2263476  java 多线程编程 入门到精通  将cpu 的那个 张振华 多个用户可以同时用一个 vhost,但是vhost之间是隔离的. ...

  5. [转]Delphi多线程编程入门(二)——通过调用API实现多线程

    以下是一篇很值得看的关于Delphi多线程编程的文章,内容很全面,建议收藏. 一.入门 ㈠. function CreateThread(    lpThreadAttributes: Pointer ...

  6. windows多线程编程实现 简单(1)

    内容:实现win32下的最基本多线程编程 使用函数: #CreateThread# 创建线程 HANDLE WINAPI CreateThread( LPSECURITY_ATTRIBUTES lpT ...

  7. C++多线程编程入门之经典实例

    多线程在编程中有相当重要的地位,我们在实际开发时或者找工作面试时总能遇到多线程的问题,对多线程的理解程度从一个侧面反映了程序员的编程水平. 其实C++语言本身并没有提供多线程机制,但Windows系统 ...

  8. [转]Delphi多线程编程入门(一)

    最近Ken在比较系统地学习Delphi多线程编程方面的知识,在网络上查阅了很多资料.现在Ken将对这些资料进行整理和修改,以便收藏和分享.内容基本上是复制粘贴,拼拼凑凑,再加上一些修改而来.各个素材的 ...

  9. Windows多线程编程总结

    1 内核对象 1 .1 内核对象的概念 内核对象是内核分配的一个内存块,这种内存块是一个数据结构,表示内核对象的各种特征.并且只能由内核来访问.应用程序若需要访问内核对象,需要通过操作系统提供的函数来 ...

随机推荐

  1. xunsearch开发流程(三)

    (一).编写项目配置文件 通过创建一个项目文件来创建一个新的项目cd /data/local/xunsearch/sdk/php/apptouch njw.ini文件内容如下 project.name ...

  2. OPC接口相关资料地址

    OPC官方网址:https://opcfoundation.org/ OPC中国官网: http://www.chinaopc.org/ ------------------------------- ...

  3. 打包python文件,让文件程序化

    通过对源文件打包,Python程序可以在没有安装 Python的环境中运行,也可以作为一个独立文件方便传递和管理. 现在网上主流的打包方式有两种py2exe或者pyinstaller两款多平台的Pyt ...

  4. qt下用启动图

    void showSplash(void) { QSplashScreen*splash=newQSplashScreen; splash->setPixmap(QPixmap(":/ ...

  5. Range(转)

    原文链接:http://www.cnblogs.com/peida/p/Guava_Range.html 在Guava中新增了一个新的类型Range,从名字就可以了解到,这个是和区间有关的数据结构.从 ...

  6. tomcat 关闭出现 Connection refused 解决方法

    1.找到tomcat占用的端口 ps -eaf|grep tomcat 2.杀掉tomcat进程 kill -p  6453 后记: 杀其他进程的时候,上面的方法不可以,用下面的就ok了 lsof - ...

  7. 1、Window.document对象

    1.Window.document对象 一.找到元素: docunment.getElementById("id"):根据id找,最多找一个:    var a =docunmen ...

  8. spring 声明式事务的坑 @Transactional 注解

    1.首先环境搭建,jar 我就不写了,什么一些spring-core.jar spring-beans.jar spring-content.jar 等等一些包 省略..... 直接上图: sprin ...

  9. linux find中的-print0和xargs中-0的奥妙

    默认情况下, find 每输出一个文件名, 后面都会接着输出一个换行符 ('n'), 因此我们看到的 find 的输出都是一行一行的: 比如我想把所有的 .log 文件删掉, 可以这样配合 xargs ...

  10. C#做的在线升级小程序

    转自原文C#做的在线升级小程序 日前收到一个小任务,要做一个通用的在线升级程序.更新的内容包括一些dll或exe或.配置文件.升级的大致流程是这样的,从服务器获取一个更新的配置文件,经过核对后如有新的 ...