【Linux】多线程——线程概念|进程VS线程|线程控制(下)

简介: 【Linux】多线程——线程概念|进程VS线程|线程控制(下)

【Linux】多线程——线程概念|进程VS线程|线程控制(上)     https://developer.aliyun.com/article/1565756


🌙 进程VS线程


💫 进程和线程

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  1. 线程ID
  2. 一组寄存器
  3. errno
  4. 信号屏蔽字
  5. 调度优先级
  • 为什么线程切换的成本更低
  1. 地址空间和页表不需要切换。
  2. CPU内部是有L1~L3 cache,如果进程切换,cache就立即失效,新进程过来,只能重新缓存。

💫 进程和线程的资源共享

  • 进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的:

如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到。

  • 除此之外,各线程还共享以下进程资源和环境:
  1. 文件描述符表。
  2. 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)。
  3. 当前工作目录。
  4. 用户id和组id。
  • 补充说明:
__thread int g_val = 100; // 修饰全局变量,让每一个线程各自拥有一个全局的变量  --  线程的局部存储


💫 进程和线程的关系



💫 关于进程线程的问题

如何看待之前学习的单进程?

具有一个线程执行流的进程。

引入线程后,如何重新理解之前的进程?

  • 红色方框框起来的内容,将这个整体称作进程
  • 曾经理解的进程 = 内核数据结构 + 进程对应的代码和数据
  • 现在的进程,从内核角度看:承担分配系统资源的基本实体



一个进程内部一定存在多个执行流,那么这些执行流在CPU角度有区别吗?

没有任何区别,CPU不关心当前是进程还是线程这样的概念,只关心PCB,CPU调度的时候照样以task_struct为单位来进行调度。


  • 只是这里task_struct背后的代码和页表只是曾经的代码和页表的一小部分而已。
  • 所以CPU执行的只是一小块代码和数据,但并不妨碍CPU执行其它执行流。
  • 所以就可以把原本串行的所有代码转变成并发或并行的,让这些代码在同一时间点得以推进。


总结如下:

以前CPU看到的所有的task_struct都是一个进程,现在CPU看到的所有的task_struct都是一个执行流(线程)


🌙线程控制


💫 POSIX线程库

使用:

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”开头的
  • 要使用这些函数库,要通过引入头文
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

pthread线程库是应用层的原生线程库:


  • 我们说过,在Linux没有真正意义上的线程,无法直接提供创建线程的系统接口,只能给我们提供创建轻量级进程的接口。但是在用户的角度上,当我们想创建一个线程时会使用thread_create这样的接口,而不是我们上面所使用vfork函数,用户不能直接访问OS,所以OS在用户和系统调用之间提供了编写好的用户级线程库,这个库一般称为pthread库。任何Linux操作系统都必须默认携带这个库,这个库称为原生线程库。
  • 原生的线程库本质上就是对轻量级进程的系统调用(clone)进行了封装pthread_create,使用户层模拟实现了一套线程相关的接口。
  • 我们认为的线程实际在OS内部会被转化成我们所谓的轻量级进程。


错误检查:


  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回。
  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小。


💫 创建线程——pthread_create

pthread_create讲解:

  • pthread_create:创建线程的函数
#include <pthread.h>
 
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);


  • thread:获取线程的ID,该参数是一个输出型参数
  • attr:用于设置创建线程的属性,传入nullptr表示默认,这个属性基本不管
  • start_routine:函数地址,表示线程启动后要执行的函数
  • arg:传给线程例程的参数
  • 返回值:成功返回0,失败返回错误码


举个栗子:

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
using namespace std;
void * thread_routine(void *args)
{
    const char*name = (const char*)args;
    while(true)
    {
        cout<<"这是新线程,我正在运行!"<<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)
    {
        cout<<"这是主线程,我真正运行!"<<endl;
        sleep(1);
    }
 
    return 0;
}


这里编译运行需要注意:这个接口是库给我们提供的,使用的接口如果不是语言上的接口或者操作系统上的接口,如果是库提供的,那在编译时是不通过的,我们需要找到这个库。-L:找到库在哪里,-I:找到头文件在哪里,但是这个库已经在系统里安装好了,除了告诉库和头文件在哪之外,还需要知道链接哪一个库!



此时我们用ps axj命令查看当前进程的信息时,虽然此时该进程中有两个线程,但是我们看到的进程只有一个,因为这两个线程都是属于同一个进程的:



而使用ps -aL指令,就可以显示当前的轻量级进程了:



