TCP/IP网络编程 第十九章:Windows平台下线程的使用

内核对象

要想掌握Windows平台下的线程,应首先理解“内核对象”(Kernel Objects)的概念。如果仅介绍Windows平台下的线程使用技巧,则可以省略相对陌生的内核对象相关内容。但这并不能使各位深入理解Windows平台下的线程。


内核对象的定义


操作系统创建的资源有很多种,如进程、线程、文件及即将介绍的信号量、互斥量等。其中大部分都是通过程序员的请求创建的,而且请求方式各不相同。虽然存在一些差异,但它们之间也有如下共同点:“都是由Windows操作系统创建并管理的资源。”
不同资源类型在“管理”方式也有差异。例如,文件管理中应注册并更新文件相关的数据I/O位置、文件的打并模式(rcad or write)等。如果是线程,则应注册并维护线程ID、线程所属进程等信息。操作系统为了以记录相关信息的方式管理各种资源,在其内部生成数据块。当然,每种资源需要维护的信息不同,所以每种资源拥有的数据块格式也有差异。这类数据块称为“内核对象”。
假设在Windows下创建了mydata.txt文件,此时Windows操作系统将生成1个数据块以便管理,该数据块就是内核对象。同理,Windows在创建进程、线程、线程同步信号量时也会生成相应的内核对象,用于管理操作系统资源。

内核对象归操作系统所有

线程、文件等资源的创建请求均在进程内部完成,因此,很容易产生“此时创建的内核对象所有者就是进程”的错觉。其实,内核对象所有者是内核(操作系统)。“所有者是内核”具有如下含义:
“内核对象的创建、管理、销毁时机的决定等工作均由操作系统完成!"

基于 Windows的线程创建

进程和线程的关系

既然在第18章学习过线程,那么请回答如下问题:“程序开始运行后,调用main函数的主体是进程还是线程?”


调用main函数的主体是线程!实际上,过去的正确答案可能是进程(特别是在UNIX系列的操作系统中)。因为早期的操作系统并不支持线程,为了创建线程,经常需要特殊的库函数支持。换言之,操作系统无法意识到线程的存在,而进程实际上成为运行的最小单位。即便在这种情况下,需要线程的程序员们也会利用特殊的库函数,以拆分进程运行时间的方式创建线程。但归根结底,这仅仅是应用程序级别创建的线程,与现在讨论的操作系统级别的线程存在巨大差异。

现代的Linux系列、Windows系列及各种规模不等的操作系统都在操作系统级别支持线程,因此,非显式创建线程的程序(如基于select的服务器端)可描述如下:“单一线程模型的应用程序”
反之,显式创建单独线程的程序可描述如下:“多线程模型的应用程序。”这就意味着main函数的运行同样基于线程完成,此时进程可以比喻为装有线程的篮子。实际的运行主体是线程。

Windows 中线程的创建方法

调用该函数将创建线程,操作系统为了管理这些资源也将同时创建内核对象。最后返回用于区分内核对象的整数型“句柄”(Handle)。第1章已介绍过,句柄相当于Linux的文件描述符。

#include <windows.h>
HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,SIZE_T dwStacksize,LPTHREAD_START_ROUTINE IpStartAddress,LPVOID lpParameter,DWORD dwCreationFlags,LPDWORD lpThreadId  
);
//成功时返回线程句柄,失败时返回NULL。IpThreadAttributes //线程安全相关信息,使用默认设置时传递NULL。   dwStackSize        //要分配给线程的栈大小,传递0时生成默认大小的栈。IpStartAddress     //传递线程的main函数信息。lpParameter        //调用main函数时传递的参数信息。dwCreationFlags    //用于指定线程创建后的行为,传递0时,线程创建后立即进入可执行状态。  IpThreadld         //用于保存线程ID的变量地址值。     

上述定义看起来有些复杂,其实只需要考虑IpStartAddress和lpParameter这2个参数,剩下的只需传递0或NULL即可。

