线程控制
Linux下没有真正意义的线程 , 只有轻量级进程的概念, 所以Linux OS只会提供轻量级进程创建的系统调用, 不会直接提供线程创建的接口.
用户在使用线程接口创建了一个线程, 实际是在中间的软件层把一个线程 对应到内核里的一个LWP , 用户认为的线程在内核就是对应一个LWP. 所以Linux对于线程的解决方案是中间的软件层解决, 但这个软件层并不属于OS, 而是设计者封装 的名为pthread原生线程库.
pthread 线程库是应用层 的原生 线程库, 应用层 指的是这个线程库并非系统调用接口提供的, 而是第三方为我们提供的, 原生指的是大部分Linux系统都会默认帮我们安装好该线程库.
线程创建
之前在线程的简单控制下, 我们只给线程传递了一个参数, 要想传递多个参数, 我们直接给线程传递一个对象即可:
我们创建了一个数据对象, 其中包括了线程名称, 创建时间, 执行的函数, 向pthread_create传参时只需把Thread_Data*转为void*, 然后在ThreadRoutine内使用的时候转换回去即可:
cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <functional>
#include <vector>
using func_t = std::function<void()>;
const int threadnum = 5;
class Thread_Data
{
public:
Thread_Data(const std::string& s, uint64_t t, func_t f)
:threadName(s)
,createTime(t)
,func(f)
{}
std::string threadName;
uint64_t createTime;
func_t func;
};
void Print()
{
std::cout << "I am one of the threads!" << std::endl;
}
void* ThreadRountine(void* arg)
{
Thread_Data* td = static_cast<Thread_Data*>(arg);//隐式类型转换
usleep(1000);//调整一下函数执行次序
while(true)
{
std::cout << "new thread" << " thread name: " << td->threadName << " create time: " << td->createTime << std::endl;
td->func();
sleep(1);
}
}
int main()
{
pthread_t tid;
std::vector<pthread_t> tids;
for(int i = 1; i <= threadnum;i++)
{
char name[64];
snprintf(name, sizeof(name), "%s-%d", "thread", i);
Thread_Data* arg = new Thread_Data(name, time(nullptr), Print);
pthread_create(&tid, nullptr, ThreadRountine, (void*)arg);
tids.push_back(tid);
sleep(1);
}
std::cout << std::endl;
while(true)
{
std::cout << "I am main thread" << std::endl;
sleep(1);
}
return 0;
}
调用ps -aL可以发现有6个线程在运行, 运行结果有些不整齐, 不能确定哪个线程先被调度:
在运行函数内故意触发一个异常:
cpp
void* ThreadRountine(void* arg)
{
Thread_Data* td = static_cast<Thread_Data*>(arg);//隐式类型转换
usleep(1000);//调整一下函数执行次序
while(true)
{
std::cout << "new thread" << " thread name: " << td->threadName << " create time: " << td->createTime << std::endl;
td->func();
if(td->threadName == "thread-4")
{
std::cout << td->threadName << " 触发了异常!!!!!" << std::endl;
int a = 1;
a /= 0; // 故意制作异常
}
sleep(1);
}
}
如我们之前所说, 多线程如果一个线程崩溃, 整个进程都会崩溃:
线程获取自身id
pthread_ create函数会产生一个线程ID, 存放在第一个参数指向的地址中. 该线程ID和前面说的线程LWP不是一回事, 线程LWP属于进程调度的范畴, 因为线程是轻量级进程, 是操作系统调度器的最小单位, 所以需要一个数值来唯一表示该线程.
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID, 属于NPTL线程库的范畴. 线程库的后续操作, 就是根据该线程ID来操作线程的.
线程库NPTL提供了pthread_ self函数, 可以获得线程自身的ID:
pthread_t pthread_self(void);
头文件: pthread.h
功能: 获取线程自己的tid
返回值: 返回自己的tid
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
-
从线程函数return. 这种方法对主线程不适用, 从main函数return相当于调用exit.
-
线程可以调用pthread_ exit终止自己。
-
一个线程可以调用 pthread_ cancel 终止同一进程中的另一个线程
方法一: return
线程执行的函数返回值是void*, 不需要返回值的话, 我们返回空指针就能终止该线程:
cpp
#include<pthread.h>
#include<iostream>
#include<unistd.h>
void* threadRoutine(void*arg)
{
usleep(1000);
std::string name = static_cast<const char*>(arg);
int cnt = 5;
while(cnt--)
{
std::cout << "new thread is running, thread name: " << name << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
while(true)
{
std::cout << "I am main thread" << std::endl;
sleep(1);
}
return 0;
}
**方法二:**pthread_ exit
POSIX线程库提供了一个接口用于结束线程, void pthread_exit(void* retval);
void pthread_exit(void* retval);
头文件:pthread.h
功能:终止当前线程
参数:void* retval是线程的返回值, 目前暂时设置为空指针, 注意不要指向一个局部变量.
返回值:无返回值,跟进程一样, 线程结束的时候无法返回到它的调用者(自身)
cpp
void* threadRoutine(void*arg)
{
usleep(1000);
std::string name = static_cast<const char*>(arg);
int cnt = 5;
while(cnt--)
{
std::cout << "new thread is running, thread name: " << name << std::endl;
sleep(1);
}
//return nullptr;
pthread_exit(nullptr);
}
运行结果和上面一样.
方法三: pthread_cancel
int pthread_cancel(pthread_t thread);
头文件:pthread.h
功能:取消标识符为thread的线程。
参数:pthread_t thread是需要取消的线程标识符
返回值:取消成功返回0,取消失败返回错误码。
cpp
#include<pthread.h>
#include<iostream>
#include<unistd.h>
void* threadRoutine(void*arg)
{
usleep(1000);
std::string name = static_cast<const char*>(arg);
int cnt = 2;
while(cnt--)
{
std::cout << "new thread is running, thread name: " << name << std::endl;
sleep(1);
}
return (void *)10;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
sleep(2);
int n = pthread_cancel(tid);//主线程休眠2秒后, 终止进程
std::cout << "cancel success: " << tid << ", n: " << n << std::endl;
return 0;
}
可以看到主线程把创建的子线程取消成功, 返回值为0
注意: 如果直接调用exit()呢?
进程直接被终止, 所以多线程中线程退出不要轻易使用exit, 会导致整个进程都退出.
线程等待与线程返回值
和进程一样, 线程在执行完毕时, 如果task_struct结构体不回收, 就会导致内存泄漏(类似未被回收的僵尸进程). 所以我们需要使用 pthread_join 函数将线程加入等待队列, 加入等待队列的线程会被回收, 但是回收的现象我们是看不到的.
int pthread_join(pthread_t thread, void** retval);
头文件: pthread.h
功能: 将标识符为tid的线程加入等待队列。
参数: pthread_t thread是需要等待的线程标识符, void** retval是线程结束返回的信息, 是一个输出型参数
**返回值:**等待成功返回0, 等待失败返回错误码.
cpp
#include<pthread.h>
#include<iostream>
#include<unistd.h>
void* threadRoutine(void*arg)
{
usleep(1000);
std::string name = static_cast<const char*>(arg);
int cnt = 5;
while(cnt--)
{
std::cout << "new thread is running, thread name: " << name << std::endl;
sleep(1);
}
pthread_exit(nullptr);
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
pthread_join(tid, nullptr);
std::cout << "wait success: " << tid << std::endl;
return 0;
}
即使不调用pthread_join, 线程依然会正常退出, 但是资源将不会被回收, 从而导致资源泄漏
返回值
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数, 正如我们上面那样.
如果需要线程给主线程一个返回值呢? 分为3种情况:
2. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值
3. 如果thread线程是自己调用pthread_exit终止的, value_ptr所指向的单元存放的是pthread_exit的参数
比如, 可以在threadRoutine函数中以void*的格式返回10:
但是这个变量要怎么让主线程接收到呢?
pthread_join函数有一个**输出型参数void** retval,**由于是输出型参数, 需要被修改的内容是void*类型, 所以使用二级指针. 我们在主线程内定义一个void*类型的ret指针变量, 当一个线程被回收的时候, 将&ret传参, 它的返回值就会被放进这个ret里.
cpp
#include<pthread.h>
#include<iostream>
#include<unistd.h>
void* threadRoutine(void*arg)
{
usleep(1000);
std::string name = static_cast<const char*>(arg);
int cnt = 2;
while(cnt--)
{
std::cout << "new thread is running, thread name: " << name << std::endl;
sleep(1);
}
pthread_exit((void *)10);
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
void* ret = nullptr;
pthread_join(tid, &ret);
std::cout << "wait success: " << tid << ", ret: "<< (int64_t)ret <<std::endl;
return 0;
}
不仅传参可以传递一个对象参数, 我们也可以返回一个对象:
cpp
#include<pthread.h>
#include<iostream>
#include<unistd.h>
class ThreadReturn
{
public:
ThreadReturn(pthread_t id, const std::string &info, int code)
: _id(id), _info(info), _code(code)
{}
public:
pthread_t _id;
std::string _info;
int _code;
};
void* threadRoutine(void*arg)
{
usleep(1000);
std::string name = static_cast<const char*>(arg);
int cnt = 2;
while(cnt--)
{
std::cout << "new thread is running, thread name: " << name << std::endl;
sleep(1);
}
ThreadReturn* ret = new ThreadReturn(pthread_self(), "thread quit normal", 10);
pthread_exit((void*)ret);
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
void* ret = nullptr;
int n = pthread_join(tid, &ret);
std::cout << "wait success: " << tid << "n: " << n << std::endl;
ThreadReturn* r = static_cast<ThreadReturn*>(ret);
std::cout << "main thread get new thread info:" << r->_code << ", " << r->_id << ", " << r->_info << std::endl;
delete r;
return 0;
}
- 如果thread线程被别的线程调用pthread_ cancel异常终掉, value_ ptr所指向的单元里存放的是常数-1, 库中其实是一个宏 PTHREAD_ CANCELED:
cpp
#define PTHREAD_CANCELED ((void *) -1)
cpp
#include<pthread.h>
#include<iostream>
#include<unistd.h>
class ThreadReturn
{
public:
ThreadReturn(pthread_t id, const std::string &info, int code)
: _id(id), _info(info), _code(code)
{}
public:
pthread_t _id;
std::string _info;
int _code;
};
std::ostream& operator<<(std::ostream& o, ThreadReturn* tr)
{
o << tr->_code << ", " << tr->_id << ", " << tr->_info;
return o;
}
void* threadRoutine(void*arg)
{
usleep(1000);
std::string name = static_cast<const char*>(arg);
int cnt = 2;
while(cnt--)
{
std::cout << "new thread is running, thread name: " << name << std::endl;
sleep(1);
}
ThreadReturn* ret = new ThreadReturn(pthread_self(), "thread quit normal", 10);
pthread_exit((void*)ret);
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
sleep(1);
int n = pthread_cancel(tid);//1秒后取消tid进程
std::cout << "cancel success: " << tid << ", n: " << n << std::endl;
void* ret = nullptr;
n = pthread_join(tid, &ret);//等待被取消的进程
std::cout << "wait success: " << tid << ", n: " << n << ", ret: " << (int64_t)ret << std::endl;
return 0;
}
线程分离
默认情况下, 新创建的线程是 joinable的, 线程退出后, 需要对其进行pthread_join操作, 否则无法释放资源, 从而造成系统泄漏.
但是有时根本不关心线程的返回值, 那pthread_join的阻塞式等待就会成为负担. 这个时候, 我们可以用pthread_detach函数将线程分离, 当线程退出时,系统自动释放线程资源.
int pthread_detach(pthread_t thread);
头文件:pthread.h
功能:设置标识符为thread的线程分离状态。
参数:pthread_t thread是需要分离的线程标识符。
返回值:取消成功返回0,取消失败返回错误码。
这个函数既可以分离线程组内的其他线程, 也可以调用pthread_self分离自己. 我们创建一个新线程, 让新进程在第一步就分离执行, 观察主线程的返回值判断能否成功回收它.
cpp
#include<pthread.h>
#include<iostream>
#include<unistd.h>
void* threadRoutine(void*arg)
{
pthread_detach(pthread_self());//线程函数内部分离自己
std::string name = static_cast<const char*>(arg);
int cnt = 2;
while(cnt--)
{
std::cout << "new thread is running, thread name: " << name << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
sleep(1);
int n = pthread_join(tid, nullptr);//等待被取消的进程
std::cout << "wait success: " << tid << ", n: " << n << std::endl;
return 0;
}
线程只要分离, 主线程就管不了它了, 而且我们发现确实不能回收该分离的线程了, 返回错误码22
其它线程也可以分离它, 只要有tid即可.
总结: 如果线程需要给主线程返回结果, 主线程用pthread_join回收; 主线程如果不关心线程的结果, 那么就可以把这个线程设为分离状态.
关于pthread进一步讨论
如何理解pthread_t?
在正式讨论之前, 首先之前说过, 我们之前用到的关于线程控制的接口, 都不是系统直接提供的接口, 而是原生线程库pthread提供的.
Linux系统不存在真的线程, 而是用轻量级进程模拟线程, OS它不会提供创建线程的接口, 顶多会提供创建轻量级进程的接口, 但是我们平时只谈线程, 所以在系统之上有一层软件层(pthread库)提供线程接口. 所以Linux中的线程称为用户级线程 , 内核中只会创建轻量级进程. 轻量级进程由内核进行管理, 那线程由谁管理呢? ----pthread库
所以管理就涉及先描述再组织, 就要涉及类似struct tcb的概念, 对内核 中的LWP之类的属性作封装, 所以tcb是在pthread库内管理, 而不是内核中. 因为pthread是动态库, 属于用户空间.
既然程序运行期间要使用pthread库里的函数, 就要把pthread库动态加载进内存中, 并通过页表映射到地址空间中的共享区.
此时进程内部所有线程 都可以看到动态库中的数据, 而我们所说的每个线程都有自己私有的栈, 其中除了主线程 采用的栈是进程地址空间原生的栈 , 其余的线程采用的栈 就是在堆中开辟的, 在共享区中用指针和size等属性维护这些堆空间. 当然, 共享区中也有每个线程自己的tcb, 还要有自己的线程局部存储(TLS), 下个话题再谈. (TLS和线程上下文不是一个东西, 线程上下文信息保存在PCB里属于内核空间不需要用户关心, 而TLS在共享区是属于用户空间的)
此外, 线程库是动态库, 动态库是共享的, 整个系统只有一份, 所以动态库的内部要管理整个系统的由多个用户创建的所有线程.
现在再来谈什么是pthread_t tid.
库为了能够快速的找到每一个线程, 所以提供了一个pthread_t tid, 代表线程属性集合在库中的起始地址. 因此我们想要找到一个用户级线程, 只需要知道线程的 tid, 之后就可以从该结构体中获取线程的各种信息(比如返回值).
线程局部存储
正如预期, 主线程和新线程能看到同样的全局变量:
cpp
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
int g_val = 100; //全局变量本身就是被所有线程所共享的
void* threadRoutine(void* argc)
{
std::string name = static_cast<const char*>(argc);
while(true)
{
std::cout << name << ", g_val: " << g_val << std::endl;
g_val++;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");
while(true)
{
std::cout << "main thread, " << "g_val: " << g_val << std::endl;
sleep(1);
}
pthread_join(tid, nullptr);
return 0;
}
现在在g_val的前面加上__thread修饰:
再次编译运行发现只有新线程的g_val在发生变化, 实际上__thread是一个编译选项, 告诉编译器被__thread修饰的变量在每一个线程内保留一份自己的线程局部存储.
程序语言角度理解pthread
C++11内部的多线程的本质 是对**原生线程库的封装.**具体在其它文章展开.
fork和exec*
线程中可以fork吗? 线程中可以exec*程序替换吗? 如何理解?
可以, 子进程将会成为调用
fork
的线程的副本.可以, 在一个多线程程序中调用
exec*
函数时, 它会替换当前进程为新的程序. 当前进程的所有线程都将被终止, 只有调用exec*
的线程会继续执行(但实际上是作为新程序的入口点)。