其中,LWP(Light Weight Process)表示的就是轻量级进程的ID,可以看到显示的两个轻量级进程的PID是相同的,因为它们是属于同一个进程的。每个轻量级进程都有唯一的LWP。


  • 注意:主线程的PID和LWP是一样的。不一样的就是新线程。所以CPU调度的时候,是以LWP为标识符表示特定一个执行流。
  • 线程一旦被创建,几乎所有的资源都是被所有线程共享的。所以线程之间想交互数据就容易了,直接就能看到。


线程也一定要有自己私有的资源:

  1. 线程被调度就要有独立的PCB属性私有。
  2. 线程切换时正在运行,需要进行上下文保存,要有私有的上下文结构。
  3. 每个进程都要独立的运行,每个线程都要有自己独立的栈结构。


主线程创建一批新线程:

我们让主线程一次性创建十个新线程,并让创建的每一个新线程都去执行start_routine函数,也就是说start_routine函数会被重复进入,即该函数是会被重入的:

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <vector>
using namespace std;
 
class ThreadData
{
public:
    pthread_t tid;
    char namebuffer[64];
    
};
 
//创建一批新线程
void* start_routine(void* args)
{
    sleep(1);
    ThreadData *td = static_cast<ThreadData *>(args);
    int cnt = 10;
    while(cnt)
    {
        cout<<"new thread create success,name: "<<td->namebuffer<<" cnt : "<<cnt--<<endl;
        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);
        pthread_create(&td->tid,nullptr,start_routine,td);
        threads.push_back(td);
       // sleep(1);
    }
 
    for(auto&iter:threads)
    {
        cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" sucess" <<endl;
    }
    while(true)
    {
        cout<<"new thread create success,name: main thread"<<endl;
        sleep(1);
    }
    return 0;
}



并且start_routine是可重入函数,没有产生二义性,没有因为一个线程去影响另一个线程。并且在函数内定义的变量都是局部变量具有临时性,在多线程情况下也没有问题。这也说明了每一个线程都有自己独立的栈结构


获取线程ID——pthread_self:

获取线程ID:1.创建线程时通过输出型参数获得;2.通过pthread_self接口函数获得

#include <pthread.h>
pthread_t pthread_self(void);


我们可以打印出主线程打印出新线程的ID,新线程打印自己的ID,看是否相同:结果是相同的

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <vector>
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)
{
    std::string threadname = static_cast<const char*>(args);
    while(true)
    {
        cout<<threadname<<" running ... "<<changeId(pthread_self())<<endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
    
    cout<<"main thread running ... new thread id: "<<changeId(tid)<<endl;
    pthread_join(tid,nullptr);
    return 0;
}



💫 线程等待——pthread_join

概念:

一个线程创建出来,那就要如同进程一样,也是需要被等待的。如果线程不等待,对应的PCB没被释放,也会造成类似僵尸进程的问题:内存泄漏。所以线程也要被等待:

  1. 获取新线程的退出信息
  2. 回收新线程对应的PCB等内核资源,防止内存泄漏。


可以不关心线程的退出信息。

pthread_join:等待线程的函数

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);


  • 参数:thread:被等待线程的ID,retval:线程退出时的退出码信息
  • void** retval:输出型参数,主要用来获取线程函数结束时返回的退出结果。之所以是void**,是因为如果想作为输出型结果返回,因为线程函数的返回结果是void*,而要把结果带出去就必须是void**,
  • 返回值:线程等待成功返回0,失败返回错误码


举个栗子:

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <vector>
using namespace std;
 
class ThreadData
{
public:
    int number;
    pthread_t tid;
    char namebuffer[64];
    
};
class ThreadReturn
{
public:
    int exit_code;
    int exit_result;
};
//创建一批新线程
void* start_routine(void* args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    int cnt = 10;
    while(cnt)
    {
        cout<<"cnt:"<<cnt<<"&cnt:"<<&cnt<<endl;
        cnt--;
        sleep(1);
    }
   ThreadReturn* tr = new ThreadReturn();
   tr->exit_code = 1;//线程退出码
   tr->exit_result = 100;//线程退出结果
   return (void*)tr;
   //return (void*)td->number;//waring void*ret = (void*)td->number;8字节、4字节
}
int main()
{
    vector<ThreadData*> threads;
#define NUM 10
    for(int i = 0;i<NUM;i++)
    {
        ThreadData *td = new ThreadData();
        td->number = i+1;
        snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);
        pthread_create(&td->tid,nullptr,start_routine,td);
        threads.push_back(td);
    }
    for(auto&iter:threads)
    {
        cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" sucess" <<endl;
    }
    for(auto&iter:threads)
    {
        ThreadReturn*ret = nullptr;
        int n = pthread_join(iter->tid,(void**)&ret);
        assert(n==0);
        cout<<"join : "<<iter->namebuffer<<" success,exit_code: "<<ret->exit_code<<",exit_result: "<<ret->exit_result<<endl;
        delete iter;
    }
    cout<<"main thread quit"<<endl;
    return 0;
}