Windows线程的销毁时间点

Windows 线程在首次调用的线程main 函数返回时销毁(销毁时间点和销毁方法与Linux不同)。还有其他方法可以终止线程,但最好的方法就是让线程main函数终止(返回)。

创建“使用线程安全标准C函数”的线程

之前介绍过创建线程时使用的CreateThread函数,如果线程要调用C/C++标准函数,需要通过如
下方法创建线程。因为通过CreateThread函数调用创建出的线程在使用C/C++标准函数时并不稳定。

#include <process.h>
uintptr_t _beginthreadex(void * security,unsigned stack _size,unsigned (* start_address)(void *),void * arglist,unsigned initflag,unsigned * thrdaddr);
//成功时返回线程句柄,失败时返回0。

上述函数与之前的CreateThread函数相比,参数个数及各参数的含义和顺序的相同,只是变

量名和参数类型有所不同。因此,用上述函数替换CreateThread函数时,只需适当更改数据类型。上述函数的返回值类型uintptr_t是64位unsigned整数型。但下述示例将通过声明CreateThread
函数的返回值类型HANDLE(这同样是整数型)保存返回的线程句柄。

#include<stdio.h>
#include<windows.h>
#include<process.h>
unsigned WINAPI ThreadFunc(void*arg);  //这个WINAPI只是函数调用惯例声明,和链接时的操作有关int main(int argc,char *argv[]){HANDLE hThread;unsigned threadID;int param=5;hThread=(HANDLE)_beginthreadex(NULL,0,ThreadFunc,(void*)&param,0,&threadID);if(hTread==0){puts("_beginthreadex() error");return -1;}sleep(3000);puts("end of main");return 0;
}unsigned WINAPI ThreadFunc(void*arg){int i;int cnt=*((int*)arg);for(i=0;i<cnt;++i){sleep(1000);puts("runnning thread");}return 0;
}

与Linux相同,Windows同样在main函数返回后终止进程,也同时终止其中包含的所有线程。另外,如果对上述代码进行运行的话,最后输出的内容并非字符串"end of main",而是"running thread"。但这是在main函数返回后,完全销毁进程前输出的字符串。

句柄、内核对象和ID间的关系

线程也属于操作系统管理的资源,因此会伴随着内核对象的创建,并为了引用内核对象而返回句柄。可以利用句柄发送如下请求:“我会一直等到该句柄指向的线程终止。”可以通过句柄区分内核对象,通过内核对象可以区分线程。最终,线程句柄成为区分线程的工具。那线程ID又是什么呢?如上述示例所示,通过_beginthreadex函数的最后一个参数可以获取线程ID。各位或许对句柄和ID的并存感到困惑,其实它们有如下显著特点:“句柄的整数值在不同进程中可能出现重复,但线程在跨进程范围内不会出现重复。"线程ID用于区分操作系统创建的所有线程,但通常没有这种需求。

内核对象的2种状态

资源类型不同,内核对象也含有不同信息。其中,应用程序实现过程中需要特别关注的信息被赋予某种“状态”。例如,线程内核对象中需要重点关注线程是否已终止,所以终止状态又称“signaled状态”,未终止状态称为“non-signaled状态”。

内核对象的状态及状态查看

我们通常比较关注进程的终止时间和线程的终止时间,所以自然会问:“该进程何时终止?”或“该线程何时终止?”操作系统将这些重要信息保存到内核对象,同时给出如下约定:“进程或线程终止时,我会把相应的内核对象改为signaled状态!"这也意味着,进程和线程的内核对象初始状态是non-signaled状态。那么,内核对象的signaled、non-signaled状态究竟如何表示呢?

非常简单!通过1个boolean变量表示。内核对象带有1个boolean变量,其初始值为FALSE,此时的状态就是non-signaled状态。如果发生约定的情况,把该变量改为TRUE,此时的状态就是signaled状态。内核对象类型不同,进入signaled状态的情况也有所区别(即对应事件也有区别)。


