从C++角度看内核与多线程

linux内核使用C写的,文本重点在于理解内核、进程、线程的本质区别

一、从内核角度看进程与线程的实现

1.1、内核简介

内核就是一套软件,是OS这套系统对于所有硬件的协调软件, 他存在的初始目的就是去协调硬件,以根据用户给与的可读(阅读理解)指令进行硬件操作

img

  • 进程管理子系统

负责对于CPU资源的分配,管理等,对于想要在当前OS上运行的软件,内部给与一个称呼,进程
当前系统主要核心目的是管理OS上运行的所谓软件
OS出现的目的是为了让我们更方便的计算机资源

  • 内存管理子系统

存储设备管理子系统,主要是对于内存的管理建立起更加方便的操作方案
例如,在读写层面,1次一个地址(字节)比较慢,建立页表机制,4K做一次单位,对于所有地址进行统一规划

  • 文件子系统

对于文件建立索引及管理策略

  • 网络子系统

TCP/UDP/IP等基层协议的解析实现者
网卡驱动的管理协调者

  • 设备子系统

输入/输出设备管理
外置设备管理

1.2、线程实现方案区分

  • 进程:计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

  • 线程:操作系统能够进行运算调度的最小单位,被包含在进程之中,是进程中的实际运作单位

由于进程调度对CPU开销大,所以产生了线程的概念,线程有用户级线程、内核级线程实现方案

1.2.1、用户级线程
  • 用户空间运行线程库,任何应用程序都可以通过使用线程库被设计成多线程程序。线程库是用于用户级线程管理的一个例程包,它提供多线程应用程序的开发和运行的支撑环境,包含:用于创建和销毁线程的代码、在线程间传递数据和消息的代码、调度线程执行的代码以及保存和恢复线程上下文的代码。

  • 线程的创建、消息传递、线程调度、保存/恢复上下文都由线程库来完成。内核感知不到多线程的存在。内核继续以进程为调度单位,并且给该进程指定一个执行状态(就绪、运行、阻塞等)

img

创建销毁快,支持大量的用户线程(创建用户线程比内核线程需要的空间要小的多),但是不能利用CPU多核,同时需要自己实现阻塞调度,否则会影响其他用户线程的执行

1.2.2、内核级线程
  • 内核级线程通常使用几个进程表在内核中实现,每个任务都会对应一个进程表。在这种情况下,内核会在每个进程的时间片内调度每个线程

  • 所有能够阻塞的调用都会通过系统调用的方式来实现,当一个线程阻塞时,内核可以进行选择,是运行在同一个进程中的另一个线程(如果有就绪线程的话)还是运行一个另一个进程中的线程

  • 从用户空间 -> 内核空间 -> 用户空间的开销比较大,但是线程初始化的时间损耗可以忽略不计。这种实现的好处是由时钟决定线程切换时间,因此不太可能将时间片与任务中的其他线程占用时间绑定到一起。同样,I/O 阻塞也不是问题

img

1.2.3、混合实现
  • 结合用户空间和内核空间的优点,设计人员采用了一种内核级线程的方式,然后将用户级线程与某些或者全部内核线程多路复用起来

  • 在这种模型中,编程人员可以自由控制用户线程和内核线程的数量,具有很大的灵活度

img

采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。

img

1.3、调度模型

对应用户级线程1:1的调度方式

img

对应系统级线程n:1的调度方式

img

对应混合线程n:m的调度方式

img

1.4、线程的优点

优点

备注:上下文指的是,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容

  • 创建线程的代价要小于创建进程

  • 与进程切换相比,线程之间的切换需要操作系统做的工作很少,线程切换只需要交换上下文信息即可

  • 线程占用的资源比进程少很多

  • 可以充分利用多处理器的可并行数量

  • 在等待慢速I/O操作结束的同时,系统可以执行其他的计算任务

  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
    I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

缺点

  • 性能损失(如果一个进程中的多个线程在频繁的进行切换,则程序的运行效率会降低,因为性能损失在了线程切换当中。进程的执行效率,随着线程数量的增多,性能呈现出正态分布的状况)

  • 代码健壮性降低(一个线程的崩溃,会导致整个进程的崩溃)

  • 缺乏访问控制(多个线程在访问同一个变量时可能会导致程序结果的二义性)

二、C++ 对于线程的控制实现

2.1、线程创建

下面示例中:主线程每隔1秒打印一次,共计打印3次,两个子线程每隔两秒打印,循环打印

结果:这里C++主线程停止后子线程也不再打印,但是java是可以继续打印

java与C++线程模式对比

  • java:java优化了线程,是一个多线程程序,属于上面的n:1的调度方式

  • C++:C++不去优化走内核策略,属于上面的1:1的调度方式

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

struct ThreadNum {
    int thread_num_;
};

//函数指针去指定要运行的代码位置
void *MyThreadStrat(void *arg) {
    struct ThreadNum *tn = (struct ThreadNum *) arg;
    while (1) {
        printf("%d号子线程\n", tn->thread_num_);
        sleep(2);
    }
    delete tn;
    return NULL;
}

