前言
前面我们对线程的概念做了介绍,并介绍了Linux下的线程!本期就来介绍Linux的下线程的相关操作!
目录
• POSIX线程库
Linux系统中是没有线程的概念的,为了满足用户使用线程的需求,有人使用LWP封装了一套用户级的线程库!
• 与线程相关的函数构成了一个完整的系列,绝大多数的函数名都是以"pthread_"大头的;
• 要使用这些函数库,要通过引入头文件 <pthread.h>
• 由于Linux中没有线程,线程是用户层面的,所以链接这些函数时要使用 -lpthread 选项链接用户级的线程库;
• 创建线程
作用
创建一个新的线程
参数
• thread : 输出型参数,返回的线程的ID
• attr : 设置线程的属性,如果是空则是线程的默认属性
• start_routine : 函数指针,线程启动时要执行的函数(回调)
• arg : 传给线程启动时执行函数的参数
返回值
成功返回0, 失败返回错误码errno
#include <iostream>
#include <pthread.h>
#include <unistd.h>
// 新线程执行的任务函数
void* ThreadRun(void* args)
{
std::string name =static_cast<const char*>(args);
while (true)
{
std::cout << "new thread, run...." << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;// 线程的id
int n = pthread_create(&tid, nullptr, ThreadRun, (void*)"thread-1");// 创建一个线程
if(n != 0) // 创建失败
{
std::cerr << "pthread_create failed!" << std::endl;
return 1;
}
// 主线程
while (true)
{
std::cout << "main thread, run...." << std::endl;
sleep(1);
}
return 0;
}
这样就创建好了一个新的线程,并执行起了一个任务函数!这里的线程的id后面专门介绍是啥东西!当然这里的线程函数的参数不只是简单的字符串/内置类型的变量,也可以是自定义类型(也就是可以传递多个参数):
这里需要注意的是,这个参数一般是定义在堆上,当调用时给每个线程拷贝过去,这样做到了每个线程就相当于自己私有一份,变相的做了保护!如果使用主线程的栈上的数据,那多个线程共享数据就可能会错乱!
• 线程的终止
如果只需要终止某个线程,而不终止某个进程,可以有三种方法:
1、线程执行的函数 return 即可;(注意,这里可不敢在主线程即main函数中return,main中return代表的是进程的结束!)
2、线程可以通过调用pthread_exit终止自己!(注意,线程中不能调用exit让他代表进程结束!)
3、一个线程可以调用pthread_cancle终止同一进城中的另一个线程。
这里的return 就不在介绍了!
• pthread_exit
作用
终止线程
参数
retval是一个void*指针,意思是,给等待他的线程返回一个值!
注意:retval不能是线程内部的局部变量,一般是全局/堆上的空间!因为如果是线程内部的局部变量,当外面等待的线程获取到的时候,该线程已经退出了!
• pthread_cancle
作用
取消一个执行中的线程
参数
thread : 线程的id
返回值
成功返回0, 失败返回错误码
这个暂时就不演示例子了,等把线程等待介绍了一起演示!
• 线程等待
• 为什么需要线程等待?
• 已经退出的线程虽然它的LWP已经被OS回收了,但库中还是维护了描述该线程的空间,这段空间依旧在地址空间中,需要让主线程等待释放!
• 创建线程不会复用刚才退出线程的地址空间
所以如果不等待就会出现类似于"僵尸进程"的情况!
• pthread_join
作用
等待线程的结束
参数
thread:线程ID
retval:输出型参数,获取线程退出时的退出信息
返回值
成功返回0, 失败返回错误码
调该函数的线程,会阻塞的等待id为thread的线程终止。thread线程以不同的方式终止,pthread_join得到的终止状态的信息也是不一样的!
1、线程以return的方式退出,retval指向的空间中存的是线程return的返回值;
2、线程以pthread_cancle的方式异常退出,retval指向的空间中存的是PTHREAD_CANCELED
3、线程以自己调用pthread_exit终止,retval指向的空间中存的是pthread_exit的参数;
4、如果对线程的终止状态不感兴趣,直接将retvaal置为空即可;
我们先以 return 返回终止,reval存的是return的值
class Thread
{
public:
std::string name;
int num;
};
// 新线程执行的任务函数
void* ThreadRun(void* args)
{
Thread* pth = static_cast<Thread*>(args);
while (pth->num)
{
std::cout << pth->name << " is run...., num: " << pth->num-- << std::endl;
sleep(1);
}
return (void*)240909;
}
int main()
{
pthread_t tid;// 线程的id
Thread* pth = new Thread();
pth->name = "thread-1";
pth->num = 10;
int n = pthread_create(&tid, nullptr, ThreadRun, (void*)pth);// 创建一个线程
if(n != 0) // 创建失败
{
std::cerr << "pthread_create failed!" << std::endl;
return 1;
}
// 主线程
std::cerr << "pthread_join begin..." << std::endl;
void* ret = nullptr;
n = pthread_join(tid, &ret);
if(n == 0) // 创建失败
{
std::cerr << "thread join success..., retval: "<< (uint64_t)ret << std::endl;
return 1;
}
return 0;
}
此时,reval指向的就是return 的返回值!当然这里还是不止可以返回这种,也可以返回自定义类型!
pthread_cancle的方式终止,reval存的是PTHREAD_CANCELED
这里咋是-1呢?其实这里是-1很正常,上面说了,当使用pthread_cancle终止时,retval指向空间存的是PTHREAD_CANCELED其实这个宏的值就是-1 :
以pthread_exit的方式终止,reval存的是pthread_exit的参数
如果对线程的终止信息不关心直接设置为nullptr即可:
• 创建一批线程
这个问题和我们前面再多进程的时候遇到的一模一样!我们可以利用循环批量化的创建:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#define N 3
class Thread
{
public:
Thread(std::string name = "thread", int num = 0)
: _name(name), _num(num)
{}
std::string get_name()
{
return _name;
}
int get_num()
{
return _num;
}
private:
std::string _name;
int _num;
};
void *down_load(void *args)
{
Thread* pth = static_cast<Thread*>(args);
int cnt = 0;
while (true)
{
std::cout << pth->get_name() << " is down load....., num: " << pth->get_num() << std::endl;
sleep(1);
if(++cnt == 3)
{
break;
}
}
return (void *)"down_load";// return 返回
}
void *print(void *args)
{
Thread* pth = static_cast<Thread*>(args);
int cnt = 0;
while (true)
{
std::cout << pth->get_name() << " is print....., num: " << pth->get_num() << std::endl;
sleep(1);
if(++cnt == 3)
{
break;
}
}
pthread_exit((void *)"print");//pthread_exit 退出
}
void *do_other(void *args)
{
Thread* pth = static_cast<Thread*>(args);
int cnt = 0;
while (true)
{
std::cout << pth->get_name() << " is do_other....., num: " << pth->get_num() << std::endl;
sleep(1);
}
return (void *)"do_other...";// 执行中途取消
}
int main()
{
std::vector<pthread_t> tids;
void*(*mth[N])(void*) = {down_load, print, do_other};// 函数指针数组
// 创建 N 个线程,并派发任务
for (int i = 0; i < N; i++)
{
Thread* pth = new Thread("thread_"+std::to_string(i), i);
pthread_t tid;
pthread_create(&tid, nullptr, mth[i], (void*)pth);
// 将这一批进程管理起来
tids.push_back(tid);
}
// 等待 print 线程
void* ret = nullptr;
pthread_join(tids[0], &ret);
std::cout << "pthread——join print retval : " << (const char*)ret << std::endl;
sleep(3);
// 等待 down_load 线程
ret = nullptr;
pthread_join(tids[1], &ret);
std::cout << "pthread——join down_load retval : " << (const char*)ret << std::endl;
sleep(3);
// 取消 并等待 do_other 线程
pthread_cancel(tids[2]);
ret = nullptr;
pthread_join(tids[2], &ret);
std::cout << "pthread——join do_other retval : " << (long long int)ret << std::endl;
return 0;
}
• 线程分离
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏!但是如果,对线程的返回值不关心,join就是一种负担,此时我们可以告诉系统当系统退出时,自动释放线程!
如何做呢?我们可以使用 pthread_detach 将线程给分离了,也就是不需要等待了!
作用
分离一个线程,是其他线程不在等待,自动退出!
注意:线程自己调用 pthread_self 获取当前线程的id然后分离,也可以是其他线程进行离,但是后者的前提是分离的线程必须存在!
参数
thread : 分离目标线程的id
返回值
成功返回0, 失败返回错误码
注意:一但分离了,再去join就是非法的!
void* thread_task(void* args)
{
// 线程自己分离自己
pthread_detach(pthread_self());
// 执行任务
int num = (long long int)args;
while (num)
{
std::cout << "new thread.... , num: " << num-- << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr,thread_task, (void*)5);
sleep(2);// 这里等待一下,让新的线程跑起来
void* ret = nullptr;
int n = pthread_join(tid, &ret);
std::cout << "main thread... n: " << n << std::endl;//这里的n是一个非0值,因为分离的线程不允许等待
return 0;
}
这里就不在和进程一样在OS中查了,因为没有意义!线程等待需要释放的是库中的资源,LWP早就退了!
当然也可以是其他线程进行分离:
• 线程ID和线程的局部存储
• 线程ID的理解
前面我们一直在用线程的id但是一直没有关注过他的值!我们这里先来打印一下看看:
void* thread_task(void* args)
{
// 执行任务
int num = (long long int)args;
while (num)
{
std::cout << "new thread.... , num: " << num-- << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr,thread_task, (void*)5);
sleep(3);// 这里等待一下,让新的线程跑起来
void* ret = nullptr;
int n = pthread_join(tid, &ret);
std::cout << "main thread... tid: " << tid << std::endl;//这里的n是一个非0值,因为分离的线程不允许等待
return 0;
}
我们发现他是一个很大的数字,但我们发现创建线程的ID和LWP是不一样的!所以,我们可以得出结论:这里的线程ID和内核中标识一个轻量级进程的ID不是一个东西!那这个tid是啥呢?先说结论:tid是一个地址空间中pthread库中描述线程的结构体TCB的内存空间的首地址(虚拟地址)!
如何理解tid是一个虚拟地址呢?
要搞明白这个问题就需要谈一谈,pthread库了!我们知道Linux是没有线程的,只有轻量级进程LWP,虽然LWP的本质就是线程,但是Linux没有线程的概念!但是又要给上层的用户提供线程的操作,而LWP属于OS内核,所以他只能给用户提供相关的系统调用,例如clone等!
让用户直接用系统调用显然是不现实的,所以,就有人对系统调用进行了封装,在系统调用的上层封装了一个用LWP的系统调用接口实现的线程库pthread,以后用户用现成就直接使用pthread的接口即可!这也体现了,“任何问题都可以用加一层软件层解决”的思想!
既然pthread是一个库,由前面的动态库加载,我们知道,一个库在没有被加载前他是在磁盘的,当加载到内存后,会映射在虚拟地址空间中的,堆栈中间的文件共享区!
OK,此时我们的进程中就可以创建线程了!我们可以在一个进程中创建一个线程也可以创建多个线程,那要不要对这么多的线程进行管理呢?答案是需要的!如何管理?先描述,在组织!但是我们也没有看到相关的管理啊!它是维护在pthread库里面的!
先描述用的是一个结构体TCB,描述线程的所有属性(类似如下):
struct tcb
{
struct pthread other; // 属性
char stack[N];//独立栈
//....
};
在组织,可以认为pthread是一个“数组”,所以从此以后对每一个用户创建的线程的管理就变成了对"数组"的增删查改!
当用户创建好了线程后,如何操作呢?是不是只需要把tcb那块空间的首地址给他即可!而这个首地址就是,我们在使用pthread_create的第一个参数!外面的用户拿到以后就可以对线程各种操作了!
• 线程的局部存储
当多个线程共享一个全局的变量时,不同的线程会将该全局变量进行修改:
int g_val = 100;
void *thread_task(void *args)
{
// 执行任务
int num = (long long int)args;
g_val++;
std::cout << "thread-" << num << "g_val: " << g_val << "&g_val: " << &g_val << std::endl;
return nullptr;
}
int main()
{
pthread_t tids[3];
for (int i = 0; i < 3; i++)
{
pthread_create(tids+i, nullptr, thread_task, (void *)5);
sleep(1);
}
for(int i = 0; i < 3; i++)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
我们如果不想让这个全局的变量让多个线程修改而是让想让他们各自私有一份,如何实现呢?我们可以用 __thread (只在Linux有用)修改这个全局变量即可:
__thread int g_val = 100;// 只在Linux下有用
此时他们三个线程的g_val在各自的线程的局部存储中:
• 总结
1、main 和 new 线程谁先运行?
不确定(Linux的线程是进程模拟的,进程的调度取决于调度器)
2、我们期望主线程最后退出,等待其他的新线程;如何保障?
通过join,不join或造成类似于“僵尸进程”的效果
3、tid是啥?
tid是用户创建线程返回的ID,是一个虚拟地址,是线程库中描述用户线程结构体空间的起始地址
4、全面看待线程函数的参数?
参数和返回值都是void*的,所以既可以传递内置类型,也可以传递自定义类型
5、全面看待线程函数的返回值?
线程函数的返回值,只有正确的情况!因为异常终止的情况,会给整个进程发信号,等待他的线程更本就收不到结束了!
6、如何创建多线程?
通过循环,批量的创建!
7、如何终止线程?
终止线程的方式有三种:a、线程函数的return b、pthread_exit c、main thread call pthread_cancle
8、可不可以不用join ?
可以!一个线程默认是被join的,如果不想被join,只需将线程分离即可!
9、Linux的线程 = pthread库中的线程属性 + LWP
OK,本期分享就到这里,我是cp我们下期再见!