前言


  多线程(Multi-Thread),是指从软件或者硬件上实现多个线程并发执行的技术。无论你是软件开发工程师(Software Engineer),还是算法工程师(Algorithm Engineer),当遇到性能优化需求时,多线程技术是不可绕开的一项。

  现代处理器是多核处理器架构,单处理器有多个独立运算核,可以并发运算,这个特性使得总任务可以被划分为多个独立的子任务同时运行,大大提高运行效率。本系列将对c/c++多线程编程中运用最广泛的概念做一个入门介绍,希望能对读者学习多线程编程有所帮助。

  本文要介绍的是最基础的那个概念:线程Thread

线程 Thread


  我们先看看Wiki对Thread的介绍:

  In computer science, a thread of execution is the smallest sequence of programmed instructions that can be managed independently by a scheduler.
  Multiple threads can exist within one process, executing concurrently and sharing resources such as memory, while different processes do not share these resources.

  在计算机科学中,执行线程是能被调度器独立管理的程序化指令中的最小序列。
  多个线程可存在与一个进程中,同时执行且共享资源(如内存资源),而不同的进程则不能共享这些资源。

  从以上介绍中,我们提炼出两个关键点:线程之间彼此独立、共享资源。这表明线程不仅可以独立并发,而且可以通信协作。也就是说,多线程不是各自为战,而是同舟共济。

  在多线程编程中,线程是最基本的概念。面对一个任务并行的多线程问题,我们首先要将总任务分为多个子任务,一个子任务就可以放入一个线程中执行,这时我们需要编写多个线程,每个线程负责独立的任务;而面对一个数据并行的多线程问题,我们只有一个任务,是将数据分为多个独立的子数据,每个数据都执行同样的任务,这时我们只需要编写一个线程,通过管理线程的输入来完成多个子数据的并行任务。所以我们把线程想象成任务即可,这个任务要完成多少工作无关紧要,只要多个任务是独立的,或者同一个任务有多个独立的输入。

创建线程

CreateThread

  在C语言中,如何创建一个线程?在Windows系统中,我们可以使用CreateThread函数:

HANDLE WINAPI CreateThread(
    _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
    _In_ SIZE_T dwStackSize,
    _In_ LPTHREAD_START_ROUTINE lpStartAddress,
    _In_opt_ __drv_aliasesMem LPVOID lpParameter,
    _In_ DWORD dwCreationFlags,
    _Out_opt_ LPDWORD lpThreadId
);

  参数说明:
  lpThreadAttributes:指向SECURITY_ATTRIBUTES型态的结构的指针。在Windows 98中忽略该参数。在Windows NT中,NULL使用默认安全性,不可以被子线程继承,否则需要定义一个结构体将它的bInheritHandle成员初始化为TRUE
  dwStackSize:设置初始栈的大小,以字节为单位,如果为0,那么默认将使用与调用该函数的线程相同的栈空间大小。任何情况下,Windows根据需要动态延长堆栈的大小。
  lpStartAddress:指向线程函数的指针,形式:函数名,函数名称没有限制,但是必须以下列形式声明:

DWORD WINAPI 函数名 (LPVOID lpParam)

  格式不正确将无法调用成功。
  lpParameter:向线程函数传递的参数,是一个指向结构的指针,不需传递参数时,为NULL。
  dwCreationFlags :线程标志,可取值如下
  (1)CREATE_SUSPENDED(0x00000004):创建一个挂起的线程,
  (2)0:表示创建后立即激活。
  (3)STACK_SIZE_PARAM_IS_A_RESERVATION(0x00010000):dwStackSize参数指定初始的保留堆栈 的大小,否则,dwStackSize指定提交的大小。该标记值在Windows 2000/NT and Windows Me/98/95上不支持。
  lpThreadId:保存新线程的id。若不想返回线程ID,设置值为NULL。
  返回值:函数成功,返回线程句柄;函数失败返回false。

  代码示例:

#include "stdafx.h"
#include "windows.h"

//线程函数定义
DWORD WINAPI ThreadFunc(LPVOID lpParam)
{
	Sleep(1000);
	return 0;
}
int main()
{
	DWORD dwThreadId = 0;	//线程ID
	//创建线程
	HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &dwThreadId);

    return 0;
}

  