int main() {
    //线程信息
    pthread_t tid;
//    功能		线程
//    创建		pthread_create()
//    退出		pthread_exit()
//    等待		pthread_join()
//    取消		pthread_cancel()
//    获取ID		pthread_self()
//    调度策略	SCHED_OTHER、SCHED_FIFO、SCHED_RR
//    通信机制	信号、信号量、互斥锁、读写锁、条件变量

    for (int i = 0; i < 2; i++) {
        struct ThreadNum *tn = new ThreadNum;
        if (tn == NULL) {
            exit(1);
        }

        tn->thread_num_ = i;

        int ret = pthread_create(&tid, NULL, MyThreadStrat, (void *) tn);
        if (ret != 0) {
            perror("pthread_create");
            return 0;
        }
    }
    int a = 0;
    while (1) {
        a++;
        printf("主线程计数:%d\n", a);
        sleep(1);
        if (a == 3) {
            break;
        }
    }
    void *ret;
    //需要自己协调线程状态
    //pthread_join(tid, &ret);
    return 0;
}
//0号子线程
//主线程计数:1
//1号子线程
//主线程计数:2
//0号子线程
//1号子线程
//主线程计数:3

2.1、线程等待

在线程创建的例子中,如果需要做到跟java一样,等子线程结束后再终止整个进程,需要用到线程等待pthread_join,具体示例看上面注释部分

pthread_join(tid, &ret);

2.1、线程终止

下面示例中,执行线程终止方法pthread_exit或者pthread_cancel,就不再打印

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void *MyThreadStrat(void *arg) {
    // 两种退出方法,退出则不会打印下方内容
//    pthread_exit(NULL);
//    pthread_cancel(pthread_self());
    printf("子线程 :%s\n", (char *) arg);
    return NULL;
}

int main() {
    pthread_t tid;
    for (int i = 0; i < 2; i++) {
        int ret = pthread_create(&tid, NULL, MyThreadStrat, NULL);
        //start 是JAVA加入的概念,这里pthread_create就加入队列了
        //tid.start();
        if (ret != 0) {
            perror("pthread_create");
            return 0;
        }
    }

    while (1) {
        printf("主线程\n");
        sleep(1);
    }
    return 0;
}
//子线程 :(null)
//主线程
//子线程 :(null)
//主线程
//主线程

2.1、线程分离

一个线程被设置为分离属性,则该线程在退出之后,不需要其他执行流回收该进程的资源,而是由操作系统统一回收

下面示例中,子线程在延迟5秒之后调用线程分离,后续不再打印

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

//线程分离
void *MyThreadStrat(void *arg) {
    (void) arg;
    int i = 0;
    while (i < 10) {
        i++;
        printf("子线程\n");
        pthread_cancel(pthread_self());
        sleep(1);
    }
    sleep(5);
    int err_code = pthread_detach(pthread_self());
    printf("线程分离:%d\n", err_code);
    return NULL;
}

int main() {
    pthread_t tid;
    void *ref;
    int ret = pthread_create(&tid, NULL, MyThreadStrat, NULL);
    pthread_join(tid, &ref);
    if (ret != 0) {
        perror("子线程创建错误");
        return 0;
    }

    while (1) {
        printf("主线程\n");
        sleep(2);
    }
    return 0;
}

三、C++线程安全处理策略

3.1、无锁案例

模拟了一个黄牛抢票的系统,使用4个线程同时去抢10张票,我们的预期结果是,4个线程每人拿到的票都不相同,不会拿到同一张票。

可以看到抢票重复了

#include <stdio.h>
#include <pthread.h>

#define THREADNUM 4
int g_val = 10;

//无锁数据异常案例
void *buyTicket(void *arg) {
    while (1) {
        if (g_val > 0) {
            printf("%p抢到%d号票\n", pthread_self(), g_val);
            g_val--;
        } else {
            break;
        }
    }
    return NULL;
}

int main() {
    pthread_t tid[THREADNUM];
    for (int i = 0; i < THREADNUM; i++) {
        int ret = pthread_create(&tid[i], NULL, buyTicket, NULL);
        if (ret < 0) {
            perror("pthread_create");
            return 0;
        }
    }
    for (int i = 0; i < THREADNUM; i++) {
        pthread_join(tid[i], NULL);
    }
    return 0;
}
//0x700003eb8000抢到10号票
//0x700003eb8000抢到9号票
//0x700003eb8000抢到8号票
//0x700003eb8000抢到7号票
//0x700003eb8000抢到6号票
//0x700003eb8000抢到5号票
//0x700003eb8000抢到4号票
//0x700003eb8000抢到3号票
//0x700003eb8000抢到2号票
//0x700003eb8000抢到1号票
//0x700003f3b000抢到10号票
//0x700004041000抢到10号票
//0x700003fbe000抢到10号票

3.2、添加互斥锁

C++里面提供了互斥锁,没有synchronized关键字

java的synchronized关键字是自己维护了一个Monitor对象

#include <stdio.h>
#include <pthread.h>

