当前位置:首页 > 操作系统 > 正文内容

浅谈Windows下的线程细节

邻居的猫1个月前 (12-09)操作系统1864

序言

最近阅览了《windows中心编程》关于线程的章节,原书作者评论得较为深化,初读者极易被绕晕,我专门写这篇文章供初读者参阅阅览。本文的最终,侧重评论了Windows线程API与c/c++运转时库的留意事项。由于自己水平有限,文章不免有疏忽,还望各位读者纠正。

Windows供给的创捷与销完毕程的函数

咱们知道,Windows供给了CreateThread函数用于创立线程,CreateThread函数原型如下:

HANDLE CreateThread(
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in, optional] __drv_aliasesMem LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out, optional] LPDWORD lpThreadId
);

其间各参数的含义在此不多赘述。接下来咱们侧重评论一下lpStartAddresslpParameter参数。

lpStartAddresslpParameter参数

lpStartAddress

指向要由线程履行的应用程序界说函数的指针。 此指针表明线程的开端地址。

lpParameter

指向要传递给线程的变量的指针。

从字面意思上了解这两个参数是不难的,问题在于Windows是一个抢占式的多线程体系,也便是说,调用CreateThread函数的线程与CreateThread函数创立的线程是一起履行的,这会引发一些难以捕捉的反常。

考虑如下C++代码:

static void WINAPI Print(int* x)
{
    _tprintf(TEXT("Print函数开端履行 -> %d\n"),*x);
}

void father_thread()
{
    DWORD dwThreadId;
    int x = 5;
    HANDLE hThread = CreateThread(NULL, 0,
        (LPTHREAD_START_ROUTINE)Print,
        &x, 0, &dwThreadId);
    if (hThread == NULL)
    {
        _tprintf(TEXT("创立线程失利 -> %d\n"), GetLastError());
        return 0;
    }

    //留意,我把下面一行代码注释掉了,先记住这行代码
    //WaitForSingleObject(hThread, INFINITE);

    CloseHandle(hThread);

    return 0;
}

咱们在main函数中创立了一个子线程,这个子线程履行Print函数的代码,Print函数的功用很简单,仅仅一个一般的输出一段文本。前面说了,调用CreateThread函数的线程与CreateThread函数创立的线程是一起履行的,这就或许产生这样的状况:father_thread函数的线程现已完毕了,可是Print的线程还没有履行完毕。所以Print函数所拜访的x的值很或许现已产生了改变。据我试验发现,履行这段代码后,操控台上仅仅输出了“Print”,并没有把整句文本悉数输出。原因也很简单,正如前面所说,Windows是一个抢占式的多线程体系,在调用了Print函数后,father_thread函数并没有等候Print函数履行完毕,而是持续履行main的代码,这就导致还没有打印完文本整个进程就完毕了。要处理这个问题也很简单,把代码中的注释句子写入代码中就能够了。

这个问题也算是一个小小的坑吧

关于完毕线程

书中说到,完毕线程运转有四种办法:

  • 线程函数自己回来(强烈引荐)
  • 线程调用ExitThread来完毕自己
  • 其它线程调用TerminateThread完毕线程
  • 线程地点的进程完毕

原书作者引荐运用榜首种办法完毕线程,由于这样能够确保线程的资源能够被正确的开释;关于第二种办法,ExitThread函数会使线程中止运转,并整理该线程的操作体系资源,可是线程的C/C++资源不会被整理;关于第三种办法,TerminateThread函数是异步的,在TerminateThread函数回来时,它并不能确保所要完毕的线程必定中止了,其它的线程有或许还需求拜访要完毕的线程的仓库内存;最终一种办法相当于为每一个线程都调用了TerminateThread办法。

线程的完成细节

Windows下创立线程的细节
接下来咱们细心研究一下这张图片。首要咱们能够看到调用CreateThread导致体系创立了线程内核目标,该目标开端的运用计数器为2,由于被创立的线程自身算一个,CreateThread函数回来的句柄也算一个。该线程的其它特点也被初始化。

创立线程内核目标之后,体系在包括该线程的进程中为线程分配仓库。在仓库中,体系从高位地址到低位地址顺次将lpParameter和lpStartAddress写入仓库中(Windows更新了形参的称号,所以图中和这儿所说的不一样)。

每个线程都有自己的寄存器,称为线程的上下文(关于这部分的具体内容,能够参阅操作体系相关常识)。
线程的一切寄存器都保存在一个CONTEXT的结构中。咱们能够看到,指令指针寄存器指向了一个名为RtlUserThreadStart的函数,
这个函数便是线程开端履行的进口。RtlUserThreadStart函数的原型如下[^1]:

VOID RtlUserThreadStart(PTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter);

[^1];这个函数现在在Windows协助文档里边现已找不到了,具体细节有待讲究

RtlUserThreadStart函数有两个参数,是由操作体系显式写入的。当新线程履行RtlUserThreadStart函数时,
会有如下工作产生:

  • 环绕线程函数,会设置SEH,使得线程履行期间一切的反常都能够由体系默许处理
  • 体系调用线程函数,把CreateThread函数的lpParameter参数传递给线程函数
  • 线程函数回来时,RtlUserThreadStart调用ExitThread函数完毕线程
  • 若线程产生了反常,RtlUserThreadStart会调用EixtProcess中止整个线程

留意以上履行进程,RtlUserThreadStart最终会调用ExitThread函数,着意味着RtlUserThreadStart函数永久不会退出,也便是说,RtlUserThreadStart函数永久不会回来。