总结:

没有看到线程退出时对应的退出信号:这是因为线程出异常收到信号,整个进程都会退出,所以退出信号要由进程来关心,所以pthread_join默认会认为函数会调用成功,不考虑异常问题,异常问题是进程该考虑的问题。


💫 线程终止——return、pthread_exit、pthread_cancel

一个新创建出来的线程,如果想终止线程而不是整个进程,有三种做法:


  1. 直接从线程函数结束,return的时候,线程就算终止了。
  2. 线程可以自己调用pthread_exit函数终止自己。
  3. 一个线程可以调用pthread_cancel函数终止同一进程中的另一个线程。


return终止线程:

注意:exit不能用来终止线程,因为exit是来终止进程的。任何一个执行流调用exit都会让整个进程退出,所以终止线程不能采用exit,而是采用return来终止线程。

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <vector>
using namespace std;
 
class ThreadData
{
public:
    pthread_t tid;
    char namebuffer[64];
    
};
//创建一批新线程
void* start_routine(void* args)
{
    sleep(1);
    ThreadData *td = static_cast<ThreadData *>(args);
    int cnt = 10;
    while(cnt)
    {
        cout<<"cnt:"<<cnt<<"&cnt:"<<&cnt<<endl;
        cnt--;
        sleep(1);
        return nullptr;
    }
    delete td;
}
 
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,start_routine,td);
        threads.push_back(td);
    }
    for(auto&iter:threads)
    {
        cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" sucess" <<endl;
    }
    while(true)
    {
        cout<<"new thread create success,name: main thread"<<endl;
        sleep(1);
    }
    return 0;
}#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <vector>
using namespace std;
 
class ThreadData
{
public:
    pthread_t tid;
    char namebuffer[64];
    
};
//创建一批新线程
void* start_routine(void* args)
{
    sleep(1);
    ThreadData *td = static_cast<ThreadData *>(args);
    int cnt = 10;
    while(cnt)
    {
        cout<<"cnt:"<<cnt<<"&cnt:"<<&cnt<<endl;
        cnt--;
        sleep(1);
        return nullptr;
    }
    delete td;
}
 
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,start_routine,td);
        threads.push_back(td);
    }
    for(auto&iter:threads)
    {
        cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" sucess" <<endl;
    }
    while(true)
    {
        cout<<"new thread create success,name: main thread"<<endl;
        sleep(1);
    }
    return 0;
}



最终新建线程终止。

pthread_exit函数:

pthread_exit函数的功能就是终止线程:

#include <pthread.h>
void pthread_exit(void *retval);


retval:线程退出时的退出码信息,默认设置为nullptr

举个栗子:

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <vector>
using namespace std;
 
class ThreadData
{
public:
    pthread_t tid;
    char namebuffer[64];
    
};
//创建一批新线程
void* start_routine(void* args)
{
    sleep(1);
    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);
}
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,start_routine,td);
        threads.push_back(td);
    }
    for(auto&iter:threads)
    {
        cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" sucess" <<endl;
    }
    while(true)
    {
        cout<<"new thread create success,name: main thread"<<endl;
        sleep(1);
    }
    return 0;
}



pthread_cancel:

线程是可以被其他线程取消的,但是线程要被取消,前提是这个线程是已经运行起来了。pthread_create取消也是线程终止的一种

#include <pthread.h>
int pthread_cancel(pthread_t thread);


我们以取消一半的线程为例:

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <vector>
using namespace std;
 