正常运行之前示例前需要考虑如下问题:“该内核对象当前是否为signaled状态?”
为回答类似问题,系统定义了WaitForSingleObject和WaitForMultipleObjects函数。

WaitForSingleObject & WaitForMultipleObjects

首先介绍WaitForSingleObject函数,该函数针对单个内核对象验证signaled状态。

#include<windows.h>
DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);
//成功时返回事件信息,失败时返回WAIT_FAILED。hHandle        //查看状态的内核对象句柄。dwMilliseconds //以1/1000秒为单位指定超时,传递INFINITE时函数不会返回,直到内核对象变成 //signaled状态。返回值          //进入signaled状态返回WAIT_OBJECT_0,超时返回WAIT_TIMEOUT。

该函数由于发生事件(变为signaled状态)返回时,有时会把相应内核对象再次改为non-signaled状态。这种可以再次进入non-signaled状态的内核对象称为“auto-reset模式”的内核对象,而不会自动跳转到non-signaled状态的内核对象称为“manual-reset模式”的内核对象。

即将介绍的函数与上述函数不同,可以验证多个内核对象状态。

#include<windows.h>
DWORD WaitForMultipleObeject(DWORD nCount,const HANDLE * lpHandles,BOOL bWaitALL,DWORD dwMilliseconds);
//成功时返回事件信息,失败时返回WAIT_FAILED。nCount         //需验证的内核对象数。IpHandles      //存有内核对象句柄的数组地址值。bWaitAll       //如果为TRUE,则所有内核对象全部变为signaled时返回;如果为FALSE,则只要有1 //个对象的状态变为signaled就会返回。dwMilliseconds //以1/1000秒为单指定超时,传递INFINITE时函数不会返回,直到内核对象变为//signaled状态。

下面利用WaitForSingleObject函数尝试解决示例的问题。

#include<stdio.h>
#include<windows.h>
#include<process.h>
unsigned WINAPI ThreadFunc(void *arg);int main(int argc,char *argv[]){HANDLE hTread;DWORD wr;unsigned threadID;int param=5;hTread=(HANDLE)_beginthreadex(NULL,0,ThreadFunc,(void*)&param,0,&threadID);if(hTread==0){puts("_beginthreadex() error");return -1;}if((wr=WaitForSingleObject(hThread,INFINITE))==WAIT_FAILED){puts("thread wait error");return -1;}printf("wait result: %s \n",(wr==WAIT_OBJECT_0)?"signaled":"time-out");puts("end of main");return 0;
}unsigned WINAPI ThreadFunc(void *arg){int i;int cnt=*((int*)arg);for(i=0;i<cnt;++i){Sleep(1000);puts("running thread");}return 0;
}

WaitForSingleObject & WaitForMultipleObjects的演示

第18章在Linux平台下分析了临界区问题,本章最后的内容将留给Windows平台下的临界区
问题。

#include <stdio.h>
#include <windows.h>
#include <process.h>
#define NUM_THREAD 50
unsigned WINAPI threadInc(void * arg);
unsigned WINAPI threadDes(void * arg);
1ong long num=0;int main(int argc, char *argv[]){HANDLE tHandles[NUM_THREAD];int i;printf("sizeof long long: %d \n",sizeof(long,long));for(i=0;i<NUM_THREAD;i++){if(i%2)tHandles[i]=(HANDLE)_beginthreadex(NULL,0, threadInc,NULL,0,NULL);else tHandles[i]=(HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);}WaitForMultipleObjects(NUM_THREAD,tHandles,TRUE,INFINITE);printf("result: %1ld \n",num);return 0;
}unsigned WINAPI threadInc(void * arg){int i;for(i=0; i<50000000; i++)num+=1;return 0;
}unsigned WINAPI threadDes(void * arg){int i;for(i=0; i<50000000; i++)num-=1;return 0;
}