调用CreateThread

最终要点:Windows线程API与C/C++运转时库

从_beginthreadex到_endthreadex

在之前,一个库别离有两个版别:单线程多线程。规范C/C++运转库开端不是为多线程言语程序而规划的。
为了确保C/C++多线程应用程序正常运转,有必要创立一个数据结构与运用了C/C++运转库函数的线程相关联,这样在调用运转库函数时,这些函数会去查找主调线程的数据块,
防止影响其它线程。问题是,体系并不知道要在什么时候分配这种数据块,由于体系并不知道应用程序运用了C/C++运转库,也不知道咱们调用的函数是线程安全的。
记住,确保线程安满是程序员的职责。

直接运用CreateThread函数创立线程,不能确保在多线程环境下线程的安全,咱们经过调用_beginthreadex函数来处理这个问题。
这个函数的原型如下:

_MCRTIMP uintptr_t __cdecl _beginthreadex (
    void *security,
    unsigned stacksize,
    unsigned (__CLR_OR_STD_CALL * initialcode) (void *),
    void * argument,
    unsigned createflag,
    unsigned *thrdaddr
    );

_beginthreadex函数对CreateThread函数进行了封装,_beginthreadex函数让每个线程都有一个专用的_tiddata内存块,这个内存块保存了传递给_beginthreadex的线程函数地址等数据
经过调查书中给出的代码,咱们发现在_beginethread里边,调用了CreateThread函数,而且传递给CreateThread函数的函数地址是_threadstartex,参数地址是_tiddata结构的地址。
接着,咱们进入了_threadstartex中履行代码。在_threadstartex函数中又调用TlsSetValue和_callthreadstartex函数。
然后,咱们跳转到_callthreadstartex函数中,在这个函数中又调用了_endthreadex函数,用来正确的处理_tiddata内存块
咱们是永久不会履行_threadstartex函数的回来句子的。

线程创立的具体进程

假如咱们直接调用CreateThread,当这个子线程需求_tiddata时,假如没有,则CreateThread创立一个揭露的_tiddata。
这会导致子线程在调用一些函数时,会让整个进程中止运转,而且,假如不调用_endthreadex完毕线程,
_tiddata内存块就不会被毁掉,然后导致内存走漏。

一起书中还主张,不要运用_endthread完毕线程。_endthread完毕线程之前,会调用CloseHand封闭线程句柄,
假如子线程现已完毕了,但父线程中运用了子线程的句柄,就会引发问题。所以咱们应该运用endthreadex完毕线程。

结语

本文对Windows线程机制进行了非常根本的解说,其间的更多细节,会在今后的博文中更新,感兴趣的读者敬请重视

扫描二维码推送至手机访问。

版权声明:本文由51Blog发布,如需转载请注明出处。

本文链接:https://www.51blog.vip/?id=598

分享给朋友:

“浅谈Windows下的线程细节” 的相关文章

创立子域

创立子域

一:在dns3上装置AD域服务 装备dns3IP 首选dns服务器填dns1的IP 备用dns服务器填dns2的IP 由于在咱们的设置里,dns1损坏了,dns2替代它作业,所以dns2是备用服务器 勾选AD域服务 一向点下一步直到装置 二:将dns3升为子域的域控制器 点击感叹号挑选提高 挑选添...

【Linux Ops】怎么替换 libstdc++ 提高 GLIBCXX 版别

【Linux Ops】怎么替换 libstdc++ 提高 GLIBCXX 版别

【环境】kos5.8sp2, kernel5.10 仍是上一篇说到的那个软件环境,其依靠的 GLIBCXX 版别较高,因此在装置时给出了以下过错: xxx: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.29' not found (required by...

360浏览器linux,轻量级、安全高效的网络浏览体验

360浏览器linux,轻量级、安全高效的网络浏览体验

下载地址 你可以从360浏览器的官方网站下载适用于Linux的安装包。具体下载地址是:。 支持的Linux发行版360浏览器支持多种Linux发行版,包括但不限于: Ubuntu Deepin 银河麒麟 中标麒麟 Linux Mint。 安装步骤1. 下载安装包: 访问360浏览器的官方网站...

windows软件类型,Windows软件类型详解

windows软件类型,Windows软件类型详解

1. 系统工具:包括系统优化、磁盘管理、文件管理、安全防护等工具,如CCleaner、Defraggler、Recuva、Norton等。2. 办公软件:包括文字处理、表格制作、演示文稿、邮件管理、项目管理等软件,如Microsoft Office、WPS Office、Adobe Acrobat等...

windows10桌面背景,windows10官方桌面壁纸

windows10桌面背景,windows10官方桌面壁纸

Windows 10提供了多种方法来设置和更改桌面背景,以下是几种常见的方式: 1. 使用系统自带主题Windows 10系统自带了许多主题,您可以通过以下步骤来应用这些主题: 点击“开始”按钮。 选择“设置”。 点击“个性化”。 在个性化设置中,选择“主题”。 从默认主题中选择一个,或者点击“从M...

红帽linux官网,红帽Linux官网——探索企业级开源解决方案的权威平台

红帽linux官网,红帽Linux官网——探索企业级开源解决方案的权威平台

1. 红帽全球官网: 主要内容: 提供关于红帽企业Linux、OpenShift和Ansible自动化平台等产品的信息。2. 红帽中国官网: 主要内容: 提供红帽旗舰产品的免费试用、市场活动、客户成功案例、培训服务等信息。3. 红帽企业Linux下载:...