class ThreadData
{
public:
    int number;
    pthread_t tid;
    char namebuffer[64];  
};
//创建一批新线程
void* start_routine(void* args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    int cnt = 10;
    while(cnt)
    {
        cout<<"cnt:"<<cnt<<"&cnt:"<<&cnt<<endl;
        cnt--;
        sleep(1);
    }
  return (void*)100;
}
int main()
{
    vector<ThreadData*> threads;
#define NUM 10
    for(int i = 0;i<NUM;i++)
    {
        ThreadData *td = new ThreadData();
        td->number = i+1;
        snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);
        pthread_create(&td->tid,nullptr,start_routine,td);
        threads.push_back(td);
    }
    for(auto&iter:threads)
    {
        cout<<"create thread: "<<iter->namebuffer<<" : "<<iter->tid<<" sucess" <<endl;
    }
    sleep(5);
    //取消一半的线程
    for(int i = 0;i<threads.size()/2;i++)
    {
        pthread_cancel(threads[i]->tid);
        cout<<"ptheread_cancel : "<<threads[i]->namebuffer<<" success"<<endl;
    }
 
    for(auto&iter:threads)
    {
        void*ret = nullptr;
        int n = pthread_join(iter->tid,(void**)&ret);
        assert(n==0);
        cout<<"join : "<<iter->namebuffer<<" success,exit_code: "<<(long long)ret<<endl;
        delete iter;
    }
    cout<<"main thread quit"<<endl;
    return 0;
}



线程如果是被取消的,退出码是-1,-1是一个宏,PTHREAD_CANCELED,我们可以查看定义:

#define PTHREAD_CANCELED ((void *) -1)


初步重新认识我们的线程库(语言版)

任何语言,在Linux中,如果要实现多线程,必定要使用pthread库,如何看待C++11中的多线程:C++11的多线程,在Linux环境中本质就是对pthread库的封装。


💫 分离线程——pthread_detach

概念:

线程是可以等待的,等待的时候,是join的等待的,阻塞式等待。而如果线程我们不想等待:不要等待,该去进行分离线程处理。默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成内存泄漏而如果我们不关心线程的返回值,join是一种负担,这个时候我们可以告诉OS,当线程退出时,自动释放线程资源,这种策略就是线程分离。


phread_detach使用:

#include <pthread.h>
int pthread_detach(pthread_t thread);


下面我们创建新线程,让主线程与新线程运行起来,主线程等待新线程退出,等待完毕返回n,而现在让创建的新线程进行分离,按照我们的预料:此时应该是等待失败:

#include <iostream>
#include <pthread.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <vector>
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);
    pthread_detach(pthread_self());//线程分离,设置为分离状态
    int cnt = 5;
    while(cnt--)
    {
        cout<<threadname<<" running ... "<<changeId(pthread_self())<<endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
    string main_id = changeId(pthread_self());
    cout<<"main thread running... new thread id:"<<changeId(tid)<<"main thread id: "<<main_id<<endl;
    //一个线程默认是joinable的,设置了分离状态,不能够进行等待了
    int n = pthread_join(tid,nullptr);
    cout<<"result:"<<n<<": "<<strerror(n)<<endl;
    return 0;
}



🌟结束语 

      今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。



目录
相关文章
|
5天前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
24天前
|
Python
Python中的多线程与多进程
本文将探讨Python中多线程和多进程的基本概念、使用场景以及实现方式。通过对比分析,我们将了解何时使用多线程或多进程更为合适,并提供一些实用的代码示例来帮助读者更好地理解这两种并发编程技术。
|
1月前
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
29 0
Linux C/C++之线程基础
|
1月前
|
安全 Linux
Linux线程(十一)线程互斥锁-条件变量详解
Linux线程(十一)线程互斥锁-条件变量详解
|
2月前
|
数据采集 消息中间件 并行计算
进程、线程与协程:并发执行的三种重要概念与应用
进程、线程与协程:并发执行的三种重要概念与应用
57 0
|
2月前
|
数据采集 Linux 调度
Python之多线程与多进程
Python之多线程与多进程
|
3月前
|
存储 设计模式 NoSQL
Linux线程详解
Linux线程详解
|
3月前
|
负载均衡 Linux 调度
在Linux中,进程和线程有何作用?
在Linux中,进程和线程有何作用?
|
3天前
|
Linux
在 Linux 系统中,“cd”命令用于切换当前工作目录
在 Linux 系统中,“cd”命令用于切换当前工作目录。本文详细介绍了“cd”命令的基本用法和常见技巧,包括使用“.”、“..”、“~”、绝对路径和相对路径,以及快速切换到上一次工作目录等。此外,还探讨了高级技巧,如使用通配符、结合其他命令、在脚本中使用,以及实际应用案例,帮助读者提高工作效率。
17 3
|
3天前
|
监控 安全 Linux
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景,包括 ping(测试连通性)、traceroute(跟踪路由路径)、netstat(显示网络连接信息)、nmap(网络扫描)、ifconfig 和 ip(网络接口配置)。掌握这些命令有助于高效诊断和解决网络问题,保障网络稳定运行。
15 2