Linux线程控制
POSIX线程库
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
要使用这些函数库,要通过引入头文<pthread.h>
链接这些线程函数库时要使用编译器命令的“-lpthread”选项
线程创建
pthread_create函数
功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
错误检查:
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,
建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
代码练习
#include <iostream>
#include <vector>
#include <unistd.h>
using namespace std;
// 这里的类当做结构体使用(方便理解)
class ThreadData
{
public:
// 编号
int number;
// tid
pthread_t tid;
// 将数据刷新到buffer中
char namebuffer[64];
};
// 函数结构必须和pthread_create提供的接口相同
void *start_routine(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
int cnt = 10;
while (cnt)
{
cout << "cnt: " << cnt << " &cnt: " << &cnt << endl;
cnt--;
// sleep(10);
}
return nullptr;
}
int main()
{
// 1. 想要创建一批线程
// 放在vector容器中
vector<ThreadData *> threads;
#define NUM 10
for (int i = 0; i < NUM; i++)
{
// new出ThreadData对象
ThreadData *td = new ThreadData();
td->number = i + 1;
// 打印到namebuffer中
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i + 1);
// 创建线程
// argv参数先不学习 设置为nullptr 创建start_routine回调函数 也就是线程执行任务
pthread_create(&td->tid, nullptr, start_routine, td);
threads.push_back(td);
sleep(10);
}
for (auto &iter : threads)
{
cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " success" << endl;
sleep(1);
}
return 0;
}
一个线程如果出现了异常,会影响其他线程吗?会的(健壮性或者鲁棒性较差)对于信号而言,信号是整体发给进程的!
exit(0); 能不能用来终止线程,不能,因为exit是终止进程的!,任何一个执行流调用exit都会让整个进程退出
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
pthread_exit函数
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,
不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
pthread_cancel函数
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
线程等待
为什么需要线程等待?
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
创建新的线程不会复用刚才退出线程的地址空间。
pthread_join函数
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。
thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
线程退出(等待+终止)
#include <iostream>
#include <vector>
#include <cassert>
#include <unistd.h>
using namespace std;
// 这里的类当做结构体使用(方便理解)
class ThreadData
{
public:
// 编号
int number;
// tid
pthread_t tid;
// 将数据刷新到buffer中
char namebuffer[64];
};
// 将返回信息定义成对象
class ThreadReturn
{
public:
int exit_code;
int exit_result;
};
// 函数结构必须和pthread_create提供的接口相同
void *start_routine(void *args)
{
sleep(3);
ThreadData *td = static_cast<ThreadData *>(args);
int cnt = 10;
while (cnt)
{
cout << "cnt: " << cnt << " &cnt: " << &cnt << endl;
cnt--;
sleep(1);
}
// 线程如何终止
// delete td;
// 线程函数结束时,return的时候线程就算终止了
// return nullptr;
// 传编号过去
// return (void *)6; // 这里会出现waring -- 将整数强转成指针类型 指针类型在64位机器上是8字节的
// return (void*)td->number;
// pthread_exit((void*)106); --既然假的地址,整数都能被外部拿到,那么如何返回的是,堆空间的地址呢?对象的地址呢?
// 定义成对象
ThreadReturn *tr = new ThreadReturn();
// 自定义
tr->exit_code = 1;
tr->exit_result = 106;
// 不能定义成ThreadReturn tr; --这样就是在栈上开辟空间了
return (void*)tr;
}
int main()
{
// 1. 想要创建一批线程
// 放在vector容器中
vector<ThreadData *> threads;
#define NUM 10
for (int i = 0; i < NUM; i++)
{
// new出ThreadData对象
ThreadData *td = new ThreadData();
td->number = i + 1;
// 打印到namebuffer中
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i + 1);
// 创建线程
// argv参数先不学习 设置为nullptr 创建start_routine回调函数 也就是线程执行任务
pthread_create(&td->tid, nullptr, start_routine, td);
threads.push_back(td);
// sleep(10);
}
for (auto &iter : threads)
{
cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " success" << endl;
// sleep(1);
}
// 线程也可以被取消!调用pthread_cancel方法
// 但是线程要被取消的前提是该线程已经跑起来了!
// sleep(5);
// 取消一半的线程
// for (int i = 0; i < threads.size() / 2; i++)
// {
// // int pthread_cancel(pthread_t thread);
// // 线程如果是被取消的 其退出码是-1
// pthread_cancel(threads[i]->tid);
// cout << "pthread_cancel : " << threads[i]->namebuffer << " success" << endl;
// }
// 线程也是需要等待的,如果不进行等待就会造成类似僵尸进程的问题 -- 内存泄漏
// 线程等待的作用:
// 1. 获取新线程的退出信息 -- 也可以不关心
// 2. 回收新线程的PCB等内核资源,防止内存泄漏 -- 暂时无法查看
for (auto &iter : threads)
{
void *ret = nullptr;
// pthread_join函数默认调用成功!在线程中不考虑异常问题(异常是进程考虑的)
int n = pthread_join(iter->tid, (void **)&ret);
// int n = pthread_join(iter->tid, &ret);
// 若是等待不成功那就直接报错
assert(n == 0);
cout << " join : " << iter->namebuffer << " success ,exit_code: " << ((ThreadReturn*)ret)->exit_code << " exit_result: " << ((ThreadReturn*)ret)->exit_result << endl;
delete iter;
}
cout << "main thread quit " << endl;
return 0;
}
线程分离
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
处理线程分离有两种方式
- 主线程获取到新线程的线程标识符后分离
pthread_detach(tid);
- 新线程获取到自身的线程标识符后分离
pthread_detach(pthread_self());
但是新线程和主线程的运行顺序是不可知的(由CPU调度器决定),所以可能新线程还没进行线程分离,主线程就进入阻塞等待了
所以推荐让主线程对新线程做分离操作
线程ID及进程地址空间布局
pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID
不是一回事。
前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要
一个数值来唯一表示该线程。
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,
属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
pthread_t pthread_self(void);
pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质
就是一个进程地址空间上的一个地址。
线程的局部存储
为啥g_val的地址值变化很大呢?
一开始全局变量是在已初始化数据段,所以地址很小
但是变成线程局部存储时,就位于共享区,地址变大很多
(地址由低到高)