_beginthread

  实际上,CreateThread函数并不被推荐使用,当软件需要调用CRT库时,使用CreateThread因为没有对子线程为CRT库分配堆,会导致内存错误而崩溃。

  我们可以使用另一个创建线程函数:_beginthread():

uintptr_t _beginthread(
void( *start_address )( void * ),
unsigned stack_size,
void *arglist
);

  

  参数说明:
  start_address:新线程的起始地址 ,指向新线程调用的函数的起始地址,为函数名。函数的声明方式很简单:

void 函数名(LPVOID lpParam)

  stack_size:新线程的堆栈大小,一般为0
  arglist:传递给线程的参数列表,无参数时为NULL

  _beginthread要比CreateThread安全,它是对CreateThread函数的封装,并针对CreateThread在调用CRT库时的内存泄漏问题进行了处理,所以比CreateThread更加安全。_beginthrad对应的关闭线程函数为为_endthread。

  代码示例:

#include "stdafx.h"
#include "windows.h"
#include "process.h"

//线程函数定义
void ThreadFunc(void* param)
{
	Sleep(1000);
}

int main()
{
	//创建线程
	_beginthread(ThreadFunc, 0, NULL);

	return 0;
}

_beginthreadex

  _beginthread函数非常简单,易上手,但参数太少也有缺点,和CreateThread比起来似乎可控制性不强?别慌,我们还有另一个函数_beginthreadex:

unsigned long _beginthreadex(
    void *security,
    unsigned stack_size,
    unsigned(_stdcall *start_address)(void *),
    void *argilist,
    unsigned initflag,
    unsigned *threaddr
);

  参数说明:
  security:安全属性, 为NULL时表示默认安全性
  stack_size:线程的堆栈大小, 一般默认为0
  start_address:所要启动的线程函数
  argilist:线程函数的参数, 是一个void*类型, 传递多个参数时用结构体
  flag:新线程的初始状态,0表示立即执行,CREATE_SUSPEND表示创建之后挂起
  threaddr:线程ID地址
  返回值:成功返回新线程句柄, 失败返回0

  可以看到,_beginthreadex就像_beginthread的扩展体,增加了三个参数,是可控制性更好一些。两个函数的不同点如下:
  (1)_beginthreadex()比_beginthread()多3个参数:intiflag,security和threadaddr。
  (2)线程函数的声明方式不同。_beginthreadex()的线程函数必须使用_stdcall修饰符,且必须返回一个unsigned int型的退出码。

unsigned __stdcall 函数名(void* param)

  (3)_beginthreadex()在创建线程失败时返回0,而_beginthread()在创建线程失败时返回-1。这一点是在检查返回结果时必须注意的。
  (4)如果是调用_beginthread创建线程,则相应地调用_endthread结束线程时,系统会自动关闭线程句柄;而调用_beginthreadx创建线程,相应地调用_endthreadx结束线程时,系统不能自动关闭线程句柄。因此调用_beginthreadx创建线程还需程序员自己关闭线程句柄,以清除线程的地址空间。

  _beginthread和_beginthreadex都包含于头文件 process.h 中。
  代码示例:

#include "stdafx.h"
#include "windows.h"
#include "process.h"

//线程函数定义
unsigned __stdcall ThreadFunc(void* param)
{
	Sleep(1000);
	return 0;
}

int main()
{
	unsigned uiThreadId = 0;
	//创建线程
	HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uiThreadId);

	return 0;
}

pthread_create

  pthread_create是类Unix操作系统(Unix、Linux、Mac OS X等)的创建线程的函数。包含于头文件pthread.h中。

int pthread_create(
	pthread_t *tidp,
	const pthread_attr_t *attr,
	(void*)(*start_rtn)(void*),
	void *arg
);

  参数说明:
  tidp:第一个参数为指向线程标识符的指针。
  attr:用来设置线程属性。
  start_rtn:线程运行函数的起始地址。
  arg:运行函数的参数。
  若线程创建成功,则返回0。若线程创建失败,则返回出错编号,并且*thread中的内容是未定义的。
  返回成功时,由tidp指向的内存单元被设置为新创建线程的线程ID。attr参数用于指定各种不同的线程属性。新创建的线程从start_rtn函数的地址开始运行,该函数只有一个万能指针参数arg,如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg的参数传入。

