Linux —— 线程控制
我们今天接着来学习线程:
创建多个线程
我们可以结合以前的知识,创建多个线程:
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <vector>
#include <functional>
#include <time.h>
#include <pthread.h>
// 使用std::function作为函数包装器类型别名
using func_t = std::function<void()>;
// 声明线程池中线程的数量
const int thread_num = 5;
// 定义一个结构体来保存线程的相关数据
class ThreadData {
public:
// 构造函数初始化线程名称、创建时间和函数执行体
ThreadData(const std::string &name, const uint64_t &ctime, func_t f)
: thread_name(name), create_time(ctime), func(f) {}
// 线程名称
std::string thread_name;
// 创建时间(以Unix时间戳表示)
uint64_t create_time;
// 线程执行的函数对象
func_t func;
};
// 线程处理函数,由pthread_create调用
void *Threadhandler(void *args) {
// 强制类型转换从void*到ThreadData*
ThreadData *td = static_cast<ThreadData *>(args);
while (true) {
// 打印线程信息并执行函数体
std::cout << "new thread thread name: " << td->thread_name
<< " create time: " << td->create_time << std::endl;
td->func();
// 模拟工作并让线程休眠1秒
sleep(1);
}
// 注意:此函数理论上不会返回,因为while(true),但C++要求非void*函数有返回值,这里返回nullptr以满足编译要求
return nullptr;
}
// 示例函数,供线程执行
void Print() {
std::cout << "This is a process" << std::endl;
}
int main() {
// 用于存储所有创建的线程ID的向量
std::vector<pthread_t> threads;
// 循环创建指定数量的线程
for(int i = 0; i < thread_num; i++) {
// 生成线程名称
std::string name = "thread " + std::to_string(i + 1);
pthread_t tid; // 线程ID
// 初始化每个线程的数据结构
ThreadData *td = new ThreadData(name, (uint64_t)time(nullptr), Print);
// 创建线程,传入处理函数和参数
pthread_create(&tid, nullptr, Threadhandler, td);
// 将创建的线程ID添加到向量中
threads.push_back(tid);
// 主线程休眠1秒,模拟间隔创建线程的效果
sleep(1);
}
// 打印所有创建的线程ID
std::cout << "thread ids: ";
for(const auto &tid: threads) {
std::cout << tid << ",";
}
std::cout << std::endl;
// 主线程可以在这里执行其他操作或等待,这里为了简化直接结束
// 实际应用中可能需要适当同步机制来管理这些线程的生命周期
return 0;
}
我们也可以写段代码,监视我们的线程:
while :; do ps -aL | head -1 && ps -aL | grep mocess; sleep 1; done
线程的优缺点
线程(Threads)是现代操作系统中一种重要的并发执行机制,允许程序内部的多个控制流并发执行。以下是线程的一些主要优缺点:
优点
缺点
同时,线程的健壮性并不是很优秀:
我们举个例子,我们故意触发异常:
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <vector>
#include <functional>
#include <time.h>
#include <pthread.h>
// 使用std::function作为函数包装器类型别名
using func_t = std::function<void()>;
// 声明线程池中线程的数量
const int thread_num = 5;
// 定义一个结构体来保存线程的相关数据
class ThreadData {
public:
// 构造函数初始化线程名称、创建时间和函数执行体
ThreadData(const std::string &name, const uint64_t &ctime, func_t f)
: thread_name(name), create_time(ctime), func(f) {}
// 线程名称
std::string thread_name;
// 创建时间(以Unix时间戳表示)
uint64_t create_time;
// 线程执行的函数对象
func_t func;
};
// 线程处理函数,由pthread_create调用
void *Threadhandler(void *args) {
int a = 10;
// 强制类型转换从void*到ThreadData*
ThreadData *td = static_cast<ThreadData *>(args);
while (true)
{
// 打印线程信息并执行函数体
std::cout << "new thread thread name: " << td->thread_name
<< " create time: " << td->create_time << std::endl;
td->func();
if(td->thread_name == "thread 1")
{
std::cout << td->thread_name << " alarm !!!!!" << std::endl;
a /= 0; // 故意制作异常
}
// 模拟工作并让线程休眠1秒
sleep(1);
}
// 注意:此函数理论上不会返回,因为while(true),但C++要求非void*函数有返回值,这里返回nullptr以满足编译要求
return nullptr;
}
// 示例函数,供线程执行
void Print() {
std::cout << "This is a process" << std::endl;
}
int main() {
// 用于存储所有创建的线程ID的向量
std::vector<pthread_t> threads;
// 循环创建指定数量的线程
for(int i = 0; i < thread_num; i++) {
// 生成线程名称
std::string name = "thread " + std::to_string(i + 1);
pthread_t tid; // 线程ID
// 初始化每个线程的数据结构
ThreadData *td = new ThreadData(name, (uint64_t)time(nullptr), Print);
// 创建线程,传入处理函数和参数
pthread_create(&tid, nullptr, Threadhandler, td);
// 将创建的线程ID添加到向量中
threads.push_back(tid);
// 主线程休眠1秒,模拟间隔创建线程的效果
sleep(1);
}
// 打印所有创建的线程ID
std::cout << "thread ids: ";
for(const auto &tid: threads) {
std::cout << tid << ",";
}
std::cout << std::endl;
// 主线程可以在这里执行其他操作或等待,这里为了简化直接结束
// 实际应用中可能需要适当同步机制来管理这些线程的生命周期
return 0;
}
我们这里1号线程触发异常,整个进程直接挂掉。
因此,虽然多线程可以提高程序的效率和响应性,但其健壮性依赖于开发者对并发控制和资源管理的精细设计。实践中,通常推荐使用高级并发工具、遵循最佳实践,并进行充分的测试来确保线程安全和提高程序的健壮性。
pthread_self
pthread_self可以获取自身的线程id:
进程和线程的关系
一般来说,线程和进程的关系是,牵一发而动全身,比如:一个线程退出,不能直接exit退出:
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <vector>
#include <functional>
#include <time.h>
#include <pthread.h>
// 使用std::function作为函数包装器类型别名
using func_t = std::function<void()>;
// 声明线程池中线程的数量
const int thread_num = 5;
// 定义一个结构体来保存线程的相关数据
class ThreadData
{
public:
// 构造函数初始化线程名称、创建时间和函数执行体
ThreadData(const std::string &name, const uint64_t &ctime, func_t f)
: thread_name(name), create_time(ctime), func(f) {}
// 线程名称
std::string thread_name;
// 创建时间(以Unix时间戳表示)
uint64_t create_time;
// 线程执行的函数对象
func_t func;
};
// 线程处理函数,由pthread_create调用
void *Threadhandler(void *args)
{
int a = 10;
// 强制类型转换从void*到ThreadData*
ThreadData *td = static_cast<ThreadData *>(args);
// 打印线程信息并执行函数体
std::cout << "new thread thread name: " << td->thread_name
<< " create time: " << td->create_time << std::endl;
td->func();
// if(td->thread_name == "thread 1")
// {
// std::cout << td->thread_name << " alarm !!!!!" << std::endl;
// a /= 0; // 故意制作异常
// }
// 模拟工作并让线程休眠1秒
sleep(1);
// 注意:此函数理论上不会返回,因为while(true),但C++要求非void*函数有返回值,这里返回nullptr以满足编译要求
exit(0); //exit返回
//return nullptr;
}
// 示例函数,供线程执行
void Print()
{
std::cout << "This is a process" << std::endl;
}
int main() {
// 用于存储所有创建的线程ID的向量
std::vector<pthread_t> threads;
// 循环创建指定数量的线程
for(int i = 0; i < thread_num; i++)
{
// 生成线程名称
std::string name = "thread " + std::to_string(i + 1);
pthread_t tid; // 线程ID
// 初始化每个线程的数据结构
ThreadData *td = new ThreadData(name, (uint64_t)time(nullptr), Print);
// 创建线程,传入处理函数和参数
pthread_create(&tid, nullptr, Threadhandler, td);
// 将创建的线程ID添加到向量中
threads.push_back(tid);
// 主线程休眠1秒,模拟间隔创建线程的效果
sleep(1);
}
// 打印所有创建的线程ID
std::cout << "thread ids: ";
for(const auto &tid: threads)
{
std::cout << tid << ",";
}
std::cout << std::endl;
// 主线程可以在这里执行其他操作或等待,这里为了简化直接结束
// 实际应用中可能需要适当同步机制来管理这些线程的生命周期
return 0;
}
我们这里创建了5个线程,但是线程1 exit会直接导致整个进程退出,所以,如何优雅的实现线程的退出呢?一般返回nullptr即可:
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <vector>
#include <functional>
#include <time.h>
#include <pthread.h>
// 使用std::function作为函数包装器类型别名
using func_t = std::function<void()>;
// 声明线程池中线程的数量
const int thread_num = 5;
// 定义一个结构体来保存线程的相关数据
class ThreadData
{
public:
// 构造函数初始化线程名称、创建时间和函数执行体
ThreadData(const std::string &name, const uint64_t &ctime, func_t f)
: thread_name(name), create_time(ctime), func(f) {}
// 线程名称
std::string thread_name;
// 创建时间(以Unix时间戳表示)
uint64_t create_time;
// 线程执行的函数对象
func_t func;
};
// 线程处理函数,由pthread_create调用
void *Threadhandler(void *args)
{
int a = 10;
// 强制类型转换从void*到ThreadData*
ThreadData *td = static_cast<ThreadData *>(args);
// 打印线程信息并执行函数体
std::cout << "new thread thread name: " << td->thread_name
<< " create time: " << td->create_time << std::endl;
td->func();
// if(td->thread_name == "thread 1")
// {
// std::cout << td->thread_name << " alarm !!!!!" << std::endl;
// a /= 0; // 故意制作异常
// }
// 模拟工作并让线程休眠1秒
sleep(1);
// 注意:此函数理论上不会返回,因为while(true),但C++要求非void*函数有返回值,这里返回nullptr以满足编译要求
//exit(0); //exit返回
return nullptr;
}
// 示例函数,供线程执行
void Print()
{
std::cout << "This is a process" << std::endl;
}
int main() {
// 用于存储所有创建的线程ID的向量
std::vector<pthread_t> threads;
// 循环创建指定数量的线程
for(int i = 0; i < thread_num; i++)
{
// 生成线程名称
std::string name = "thread " + std::to_string(i + 1);
pthread_t tid; // 线程ID
// 初始化每个线程的数据结构
ThreadData *td = new ThreadData(name, (uint64_t)time(nullptr), Print);
// 创建线程,传入处理函数和参数
pthread_create(&tid, nullptr, Threadhandler, td);
// 将创建的线程ID添加到向量中
threads.push_back(tid);
// 主线程休眠1秒,模拟间隔创建线程的效果
sleep(1);
}
// 打印所有创建的线程ID
std::cout << "thread ids: ";
for(const auto &tid: threads)
{
std::cout << tid << ",";
}
std::cout << std::endl;
// 主线程可以在这里执行其他操作或等待,这里为了简化直接结束
// 实际应用中可能需要适当同步机制来管理这些线程的生命周期
return 0;
}
同时,我们还可以用pthread_exit:
pthread_exit
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <vector>
#include <functional>
#include <time.h>
#include <pthread.h>
// 使用std::function作为函数包装器类型别名
using func_t = std::function<void()>;
// 声明线程池中线程的数量
const int thread_num = 5;
// 定义一个结构体来保存线程的相关数据
class ThreadData
{
public:
// 构造函数初始化线程名称、创建时间和函数执行体
ThreadData(const std::string &name, const uint64_t &ctime, func_t f)
: thread_name(name), create_time(ctime), func(f) {}
// 线程名称
std::string thread_name;
// 创建时间(以Unix时间戳表示)
uint64_t create_time;
// 线程执行的函数对象
func_t func;
};
// 线程处理函数,由pthread_create调用
void *Threadhandler(void *args)
{
int a = 10;
// 强制类型转换从void*到ThreadData*
ThreadData *td = static_cast<ThreadData *>(args);
// 打印线程信息并执行函数体
std::cout << "new thread thread name: " << td->thread_name
<< " create time: " << td->create_time << std::endl;
td->func();
// if(td->thread_name == "thread 1")
// {
// std::cout << td->thread_name << " alarm !!!!!" << std::endl;
// a /= 0; // 故意制作异常
// }
// 模拟工作并让线程休眠1秒
sleep(1);
// 注意:此函数理论上不会返回,因为while(true),但C++要求非void*函数有返回值,这里返回nullptr以满足编译要求
//exit(0); //exit返回
//return nullptr;
pthread_exit(nullptr);
}
// 示例函数,供线程执行
void Print()
{
std::cout << "This is a process" << std::endl;
}
int main() {
// 用于存储所有创建的线程ID的向量
std::vector<pthread_t> threads;
// 循环创建指定数量的线程
for(int i = 0; i < thread_num; i++)
{
// 生成线程名称
std::string name = "thread " + std::to_string(i + 1);
pthread_t tid; // 线程ID
// 初始化每个线程的数据结构
ThreadData *td = new ThreadData(name, (uint64_t)time(nullptr), Print);
// 创建线程,传入处理函数和参数
pthread_create(&tid, nullptr, Threadhandler, td);
// 将创建的线程ID添加到向量中
threads.push_back(tid);
// 主线程休眠1秒,模拟间隔创建线程的效果
sleep(1);
}
// 打印所有创建的线程ID
std::cout << "thread ids: ";
for(const auto &tid: threads)
{
std::cout << tid << ",";
}
std::cout << std::endl;
// 主线程可以在这里执行其他操作或等待,这里为了简化直接结束
// 实际应用中可能需要适当同步机制来管理这些线程的生命周期
return 0;
}
线程等待pthread_ join
线程既然是进程的迷你版,肯定也会有跟进程相关的地方,比如我们的线程退出时也是需要被等待的,而我们所用的接口就是就是pthread_join:
我们来举个例子:
#include <iostream> // 标准输入输出库
#include <unistd.h> // 定义了 usleep 函数,用于延迟线程执行
#include <cstring> // 字符串操作函数库
#include <vector> // 动态数组容器
#include <functional> // 函数对象包装器库
#include <time.h> // 时间相关函数库
#include <pthread.h> // POSIX 线程库
// 线程处理函数
void *Threadhandler(void *args)
{
usleep(1000); // 线程启动后暂停1毫秒
// 将传入的void指针转换为std::string类型,作为线程名称
std::string name = static_cast<const char*>(args);
int cnt = 5; // 循环计数器
// 输出线程信息并等待一秒,循环5次
while(cnt--)
{
std::cout << "I am a Light process My LWP is :" << pthread_self() << std::endl; // 打印当前线程的轻量级进程ID(LWP)
sleep(1); // 线程休眠1秒
}
return nullptr; // 线程结束,返回空指针
}
int main()
{
pthread_t tid; // 定义线程ID变量
// 创建新线程,传入线程处理函数、线程属性(nullptr表示使用默认属性)、入口参数和线程ID的地址
pthread_create(&tid, nullptr, Threadhandler, (void *)"thread - 1");
std::cout << "I am the main Light process My LWP is :" << pthread_self() << std::endl; // 打印主线程的LWP
sleep(10); // 主线程休眠10秒,保证子线程有足够时间运行
// 等待子线程tid结束,成功返回0,失败返回非零值。第二个参数接收线程的返回值,这里不需要所以传入nullptr
int n = pthread_join(tid, nullptr);
std::cout << "return value is :" << n << std::endl; // 打印pthread_join的返回值,表示是否成功加入线程
return 0; // 主程序结束
}
这里注意一下,如果线程退出并没有被等待,会导致类似像僵尸进程这样的问题,但是这个不怎么容易观察。
线程的返回值
我们来看看pthread_join的手册:
后面这个retval这个参数,在线程退出的时候,会把退出的结果放到retval中,意思就是这个retval是一个输出型参数,输入之后会把线程的退出结果带出来:
我们可以试一试:
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <vector>
#include <functional>
#include <time.h>
#include <pthread.h>
void *Threadhandler(void *args)
{
usleep(1000);
std::string name = static_cast<const char*>(args);
int cnt = 5;
while(cnt--)
{
std::cout << "I am a Light process My LWP is :" << pthread_self() << std::endl;
sleep(1);
}
return (void *)"thread-1";
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,Threadhandler,(void *)"thread - 1");
std::cout << "I am the main Light process My LWP is :" << pthread_self() << std::endl;
void *ret = nullptr;
int n = pthread_join(tid,&ret);
std::cout << "return value is :" << (const char*)ret << std::endl;
return 0;
}
我们这里返回的是一个字符串,我们看看我们能否打印的出来:
这里用pthread_exit也是可以的。
这里既然是void *说明我们是可以传递任何类型,我们也可以传递一个结构体回去:
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <string>
// 自定义线程返回结构体,用于存储线程退出时的相关信息
class ThreadReturn
{
public:
// 构造函数,初始化线程ID、信息和退出码
ThreadReturn(pthread_t id, std::string info, int code)
: _id(id), // 线程ID
_info(info), // 线程退出时的信息
_code(code) // 线程退出码
{
}
// 数据成员
pthread_t _id; // 线程的ID,在线程退出时记录
std::string _info; // 线程退出时的描述信息
int _code; // 线程退出的状态码
};
// 线程处理函数
void *Threadhandler(void *args)
{
usleep(1000); // 暂停1毫秒
std::string name = static_cast<const char*>(args); // 获取线程名称
int cnt = 5; // 循环计数器
// 循环输出线程信息并休眠,模拟工作过程
while(cnt--)
{
std::cout << "I am a Light process My LWP is :" << pthread_self() << std::endl;
sleep(1);
}
// 线程处理完毕,创建ThreadReturn实例以传递退出信息
ThreadReturn* ret = new ThreadReturn(pthread_self(), name, 10);
return ret; // 将ThreadReturn对象的地址作为线程的返回值
}
int main()
{
pthread_t tid; // 主线程中定义线程ID
// 创建新线程,传入处理函数、线程属性、参数和线程ID指针
pthread_create(&tid, nullptr, Threadhandler, (void *)"thread - 1");
std::cout << "I am the main Light process My LWP is :" << pthread_self() << std::endl; // 输出主线程ID
void *ret = nullptr; // 定义一个void指针来接收线程的返回值
int n = pthread_join(tid, &ret); // 等待线程tid结束,并获取其返回值到ret指针
// 将ret指针转换为ThreadReturn对象指针,以便访问其中的数据
ThreadReturn* r = static_cast<ThreadReturn*>(ret);
if(r != nullptr) { // 确保转换成功
std::cout << "return value is :" << "id :" << r->_id << std::endl;
std::cout << "return value is :" << "info :" << r->_info << std::endl;
std::cout << "return value is :" << "code :" << r->_code << std::endl;
delete r; // 释放分配的内存
}
return 0; // 主程序结束
}
线程分离
我们之前通过实验看到了线程和线程之间的关联,线程退出之后要进行回收。
但其实,如果我们的主线程只想完成自己的任务,而并不想管其他的线程可不可以呢?答案是可以的,我们可以进行线程分离,使之主线程不管其他线程的死活:
pthread_detach
pthread_detach()
是POSIX线程库中的一个函数,用于改变指定线程的分离状态。当一个线程被“分离”(detached)时,它会在执行结束后自动被系统回收资源,而不需要其他线程调用pthread_join()
来显式等待它结束。这对于那些不需要收集线程返回值或者不需要精确控制线程结束时间的场景非常有用。
函数原型如下:
int pthread_detach(pthread_t thread);
- 参数:
- 返回值:
功能:
使用场景:
注意事项:
void *Threadhandler(void *args)
{
usleep(1000);
std::string name = static_cast<const char*>(args);
std::cout << "I am a Light process My LWP is :" << pthread_self() << std::endl;
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,Threadhandler,(void *)"thread - 1");
pthread_detach(tid);
std::cout << "I am the main Light process My LWP is :" << pthread_self() << std::endl;
void *ret = nullptr;
int n = pthread_join(tid,&ret);
std::cout << "return value is :" << "code :"<< (long long int)n << std::endl;
return 0;
}
我们的错误码设置为了22,表示一个无效的输入:
线程取消
pthread_cancel
pthread_cancel()
是POSIX线程库中的一个函数,用于请求取消(cancellation)指定的线程。这意味着请求线程(调用pthread_cancel
的线程)向目标线程发送一个取消请求,目标线程在接收到这个请求后,根据其取消状态和取消类型,可以选择立即终止或在某个合适的时机终止执行。
函数原型如下:
int pthread_cancel(pthread_t thread);
- 参数:
- 返回值:
功能:
使用场景:
注意事项:
比如我们可以这样:
void *Threadhandler(void *args)
{
usleep(1000);
std::string name = static_cast<const char*>(args);
int cnt = 5;
while(cnt--)
{
std::cout << "I am a Light process My LWP is :" << pthread_self() << std::endl;
sleep(1);
}
//pthread_detach(pthread_self());
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,Threadhandler,(void *)"thread - 1");
std::cout << "I am the main Light process My LWP is :" << pthread_self() << std::endl;
//pthread_detach(tid);
int n = pthread_cancel(tid);
std::cout << "cancel value :" << strerror((int64_t)n) << std::endl;
void *ret = nullptr;
n = pthread_join(tid,&ret);
std::cout << "return value is :" << "code :"<< strerror((long long int)n) << std::endl;
return 0;
}
我们看到取消和等待都是成功了的,但是如果我们线程分离了呢?
void *Threadhandler(void *args)
{
usleep(1000);
std::string name = static_cast<const char*>(args);
int cnt = 5;
while(cnt--)
{
std::cout << "I am a Light process My LWP is :" << pthread_self() << std::endl;
sleep(1);
}
pthread_detach(pthread_self());
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,Threadhandler,(void *)"thread - 1");
std::cout << "I am the main Light process My LWP is :" << pthread_self() << std::endl;
pthread_detach(tid);
int n = pthread_cancel(tid);
std::cout << "cancel value :" << strerror((int64_t)n) << std::endl;
void *ret = nullptr;
n = pthread_join(tid,&ret);
std::cout << "return value is :" << "code :"<< strerror((long long int)n) << std::endl;
return 0;
}
我们发现,线程分离之后,可以完成线程取消,但是不能完成线程等待。
pthread_t 的理解
我们之前打印过pthread_t 的编号:
这串数字换成十六进制,其实就是一个地址,每一个线程都有自己的地址。
那么这个地址到底是什么呢?
我们首先知道,Linux在系统上并没有提供关于线程的接口,管理线程的是它原生的pthread库:
那么,我们每创建一个线程,库都要组织管理它,所以最后库中就会存储的有每个线程的结构地址:
而我们打印的那一大串数字就是每个struct_pthread 的地址。