01创建线程CreateThread和_beginthreadex

Windows多线程之线程创建

一. 线程创建函数 CreateThread

1. 函数原型
    HANDLE WINAPI CreateThread(
      _In_opt_  LPSECURITY_ATTRIBUTES  lpThreadAttributes,   
      _In_      SIZE_T                 dwStackSize,
      _In_      LPTHREAD_START_ROUTINE lpStartAddress,
      _In_opt_  LPVOID                 lpParameter,
      _In_      DWORD                  dwCreationFlags,
      _Out_opt_ LPDWORD                lpThreadId
    );
2, 参数说明
  • 第一个参数 lpThreadAttributes 表示线程内核对象的安全属性,一般传入 NULL 表示使用默认设置。

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

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

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

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

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

3. 返回值

​ 线程创建成功返回新线程句柄,失败返回 NULL。

4. 示例代码
#include <windows.h>
#include <stdio.h>

DWORD WINAPI ThreadFunc(LPVOID);

int main()
{
	HANDLE hThread;
	DWORD threadID;
	printf("Enter the main thread ...\n");
	hThread = CreateThread(NULL, 0, ThreadFunc, 0, 0, NULL);
	for (int i = 0; i < 2; i++)
	{
		printf("I am the main thread,PID = %d\n", GetCurrentThreadId());
		Sleep(1000);
	}
	printf("Quit the main thread ...\n");
	return 0;
}

DWORD WINAPI ThreadFunc(LPVOID)
{
	printf("Enter the child thread ...\n");
	for (int i = 0; i < 2; i++)
	{
		printf("I am child thread,PID = %d\n", GetCurrentThreadId());
		Sleep(500);
	}
	printf("Quit the child thread ...\n");
	return 0;
}

执行结果如下:

01CreateThread

二. 线程创建函数 _beginthreadex()

1. 概述

​ CreateThread() 函数是 Windows 提供的 API 接口,在 C/C++ 语言中有另一个创建线程的函数 _beginthreadex(),我们应该尽量使用 _beginthreadex() 来代替使用 CreateThread(),因为它比 CreateThread 更安全。

​ 其原因首先要从标准 C 运行库与多线程的矛盾说起,标准 C 运行库在 1970 年被实现了,由于当时没有任何一个操作系统提供对多线程的支持。因此编写标准 C 运行库的程序员根本没考虑多线程程序使用标准 C 运行库的情况。比如标准 C 运行库的全局变量 errno。很多运行库中的函数在出错时会将错误代号复制给这个全局变量,这样可以方便调试。但如果有这样的一个代码片段:

if (system("notepad.exe readme.txt") == -1)  
{  
    switch(errno)  
    {  
        ...//错误处理代码  
    }  
} 

​ 假设某个线程 A 在执行上面的代码,该线程在调用 system() 之后尚未调用 switch() 语句时另一个线程 B 启动了,这个线程 B 也调用了标准 C 运行库的函数,不幸的是这个函数执行出错了并将错误代号写入全局变量 errno 中。这样线程 A 一旦开始执行 switch() 语句时,它将访问一个被 B 线程改动了的 errno。这种情况必须要加以避免!因为不单单是这一个变量会出问题,其他像 strerror()、strtok()、tmpname()、gmtime()、asctime()等函数也会遇到这种有多个线程访问修改导致的数据覆盖问题。

​ 为了解决这个问题,Windows 操作系统提供了这样一种解决方案 —— 每个线程都将拥有自己专用的一块内存区域来提供标准 C 运行库中所有有需要的函数使用。而且这块内存区域的创建就是由 C/C++ 运行库函数 _beginthreadex() 来负责的。 _beginthreadex() 函数在创建新线程时会分配并初始化一个 _tiddata 块。这个 _tiddata 块自然是用来存放一些需要线程独享的数据。新线程运行时会首先将 _tiddata 块与自己进一步关联起来。然后新线程调用标准 C 运行库函数如 strtok() 时就会先取得 _tiddata 块的地址再讲需要保护的数据存入 _tiddata 块中。这样每个线程就只会访问和修改自己的数据而不会去篡改其他线程的数据了。因此,如果在代码中有使用标准 C 运行库中的函数时,尽量时用 _beginthreadex() 来代替 CreateThread()。

2. 函数原型
unsigned long _beginthreadex(void* security,
                            unsigned stack_size,
                            unsigned(_stdcall *start_address)(void*),
                            void* arglist,
                            unsigned initflag,
                            unsigned* thrdaddr
                            );
3. 参数说明
  • 第一个参数 security 表示安全属性,NULL 为默认安全属性。

  • 第二个参数 stack_size 指定线程堆栈的大小。如果为 0 ,则线程堆栈大小和创建它的线程的相同。一般用 0 。

  • 第三个参数 start_address 指定线程函数的地址,也就是线程调用执行的函数地址(用函数名称即可,函数名称就表示地址)。

  • 第四个参数 arglist 表示传递给线程的参数的指针,可以通过传入对象的指针,在线程函数中再转化为对应类的指针。

  • 第五个参数 initflag 表示线程初始化状态,0 表示立即运行;CREATE_SUSPEND 表示悬挂。

  • 第六个参数 thrdaddr 用于记录线程 ID 的地址。

4. 返回值

​ 线程创建成功返回新线程句柄,失败返回 NULL。

5. 示例代码
#include <Windows.h>
#include <process.h>
#include <stdio.h>

unsigned int _stdcall ThreadFunc(PVOID pParam)
{
	printf("the child thread %d: Hello world!\n", GetCurrentThreadId());
	return 0;
}

int main()
{
	const int THREAD_MUN = 5;
	HANDLE handles[THREAD_MUN];
	for (int i = 0; i < THREAD_MUN; i++)
	{
		handles[i] = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, NULL);
	}
	WaitForMultipleObjects(THREAD_MUN, handles, TRUE, INFINITE);
	return 0;
}

执行结果如下:

image

posted @ 2018-08-04 16:40  洛克十年  阅读(222)  评论(0编辑  收藏  举报