2.1 线程的创建
在使用线程之前,我们首先要学会如何创建一个新的线程。不管是哪个库还是哪种高级语言(如Java),线程的创建最终还是调用操作系统的API来进行的。我们这里先介绍操作系统的接口,这里分Linux和Windows两个常用的操作系统平台来介绍。当然,这里并不是照本宣科地把Linux man手册或者msdn上的函数签名搬过来,这里只介绍我们实际开发中常用的参数和需要注意的重难点。
1. Linux 线程创建
Linux平台上使用pthread_create这个API来创建线程,其函数签名如下:
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),
void *arg);
-
参数thread,是一个输出参数,如果线程创建成功,通过这个参数可以得到创建成功的线程ID(下文会介绍线程ID的知识)。
-
参数attr指定了该线程的属性,一般设置为NULL,表示使用默认属性。
-
参数start_routine指定了线程函数,这里需要注意的是这个函数的调用方式必须是__cdecl调用,即C Declaration的缩写,这是C/C++中定义函数时默认的调用方式 ,一般很少有人注意到这一点。而后面我们介绍在Windows操作系统上使用CreateThread定义线程函数时必须使用__stdcall调用方式时,由于函数不是默认函数调用方式,所以我们必须显式声明函数的调用方式了。
也就是说,如下函数的调用方式是等价的:
//代码片段1:不显式指定函数调用方式,其调用方式为默认的__cdecl
void* start_routine (void* args)
{
}
//代码片段2:显式指定函数调用方式为默认的__cdecl,等价于代码片段1
void* __cdecl start_routine (void* args)
{
}
-
参数arg,通过这一参数可以在创建线程时将某个参数传入线程函数中,由于这是一个void*类型,可以方便我们最大化地传入任意多的信息给线程函数。(下文会介绍一个使用示例)
-
返回值:如果成功创建线程,返回0;如果创建失败,则返回响应的错误码,常见的错误码有EAGAIN、EINVAL、EPERM。
下面是一个使用pthread_create创建线程的简单示例:
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
void* threadfunc(void* arg)
{
while (1)
{
//睡眠1秒
sleep(1);
printf("I am New Thread!\n");
}
return NULL;
}
int main()
{
pthread_t threadid;
pthread_create(&threadid, NULL, threadfunc, NULL);
//权宜之计,让主线程不要提前退出
while (1)
{
sleep(1);
}
return 0;
}
2. Windows线程创建
Windows上创建线程使用CreateThread,其函数签名如下:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
-
参数lpThreadAttributes,是线程的安全属性,一般设置为NULL。
-
参数dwStackSize,线程的栈空间大小,单位为字节数,一般指定为0,表示使用默认大小。
-
参数lpStartAddress,为线程函数,其类型是LPTHREAD_START_ROUTINE,这是一个函数指针类型,其定义如下:
typedef DWORD ( __stdcall *LPTHREAD_START_ROUTINE )(LPVOID lpThreadParameter);
需要注意的是,Windows上创建的线程的线程函数其调用方式必须是__stdcall,如果将如下函数设置成线程函数是不行的:
DWORD threadfunc(LPVOID lpThreadParameter);
如上文所说,如果不指定函数的调用方式,默认使用默认调用方式__cdecl,而这里的线程函数要求是__stdcall,因此必须在函数名前面显式指定函数调用方式为__stdcall。
DWORD __stdcall threadfunc(LPVOID lpThreadParameter);
Windows上的宏WINAPI和CALLBACK这两个宏的定义都是__stdcall,因此在很多项目中看到的线程函数的签名大多写成如下两种形式的一种:
//写法1
DWORD WINAPI threadfunc(LPVOID lpThreadParameter);
//写法2
DWORD CALLBACK threadfunc(LPVOID lpThreadParameter);
-
参数lpParameter 为传给线程函数的参数,和Linux下的pthread_create函数的arg一样,这实际上也是一个void*类型(LPVOID类型实际上是用typedef 包装后的void*类型)。 typedef void* LPVOID;
-
参数dwCreationFlags,是一个32位无符号整型(DWORD),一般设置为0,表示创建好线程后立即启动线程的运行;有一些特殊的情况,我们不希望创建线程后立即开始执行,可以将这个值设置为4(对应Windows定义的宏CREATE_SUSPENDED),后面在需要的时候,再使用ResumeThread这个API让线程运行起来。
-
参数lpThreadId,为线程创建成功返回的线程ID,这也是一个32位无符号整数(DWORD)的指针(LPDWORD)。
-
返回值:Windows上使用句柄(HANDLE类型)来管理线程对象,句柄本质上是内核句柄表中的索引值。如果成功创建线程,返回该线程的句柄;如果创建失败,返回NULL。
下面的代码片段,演示了Windows上如何创建一个线程:
#include <Windows.h>
#include <stdio.h>
DWORD WINAPI ThreadProc(LPVOID lpParameters)
{
while (true)
{
//睡眠1秒,Windows上的Sleep函数参数事件单位为毫秒
Sleep(1000);
printf("I am New Thread!\n");
}
}
int main()
{
DWORD dwThreadID;
HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadID);
if (hThread == NULL)
{
printf("Failed to CreateThread.\n");
}
//权宜之计,让主线程不要提前退出
while (true)
{
Sleep(1000);
}
return 0;
}
3. Windows CRT提供的线程创建函数
这里的CRT,指的是C Runtime(C运行时),通俗地说就是C函数库。在Windows操作系统上,微软实现的C库也提供了一套用于创建线程的函数(当然这个函数底层还是调用相应的操作系统平台的线程创建 API)。在实际项目开发中推荐使用这个函数来创建线程而不是使用CreateThread函数。
Windows C库创建线程常用的函数是_beginthreadex,声明位于process.h头文件中,其签名如下:
uintptr_t _beginthreadex(
void *security,
unsigned stack_size,
unsigned ( __stdcall *start_address )( void * ),
void *arglist,
unsigned initflag,
unsigned *thrdaddr
);
_beginthreadex函数签名和Windows的CreateThreadAPI 函数基本一致,这里就不再赘述了。
以下是使用_beginthreadex创建线程的一个例子:
#include <process.h>
#include <stdio.h>
unsigned int __stdcall threadfun(void* args)
{
while (true)
{
printf("I am New Thread!\n");
}
}
int main(int argc, char* argv[])
{
unsigned int threadid;
_beginthreadex(0, 0, threadfun, 0, 0, &threadid);
//权宜之计,让主线程不要提前退出
while (true)
{
}
return 0;
}
4. C++11提供的std::thread类
无论是Linux还是Windows上创建线程的API,都有一个非常不方便的地方,就是线程函数的签名必须是固定的格式(参数个数和类型、返回值类型都有要求)。C++11新标准引入了一个新的类std::thread(需要包