线程的创建与结束
Posted vector6_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程的创建与结束相关的知识,希望对你有一定的参考价值。
线程的创建与结束
我们知道不管是哪个库还是哪种高级语言(如Java),线程的创建最终还是调用操作系统的 API 来进行的。本文主要对操作系统的接口进行介绍。
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");
}
}
int main()
{
pthread_t threadid;
pthread_create(&threadid, NULL, threadfunc, NULL);
while (1)
{
sleep(1);
//权宜之计,让主线程不要提前退出
}
return 0;
}
CRT线程创建
这里的CRT,指的是C Runtime(C运行时),通俗地说就是 C 函数库。C 库也提供了一套用于创建线程的函数(当然这个函数底层还是调用相应的操作系统平台的线程创建 API),这里之所以提到这点是因为,由于C库函数是同时被 Linux 和 Windows 等操作系统支持的,所以使用 C 库函数创建线程可以直接写出跨平台的代码。由于其跨平台性,实际项目开发中推荐使用这个函数来创建线程。
C库创建线程常用的函数是 _beginthreadex,声明位于 process.h 头文件中,其签名如下:
uintptr_t _beginthreadex(
void *security,
unsigned stack_size,
unsigned ( __stdcall *start_address )( void * ),
void *arglist,
unsigned initflag,
unsigned *thrdaddr
);
函数签名基本上和 Windows 上的 CreateThread 函数基本一致。
以下是使用 _beginthreadex 创建线程的一个例子:
#include <process.h>
//#include <Windows.h>
#include <stdio.h>
unsigned int __stdcall threadfun(void* args)
{
while (true)
{
//Sleep(1000);
printf("I am New Thread!\\n");
}
}
int main(int argc, char* argv[])
{
unsigned int threadid;
_beginthreadex(0, 0, threadfun, 0, 0, &threadid);
while (true)
{
//Sleep(1000);
//权宜之计,让主线程不要提前退出
}
return 0;
}
C++ 11 提供的 std::thread 类
无论是 Linux 还是 Windows 上创建线程的 API,都有一个非常不方便的地方,就是线程函数的签名必须是固定的格式(参数个数和类型、返回值类型都有要求)。C++11 新标准引入了一个新的类 std::thread(需要包含头文件\\),使用这个类的可以将任何签名形式的函数作为线程函数。以下代码分别创建两个线程,线程函数签名不一样:
#include <stdio.h>
#include <thread>
void threadproc1()
{
while (true)
{
printf("I am New Thread 1!\\n");
}
}
void threadproc2(int a, int b)
{
while (true)
{
printf("I am New Thread 2!\\n");
}
}
int main()
{
//创建线程t1
std::thread t1(threadproc1);
//创建线程t2
std::thread t2(threadproc2, 1, 2);
while (true)
{
//Sleep(1000);
//权宜之计,让主线程不要提前退出
}
return 0;
}
当然,std::thread 在使用上容易犯一个错误,即在 std::thread 对象在线程函数运行期间必须是有效的。例如:
#include <stdio.h>
#include <thread>
void threadproc()
{
while (true)
{
printf("I am New Thread!\\n");
}
}
void func()
{
std::thread t(threadproc);
}
int main()
{
func();
while (true)
{
//Sleep(1000);
//权宜之计,让主线程不要提前退出
}
return 0;
}
上述代码在 func 中创建了一个线程,然后又在 main 函数中调用 func 方法,乍一看好像代码没什么问题,但是在实际运行时程序会崩溃。崩溃的原因是,当 func 函数调用结束后,func 中局部变量 t (线程对象)被销毁了,而此时线程函数仍然在运行。这就是上文所说的,使用 std::thread 类时,必须保证线程函数运行期间,其线程对象有效。
对于这个错误,解决这个问题的方法是:std::thread 对象提供了一个 detach 方法,这个方法让线程对象与线程函数脱离关系,这样即使线程对象被销毁,仍然不影响线程函数的运行。我们只需要在在 func 函数中调用 detach 方法即可,代码如下:
//其他代码保持不变,这里就不重复贴出来了
void func()
{
std::thread t(threadproc);
t.detach();
}
然而,在实际编码中,这也是一个不推荐的做法,原因是我们需要使用线程对象去控制和管理线程的运行和生命周期。所以,我们的代码应该尽量保证线程对象在线程运行期间有效,而不是单纯地调用 detach 方法使线程对象与线程函数的运行分离。
Linux 系统线程 ID 的本质
Linux 系统中有三种方式可以获取一个线程的 ID:
方法一
调用 pthread_create 函数时,第一个参数在函数调用成功后可以得到线程 ID:
#include <pthread.h>
pthread_t tid;
pthread_create(&tid, NULL, thread_proc, NULL);
方法二
在需要获取 ID 的线程中调用 pthread_self() 函数获取。
#include <pthread.h>
pthread_t tid = pthread_self();
方法三
通过系统调用获取线程 ID
#include <sys/syscall.h>
#include <unistd.h>
int tid = syscall(SYS_gettid);
方法一和方法二获取的线程 ID 结果是一样的,这是一个 pthread_t,输出时本质上是一块内存空间地址,示意图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z6ZnHTwG-1622732945153)(C:\\Users\\cfs\\Desktop\\博客图片\\threadid.png)]
由于不同的进程可能有同样地址的内存块,因此方法一和方法二获取的线程 ID 可能不是全系统唯一的,一般是一个很大的数字(内存地址)。而方法三获取的线程 ID 是 系统范围内全局唯一的,一般是一个不会太大的整数,这个数字也是就是所谓的 LWP (Light Weight Process,轻量级进程,早期的 Linux 系统的线程是通过进程来实现的,这种线程被称为轻量级线程)的 ID。
C++11 的获取当前线程ID的方法
C++11的线程库可以使用 std::this_thread 类的 get_id 获取当前线程的 id,这是一个类静态方法。
当然也可以使用 std::thread 的 get_id 获取指定线程的 id,这是一个类实例方法。
但是 get_id 方法返回的是一个包装类型的 std::thread::id 对象,不可以直接强转成整型,也没有提供任何转换成整型的接口。所以,我们一般使用 std::cout 这样的输出流来输出,或者先转换为 std::ostringstream 对象,再转换成字符串类型,然后把字符串类型转换成我们需要的整型。
//test_cpp11_thread_id.cpp
#include <thread>
#include <iostream>
#include <sstream>
void worker_thread_func()
{
while (true)
{
}
}
int main()
{
std::thread t(worker_thread_func);
//获取线程t的ID
std::thread::id worker_thread_id = t.get_id();
std::cout << "worker thread id: " << worker_thread_id << std::endl;
//获取主线程的线程ID
std::thread::id main_thread_id = std::this_thread::get_id();
//先将std::thread::id转换成std::ostringstream对象
std::ostringstream oss;
oss << main_thread_id;
//再将std::ostringstream对象转换成std::string
std::string str = oss.str();
std::cout << "main thread id: " << str << std::endl;
//最后将std::string转换成整型值
unsigned long long threadid = std::stoull(str);
std::cout << "main thread id: " << threadid << std::endl;
while (true)
{
//权宜之计,让主线程不要提前退出
}
return 0;
}
等待线程结束
Linux 下等待线程结束
Linux 线程库提供了 pthread_join 函数,用来等待某线程的退出并接收它的返回值。这种操作被称为连接(joining),pthread_join 函数签名如下:
int pthread_join(pthread_t thread, void** retval);
参数 thread,需要等待的线程 id。
参数 retval,输出参数,用于接收等待退出的线程的退出码(Exit Code),线程退出码可以通过调用 pthread_exit 退出线程时指定,也可以在线程函数中通过 return 语句返回。
#include <pthread.h>
void pthread_exit(void* value_ptr);
参数 value_ptr 的值可以在 pthread_join 中拿到,没有可以设置为 NULL。
pthread_join 函数等待其他线程退出期间会挂起等待的线程,被挂起的线程不会消耗任何CPU时间片。直到目标线程退出后,等待的线程会被唤醒。
C++ 11 提供的等待线程结果函数
可以想到,C++ 11 的 std::thread 既然统一了 Linux 和 Windows 的线程创建函数,那么它应该也提供等待线程退出的接口,确实如此,std::thread 的 join 方法就是用来等待线程退出的函数。当然使用这个函数时,必须保证该线程还处于运行中状态,也就是说等待的线程必须是可以 “join”的,如果需要等待的线程已经退出,此时调用join 方法,程序会产生崩溃。因此,C++ 11 的线程库同时提供了一个 joinable 方法来判断某个线程是否可以join,如果不确定您的线程是否可以”join”,可以先调用 joinable 函数判断一下是否需要等待。例如:
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <thread>
#define TIME_FILENAME "time.txt"
void FileThreadFunc()
{
time_t now = time(NULL);
struct tm* t = localtime(&now);
char timeStr[32] = { 0 };
sprintf_s(timeStr, 32, "%04d/%02d/%02d %02d:%02d:%02d",
t->tm_year + 1900,
t->tm_mon + 1,
t->tm_mday,
t->tm_hour,
t->tm_min,
t->tm_sec);
//文件不存在,则创建;存在,则覆盖。
FILE* fp = fopen(TIME_FILENAME, "w");
if (fp == NULL)
{
printf("Failed to create time.txt.\\n");
return;
}
size_t sizeToWrite = strlen(timeStr) + 1;
size_t ret = fwrite(timeStr, 1, sizeToWrite, fp);
if (ret != sizeToWrite)
{
printf("Write file error.\\n");
}
fclose(fp);
}
int main()
{
std::thread t(FileThreadFunc);
if (t.joinable())
t.join();
//使用r选项,要求文件必须存在
FILE* fp = fopen(TIME_FILENAME, "r");
if (fp == NULL)
{
printf("open file error.\\n");
return -2;
}
char buf[32] = { 0 };
int sizeRead = fread(buf, 1, 32, fp);
if (sizeRead == 0)
{
printf("read file error.\\n");
return -3;
}
printf("Current Time is: %s.\\n", buf);
return 0;
}
以上是关于线程的创建与结束的主要内容,如果未能解决你的问题,请参考以下文章
C++并发与多线程 2_线程启动结束,创建线程多种方法,join,detach