Linux线程的概念和控制

简介: Linux线程的概念和控制

线程概念

  1. 线程是进程内的一个执行流。只创建PCB,不再单独创建父进程共享虚拟内存和页表,能够执行父进程代码的一部分。
  2. 线程在进程内部运行(在进程的地址空间中运行),拥有该进程的一部分资源。
  3. 线程是CPU调度的基本单位,而进程是分配系统资源的基本实体。进程用来申请资源,线程向进程要资源。
  4. Linux内核中没有真正意义上的线程,而是通过进程PCB来模拟,拥有属于自己的独立线程方案,称为轻量级进程。
  5. 对于CPU而言,每一个PCB都可以称为轻量级进程。

bdba5e0980b73981eb1198e7311648d6.png

在调度角度而言,线程和进程有很多地方是重叠的。因此Linux的设计中并不给线程专门设计对应的数据结构,而是复用PCB。这样实现可以复用进程的结构和代码,不用再去刻意的实现进程和线程之间的关系,降低编写的难度,的维护成本大大降低,既可靠又高效。但是这样的缺点就在于,Linux并不能直接提供创建线程的系统调用接口,只能提供创建轻量级进程的接口。因此想要实现线程的操作就需要调用原生库 — pthread

线程控制

需要注意,因为Linux没有真正意义上的线程,因此关于线程的调用接口都是用户级的线程库所提供的。这个库叫: pthread ,任何的Linux操作系统都默认携带这个库,原生线程库。因此在编译有线程的程序时需要加上 -lpthread 选项

创建

创建:

一个进程里可以有多个线程,而main函数里的执行流也就是主线程,其余线程需要被创建出来

pthread_create:创建新线程

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, 
          void *(*start_routine) (void *), void *arg);

参数一为线程id,需要传地址,

参数二位线程属性,不需要则设为nullptr

参数三为函数指针,也就是这个线程被创建出来后执行的任务

参数四为参数三的参数

成功返回0

void* thread_pp(void* args){
    char* s = (char*)args;
    while(1){
        cout << s << endl;
        sleep(1);
    }
}
int main(){
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,thread_pp, (void*)"I am new thread");
    assert(n == 0);
    while(1){
        cout << "I am old thread" << endl;
        sleep(1);
    }
    return 0;
}

ddd125a2483746c4bccc6966bd4698cb.gif

创建多个线程

//定义线程的属性归并成类
class ThreadDate{
public:
    pthread_t tid;
    char name[64];
};
void* thread_pp(void* args){
    //强转回线程对象的类型后就可以访问到线程对象的属性了
    ThreadDate* td = (ThreadDate*)args;
    while(1){
        cout <<"新线程:" << td->name << endl;
        sleep(1);
    }
}
int main(){
    //为了方便管理和调用,将每一个创建好的线程放到数组中
    vector<ThreadDate*> threads;
    for(int i = 0; i < 10; ++i){
        //创建新线程前先创建出一个线程对象,将线程的属性自定义好之后在创建线程
        ThreadDate* td = new(ThreadDate);
        //将线程的名字自定义
        snprintf(td->name, sizeof(td->name),"%s: %d", "I am new Thread", i + 1);
        //创建线程,将整个对象作为参数传入
        pthread_create(&td->tid,nullptr, thread_pp, (void*)td);
        threads.push_back(td);
    }
    while(1){
        cout << "I am old Thread" << endl;
        sleep(1);
    }
    return 0;
}

e9ef28caf9e54da5b0049ff0026a7105.gif

终止

线程可以被终止掉,但是不能使用exit,exit是进程的终止,如果进程被终止了那么所有的线程都没有了

#include <pthread.h>
void pthread_exit(void *retval);
void* thread_pp(void* args){
    //强转回线程对象的类型后就可以访问到线程对象的属性了
    ThreadDate* td = ( ThreadDate*)args;
    int cnt = 10;
    while(cnt--){
        cout << "新线程: " << td->name << endl;
        sleep(1);
    }
    pthread_exit(nullptr);
}

c616832938094f32b1c8f042325e7c1f.gif

等待

线程也是需要被等待的,也是需要回收对应的PCB,不回收则会导致和僵尸进程类似的问题—内存泄漏

  1. 获取线程的推出信息,可以不关心
  2. 回收线程对应的PCB,防止内存泄漏

