Thread

一、CPU

(一)CPU个数、CPU核心数、CPU线程数

CPU个数即CPU芯片个数

CPU的核心数是指物理上,也就是硬件上存在着几个核心。比如,双核就是包括2个相对独立的CPU核心单元组,四核就包含4个相对独立的CPU核心单元组。

线程数是一种逻辑的概念,简单地说,就是模拟出的CPU核心数。比如,可以通过一个CPU核心数模拟出2线程的CPU,也就是说,这个单核心的CPU被模拟成了一个类似双核心CPU的功能。我们从任务管理器的性能标签页中看到的是两个CPU。 比如Inte l赛扬G460是单核心,双线程的CPU,Intel 酷睿i3 3220是双核心 四线程,Intel 酷睿i7 4770K是四核心 八线程 ,Intel 酷睿i5 4570是四核心 四线程等等。 对于一个CPU,线程数总是大于或等于核心数的。一个核心最少对应一个线程,但通过超线程技术,一个核心可以对应两个线程,也就是说它可以同时运行两个线程。 

CPU的线程数概念仅仅只针对Intel的CPU才有用,因为它是通过Intel超线程技术来实现的,最早应用在Pentium4上。如果没有超线程技术,一个CPU核心对应一个线程。所以,对于AMD的CPU来说,只有核心数的概念,没有线程数的概念。 

CPU之所以要增加线程数,是源于多任务处理的需要。线程数越多,越有利于同时运行多个程序,因为线程数等同于在某个瞬间CPU能同时并行处理的任务数。 因此,线程数是一种逻辑的概念,简单地说,就是模拟出的 CPU 核心数。一个核心最少对应一个线程,但英特尔有个超线程技术可以把一个物理线程模拟出两个线程来用,充分发挥 CPU 性能,即一个核心可以有两个到多个线程。

设计决定,intel给他的x86设计了逻辑线程=2*物理核心数,ibm的power8是逻辑线程=8*物理核心数

二、线程

线程是进程中的一个实体,是被系统独立调度和分派的基本单位。一个进程可以拥有多个线程,但是一个线程必须有一个进程。

线程自己不拥有系统资源,只有运行所必须的一些数据结构,但它可以与同属于一个进程的其它线程共享进程所拥有的全部资源,同一个进程中的多个线程可以并发执行。

(一)CreateThread

1.说明

使用CreateThread函数创建线程

2.函数原型

WINBASEAPI HANDLE WINAPI CreateThread (
LPSECURITY_ATTRIBUTES lpThreadAttributes, 
SIZE_T dwStackSize, 
LPTHREAD_START_ROUTINE lpStartAddress, 
LPVOID lpParameter, 
DWORD dwCreationFlags, 
LPDWORD lpThreadId);

参数说明:

第一个参数 lpThreadAttributes 表示线程内核对象的安全属性,一般传入NULL表示使用默认设置。

第二个参数 dwStackSize 表示线程栈空间大小。传入0表示使用默认大小(1MB)。

栈是私有的但堆是公用的

什么是堆栈? 其实堆是堆、栈是栈, 有时 "栈" 也被叫做 "堆栈".

"栈"(或叫堆栈)适合存取临时而轻便的变量, 主要用来储存局部变量

现在我们知道了线程有自己的 "栈", 并且在建立线程时可以分配栈的大小.

第三个参数 lpStartAddress 表示新线程所执行的线程函数地址,多个线程可以使用同一个函数地址。

等线程退出后, 我们用 GetExitCodeThread 函数获取的退出码就是这个返回值!

如果线程没有退出, GetExitCodeThread 获取的退出码将是一个常量 STILL_ACTIVE (259); 这样我们就可以通过退出码来判断线程是否已退出.

第四个参数 lpParameter 是传给线程函数的参数。

线程入口函数的参数是个无类型指针(Pointer), 用它可以指定任何数据;

第五个参数 dwCreationFlags 指定额外的标志来控制线程的创建,为0表示线程创建之后立即就可以进行调度,如果为CREATE_SUSPENDED则表示线程创建后暂停运行,这样它就无法调度,直到调用ResumeThread()。

ResumeThread 恢复线程的运行; SuspendThread 挂起线程.

这两个函数的参数都是线程句柄, 返回值是执行前的挂起计数.

第六个参数 lpThreadId 将返回线程的ID号,传入NULL表示不需要返回该线程ID号

既然可以返回句柄, 为什么还要输出这个 ID? 现在我知道的是:
1、线程的 ID 是唯一的; 而句柄可能不只一个, 譬如可以用 GetCurrentThread 获取一个伪句柄、可以用 DuplicateHandle 复制一个句柄等等.
2、ID 比句柄更轻便.

返回值:

CreateThread的返回值是线程的句柄,失败的话就返回NULL

3.返回线程句柄

"句柄" 类似指针, 但通过指针可读写对象, 通过句柄只是使用对象;

有句柄的对象一般都是系统级别的对象(或叫内核对象); 之所以给我们的是句柄而不是指针, 目的只有一个: "安全";