即使多运行几次也无法得到正确结果,而且每次结果都不同。可以利用第20章的同步技术得到预想的结果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/6393.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RocksDB架构

1、rocksdb是什么? RocksDB中文网 | 一个持久型的key-value存储 rocksdb是一种KV存储引擎&#xff0c;常用于数据库存储数据&#xff0c;无法直接使用&#xff0c;没有提供sql命令&#xff0c;通过调用rocksdb提供的api进行数据库的读写等操作。 rocksdb是以leveldb为基础开…

Linux一些问题,结合gpt,自己复习用

&#xff08;一&#xff09;请介绍linux以及它和windows的区别 &#x1f427; Linux是一个开源的操作系统&#xff0c;它基于UNIX&#xff0c;并具有强大的自由度和灵活性。它被广泛用于服务器环境和嵌入式系统中。Linux有许多不同的发行版&#xff0c;例如Ubuntu、Debian、Fe…

聊聊spring-cloud的负载均衡

聊聊spring-cloud的负载均衡 1. 选择合适的负载均衡算法2. 合理设置超时时间3. 缓存服务实例列表4. 使用断路器5. 使用缓存Spring Cloud负载均衡组件对比RibbonLoadBalancerWebClient对比 总结 在微服务架构中&#xff0c;负载均衡是非常重要的一个环节&#xff0c;可以有效地提…

S32K144 GPIO外设分析

1. S32K144 GPIO外设特性 下面的内容来自于S32K用户手册的翻译&#xff0c;或者网上关于S32K系列的一些pdf文件介绍。有些内容可能会出现理解不到位或者翻译错误方面&#xff0c;如果大家有疑问最好可以查阅用户手册。 GPIO和PORT的数量 从用户手册&#xff0c;对于PCR&#x…

React Dva项目中路由跳转的方法

接下来 我们来看看路由跳转 先打开 我们Dva项目 然后我们需要在routes 下创建一个自己的路由&#xff0c;如果您尚未掌握在Dva项目中创建路由&#xff0c;可以参考我的文章 React 在Dva项目中修改路由配置&#xff0c;并创建一个自己的路由 然后 我的项目有两个路由 router.js…

ASFF Learning Spatial Fusion for Single-Shot Object Detection 论文学习

1. 解决了什么问题&#xff1f; 目标检测取得了显著成绩&#xff0c;但是检测不同尺度的目标仍然是一个挑战。金字塔或多层级特征是解决目标检测中尺度变化的常用手段。但对于单阶段目标检测器而言&#xff0c;各特征尺度之间不一致性制约了算法的表现。与图像金字塔相比&…

VMware Workstation 18 Tech Preview - 增强的 Windows 11 虚拟机安全性

VMware Workstation 18 Tech Preview - 增强的 Windows 11 虚拟机安全性 VMware Workstation Tech Preview 2023 请访问原文链接&#xff1a;https://sysin.org/blog/vmware-workstation-18/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xf…

OpenAI的Function calling 和 LangChain的Search Agent

OpenAI的Function calling openai最近发布的gpt-3.5-turbo-0613 和 gpt-4-0613版本模型增加了function calling的功能&#xff0c;该功能通过定义功能函数&#xff0c;gpt通过分析问题和函数功能描述来决定是否调用函数&#xff0c;并且生成函数对应的入参。函数调用的功能可以…

Pytorch个人学习记录总结 07

目录 神经网络-非线性激活 神经网络-线形层及其他层介绍 神经网络-非线性激活 官方文档地址&#xff1a;torch.nn — PyTorch 2.0 documentation 常用的&#xff1a;Sigmoid、ReLU、LeakyReLU等。 作用&#xff1a;为模型引入非线性特征&#xff0c;这样才能在训练过程中…

Java面试题总结记录(3)—— Spring篇

1、什么是Spring&#xff1f; Spring 是个java企业级应用的开源开发框架。Spring主要用来开发Java应用&#xff0c;但是有些扩展是针对 构建J2EE平台的web应用。 Spring 框架目标是简化Java企业级应用开发&#xff0c;并通过 POJO为基础的编程 模型促进良好的编程习惯 2、你们项…