#define THREADNUM 4
int g_val = 10;
//互斥锁
pthread_mutex_t g_lock;

//锁
void *buyTicket(void *arg) {
    while (1) {
        // step3: 即将访问临界资源,加锁,锁=计数器!
        pthread_mutex_lock(&g_lock);
        if (g_val > 0) {
            printf("%p抢到%d号票\n", pthread_self(), g_val);
            g_val--;
        } else {
            // step5: 可能会导致退出 解锁
            pthread_mutex_unlock(&g_lock);
            break;
        }

        // step4: 可能会导致退出 还锁
        // 如果不在此处进行解锁的操作,则本次循环结束后,还是会进行拿锁,但是锁在上方并没有还掉
        pthread_mutex_unlock(&g_lock);
    }
    return NULL;
}

int main() {
    // step1: 初始化互斥锁
    pthread_mutex_init(&g_lock, NULL);

    pthread_t tid[THREADNUM];
    for (int i = 0; i < THREADNUM; i++) {
        int ret = pthread_create(&tid[i], NULL, buyTicket, NULL);
        if (ret < 0) {
            perror("pthread_create");
            return 0;
        }
    }
    for (int i = 0; i < THREADNUM; i++) {
        pthread_join(tid[i], NULL);
    }

    // step2: 销毁互斥锁
    pthread_mutex_destroy(&g_lock);
    return 0;
}
//0x700009fab000抢到10号票
//0x700009fab000抢到9号票
//0x700009fab000抢到8号票
//0x70000a0b1000抢到7号票
//0x70000a134000抢到6号票
//0x70000a134000抢到5号票
//0x70000a134000抢到4号票
//0x70000a134000抢到3号票
//0x70000a134000抢到2号票
//0x70000a134000抢到1号票

3.3、死锁

如果执行流加载完毕之后不进行解锁操作则会造成死锁现象

上述互斥锁的示例中,去掉pthread_mutex_unlock则造成死锁,结果是卡死在这里

image-20220127202810611
3.3.1、死锁的必要条件
  • 互斥条件:一个执行流获取了互斥锁之后,其他执行流不能获取该锁

  • 不可剥夺:A执行流拿着互斥锁,其他执行不能释放

  • 循环等待:多个执行流各自拿着对方想要的锁,并且各个执行流还去请求对方的锁

  • 请求与保持:拿着一把锁还去申请别的锁

3.3.2、避免产生死锁的方法
  • 破环死锁的必要条件:循环等待,请求与保持

  • 加锁顺序一致

  • 避免锁未释放的场景

  • 资源一次性分配

四、线程同步与条件变量

实际上就是消费者生产者模式,概念:

  • 线程同步:在保证数据安全的情况下(互斥锁),让多个执行流按照特定的顺序进行临界区资源的访问,称之为同步

  • 条件变量:是一个PCB等待队列,在该队列中存放的是线程或者进程的PCB。如果有多个线程,则将这些线程放入该等待队列,这些队列按照队列的方式顺序的出入队

下面示例中通过线程同步与条件变量,实现了生产者与消费者模式

对于条件变量一共使用了4个接口

  • step1:初始化接口

  • step2:销毁接口

  • step3:等待接口

  • step4:唤醒接口

其中唤醒接口分为

  • 唤醒一个线程
int pthread_cond_signal(pthread_cond_t *cond);
  • 唤醒所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int g_val = 1;

pthread_mutex_t g_lock;
//条件变量,实际上是一个队列
pthread_cond_t g_cond;

void *Product(void *arg) {
    while (true) {
        pthread_mutex_lock(&g_lock);

        while (g_val >= 1) {
            // step3:等待接口
            // 等待消费者消费完
            pthread_cond_wait(&g_cond, &g_lock);
        }

        g_val++;
        printf("Product done..:%d\n", g_val);

        pthread_mutex_unlock(&g_lock);

        // step4:唤醒接口
        // 通知消费者进行消费
        pthread_cond_signal(&g_cond);
    }
}

void *Consume(void *arg) {
    while (true) {
        pthread_mutex_lock(&g_lock);

        while (g_val <= 0) {
            // step3:等待接口
            pthread_cond_wait(&g_cond, &g_lock);
        }
        g_val--;
        printf("Consume done..: %d\n", g_val);

        pthread_mutex_unlock(&g_lock);
        // step4:唤醒接口
        pthread_cond_signal(&g_cond);
    }
}

int main() {
    pthread_t pt, ct;
    pthread_mutex_init(&g_lock, NULL);
    // step1:初始化接口
    pthread_cond_init(&g_cond, NULL);

    pthread_create(&pt, NULL, Product, NULL);
    pthread_create(&ct, NULL, Consume, NULL);

    // step2:销毁接口
    sleep(1);
    pthread_cond_destroy(&g_cond);

    pthread_join(pt, NULL);
    pthread_join(ct, NULL);

    return 0;
}
//Product done..:1
//Consume done..: 0
//Product done..:1
//Consume done..: 0
//循环...
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流星雨在线

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值