linux【多线程】之线程概念&线程特征&线程控制
一、线程概念1.1宏观上的说法
线程是进程中的一个执行流,我们来说说Liunx下具体的“线程”。
1.2由进程引入线程
⚠️1.3Linux下进程与线程概念构建
⚠️进程:内核角度:承当分配系统资源的基本实体
⚠️线程:CPU调度的基本单位
之前所说的进程内部只有一个执行流,现今有多个执行流
在Linux中,CPU见到task_struct,统一称作轻量级进程
简单总结:进程是整个家庭,线程就是家庭中的成员
二、线程创建与剖析
OS只认线程,用户(程序员)也只认线程,Linux没有真正意义上线程,所以Linux便难以直接提供创建线程的系统调用插口,而只能给我们提供创建轻量级进程的插口(clone系统调用)!
因而OS在用户和系统调用之间提供了一个用户级线程库(pthread库)linux 线程,用户在使用对线程的操作时,库上面会将其转换成对轻量级进程的操作
pthread库:原生线程库,任何linux系统都有
线程创建函数——pthread库提供的方式,编译时须要“-lpthread”
intpthread_create(pthread_t*tidp,constpthread_attr_t*attr,
(void*)(*start_rtn)(void*),voidarg);
参数
第一个参数为指向线程标示符的表针。
第二个参数拿来设置线程属性,设置成nullptr就可以。
第三个参数是线程运行函数的起始地址。
最后一个参数是函数表针的参数。
返回值
若线程创建成功,则返回0。若线程创建失败,则返回出错编号,但是thread中的内容是未定义的。
//新线程
void * thread_routine(void* args)
{
const char* name =(const char*) args;
while (true)
{
cout<<"我是新进程,我正在运行 ! name: "<<name<<endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
//创建线程
int n=pthread_create(&tid,nullptr,thread_routine,(void*)"thread one");
assert(0==n);
(void)n;
//主线程
while (true)
{
char tidbuffer[64];
snprintf(tidbuffer,sizeof tidbuffer,"0x%x",tid);
cout << "我是主线程, 我正在运行!, 我创建出来的线程的tid: " << tidbuffer << endl;
sleep(1);
}
return 0;
}
以上代码我们创建一个线程,让它去执行thread_routine函数,主线程执行后续代码
运行以后我们可以通过以下图片发觉:只有一个进程20258,并且用发送讯号的时侯却把两个执行流全结束了,讯号是发送给进程的,与进程直接相关,进程结束了,进程上面无论有多少线程也会急剧结束(对讯号有疑虑的可参考我主页关于讯号的博客)
当线程在运行的时侯我们可以通过ps-aL查看“线程”信息
LWP:轻量型进程ID
PID==LWP:主线程
PID!=LWP:新线程
CPU调度的时侯,是以LWP表示特定的一个执行流
之前我们以PID辨识独立的进程并没有问题,当只有一个执行流的时侯,PID和LWP是等价的
三、线程的特征
线程一旦被创建,几乎所有的资源都是被线程共享的
⚠️线程一定有自己的私有资源,属于线程私有的资源有
1.PCB属性
2.私有的上下文数据(线程动态运行的证据,下同)
3.独立的栈结构(尽管它们共享一个地址空间,并且在线程PCB上面会有一个独立的栈结,主线程用地址空间的栈,其它线程用共享区内的栈,虽然是库上面维护的栈结构)
3.1线程的优点
在等待慢速I/O操作结束的同时,程序可执行其他的估算任务
估算密集型应用(CPU,加密,揭秘,算法等),为了能在多处理器系统上运行,将估算分解到多个线程中实现
I/O密集型应用(外设,访问c盘,显示器,网路),为了提升性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
3.2线程的缺点性能损失:一个极少被外部风波阻塞的估算密集型线程常常难以与共它线程共享同一个处理器。假如估算密集型线程的数量比可用的处理器多,这么可能会有较大的性能损失,这儿的性能损失指的是降低了额外的同步和调度开支,而可用的资源不变。强壮性增加:编撰多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微误差或则因共享了不该共享的变量而导致不良影响的可能性是很大的,换句话说线程之间是缺少保护的,一个线程崩可能影响另一个线程。缺少访问控制:进程是访问控制的基本细度,在一个线程中调用个别OS函数会对整个进程导致影响。编程难度提升:编撰与调试一个多线程程序比单线程程序困难得多四、线程控制4.1多个线程之间的互相影响
一个线程倘若发生异常,是会影响其他线程的:上面我们谈到了讯号是整体发给进程的,当遇见错误讯号,OS会给每位线程的PCB写入讯号(线程的PID都一样),每位讯号的处理行为就会把当前执行流中止掉,所以所有的线程就会退出
换个角度理解:线程的资源是进程给的,当线程由于错误退出的时侯,要收回资源,虽然其他的线程没有错误,也要被收回
void* static_routine(void * args)
{
string name=static_cast<const char*>(args);//安全转型
while (true)
{
cout<<"new thread create success,name "<<name<<endl;
sleep(1);
//检测线程间影响
int* p=nullptr;
*p=10;//野指针解引用,引发11号信号
}
}
int main()
{
pthread_t id;
pthread_create(&id,nullptr,static_routine,(void*)"new");
//没有错误也会退出
while (true)
{
cout<<"main thread create success,name main"<<endl;
sleep(1);
}
return 0;
}
可以看出线程的鲁棒性较差,若果是进程RAR FOR LINUX,由于具有独立性,即便一个进程出现问题,也不会影响另一个。
int main()
{
//创建多个线程
vector<pthread_t*> tids;
#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);
pthread_create(&tid,nullptr,static_routine,namebuffer);
}
}
当我们采用以上方式创建多个进程的时侯,会有些问题,因为pthread_create()函数最后一个参数传的是缓冲区的地址,但是主线程,新线程调度是随机的,都会引起创建的线程信息都是最新被刷新到缓冲区的信息,之前的缓冲区都被新的数据覆盖了!!!
为此我们采用以下方式创建多个线程
void* static_routine(void * args)
{
ThreadData* td=static_cast<ThreadData*>(args);//安全转型
int cnt=10;
while (cnt)
{
cout<<"cnt"<<cnt<<"&cnt"<<&cnt<<endl;//仅做演示,分析可重入函数把他忽略
cnt--;
sleep(1);
}
delete td;
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+1);
//每一个线程都有一个对象,并将对象的地址“拷贝”给函数的形参,这样每个线程都有自己的namebuffer
//并且传到函数里面的td与主线程中的td没有关系
//由于td已经是一个指针,并且是对结构体内容操作,这里不需要&td,虽然接受的形参也对不上
//小编当时在编写的时候由于指针理解问题,纠结了一会
pthread_create(&td->tid,nullptr,static_routine,td);
threads.push_back(td);
}
}
验证发觉:cnt地址都不一样,即每位线程都有一个独立的cnt
4.2线程等待–pthread_join
一个线程创建下来,那就要就像进程一样,也是须要被等待的。假如线程不等待,对应的PCB没被释放,还会导致类似僵尸进程的问题:显存泄露。
所以线程也要被等待:
1.获取新线程的退出信息
2.回收新线程对应的PCB等内核资源,避免显存泄露。
线程等待函数,默认成功调用,不考虑异常问题【如果有异常全都退出 】
int pthread_join(pthread_t thread, void **retval);
参数:thread:被等待线程的ID,retval:线程退出时的退出码信息
void**retval:输出型参数,主要拿来获取线程函数结束时返回的退出结果。
返回值:线程等待成功返回0,失败返回错误码
class ThreadData//包含线程信息的结构体
{
public:
pthread_t tid;
char namebuffer[64];
};
void* static_routine(void * args)
{
ThreadData* td=static_cast<ThreadData*>(args);//安全转型
int cnt=10;
while (cnt)
{
cout << "cnt: " << cnt << " &cnt: " << &cnt << endl;
cnt--;
sleep(1);
}
//delete td;//避免悬空指针问题,这里只使用,不回收
pthread_exit(nullptr);
//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+1);
pthread_create(&td->tid,nullptr,static_routine,td);
threads.push_back(td);
}
//线程等待
for(auto& iter:threads)
{
int n=pthread_join(iter->tid,nullptr);
assert(n==0);
cout<<"join "<<iter->namebuffer<<" : "<<iter->tid<<" success"<<endl;
//不在static_routine释放的原因:主线程与新线程是并行交叉运行的,执行到这一步有可能部分线程资源已经被释放,指针指向的地址是非法的了,访问这部分资源会引发未定义的行为
delete iter;//外部统一申请,统一释放
}
cout<<"main thread quit"<<endl;
return 0;
}
简单说明主线程创建,主线程回收因为线程之间并行交叉运行,有可能主线程先运行完,早已在线程等待了linux 线程,而要等待的线程可能还没来得及释放资源这么上述线程等待代码在解引用的时侯就不会报错,也有一部份线程早已释放了资源,这么解引用都会引起段错误。
4.3线程中止(退出)–pthread_exit&return
return方法:在线程函数调用结束return的时侯linux操作系统版本,线程都会中止,仅对从线程有用,对主线程return相当于调用exit(),整个进程就会退出
exit();任何一个执行流调用exit(),整个进程就会退出,所以不能拿来中止线程
pthread_exit():
void* static_routine(void * args)
{
ThreadData* td=static_cast<ThreadData*>(args);//安全转型
int cnt=10;
while (cnt)
{
cout << "cnt: " << cnt << " &cnt: " << &cnt << endl;
cnt--;
sleep(1);
pthread_exit(nullptr);//执行一次就终止
}
delete td;
//return nullptr;
}
通过监控脚本可以发觉一共十一个线程,执行以后立刻退出,只留下一个主线程(复印出现问题是由于线程占领资源缘由,旁边会解决)
4.3.1线程退出的返回值问题
线程退出时函数返回值会被保存在pthread库中,我们难以直接获取这个返回值,因而我们须要借用库函数,传递ret的地址,库函数内部会对retval解引用,再将函数返回值赋给它,相当于把返回值赋给了ret!!!
4.3.2线程取消–pthread_cancel
pthread_cancel
线程是可以被其他线程取消的,而且线程要被取消,前提是这个线程是早已运行上去了。pthread_create取消也是线程中止的一种
#include
int pthread_cancel(pthread_t thread);
线程若果是被取消的,退出码:-1
4.4线程分离–pthread_detach
一个线程默认是joinable的,假如设置了分离状态,就不能否进行等待了
#include
//获取线程ID
pthread_t pthread_self(void);
返回值:此函数始终成功,返回调用线程的ID。
//分离线程
int pthread_detach(pthread_t thread);
thread:线程ID
返回值:在成功时,pthread_detach()返回0; 在错误时,它返回一个错误码。
线程分离测试
//格式化线程ID
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)
{
std::string threadname = static_cast<const char *>(args);
// pthread_detach(pthread_self()); //设置自己为分离状态,但是由于线程创建之后调度随机,存在主线程已经执行线程等待,而新线程还没分离,最后退出的时候主线程还是回收了
int cnt = 5;
while (cnt--)
{
std::cout << threadname << " running ... : " << changeId(pthread_self()) << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, start_routine, (void *)"thread 1");
//获取主线程ID
std::string main_id = changeId(pthread_self());
pthread_detach(tid);//此处让主线程分离新线程,避免上述错误,分离之后也不用考虑回收问题
std::cout << "main thread running ... new thread id: " << changeId(tid) <<" main thread id: " << main_id << std::endl;
//如果设置分离状态成功,就不能再等待了,以下代码也会输出错误码
// int n = pthread_join(tid, nullptr);
// std::cout << "result: " << n << " : " <<strerror(n) << std::endl;
return 0;
}
线程分离要注意分离时刻!
五、线程概念补充5.1线程ID
当我们创建轻量级进程的时侯,pthread库中也会为我们创建对应的数据结构来描述线程。linux中我们称这些线程是用户级线程,其中用户关心的线程的线程属性在库中,内核中的提供线程执行流调度,也就是说用户级线程/内核轻量级线程=1。
虽然所谓的tid就是库中所对应的一个地址,按照地址找到线程的储存,才能找到线程的属性,当用户想要使用线程,只须要拿着线程id就可以操作了
每位线程都有只身的栈:主线程采用的栈是进程地址空间中的栈,其他线程采用的是共享区(mmap+页表映射)中的栈,虽然是库上面维护的栈结构
5.2局部储存
所有线程共享全局资源,而且假如给全局变量加上__thread,可以将一个外置类型设置为线程局部储存变量还是全局变量,并且每位线程都有一份,不会相互影响
__thread int g_val = 100;
void *start_routine(void *args)
{
std::string threadname = static_cast<const char *>(args);
while (true)
{
std::cout << threadname<<" g_val: "<< g_val << " &g_val: " << &g_val << std::endl;
sleep(1);
g_val++;
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, start_routine, (void *)"new thread");
pthread_detach(tid);
while(true)
{
std::cout << "main thread"<< " g_val: "<< g_val << " &g_val: " << &g_val << std::endl;
sleep(1);
}
return 0;
}
从上图可以看见,两个线程的的g_val值并没有同时变化且两个变量地址不一样!