利用 pthread_join 等待回收线程

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

等待成功返回0

参数一为线程的id

参数二为输出型参数,用来获取线程函数结束时返回的退出结果。不关心则设为nullptr

pthread_join不考虑异常问题,异常问题只由进程考虑

void* thread_pp(void* args){
    //强转回线程对象的类型后就可以访问到线程对象的属性了
    ThreadDate* td = (ThreadDate*)args;
    int cnt = 10;
    while(cnt--){
        cout << "新线程: " << td->name << endl;
        sleep(1);
    }
    return (void*)22;                                                                
}
int main(){
    //为了方便管理和调用,将每一个创建好的线程放到数组中
    vector<ThreadDate*> threads;
    for(int i = 0; i < 10; ++i){
        //创建新线程前先创建出一个线程对象,将线程的属性自定义好之后在创建线程
        ThreadDate* td = new(ThreadDate);
        //将线程的名字自定义
        snprintf(td->name, sizeof(td->name), "%s: %d", "I am new Thread", i + 1);
        //创建线程,将整个对象作为参数传入
        pthread_create(&td->tid, nullptr, thread_pp, (void*)td);
        threads.push_back(td);
    }
    for(auto& iter : threads)
    {
        //用于接收线程退出的返回值,也就是线程函数的返回值;
        void *ret = nullptr; 
        int n = pthread_join(iter->tid, (void**)&ret); 
        assert(n == 0);
        //要注意Linux的指针是8个字节的,所以不能强转为int
        cout << "join : " << iter->name << " success, exit_code: " << (long long)ret << endl;
        delete iter;
    }
    while(1){
        cout << "I am old Thread" << endl;
        sleep(1);
    }
    return 0;
}

在线程的函数里设置一个返回值,等待成功后就会拿到这个返回值。


de2e0a7828244206b4336b74757990e7.gif

取消

线程是可以被其他线程取消的,前提是该线程已经跑起来了。

pthread_cancel:取消一个运行中的线程

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

参数为线程的id

线程如果是被取消的,退出码为-1(宏定义:PTHREAD_CANCELED)

void* thread_pp(void* args){
    //强转回线程对象的类型后就可以访问到线程对象的属性了
    ThreadDate* td = (ThreadDate*)args;
    int cnt = 10;
    while(cnt--){
        cout << "新线程: " << td->name << endl;
        sleep(1);
    }
    return (void*)22;                                                                
}
int main(){
    //为了方便管理和调用,将每一个创建好的线程放到数组中
    vector<ThreadDate*> threads;
    for(int i = 0; i < 10; ++i){
        //创建新线程前先创建出一个线程对象,将线程的属性自定义好之后在创建线程
        ThreadDate* td = new(ThreadDate);
        //将线程的名字自定义
        snprintf(td->name, sizeof(td->name), "%s: %d", "I am new Thread", i + 1);
        //创建线程,将整个对象作为参数传入
        pthread_create(&td->tid, nullptr, thread_pp, (void*)td);
        threads.push_back(td);
    }
    //五秒后取消一半线程
    sleep(5);
    for(int i = 0; i < threads.size() / 2; i++){
        //传入对应的线程id
        pthread_cancel(threads[i]->tid);
        cout << "pthread_cancel : " << threads[i]->name << " success" << endl;
    }
    for(auto& iter : threads){
        //用于接收线程退出的返回值,也就是线程函数的返回值;
        void *ret = nullptr; 
        int n = pthread_join(iter->tid, (void**)&ret); 
        assert(n == 0);
        //要注意Linux的指针是8个字节的,所以不能强转为int
        cout << "join : " << iter->name << " success, exit_code: " << (long long)ret << endl;
        delete iter;
    }
    while(1){
        cout << "I am old Thread" << endl;
        sleep(1);
    }
    return 0;
}

8aefdbc106314ddfba578a81dfdda18d.gif

分离

一个线程默认为joinable,一旦设置了分离就不能进行等待了。但是需要注意一种场景,因为主线程将新线程创建出来后并不能确定谁先运行,所以有可能在新线程设置分离前主线程就开始等待了,此时即使新线程设置了分离后退出,主线程仍然会成功等待。一旦线程设置为分离后就不需要再关心其退出问题了

