线程的创建与结束

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;如果创建失败,则返回响应的错误码,常见的错误码有 EAGAINEINVALEPERM

下面是一个使用 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

线程的创建与结束

父线程 在创建了子线程后,是否继续执行自己的代码?它可以在子线程结束前 结束吗?

多线程实现udp网络通信

线程池与并行度

守护线程与守护进程