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


目录
相关文章
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
6月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
406 1
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
394 67
|
11月前
|
NoSQL Linux 编译器
GDB符号表概念和在Linux下获取符号表的方法
通过掌握这些关于GDB符号表的知识,你可以更好地管理和理解你的程序,希望这些知识可以帮助你更有效地进行调试工作。
465 16
|
11月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
213 20
|
10月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
178 0
|
10月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
240 0
|
存储 Linux 调度
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
470 4
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
268 26
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
291 17

热门文章

最新文章