了解线程的基本概念之后,就可以开始动手创建线程了。
在 Linux 下进行线程编程,常见的方式是使用 POSIX 线程库,也就是 Pthreads。相关函数的名称大多以 pthread_ 开头,例如创建线程的 pthread_create()、等待线程结束的 pthread_join(),以及线程退出相关的函数。
这几个函数看着不复杂,但真正写程序时,需要弄清楚线程入口函数的写法、错误处理、退出方式和资源回收问题。
一、使用 pthread_create 创建线程
先看一下函数原型:
cpp
#include <pthread.h>
int pthread_create(
pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void *),
void *arg
);
这四个参数分别是:
thread:用于接收新线程的线程 ID。attr:用于设置线程属性,传入NULL表示使用默认属性。start_routine:线程启动后要执行的函数。arg:传递给线程入口函数的参数。
成功时,函数返回 0;失败时,返回相应的错误码。
这里有个容易犯错的地方:Pthreads 函数的错误信息通常通过返回值给出,而不是统一通过 errno 判断。
因此,判断线程创建是否成功,应当检查 pthread_create() 的返回值。
一个简单的创建线程例子
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
#include <cstdlib>
void* thread_run(void* arg)
{
const char* name = static_cast<const char*>(arg);
for (int i = 0; i < 5; ++i)
{
std::cout << name << " is running" << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
int ret = pthread_create(
&tid,
nullptr,
thread_run,
const_cast<char*>("new thread")
);
if (ret != 0)
{
std::cerr << "pthread_create failed: "
<< strerror(ret) << std::endl;
return 1;
}
for (int i = 0; i < 5; ++i)
{
std::cout << "main thread is running" << std::endl;
sleep(1);
}
pthread_join(tid, nullptr);
return 0;
}
编译时可以使用:
bash
g++ main.cpp -o main -pthread
运行后,可以看到主线程和新线程分别输出自己的内容。具体输出的先后顺序不一定每次都相同,这是因为两个线程的调度由操作系统决定。
这段代码还体现了一个特点:新线程并不是从 main() 的第一行重新执行,而是从传入的 thread_run() 开始执行。
线程入口函数有什么要求?
在这个接口中,线程入口函数通常采用下面的形式:
cpp
void* thread_run(void* arg);
它接收一个 void* 参数,并返回一个 void* 类型的结果。通过这个参数,可以向线程传递需要处理的数据;通过返回值,也可以向等待线程传递结果。
不过,传递指针时需要特别注意它指向的对象的生命周期。如果传递的是某个函数的局部变量地址,而该变量在新线程使用前就已经失效,那么程序就可能出现未定义行为。
二、线程是怎么结束的?
一个线程完成任务后,需要结束自己的执行。常见的方式有三种。
1. 从线程入口函数 return
cpp
void* thread_run(void* arg)
{
// 执行任务
return nullptr;
}
当普通线程的入口函数返回时,这个线程就结束了。
不过,主线程需要区别对待。主线程从 main() 返回,相当于正常结束进程,而不是只结束主线程。因此,不能简单地认为主线程返回后,其他线程还会一直运行。
2. 调用 pthread_exit
pthread_exit() 用于终止调用它的线程:
cpp
#include <pthread.h>
void* thread_run(void* arg)
{
pthread_exit(nullptr);
}
它可以在需要的位置结束当前线程,而不必通过线程入口函数的 return 返回。
如果在主线程中调用 pthread_exit(),主线程本身会结束,但进程中的其他线程仍然可以继续执行。因此,如果主线程希望自己退出、让其他线程继续完成工作,可以了解这种方式。
3. 调用 pthread_cancel
一个线程也可以请求取消同一进程中的另一个线程:
cpp
pthread_cancel(tid);
这里的 tid 是目标线程的线程 ID。
需要注意,pthread_cancel() 不是简单地保证目标线程在函数调用的那一刻就立刻停止。线程取消涉及取消状态和取消类型等机制,实际编写程序时还需要考虑资源释放与清理问题。
对于正在执行普通任务的线程,通常更推荐先设计清楚退出条件,而不是随意使用取消操作。
三、为什么线程退出后还需要等待?
线程执行结束,并不意味着相关资源就一定会立刻全部释放。
对于默认可连接(joinable)的线程,结束后仍然需要通过线程等待操作获取终止状态并完成资源回收。否则,相关资源可能一直得不到释放。
这就需要 pthread_join()。
cpp
#include <pthread.h>
int pthread_join(pthread_t thread, void **value_ptr);
其中,thread 表示需要等待的线程,value_ptr 用于接收线程的返回值。如果对返回值不感兴趣,可以传入 nullptr。
调用 pthread_join() 的线程会等待指定线程结束。如果目标线程已经结束,等待操作可以完成回收;如果目标线程还在执行,调用者通常会阻塞等待。
例如:
cpp
#include <iostream>
#include <pthread.h>
void* thread_run(void* arg)
{
std::cout << "new thread is running" << std::endl;
return nullptr;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, nullptr, thread_run, nullptr);
if (ret != 0)
{
std::cerr << "create thread failed: " << ret << std::endl;
return 1;
}
ret = pthread_join(tid, nullptr);
if (ret != 0)
{
std::cerr << "join thread failed: " << ret << std::endl;
return 1;
}
std::cout << "thread finished" << std::endl;
return 0;
}
这里的等待操作有两个作用:一是确认目标线程已经结束,二是完成对可连接线程的回收。
需要区分线程等待和进程等待。进程学习中有 wait()、waitpid(),线程编程中则有 pthread_join()。它们解决的问题有相似之处,但操作对象不同。
四、如何获取线程的返回值?
线程入口函数的返回值可以通过 pthread_join() 获取。
例如,让线程分配一块内存保存结果:
cpp
#include <pthread.h>
#include <iostream>
#include <cstdlib>
void* thread_run(void* arg)
{
int* result = new int(123);
return result;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, nullptr, thread_run, nullptr);
if (ret != 0)
{
std::cerr << "create failed: " << ret << std::endl;
return 1;
}
void* value = nullptr;
ret = pthread_join(tid, &value);
if (ret != 0)
{
std::cerr << "join failed: " << ret << std::endl;
return 1;
}
int* result = static_cast<int*>(value);
std::cout << *result << std::endl;
delete result;
return 0;
}
这里没有返回线程函数中的局部变量地址,而是使用动态分配的内存存放结果。因为函数执行完以后,线程入口函数中的局部变量已经不再有效,不能把它们的地址当作长期有效的返回值传出去。
通过 pthread_join() 取得结果后,主线程还需要负责释放这块内存。
这也是线程编程中经常需要考虑的问题:除了线程本身何时结束,还要明确它产生的数据由谁管理、谁负责释放。
五、线程分离:不关心返回值时怎么办?
有些线程只需要在后台执行某项任务,程序并不关心它的返回值,也不打算通过 pthread_join() 等待它。
这时候可以考虑线程分离。
cpp
pthread_detach(tid);
也可以在线程内部执行:
cpp
pthread_detach(pthread_self());
pthread_self() 可以获取当前线程的 Pthreads 线程 ID。
默认情况下,新创建的线程是可连接的。线程退出后,需要通过 pthread_join() 完成回收;分离线程则不需要调用者再使用 pthread_join() 回收,线程结束时,相关资源会由系统自动处理。
要注意,可连接与分离是两种不同的线程状态,一个线程不能同时既是可连接的又是分离的。
因此,在创建和管理线程时,最好提前决定是否需要获取返回值。如果需要结果,就采用可连接的方式并进行等待;如果不需要等待和获取结果,可以考虑使用分离线程。
六、线程控制中几个容易忽略的问题
1. 创建线程之后,不要假设它立即执行
pthread_create() 成功后,新线程已经创建,但它什么时候真正获得 CPU,不能由调用者直接决定。
如果主线程立刻结束整个进程,那么新创建的线程也会随进程一起结束。因此,需要合理安排主线程的生命周期。
2. 不要把局部变量地址随便传给其他线程
假如主线程把某个局部变量的地址传给新线程,但主线程很快就离开了变量的作用域,新线程之后再访问该地址,就会出问题。
传参时要保证对象在线程使用期间始终有效。动态分配的内存、生命周期合适的共享对象等,都需要结合实际情况选择。
3. 线程函数的错误处理要规范
像 pthread_create() 和 pthread_join() 这样的函数,成功返回 0,失败则返回错误码。程序应该根据返回值判断操作是否成功,必要时通过 strerror(ret) 获取错误信息。
如果忽略这些返回值,线程没有创建成功或者等待操作失败时,后续逻辑就可能出现问题。
七、总结
这一篇主要解决线程怎么创建、怎么结束,以及结束后如何处理资源的问题。
pthread_create() 用于创建线程,return 和 pthread_exit() 可以结束当前线程,pthread_cancel() 可以请求取消目标线程。对于可连接线程,pthread_join() 用于等待线程结束并获取返回值;如果不需要等待和获取返回值,则可以考虑将线程分离。
写多线程程序时,不能只关注线程入口函数中的任务代码,还必须考虑参数的生命周期、线程什么时候结束,以及退出以后由谁负责回收资源。