「前言」文章是关于Linux多线程方面的知识,上一篇是 Linux多线程详解(一),今天这篇是 Linux多线程详解(二),讲解会比较细,下面开始!
「归属专栏」Linux系统编程
「笔者」枫叶先生(fy)
「座右铭」前行路上修真我
「枫叶先生有点文青病」
「每篇一句」
纵有千古,横有八荒;
前途似海,来日方长。
——梁启超
三、 Linux线程控制
3.1 POSIX线程库
前面我们使用的 pthread线程库是归属 POSIX线程库,pthread线程库是 POSIX线程库的一部分,POSIX线程库也叫原生线程库,遵守 POSIX标准:
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
- 要使用这些函数库,要通过引入头文<pthread.h>
- 链接这些线程函数库时要使用编译器命令的 “-lpthread” 选项
POSIX线程库错误检查:
- 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
- pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做),而是将错误代码通过返回值返回
- pthreads同样也提供了线程内的 errno 变量,以支持其它使用 errno 的代码。对于 pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
3.2 线程创建
这个在上一篇多线程(一)已经详细介绍了一部分,这里就不赘述了
线程创建使用的函数是 pthread_create
函数:pthread_create
作用: pthread_create - create a new thread(创建一个新线程)
头文件:#include <pthread.h>
函数原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数
第一个参数thread,代表线程ID,是一个输出型参数,pthread_t是一个无符号整数
第二次参数attr,用于设置创建线程的属性,传入空表示使用默认属性
第三个参数start_routine,是一个函数的地址,该参数表示新线程启动后要跳转执行的代码
第四个参数arg,是start_routine函数的参数,用于传入
返回值
成功返回0,失败返回错误码
下面进行代码测试,让主线程创建一批线程,注意循环创建线程时没有进行sleep
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void* start_routine(void* args)
{
string name = static_cast<const char*>(args);//static_cast 安全的进行强制类型转换,C++11
while(1)
{
cout << "new thread create success, 线程编号:" << name << endl;
sleep(1);
}
}
int main()
{
#define NUM 10
//创建一批线程
for(int i = 0; i < NUM; i++)
{
pthread_t tid;
char namebuffer[64];
snprintf(namebuffer, sizeof(namebuffer), "%s:%d", "thread", i+1);//i+1 使线程下标从1开始
// pthread_create(&tid, nullptr, start_routine, (void*)"thread one");
pthread_create(&tid, nullptr, start_routine, namebuffer);//给线程带上编号
//没有进行 sleep
//sellp(1);
}
//主线程
while(1)
{
cout << "new thread create success, I am main thread" << endl;
sleep(1);
}
return 0;
}
编译运行,观察现象,现象一:线程的编号都是一样的,并不是我们预想的从 1、2、3...开始
保持进程运行,ps -aL 查看,10个线程确实创建出来了,加上主线程一共 11个线程
把 sleep 注释的代码放开,再次编译运行
运行结果,现象二:编号 1-10 都有了
现象一解释:
- 主线程创建新线程太快了,新线程都没有机会运行,主线程就把10个新线程创建完毕了,
- 而传参namebuffer传过去的是 缓冲区namebuffer的起始地址,
- 第十个线程创建完成之后,缓冲区的内容都被第十个线程的编号内容覆盖了,所以第一次现象线程的编号都是 10
注意:创建的新线程谁先运行??答案是不确定,完全由调度器决定
上面的 start_routine函数,被多个执行流执行,该函数处于可重入状态。start_routine函数也是可重入函数,因为没有产生二义性
测试代码,对每个线程的的cnt进行取地址,观察地址是否相同
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
//把参数封成结构体
class ThreadData
{
public:
pthread_t tid;
char namebuffer[64];
};
void* start_routine(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);//static_cast 安全的进行强制类型转换,C++11
int cnt = 10;
while(cnt)
{
cout << "cnt:" << cnt-- << " &cnt:" << &cnt << endl;
sleep(1);
}
delete td;
return nullptr;
}
int main()
{
#define NUM 10
//创建一批线程
for(int i = 0; i < NUM; i++)
{
ThreadData* td = new ThreadData();//每次循环new的都是一个新对象
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);//i+1 使线程下标从1开始
pthread_create(&td->tid, nullptr, start_routine, td);
}
//主线程
while(1)
{
cout << "new thread create success, name:main thread" << endl;
sleep(1);
}
return 0;
}
编译运行,观察到每个线程的cnt地址都不一样
在函数内部定义的变量叫局部变量,具有临时性,在多线程的情况下依旧适用,因为每个线程都有自己的独立栈结构
获取线程ID
常见获取线程ID的方式有两种:
- 创建线程时通过输出型参数获得
- 通过调用pthread_self函数获得
man 3 pthread_self 查看:
函数:pthread_self
头文件:#include <pthread.h>
函数原型: pthread_t pthread_self(void);
测试代码,创建一个新线程,新线程获取自己的线程ID
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* start_routine(void* args)
{
string name = static_cast<const char*>(args);
while(1)
{
cout << name << " running..., ID:" << pthread_self() << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, start_routine, (void*)"thread 1:");
while(1)
{
cout << "main thread" << endl;
sleep(1);
}
return 0;
}
编译运行
3.3 线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit,整个进程退出
- 线程可以调用 pthread_ exit 终止自己
- 一个线程可以调用 pthread_ cancel 终止同一进程中的另一个线程
return终止线程
在线程中使用return代表当前线程退出,但是在main函数中使用return代表整个进程退出,也就是说只要主线程退出了那么整个进程就退出了,此时该进程曾经申请的资源就会被释放,而其他线程会因为没有了资源,自然而然的也退出了
测试代码,主线程创建3个新线程后,休眠2秒,然后进行return,那么整个进程也就退出了
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
class ThreadData
{
public:
pthread_t tid;
char namebuffer[64];
};
void* start_routine(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);//static_cast 安全的进行强制类型转换,C++11
int cnt = 10;
while(cnt)
{
cout << "new thread create success, name:" << td->namebuffer << " cnt:" << cnt-- << endl;
sleep(1);
}
delete td;
return nullptr;
}
int main()
{
#define NUM 3
//创建一批线程
for(int i = 0; i < NUM; i++)
{
ThreadData* td = new ThreadData();
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);//i+1 使线程下标从1开始
pthread_create(&td->tid, nullptr, start_routine, td);
}
//主线程
cout << "new thread create success, name:main thread" << endl;
sleep(2);//主线程两秒后退出
return 0;
}
编译运行,2秒后整个进程退出
如果其他线程执行到return,代表该线程结束,线程退出
pthread_exit函数终止线程
注意:exit 是用来终止进程的,任何一个执行流调用 exit,都会使整个进程退出
pthread_exit函数的功能就是终止线程,man 3 pthread_exit 查看:
- 函数:pthread_exit
- 头文件:#include <pthread.h>
- 函数原型: void pthread_exit(void *retval);
- 参数retval,线程退出时的退出码信息,如果不关心,可以设置为nullptr
- 无返回值,跟进程一样,线程结束的时候无法返回它的调用者(自身)
例如,在下面代码中,创建了3个线程,我们使用pthread_exit函数终止线程
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
class ThreadData
{
public:
pthread_t tid;
char namebuffer[64];
};
void* start_routine(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);//static_cast 安全的进行强制类型转换,C++11
int cnt = 10;
while(cnt)
{
cout << "new thread create success, name:" << td->namebuffer << " cnt:" << cnt-- << endl;
sleep(1);
pthread_exit(nullptr);//每个线程执行到这里就会退出
}
delete td;
return nullptr;
}
int main()
{
#define NUM 3
//创建一批线程
for(int i = 0; i < NUM; i++)
{
ThreadData* td = new ThreadData();
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);
pthread_create(&td->tid, nullptr, start_routine, td);
}
//主线程
while(1)
{
cout << "new thread create success, name: main thread" << endl;
sleep(1);
}
return 0;
}
编译运行
需要注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了
pthread_cancel函数取消线程
线程是可以被取消的,我们可以使用pthread_cancel函数取消某一个线程
man 3 pthread_cancel 查看:
函数:pthread_cancel
头文件:#include <pthread.h>
函数原型:
int pthread_cancel(pthread_t thread);
参数:
thread:被取消线程的ID
返回值:线程取消成功返回0,失败返回错误码
测试代码,让线程执行5秒后再取消线程
#include <iostream>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <pthread.h>
using namespace std;
class ThreadData
{
public:
int number; //线程编号
pthread_t tid;//线程ID
char namebuffer[64];//缓冲区
};
void* start_routine(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
int cnt = 10;
while(cnt)
{
cout << td->namebuffer << " cnt:" << cnt-- << endl;
sleep(1);
}
pthread_exit((void*)td->number);
}
int main()
{
vector<ThreadData*> threads;
#define NUM 5
//创建一批线程
for(int i = 0; i < NUM; i++)
{
ThreadData* td = new ThreadData();
td->number = i+1;//线程编号
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", td->number);//把需要传递的参数信息格式化到缓冲区namebuffer中
pthread_create(&td->tid, nullptr, start_routine, td);
threads.push_back(td);//把每个线程的信息push到threads里面
}
//主线程
for(auto& iter : threads)
{
cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " success" << endl;
}
sleep(5);
//线程取消
for(auto& iter : threads)
{
pthread_cancel(iter->tid);
cout << "pthread_cancel: " << iter->namebuffer << endl;
}
//线程等待
for(auto& iter : threads)
{
void* ret = nullptr;
int n = pthread_join(iter->tid, &ret);
assert(n == 0);
cout << "join: " << iter->namebuffer << " success, thread_exit_code: " << (long long)ret << endl;
delete iter;
}
cout << "main thread quit" << endl;
return 0;
}
编译运行,
注意:一个线程被取消,它的退出码是 -1
3.4 线程等待
一个线程被创建出来,这个线程就如同进程一般,也是需要被等待的。如果主线程不对新线程进行等待,那么这个新线程的资源也是不会被回收的。所以线程需要被等待,如果不等待会产生类似于“僵尸进程”的问题,也就是内存泄漏,关于线程产生类似于“僵尸进程”的问题,我们无法查看,线程并没有类似于僵尸进程的概念
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间
进行线程等待的函数是 pthread_join,功能:进行线程等待
man 3 pthread_join 查看:
函数:pthread_join
头文件: #include <pthread.h>
函数原型:int pthread_join(pthread_t thread, void **retval);
参数:
thread:被等待线程的ID
retval:线程退出时的退出码信息,不关心设置为nullptr
返回值:
线程等待成功返回0,失败返回错误码
例如,在下面的测试代码中我们先不关心线程的退出信息,进行线程等待
#include <iostream>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <pthread.h>
using namespace std;
class ThreadData
{
public:
pthread_t tid;
char namebuffer[64];
};
void* start_routine(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
int cnt = 5;
while(cnt)
{
cout << td->namebuffer << " cnt:" << cnt-- << endl;
sleep(1);
}
return nullptr;
}
int main()
{
vector<ThreadData*> threads;
#define NUM 5
//创建一批线程
for(int i = 0; i < NUM; i++)
{
ThreadData* td = new ThreadData();
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);//i+1 使线程下标从1开始
pthread_create(&td->tid, nullptr, start_routine, td);
threads.push_back(td);//把每个线程的信息push到threads里面
}
//主线程
for(auto& iter : threads)
{
cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " success" << endl;
}
//线程等待
for(auto& iter : threads)
{
int n = pthread_join(iter->tid, nullptr);
assert(n == 0);
cout << "join: " << iter->namebuffer << "success" << endl;
delete iter;
}
cout << "main thread quit" << endl;
return 0;
}
编译运行
下面谈线程退出码的问题,即返回值的问题
- void* retval 和 void** retval 有什么关系??
- 线程函数start_routine函数的返回值类型也是 void*, start_routine函数的返回值返回到哪里??
- 我们怎么获取线程的退出码,即线程的返回值??
- pthread_join函数的参数 void** retval 是一个输出型参数,用来获取线程函数结束时,返回的退出结果
- void** retval 是用来获取线程函数返回的退出结果,因为线程函数的返回值是 void,所以需要用 void* 来接受 void*
- 注意:线程函数返回的退出结果是返回在线程库当中,参数 void** retval 需要去线程库里面接受才可以返回
tips:指针是一个地址(字面值),是一个右值,指针变量是一个变量(变量里面保存着指针的地址),是一个左值,现在使用的Linux一般是64位的,所以指针是占8字节的
测试代码,线程函数返回 66666,pthread_join函数的第二个参数去线程库里面接受再返回
#include <iostream>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <pthread.h>
using namespace std;
class ThreadData
{
public:
int number; //线程编号
pthread_t tid;//线程ID
char namebuffer[64];//缓冲区
};
void* start_routine(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
int cnt = 5;
while(cnt)
{
cout << td->namebuffer << " cnt:" << cnt-- << endl;
sleep(1);
}
//return (void*)td->number;//返回线程的编号,返回在线程库中,函数的返回类型是void*,需要进行强转void*
return (void*)66666;//方便观察
}
int main()
{
vector<ThreadData*> threads;
#define NUM 5
//创建一批线程
for(int i = 0; i < NUM; i++)
{
ThreadData* td = new ThreadData();
td->number = i+1;//线程编号
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", td->number);//把需要传递的参数信息格式化到缓冲区namebuffer中
pthread_create(&td->tid, nullptr, start_routine, td);
threads.push_back(td);//把每个线程的信息push到threads里面
}
//主线程
for(auto& iter : threads)
{
cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " success" << endl;
}
//线程等待
for(auto& iter : threads)
{
void* ret = nullptr;//用于接收线程函数的返回值
int n = pthread_join(iter->tid, &ret);//对ret取地址 == void**,需要去线程库中接收,再返回
assert(n == 0);
//原来ret是void*,需要强转int,恢复整型;
//由于我所处的平台是64位的,指针是8字节,不能用int进行强转,会报错,因为int是4字节,
//需要用 long long 进行强转,long long 是8字节
cout << "join: " << iter->namebuffer << " success, threadnumber: " << (long long)ret << endl;
delete iter;
}
cout << "main thread quit" << endl;
return 0;
}
编译运行,pthread_join函数成功获取线程函数的返回值
解释如下图:
pthread_exit函数:void pthread_exit(void *retval),这个也是返回线程的退出信息,测试结果如下:
注意:调用pthread_join函数的线程将挂起等待,直到线程终止等待成功。
int pthread_join(pthread_t thread, void **retval);
thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,retval 所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用 pthread_ cancel 异常终掉,retval 所指向的单元里存放的是常数 PTHREAD_ CANCELED(-1)。
- 如果thread线程是自己调用 pthread_exit 终止的,retval 所指向的单元存放的是传给 pthread_exit 的参数。
- 如果对thread线程的终止状态不感兴趣,可以传空给 retval 参数
3.5 线程分离
- 默认情况下,新创建的线程是joinable(可以被等待)的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成系统泄漏
- 如果不关心线程的返回值,join是一种负担,这个时候我们可以将该线程进行分离,当线程退出时,自动释放线程资源
- 分离线程的函数叫做pthread_detach
man 3 pthread_detach 查看:
函数:pthread_detach
detach:分开,脱离
头文件:#include <pthread.h>
函数原型:
int pthread_detach(pthread_t thread);
参数:
thread:被分离线程的ID
返回值:
线程分离成功返回0,失败返回错误码
测试代码,创建了一个新线程,然后对新线程进行分离,那么此后主线程就不需要在对新线程进行join了
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
string changeID(const pthread_t& thread_id)
{
char tid[128];
snprintf(tid, sizeof(tid), "0x%x", thread_id);
return tid;
}
void* start_routine(void* args)
{
string threadname = static_cast<const char*>(args);
int cnt = 5;
while(cnt--)
{
cout << threadname << " running..., threadID:" << changeID(pthread_self()) << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, start_routine, (void*)"thread 1");
pthread_detach(tid);//分离线程
//线程默认是 joinable的,线程分离了之后不允许进行等待
//pthread_join(tid, nullptr);
string mainID = changeID(pthread_self());//主线程ID
while(1)
{
cout << "main running..., mainID:" << mainID << ", new threadID:" << changeID(tid) << endl;
sleep(1);
}
return 0;
}
编译运行
3.6 重新认识pthread库
从语言上理解pthread库
C++11也有自己的线程库,测试代码如下,使用C++11的线程库创建一个新线程
#include <iostream>
#include <unistd.h>
#include <thread>
void thread_run()
{
while (true)
{
std::cout << "我是新线程..." << std::endl;
sleep(1);
}
}
int main()
{
//创建新线程
std::thread t1(thread_run);
//主线程
while (true)
{
std::cout << "我是主线程..." << std::endl;
sleep(1);
}
//线程等待
t1.join();
return 0;
}
编译运行,在Linux上也能创建线程
如何看待C++11中的线程库??
任何语言,在linux中如果要实现多线程,必定要是用pthread库。C++11的多线程,在Linux环境中,本质是对pthread库的封装
从内核上理解pthread库
- 用户创建的线程在 pthread库中,pthread库又会帮我们调用系统调用接口clone,帮我们创建轻量级线程
- Linux用户级线程 : 内核轻量级线程 = 1 : 1
- 这两个的关系是一对一的,用户创建一个线程,在内核中就会创建一个轻量级进程
- 用户关心的线程属性在 pthread中,而内核提供的是线程执行流的调度
如何理解我们前面创建线程时的线程ID??如何理解每个线程的独立栈结构??
- pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址
进程运行时动态库被加载到内存,然后通过页表映射到进程地址空间中的共享区,此时该进程内的所有线程都是能看到这个动态库的
每一个新线程在共享区都有这样一块区域对新线程的描述,因此我们要找到一个用户级线程只需要找到该线程内存块的起始地址,然后就可以获取到该线程的各种信息
- 所以,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址,指向这个线程结构体的开始地址
- 每个线程都有一个独立的栈结构,这个栈就在描述线程的结构体里面,即在pthread库中
- 每个线程创建之后,都是使用自己独立的栈
- 主线程所用的栈,也就是我平时所说的栈,地址空间中的栈
3.7 封装线程
目的:对Linux线程接口进行封装,使封装后的接口可以像C++11线程库里面的一样使用
Thread.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <functional>
#include <pthread.h>
// 异常 == if: 意料之外用异常或者if判断
// assert: 意料之中用assert
//Context需要使用Thread,需要声明一下
class Thread;
//上下文,当成一个大号的结构体
class Context
{
public:
Thread *this_;
void *args_;
public:
Context():this_(nullptr), args_(nullptr)
{}
~Context(){}
};
class Thread
{
public:
typedef std::function<void*(void*)> func_t;//线程函数
const int num = 1024;
public:
//构造初始化并创建线程
Thread(func_t func, void *args = nullptr, int number = 0): func_(func), args_(args)
{
char buffer[num];
snprintf(buffer, sizeof buffer, "thread-%d", number);
name_ = buffer;
Context *ctx = new Context();
ctx->this_ = this;
ctx->args_ = args_;
int n = pthread_create(&tid_, nullptr, start_routine, ctx);
//编译debug的方式发布的时候存在,release方式发布,assert就不存在了,n就是一个定义了
assert(n == 0);
//但是没有被使用的变量,在有些编译器下会有warning
(void)n;
}
void *run(void *args){ return func_(args); }
// 类内成员,有缺省参数this指针,无法直接调用对应的函数,func_(args_)error
// 在类内创建线程,想让线程执行对应的方法,需要将方法设置成为static
// 静态方法不能调用成员方法或者成员变量
// 所以需要借助一个结构体Context,完成调用函数的工作
static void *start_routine(void *args)
{
Context *ctx = static_cast<Context *>(args);
void *ret = ctx->this_->run(ctx->args_);
delete ctx;
return ret;
}
// 线程等待函数
void join()
{
int n = pthread_join(tid_, nullptr);
assert(n == 0);
(void)n;
}
~Thread() {}
private:
std::string name_;//线程名称
func_t func_;//线程要执行的函数
void* args_;//给线程函数传递的参数
pthread_t tid_;//线程ID
};
测试代码
#include <iostream>
#include <unistd.h>
#include "Thread.hpp"
using namespace std;
//新线程
void* start_routine(void* args)
{
string name = static_cast<const char*>(args);
while(1)
{
cout << "new thread create success, name: " << name << endl;
sleep(1);
}
}
int main()
{
//不想传(void*)"thread 1", 1 参数,还需要进行封装
Thread t1(start_routine, (void*)"thread 1", 1);
while(1)
{
cout << "main thread" << endl;
sleep(1);
}
return 0;
}
运行结果
线程创建完结,下一篇进入互斥量和生产消费者模型
--------------------- END ----------------------
「 作者 」 枫叶先生
「 更新 」 2023.4.30
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
或有谬误或不准确之处,敬请读者批评指正。
版权归原作者 枫叶先生 所有, 如有侵权,请联系我们删除。