貌似通过句柄能做很多事情, 但一般把句柄提交到某个函数(一般是系统函数)后, 我们也就到此为止很难了解更多了; 事实上是系统并不相信我们.

不管是指针还是句柄, 都不过是内存中的一小块数据(一般用结构描述), 微软并没有公开句柄的结构细节, 猜一下它应该包括: 真实的指针地址、访问权限设置、引用计数等等.

既然 CreateThread 可以返回一个句柄, 说明线程属于 "内核对象".

实际上不管线程属于哪个进程, 它们在系统的怀抱中是平等的; 在优先级(后面详谈)相同的情况下, 系统会在相同的时间间隔内来运行一下每个线程, 不过这个间隔很小很小, 以至于让我们误以为程序是在不间断地运行.

这时你应该有一个疑问: 系统在去执行其他线程的时候, 是怎么记住前一个线程的数据状态的?

有这样一个结构 TContext, 它基本上是一个 CPU 寄存器的集合, 线程是数据就是通过这个结构切换的, 我们也可以通过 GetThreadContext 函数读取寄存器看看.

4.代码示例

    // 创建对应的线程
    for (int i = 0; i < SysInfo.dwNumberOfProcessors; i++)
    {
        HANDLE hThread = CreateThread(NULL, 0, WorkThread, this, 0, NULL);
        CloseHandle(hThread);
    }

5.线程函数参数

createthread 第三个参数为线程函数,第四个参数即为线程函数的参数。要知道很多函数都是有多个参数的,而此处只提供了一个参数。

我们知道LPVOID是一个没有类型的指针,也就是说你可以将LPVOID类型的变量赋值给任意类型的指针,比如在参数传递时就可以把任意类型传递给一个LPVOID类型为参数的方法,然后在方法内再将这个“任意类型”从传递时的“LPVOID类型”转换回来。

于是我们可以先把线程函数的所有参数放入结构体里边,然后createthread的时候把 结构体传入进去,最后在线程函数里边把LPVOID强制转换为结构体指针即可。

6.LPVOID使用样例

#include <iostream>
#include <stdlib.h>
#include <fstream>
#include <string.h>
#include <vector>
#include <WinSock2.h>
#include <Winsock2.h>
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")

using namespace std;

struct node
{

    int num;
    char s[100];
};
DWORD WINAPI multthread(LPVOID lpargs)
{
    node *p = (node *)lpargs; // 转换为结构体指针
    cout << p->s << "   " << p->num << endl;
    return 0;
}
void mutlarg()
{

    node N;
    N.num = 123456;
    strcpy(N.s, "Hello World!");
    HANDLE H1 = CreateThread(NULL, 0, multthread, &N, 0, NULL);
    WaitForSingleObject(H1, INFINITE);
}
int main()
{

    mutlarg();
    system("pause");
    return 0;
}

输出:

(二)CloseHandle()

1.说明

关闭打开的对象句柄。

2.函数原型

BOOL CloseHandle(
  [in] HANDLE hObject//打开对象的有效句柄。
);
//如果该函数成功,则返回值为非零值。
//如果函数失败,则返回值为零。 

3.使用样例

HANDLE hThread = CreateThread(NULL, 0, WorkThread, this, 0, NULL);
CloseHandle(hThread);

4.线程句柄

(1)线程的handle是指向“线程的内核对象”的,而不是指向线程本身.每个内核对象只是内核分配的一个内存块,并且只能由内核访问。该内存块是一种数据结构,它的成员负责维护对象的各种信息(eg: 安全性描述,引用计数等)。

(2)在CreateThread成功之后会返回一个hThread的handle,且内核对象的计数加1,CloseHandle之后,引用计数减1,当变为0时,系统删除内核对象。

(3)但是这个handle并不能完全代表这个线程,它仅仅是线程的一个“标识”,系统和用户可以利用它对相应的线程进行必要的操纵。如果在线程成功创建后,不再需要用到这个句柄,就可以在创建成功后,线程退出前直接CloseHandle掉,但这并不会影响到线程的运行。

所以CloseHandel(ThreadHandle );只是关闭了一个线程句柄对象,表示我不再使用该句柄,即不对这个句柄对应的线程做任何干预了。并没有结束线程。

所有的内核对象(包括线程Handle)都是系统资源,用了要还的,也就是说用完后一定要closehandle关闭之,如果不这么做,你系统的句柄资源很快就用光了。

5.线程和线程句柄

如果你CreateThread以后需要对这个线程做一些操作,比如改变优先级,被其他线程等待,强制TermateThread等,就要保存这个句柄,使用完了在CloseHandle。如果你开了一个线程,而不需要对它进行如何干预,CreateThread后直接CloseHandle就行了。

ExitThread是推荐使用的结束一个线程的方法,当调用该函数时,当前线程的栈被释放,然后线程终止,相对于TerminateThread函数来说,这样做能够更好地完成附加在该线程上的DLL的清除工作。  

 

posted @ 2023-02-26 13:35  ImreW  阅读(13)  评论(0编辑  收藏  举报