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

这篇具有很好参考价值的文章主要介绍了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章的同步技术得到预想的结果。文章来源地址https://www.toymoban.com/news/detail-599816.html

到了这里,关于TCP/IP网络编程 第十九章:Windows平台下线程的使用的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 《TCP IP网络编程》第十七章

            select 复用方法由来已久,因此,利用该技术后,无论如何优化程序性能也无法同时介入上百个客户端。这种 select 方式并不适合以 web 服务器端开发为主流的现代开发环境 ,所以需要学习 Linux 环境下的 epoll。 基于 select 的 I/O 复用技术速度慢的原因:        

    2024年02月12日
    浏览(42)
  • 《TCP IP网络编程》第十八章

    线程背景:         第 10 章介绍了多进程服务端的实现方法。多进程模型与 select 和 epoll 相比的确有自身的优点,但同时也有问题。如前所述, 创建(复制)进程的工作本身会给操作系统带来相当沉重的负担。而且,每个进程都具有独立的内存空间,所以进程间通信的实

    2024年02月12日
    浏览(53)
  • TCP/IP网络编程 第十七章:优于select的epoll

    select复用方法其实由来已久,因此,利用该技术后,无论如何优化程序性能也无法同时接入上百个客户端(当然,硬件性能不同,差别也很大)。这种select方式并不适合以Web服务器端开发为主流的现代开发环境,所以要学习Linux平台下的epoll。 基于select的I/O复用技术速度慢的原

    2024年02月16日
    浏览(42)
  • TCP/IP网络编程 第十六章:关于IO流分离的其他内容

    两次I/O流分离 我们之前通过2种方法分离过IO流,第一种是第十章的“TCPI/O过程(Routine)分离”。这种方法通过调用fork函数复制出1个文件描述符,以区分输入和输出中使用的文件描述符。虽然文件描述符本身不会根据输入和输出进行区分,但我们分开了2个文件描述符的用途

    2024年02月16日
    浏览(42)
  • TCP/IP网络编程 第十五章:套接字和标准I/O

    标准I/O函数的两个优点 将标准I/O函数用于数据通信并非难事。但仅掌握函数使用方法并没有太大意义,至少应该 了解这些函数具有的优点。下面列出的是标准I/O函数的两大优点: □标准I/O函数具有良好的移植性(Portability) □标准I/O函数可以利用缓冲提高性能。 关于移植性无需

    2024年02月16日
    浏览(54)
  • UNIX网络编程卷一 学习笔记 第二十九章 数据链路访问

    目前大多操作系统都为程序提供访问数据链路层的功能,此功能可提供以下能力: 1.能监视由数据链路层接收的分组,使得tcpdump之类的程序能运行,而无需专门的硬件设备来监视分组。如果结合使用网络接口进入混杂模式(promiscuous mode)的能力,那么应用甚至能监视本地电

    2024年02月10日
    浏览(46)
  • 《TCP/IP网络编程》阅读笔记--基于Windows实现Hello Word服务器端和客户端

    目录 1--Hello Word服务器端 2--客户端 3--编译运行 3-1--编译服务器端 3-2--编译客户端 3-3--运行 运行结果:

    2024年02月10日
    浏览(65)
  • 《TCP IP网络编程》

            2023.6.28 正式开始学习网络编程。 每一章每一节的笔记都会记录在博客中以便复习。         网络编程又叫套接字编程。所谓网络编程,就是编写程序使两台连网的计算机相互交换数据。 为什么叫套接字编程? 我们平常将插头插入插座上就能从电网中获取电力,同

    2024年02月11日
    浏览(48)
  • JAVAEE初阶相关内容第十九弹--网络原理之TCP_IP【续集2】

    上一篇博客主要介绍的是关于网络层协议-IP协议的重点介绍。需要掌握关于IP协议的协议头格式,关于IPV4分配不够的解决办法。地址管理与路由选择。 点击跳转上一篇博客 本篇博客将继续学习关于计网中协议的内容。 本篇博客主要介绍关于数据链路层的重点协议-以太网。

    2024年02月03日
    浏览(34)
  • TCP/IP网络编程(三)

    多播(Multicast)方式的数据传输是 基于 UDP 完成的 。因此 ,与 UDP 服务器端/客户端的实现方式非常接近。区别在于,UDP 数据传输以单一目标进行,而多播数据 同时传递到加入(注册)特定组的大量主机 。换言之, 采用多播方式时,可以同时向多个主机传递数据 。 14.1.1 多

    2024年02月03日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包