线程运行

  一般来说,线程创建之后,会自动运行,如使用_beginthread创建的线程,但是当设置了线程启动状态时,可以控制线程的启动时刻,使用CreateThread和_beginthreadex创建线程才能完成这个操作,将creation flag设置为CREATE_SUSPENDED,这时线程创建后将被挂起,直到调用ResumeThread函数激活线程。
  代码示例:

#include "stdafx.h"
#include "windows.h"
#include "process.h"

//线程函数定义
unsigned __stdcall ThreadFunc(void* param)
{
	Sleep(1000);
	return 0;
}

int main()
{
	unsigned uiThreadId = 0;	//线程ID
	//创建线程并挂起
	HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, NULL, CREATE_SUSPENDED, &uiThreadId);
   //激活线程
	ResumeThread(hThread);

	return 0;
}

  在线程运行过程中,我们往往要等待线程运行结束再往下执行。可以使用WaitForSingleObject来执行这一工作。

DWORD WINAPI WaitForSingleObject(
__in HANDLE hHandle,
__in DWORD dwMilliseconds
);

  参数说明:
  hHandle:线程句柄
  dwMilliseconds:等待时间,单位为milliseconds(毫秒)。如果指定一个非零值,函数处于等待状态直到hHandle标记的对象被触发,或者时间到了。如果dwMilliseconds为0,对象没有被触发信号,函数不会进入一个等待状态,它总是立即返回。如果dwMilliseconds为INFINITE,对象被触发信号后,函数才会返回。
  代码示例:

#include "stdafx.h"
#include "windows.h"
#include "process.h"

//线程函数定义
unsigned __stdcall ThreadFunc(void* param)
{
	Sleep(1000);
	return 0;
}

int main()
{
	unsigned uiThreadId = 0;	//线程ID
	//创建线程并挂起
	HANDLE hThread = (HANDLE)_beginthreadex(NULL, 0, ThreadFunc, NULL, CREATE_SUSPENDED, &uiThreadId);
   //激活线程
	ResumeThread(hThread);

	WaitForSingleObject(hThread, INFINITE);
	return 0;
}

结束线程

  每个创建线程函数,都会有着对应的结束线程函数,CreateThread对应ExitThread,_beginthread对应_endthread,_beginthreadex对应_endthreadex,可以在必须强制结束线程的时候调用它们,但是最好避免这样做,因为中途强制结束,会导致不可控制的资源泄漏。一般而言,等待线程函数自行return是最好的方式,这时系统会自动调用线程终止函数,并释放所有资源。

