目录
1.线程创建方法
例:多线程创建
2.线程终止
2.1 return nulptr;
2.2 pthread_exit(nullptr);
3. 线程等待
3.1 等待原因
3.2 等待方法
线程终止的返回值问题
4.线程取消
5. 线程分离
5.1 分离原因
5.2 分离方法
6.封装线程
用的接口是POSIX线程库(即原生线程库)
- 使用时头文件<pthrea.h>
- 编译器命令要带-lpthread选项
1.线程创建方法
- 参数:
- pthread_t * thread:输出型参数,返回线程id(tid,不是LWP)
- const pthread_attr_t *attr:线程属性,一般NULL默认
- start_routine:线程创建后要执行的回调函数
- arg:传给回调函数的参数
- 错误检查:pthread出错时不会设置errno,而是通过返回值返回。原因:因为线程缺乏访问控制,如果修改全局的errno,可能会影响到其他线程。
- 返回值:调用pthread函数时的错误码,跟回调函数返回值没关系。
例:多线程创建
#include <pthread.h>
#include <vector>
#include <iostream>
#include <unistd.h>
using namespace std;
void *start_routine(void *args)
{
string name = static_cast<char *>(args);
while (true)
{
cout << "我是线程" << name << endl;
sleep(1);
}
}
int main()
{
#define NUM 10
for (int i = 0; i < NUM; i++)
{
pthread_t tid;
char buffer[64] = {};
snprintf(buffer, sizeof(buffer), "%s:%d", "thread", i); // 给每个线程不一样的编号
pthread_create(&tid, NULL, start_routine, buffer);
sleep(1);//【创建时sleep和不sleep的两种不同结果】
}
while (true)
{
cout << "批量创建线程成功,我是主执行流" << endl;
sleep(1);
}
return 0;
}
结果1:创建时sleep
结果2:创建时不sleep
原因分析:给线程回调函数的参数是缓冲区地址buffer,主线程和子线程的调度顺序不能确定,所以可能是先子线程打印,也可能先主线程覆盖式写入buffer,每个子线程访问的都是覆盖后的buffer。
更正:
class ThreadData
{
public:
pthread_t tid;
char namebuffer[64];
};
void *start_routine(void *args)
{
sleep(1); // 子线程先不打印,让主线程先打印线程的创建情况
int cnt = 10;
ThreadData *td = static_cast<ThreadData *>(args);
while (cnt--)
{
cout << "我是线程" << td->namebuffer << " " << "还需执行任务次数:" << cnt << endl;
sleep(1);
}
return nullptr;
}
int main()
{
vector<ThreadData *> threads;
#define NUM 10
for (int i = 0; i < NUM; i++)
{
ThreadData *td = new ThreadData();
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i); // 每个线程一个new一个Data对象,一个专属buffer用来存name,不会再覆盖了
pthread_create(&td->tid, NULL, start_routine, td);
threads.push_back(td);
// sleep(1);
}
for (auto &iter : threads)
{
cout << "创建线程:" << iter->namebuffer << ":" << iter->tid << "success" << endl;
}
while (true)
{
cout << "批量创建线程成功,我是主执行流" << endl;
sleep(1);
}
return 0;
}
start_routine处于被重入状态,是可重入函数吗?是,没有访问临界资源,函数内部定义的局部变量不会互相影响,存储在线程各自的独立栈结构。
2.线程终止
exit不能用来终止线程,因为exit终止进程。
注意主线程如果退出(return或exit),整个进程退出。
2.1 return nulptr;
2.2 pthread_exit(nullptr);
返回值都暂时设置为nullptr,在线程等待部分讲解。
3. 线程等待
入如何拿到回调函数的返回值?线程等待。
线程如果不等待回收资源,会造成内存泄漏的问题,类似僵尸进程。
3.1 等待原因
1.获取线程的退出信息。
2.线程退出后,回收线程对应的PCB等内核资源,防止内存泄漏。OS没有提供方法查看"僵尸线程"。
3.2 等待方法
- 参数:
- pthread_t thread:线程tid,创建线程是的输出型参数。
- retval:二级指针
- 错误:由返回值int代表错误码,错误返回错误码,成功返回0。
- 等待方式:阻塞式等待
线程终止的返回值问题
pthread_exit退出时,参数是void*;return退出时,回调函数的返回值类型必须是void*的。void *(*start_routine) (void *)
int pthread_join(pthread_t thread, void **retval);
回调函数的返回值void*和pthread_join的二级指针void**是什么关系?
首先复习:
1.返回值是void*类型,存储void*的地址就必须用void**类型的指针变量。
2.指针和指针变量严格来说不一样
指针是一个字面值,指32位或64位地址(虚拟或物理地址),只能做右值。
指针变量是一个变量,变量里保存的是地址数据,可以做左值也可以做右值。
分析pthread_join需要二级指针来拿到返回值的原因:
函数调用的返回值存储在库当中,在库为线程创建的独立栈结构上。库想要把这个void*类型的变量传递给外部的、由用户定义的接收返回值的变量,需要【接收返回值的变量】的地址,才能直接修改外部变量。
回调函数的返回值为什么是void*,而不是直接传值返回
为了返回任意类型的对象,所以传指针返回。用户接收指针后自己强转类型就行。(类比模板。同理,回调函数的参数是void*的原因也就明白喽。)
再复习,函数返回值
1.如果传值返回,对返回值的存储位置没有要求,只需要拷贝一份给接收返回值的变量
2.如果传址返回,拷贝的是地址,只能返回【堆空间变量】的地址,不能返回【栈上变量】的地址,因为函数内的局部变量会在函数调用结束后释放,拷贝拿到的就是野指针。
例:线程回调函数返回自定义类型对象的指针
class ThreadReturn
{
public:
bool _result;
int _exit_code;
std::string _reason;
ThreadReturn(bool result, int exit_code, const string &reason)
: _result(result), _exit_code(exit_code), _reason(reason)
{
}
static ThreadReturn *ThreadReturn_success()
{
return new ThreadReturn(true, 0, "标准正确返回");
}
static ThreadReturn *ThreadReturn_success(int exit_code, const string &reason) // 自定义正确返回
{
return new ThreadReturn(true, exit_code, reason);
}
static ThreadReturn *ThreadReturn_false(int exit_code, const string &reason) // 自定义错误返回
{
return new ThreadReturn(false, exit_code, reason);
}
static ThreadReturn *ThreadReturn_false()
{
return new ThreadReturn(false, -1, "标准错误返回");
}
};
// 线程等待
for (auto &it : threads)
{
void *r = nullptr;
int ret = pthread_join(it->tid, &r);
assert(ret == 0);
cout << "回收:" << it->namebuffer << "线程返回值:" << ((ThreadReturn *)r)->_reason << endl;
delete it;
}
返回的时候,你可以return ThreadReturn::ThreadReturn_success();//这本来就是一个堆地址
不要直接 return &ThreadReturn(true, 0, "标准正确返回");//这是一个栈上的变量的地址。
通过线程等待只能拿到线程回调函数的返回值,而不包含退出信号。
因为信号发送给整个进程,子进程信号通过父进程进程等待获取,或进程调用signal捕捉信号。
4.线程取消
- 功能:一个线程可以调用pthread_ cancel终止同一进程中的线程。
- 方法
- 参数:pthread_t tid,就是线程创建时传入的输出型参数。
- 返回值:int,表示cancel函数的调用情况。
- 被取消线程的退出码(join拿到的函数返回值)是-1。
例:取消一部分线程,查看被取消线程的返回值
void *start_routine(void *args)
{
sleep(1); // 子线程先不打印,让主线程先打印线程的创建情况
int cnt = 10;
ThreadData *td = static_cast<ThreadData *>(args);
while (1)
{
cout << "我是线程" << td->namebuffer << endl;
sleep(1);
}
// return (void *)td->number;
return ThreadReturn::ThreadReturn_success();
}
int main()
{
vector<ThreadData *> threads;
#define NUM 10
for (int i = 0; i < NUM; i++)
{
ThreadData *td = new ThreadData();
td->number = i + 1;
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i); // 每个线程一个new一个Data对象,一个专属buffer用来存name,不会再覆盖了
pthread_create(&td->tid, NULL, start_routine, td);
threads.push_back(td);
}
for (auto &iter : threads)
{
cout << "创建线程:" << iter->namebuffer << ":" << iter->tid << "success" << endl;
}
// 线程取消
sleep(5); // 先让线程都调度起来才能取消
for (size_t i = 0; i < 5; i++)
{
auto iter = threads[i];
pthread_cancel(iter->tid);
cout << "取消线程" << iter->namebuffer << "success" << endl;
}
for (auto &it : threads) // 只能回收到被取消的线程,然后阻塞等待
{
void *r = nullptr;
int ret = pthread_join(it->tid, &r);
assert(ret == 0);
cout << "回收:" << it->namebuffer << "线程返回值:" << (long long)r << endl;
delete it;
}
return 0;
}
被取消线程将-1这个整形存入void*,这时注意别解引用*(int *)r
原因:
- r是64位地址,转int*精度丢失
- 转成*(uint64_t*)r,精度不丢失了,但还是不行。r里存的是-1,解引用指针变量的时候,会将变量里存的数据理解成地址-1,去访问-1地址。我们人为理解void*里存的就是函数/线程退出码,可不能解引用去访问这块地址的空间,不然会发生段错误越界访问。
这样才是对的:(long long)r
5. 线程分离
5.1 分离原因
主线程不想阻塞式等待,不在乎线程退出状态,想线程退出就自动回收线程资源。
5.2 分离方法
- 同一进程内其他线程对目标线程进行分离
- 也可以是线程自己分离
int pthread_detach(pthread_self());
- 获取调用线程的tid的方法
返回值:调用线程的tid。当pthread_self()函数被主线程(即main函数所在的线程)调用时,它返回的是主线程的线程PID。
线程一般都是joinable的,可以被pthread_join等待。如果线程为分离状态,就不能被等待,join会失败。
线程创建后,新线程和主线程的调度顺序不确定,如果主线程先join阻塞等待,而线程分离自己在后。主线程在不知道线程分离的情况下,开始阻塞等待,子线程就算后来分离成功,主线程也不知道,所以等子线程退出,就被主线程等待成功了。
所以建议由主线程去分离新线程。主线程往后被调度时,新线程一定处于分离态。
6.封装线程
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <cassert>
class Thread;
class Context
{
public:
Thread *_this;
void *_args;
public:
Context() : _this(nullptr), _args(nullptr) {}
~Context() {}
};
class Thread
{
public:
using func_t = std::function<void *(void *)>;
const int num = 1024;
public:
Thread(func_t func, void *args, int number)
: _func(func), _args(args)
{
char buffer[num] = {0};
snprintf(buffer, sizeof(buffer), "thread-%d", number);
_name = buffer;
}
static void *start_routine(void *args) // pthread_create没有直接调用_func,为了参数类型匹配,这个函数需要是static的,但是在函数内部又需要拿到类对象的内容,所以参数为结构体(this,args)
{
Context *ctx = static_cast<Context *>(args);
void *ret = ctx->_this->run(ctx->_args);
delete ctx;
return ret;
}
void *run(void *args)
{
return _func(args);
}
void start()
{
Context *ctx = new Context();
ctx->_args = _args;
ctx->_this = this;
// int n = pthread_create(&_tid, nullptr, _func, _args);
_func函数指针对象,和c的函数指针不能不能互相转化
int n = pthread_create(&_tid, nullptr, start_routine, ctx); // 调静态成员函数
assert(n == 0);
(void)n;
}
void join()
{
int ret = pthread_join(_tid, nullptr);
assert(ret == 0); // assert在release下不存在
(void)ret;
}
~Thread()
{
}
private:
std::string _name;
pthread_t _tid;
func_t _func;
void *_args;
};