pthread_detach — 分离

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

参数即为需要设置分离的线程id

获取当前线程id

pthread_self() — 获取当前线程的id

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

线程的基本性质

线程对比进程而言,线程之间由于大部分的数据都是共享的因此通信较为方便,而进程的通信就比较麻烦。但是线程的大部分数据都是共享的也就导致了数据缺乏保护

虽然线程之间的大部分数据是共享的,但线程也是会有自己独立的数据:

  1. 自身的属性是私有的
  2. 私有的上下文结构
  3. 每一线程都有自己独立的栈区,也就是说每个线程的执行函数可以创建临时变量

CPU在调度的时候,是以LWP标识一个执行流。当只有一个执行流时,LWP == pid,因此这种情况下两个标识是等价的。

线程的优点:

  1. 创建一个新线程的代价要比创建一个新进程小得多
  2. 线程不需要创建新的地址空间和页表,能够执行父进程的一部分代码
  3. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  4. 进程的切换需要切换页表,虚拟地址空间,PCB,上下文,以及CPU中的cache需要全部更新
  5. 线程的切换需要切换PCB和上下文,但是CPU中的cache不需要更新,这也是最关键的一点
  6. 线程占用的资源要比进程少很多
  7. 能充分利用多处理器的可并行数量
  8. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  9. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  10. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

线程的缺点:

  1. 如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变
  2. 线程之间是缺乏保护的,健壮性降低,一个线程出异常就会影响其他的线程
  3. 缺乏访问控制
  4. 编程难度提高

线程的独立栈区是由线程库去调用底层接口创建的,和主线程的栈区是不一样的。因为用户所关心的线程属性都是在库中的,而内核提供线程执行流的调用,在Linux中:用户级线程:内核轻量级进程 = 1:1

当在一个全局变量前加上 __thread 之后,该全局变量就不再是主线程和新线程所共享的了,而是在新线程中属于线程局部存储修改这个变量,主线程读取到的也不会更改

__thread int res = 10;
void* thread_pp(void* args){
    while(1){
        cout << "新线程: " << res << " &res: " << &res << endl;
        sleep(1);
        ++res;
  }
}
int main(){
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, thread_pp, (void*)10);
    while(1){
        cout << "主线程:" << res << " &res: " << &res << endl;
        sleep(1);
    }
    pthread_join(tid, nullptr);
    return 0;
}

9bf67c4112f05a26fb9b4b0ae36c6763.png


相关实践学习
CentOS 7迁移Anolis OS 7
龙蜥操作系统Anolis OS的体验。Anolis OS 7生态上和依赖管理上保持跟CentOS 7.x兼容,一键式迁移脚本centos2anolis.py。本文为您介绍如何通过AOMS迁移工具实现CentOS 7.x到Anolis OS 7的迁移。
目录
相关文章
|
3月前
|
Go 调度 开发者
[go 面试] 深入理解进程、线程和协程的概念及区别
[go 面试] 深入理解进程、线程和协程的概念及区别
|
3月前
|
算法 Unix Linux
linux线程调度策略
linux线程调度策略
74 0
|
1月前
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
24 0
Linux C/C++之线程基础
|
1月前
|
Ubuntu Java Linux
Linux操作系统——概念扫盲I
Linux操作系统——概念扫盲I
40 4
|
1月前
|
安全 Linux
Linux线程(十一)线程互斥锁-条件变量详解
Linux线程(十一)线程互斥锁-条件变量详解
|
2月前
|
数据采集 消息中间件 并行计算
进程、线程与协程:并发执行的三种重要概念与应用
进程、线程与协程:并发执行的三种重要概念与应用
56 0
|
3月前
|
存储 缓存 Linux
在Linux中,文件系统概念是什么?
在Linux中,文件系统概念是什么?
|
3月前
|
存储 设计模式 NoSQL
Linux线程详解
Linux线程详解
|
3月前
|
缓存 前端开发 JavaScript
一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
【8月更文挑战第11天】一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
36 0
一篇文章助你搞懂java中的线程概念!纯干货,快收藏!
|
3月前
|
Linux Shell 调度
【在Linux世界中追寻伟大的One Piece】Linux进程概念
【在Linux世界中追寻伟大的One Piece】Linux进程概念
38 1
下一篇
无影云桌面