Socks5代理在爬虫与HTTP应用中的重要性

IP代理的类型及原理常见的IP代理类型有HTTP代理、Socks代理等&#xff0c;本文重点关注Socks5代理。Socks5代理是一种网络协议&#xff0c;可以实现传输层的数据转发&#xff0c;使客户端在不直接连接服务器的情况下与其进行通信。其原理在于接收客户端的请求&#xff0c;然后将…

数组和链表、栈和队列的区别

1.数组和链表的区别 数组和链表是两种不同的数据结构&#xff0c;它们在存储和访问数据上有很大的区别。 1. 存储方式&#xff1a; 数组是一种连续内存空间的数据结构&#xff0c;其元素在内存中是按顺序存储的&#xff0c;通过索引可以直接访问元素。链表是由若干个节点组成…

[k8s] command和args

k8s中的command和args可以覆盖docker镜像中的entrypoint和cmd。其中&#xff0c;k8s-command可以覆盖docker-entrypoint&#xff0c;k8s-args可以覆盖docker-cmd。参考Difference between Docker ENTRYPOINT and Kubernetes container spec COMMAND? 了解一下entrypoint的意义…

Spring 更简单的读取和存储对象

目录 1.存储 Bean 对象 1.1 前置⼯作&#xff1a;配置扫描路径 1.2 添加注解存储 Bean 对象 1.2.1 Controller&#xff08;控制器存储&#xff09; 1.2.2 Service&#xff08;服务存储&#xff09; 1.2.3 Repository&#xff08;仓库存储&#xff09; 1.2.4 Component&a…

Python学习 - Request库

学习和使用 引入 import requests基本语法 Request常用方法总结 responserequests.get(url,params,**kwargs) responserequests.post(url,params,**kwargs)参数含义url目标URL地址params请求发起携带的数据kwargs控制请求访问的参数&#xff0c;使用后可以加入到requests请…

C++---string

String C语言中的字符串和C中的string类标准库中的string类string类的常用接口string类对象的常见构造string类对象的容量操作string类对象的访问及遍历操作 C语言中的字符串和C中的string类 在C语言中&#xff0c;字符串是一个字符数组&#xff0c;它以空字符\0结尾&#xff…

【数据结构】朴素模式匹配 KMP算法

&#x1f387;【数据结构】朴素模式匹配 & KMP 算法&#x1f387; &#x1f308; 自在飞花轻似梦,无边丝雨细如愁 &#x1f308; &#x1f31f; 正式开始学习数据结构啦~此专栏作为学习过程中的记录&#x1f31f; 文章目录 &#x1f387;【数据结构】朴素模式匹配 & K…

【数据架构】Data Fabric 架构是实现数据管理和集成现代化的关键

D&A 领导者应该了解数据编织架构的关键支柱&#xff0c;以实现机器支持的数据集成。 在日益多样化、分布式和复杂的环境中&#xff0c;数据管理敏捷性已成为组织的任务关键优先事项。为了减少人为错误和总体成本&#xff0c;数据和分析 (D&A) 领导者需要超越传统的数据…

Java相关知识点

变量的生命周期&#xff1a;位于内层中的变量可以访问并修改外层变量的值 注意&#xff1a;子类中方法的访问权限 > 父类 ReultSet不是一个集合&#xff0c;而是在使用jdbc(java database connectivity) 返回的一个结果集 enty中提供有参构造时&#xff0c; 未提供空参构…

MyBatis操作数据库

1.MyBatis是什么&#xff1f; MyBatis 是⼀款优秀的持久层框架&#xff0c;它⽀持⾃定义 SQL、存储过程以及⾼级映射。MyBatis 去除了⼏乎所有的 JDBC 代码以及设置参数和获取结果集的⼯作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接⼝和 Java POJO&#xf…