C/C++ Muti-Thread多线程编程学习(之)线程Thread | 创建、运行、结束的更多相关文章

  1. 多线程编程学习笔记——async和await(一)

    接上文 多线程编程学习笔记——任务并行库(一) 接上文 多线程编程学习笔记——任务并行库(二) 接上文 多线程编程学习笔记——任务并行库(三) 接上文 多线程编程学习笔记——任务并行库(四) 通过前面 ...

  2. 多线程编程学习笔记——async和await(二)

    接上文 多线程编程学习笔记——async和await(一) 三.   对连续的异步任务使用await操作符 本示例学习如何阅读有多个await方法方法时,程序的实际流程是怎么样的,理解await的异步 ...

  3. 多线程编程学习笔记——async和await(三)

    接上文 多线程编程学习笔记——async和await(一) 接上文 多线程编程学习笔记——async和await(二) 五.   处理异步操作中的异常 本示例学习如何在异步函数中处理异常,学习如何对多 ...

  4. 多线程编程学习笔记——使用异步IO(一)

    接上文 多线程编程学习笔记——使用并发集合(一) 接上文 多线程编程学习笔记——使用并发集合(二) 接上文 多线程编程学习笔记——使用并发集合(三) 假设以下场景,如果在客户端运行程序,最的事情之一是 ...

  5. 多线程编程学习笔记——使用异步IO

    接上文 多线程编程学习笔记——使用并发集合(一) 接上文 多线程编程学习笔记——使用并发集合(二) 接上文 多线程编程学习笔记——使用并发集合(三) 假设以下场景,如果在客户端运行程序,最的事情之一是 ...

  6. 多线程编程学习笔记——编写一个异步的HTTP服务器和客户端

    接上文 多线程编程学习笔记——使用异步IO 二.   编写一个异步的HTTP服务器和客户端 本节展示了如何编写一个简单的异步HTTP服务器. 1.程序代码如下. using System; using ...

  7. 多线程编程学习笔记——异步调用WCF服务

    接上文 多线程编程学习笔记——使用异步IO 接上文 多线程编程学习笔记——编写一个异步的HTTP服务器和客户端 接上文 多线程编程学习笔记——异步操作数据库 本示例描述了如何创建一个WCF服务,并宿主 ...

  8. 学习笔记——线程 Thread

    Thread是.net1.0 1.1时出现的 主要了解线程等待.回调.前后台线程区别 1.实例: //定义:public delegate void ThreadStart(); ThreadStar ...

  9. [Java123] JDBC and Multi-Threading 多线程编程学习笔记

    项目实际需求:DB交互使用多线程实现 多线程编程基础:1.5  :( (假设总分10) 计划一个半月从头学习梳理Java多线程编程基础以及Oracle数据库交互相关的多线程实现 学习如何通过代码去验证 ...

随机推荐

  1. VS2017编译64位CloudCompare

    需求:编译一个支持读写las点云的CC,然后再开发CC插件实现业务功能. 编译环境: 1.Windows 10 2.Visual Studio 2017 Community 3.Qt 5.9.4 开源 ...

  2. 文件上传绕过WAF

    文件上传 文件上传实质上还是客户端的POST请求,消息主体是一些上传信息.前端上传页面需要指定 enctype为multipart/from-data才能正常上传文件. 此处不讲各种中间件解析漏洞只列 ...

  3. vue中js获取组件实例

    获取到的VM实例,外部js仍然能自由调用VM的一切属性和方法. <template> </template> <script> // 声明变量currVM let ...

  4. Vue学习笔记【19】——Vue中的动画(使用第三方 CSS 动画库)

    导入动画类库:  <link rel="stylesheet" type="text/css" href="./lib/animate.css& ...

  5. thinkphp SAE

    SAE介绍 Sina App Engine(简称SAE)是新浪研发中心开发的国内首个公有云计算平台,是新浪云计算战略的核心组成部分,作为一个简单高效的分布式Web服务开发.运行平台越来越受开发者青睐. ...

  6. Linux文件大小 指令&编程

    在工作和日常的编程中时常需要确定文件的大小,一些基本的查看方式在此做一个总结. 一. linux shell环境下 df可以查看一级文件夹大小.使用比例.档案系统及其挂入点,但对文件却无能为力.    ...

  7. MakeDown渲染出错

    MakeDown渲染出错 makedown作为程序员不可或缺的编辑工具,平时的使用技巧也是非常多的. 今天给新电脑装了一个,发现出现了错误(win10环境下),如图: 错误的表现形式即:不能实时预览M ...

  8. JAVA学习之Java语音基础组成

    Java语音基础组成一.关键字:被Java赋予含义的单词(如class.interface) 二.标识符:在程序中自定义的一些名称三.注释:单行注释(//).多行注释(/**/).文档注释(被java ...

  9. IdentityServer4认证服务器集成Identity&配置持久化数据库

    文章简介 asp.net core的空Web项目集成相关dll和页面文件配置IdnetityServer4认证服务器 Ids4集成Identity Ids4配置持久化到数据库 写在最前面,此文章不详细 ...

  10. ( 转)WPF面板布局介绍Grid、StackPanel、DockPanel、WrapPanel

    回顾 上一篇,我们介绍了基本控件及控件的重要属性和用法,我们本篇详细介绍WPF中的几种布局容器及每种布局容器的使用场景,当 然这些都是本人在实际项目中的使用经验,可能还存在错误之处,还请大家指出. 本 ...