常用语言的线程模型(Java、go、C++、python3) | 京东云技术团队

天天见闻 天天见闻 2023-10-17 科技 阅读: 58
摘要: 背景知识软件如何驱动硬件?硬件需要相关的驱动程序才能运行,驱动程序安装在操作系统内核中。编写一个程序A后,A程序要操作硬件操作,首先需要进行系统调用,然后内核寻找相应的驱动程序来驱动硬件。驱动程序如何运行硬件?驱动程序作为硬件和操作系统之间的媒介,可以将与操作系统相关的命令翻译成硬件能够识别的电信号,同时,驱动程序还可以将硬件电信号转换为操作系统可以识别的命令。进程、轻量级进程、线程关系由于一个进程执行的空间不同,它被分为内核线程和用户进程,所有这些都称为内核线程,因为它没有虚拟地址空间。当创建一个新的用户进程时,分配一个新的虚拟地址空间,其中不同用户进程之间的资源被分离。创建新流程需要耗费大量资源,而且流程之间切换的成本也很高,因此引入了轻型流程。轻量化在本质上也是对内核线程的上层是抽象的,可以在不同的轻量级进程之间共享一些资源,但是由于轻量级进程本质上是内核线程,所以进行轻量级线程之间的切换需要进行系统调用,成本也很昂贵。内核本质上只能感知进程的存在,就像不同语言的多线程技术一样,是基于用户进程创建的线程库,线程本身不是参与处理器竞争,而是其所属的用户进程参与处理器的竞争。

背景知识

软件是如何驱动硬件的?

硬件是需要相关的驱动程序才能执行,而驱动程序是安装在操作系统内核中。如果写了一个程序 A,A 程序想操作硬件工作,首先需要进行系统调用,由内核去找对应的驱动程序驱使硬件工作。而驱动程序怎么让硬件工作的呢?驱动程序作为硬件和操作系统之间的媒介,可以把操作系统中相关的指令翻译成硬件能够识别的电信号,同时,驱动程序也可以将硬件的电信号转为操作系统能够识别的指令。

进程、轻量级进程、线程关系

一个进程由于所运行的空间不同,被分为内核线程和用户进程,之所有称之为内核线程,是因为其不拥有虚拟地址空间。如果创建一个新的用户进程,会分配一个新的虚拟地址空间,不同用户进程之间资源是隔离的。由于创建一个新的进程需要消耗很多的资源,并且在进程之间切换的代价也很昂贵,因此引入了轻量级进程。轻量级进行本质上也是对内核线程的高层抽象,虽然不同的轻量级进程之间可以共享某些资源,但由于轻量级进程本质上还是内核线程,如果进行轻量级线程之间的切换,需要进行系统调用,代价也是比较昂贵的。内核本质上只能感知到进程的存在,像不同语言的多线程技术,是在用户进程的基础上创建的线程库,线程本身不参与处理器竞争,而是由其所属的用户进程参与处理器的竞争。

如何理解用户态和内核态

首先我们需要理解到计算机资源是有限的,不管是 CPU 资源、内存资源、IO 资源、网络资源,为了保证这些资源的合理利用,需要有一个管控机制,而这个管控机制都是交于操作系统来处理的。用户态和内核态是操作系统的一种逻辑划分,本质上是进行权限控制,处于用户态的进程可以直接使用分配给其的内存空间,但如果想使用 CPU 等稀缺资源,处于用户态的进程就没有这个权限了,必须通过系统调用,让当前进程进入内核态,这样可以有更大的权限去申请 CPU 资源、内存资源、IO 资源等;

操作系统线程模型

java 语言

线程模型

在 Java 诞生之初,在 Java 中就引入了线程,最初称之为 “绿色线程”,完全由 JVM 进行管理,这和操作系统用户线程是多对一的实现,但随着操作系统对线程支持越来越强大,java 中的线程实现采用了一对一的实现,即一个 java 线程对应于一个操作系统用户线程,但是这个线程的堆栈大小是固定的,随着线程数量创建过多,可能导致内存溢出。在 java19 版本中引入了虚拟线程的概念,虚拟线程有一个动态的堆栈,可以增大和缩小,这和操作系统用户线程之间是一个多对多的关系,随着后面的发展,java 中的线程模型会变得越来越强大。

优缺点

作为一对一的线程模型维护起来比较简单,但是由于每一个线程栈信息是固定的,不利于创建大量的线程,并且多线程操作时可能涉及频繁的系统调用,上下文切换代价高。

使用方式 (以生产者消费者模型来说明)

public class ThreadTest {

public static final Object P = new Object;

static ListInteger list = new ArrayList;

@Test

public void test throws Exception {

Thread thread1 = new Thread(- {

while(true) {

try {

product;

}catch (Exception e) {

e.printStackTrace;

Thread thread2 = new Thread( - {

while(true) {

try {

consume;

}catch (Exception e) {

e.printStackTrace;

thread1.start;

thread2.start;

thread1.join;

thread2.join;

private static void product throws Exception {

synchronized (P) {

if(list.size == 1) {

// 让出锁

P.wait;

list.add(1);

System.out.println("produce");

P.notify;

private static void consume throws Exception {

synchronized (P) {

if(list.size == 0) {

P.wait;

list.remove(list.size - 1);

System.out.println("consume");

P.notify;

go 语言

go 语言线程模型

在 go 语言中,线程模型就是比较强大了,包含了三个概念:内核线程(M)、goroutine (G)、G 的上下文环境(P)。其中 G 表示基于协程创建的用户线程,M 直接关联一个内核线程,P 里面一般存放正在运行的 goroutine 的上下文环境(函数指针、堆栈地址和地址边界等)。

优缺点

go 语言中的线程模型算是很强大了,引用了协程,线程栈大小可以动态调整,很好地避免了 java 中目前的线程模型缺点。

使用方式 (以生产者消费者模型来说明)

package main

import (

"fmt"

type ThreadTest struct {

lock chan int

func (t *ThreadTest) produce {

for {

t.lock - 10

fmt.Println("produce:", 10)

func (t *ThreadTest) consume {

for {

v := -t.lock

fmt.Println("consume:", v)

func main {

maxLen := 10

t := ThreadTest{

make(chan int, maxLen),

// 重点在这里,开启新的协程,配合通道,让go的多线程变成非常优雅

go t.consume

go t.produce

select {}

c++ 语言

c++ 语言线程模型

在 c++11 中增加了操作 thread 库,提供对线程操作的进一步封装,而这个库底层是使用了 pthread 库,这个库底层采用了 1:1 线程模型,跟 java 中的线程模型类似。

优缺点

作为一对一的线程模型维护起来比较简单,但是由于每一个线程栈信息是固定的,不利于创建大量的线程,并且多线程操作时可能涉及频繁的系统调用,上下文切换代价高。

使用方式 (以生产者消费者模型来说明)

#include

#include

#include

#include

static const int SIZE = 10;

static const int ITEM_SIZE = 30;

std::mutex mtx;

std::condition_variable not_full;

std::condition_variable not_empty;

int items[SIZE];

static std::size_t r_idx = 0;

static std::size_t w_idx = 0;

void produce(int i) {

std::unique_lock lck(mtx);

while((w_idx+ 1) % SIZE == r_idx) {

std::cout  "队列满了"  std::endl;

not_full.wait(lck);

items[w_idx] = i;

w_idx = (w_idx+ 1) % SIZE;

not_empty.notify_all;

lck.unlock;

int consume {

int data;

std::unique_lock lck(mtx);

while(w_idx == r_idx) {

std::cout  "队列为空"  std::endl;

not_empty.wait(lck);

data = items[r_idx];

r_idx = (r_idx + 1) % SIZE;

not_full.notify_all;

lck.unlock;

return data;

void p_t {

for(int i = 0; i  ITEM_SIZE; i++) {

produce(i);

void c_t {

static int cnt = 0;

while(1) {

int item = consume;

std::cout  "消费第"  item  "个商品"  std::endl;

if(++cnt == ITEM_SIZE) {

break;

int main {

std::thread producer(p_t);

std::thread consumer(c_t);

producer.join;

consumer.join;

python 语言

python 线程模型

python 中的线程使用了操作系统的原生线程,python 虚拟机使用了一个全局互斥锁(GIL)来互斥线程对 Python 虚拟机的使用,当一个线程获取 GIL 的权限之后,其他的线程必须等待这个线程释放 GIL 锁,索引再多核 CPU 上,python 多线程也会退化为单线程,无法利用多核的优势。

优缺点

python 语言多线程由于 GIL 的存在,在计算密集型场景上,很难体现到优势,并且由于涉及线程切换的代码,反而可能性能还不如单线程好。

使用方式 (以生产者消费者模型来说明)

#! /usr/bin/python3

import threading

import random

import time

total = 100

lock = threading.Lock

totalTime = 10

gTime = 0

class Consumer(threading.Thread):

def run(self):

global total

global gTime

while True:

cur = random.randint(10, 100)

lock.acquire

if total = cur:

total -= cur

print("{}使用了{}, 当前剩余{}".format(threading.current_thread, cur, total))

else:

print("{}准备使用{},当前剩余{},不足,不能消费".format(threading.current_thread, cur, total))

if gTime == totalTime:

lock.release

break

lock.release

time.sleep(0.7)

class Producer(threading.Thread):

def run(self):

global total

global gTime

while True:

cur = random.randint(10, 100)

lock.acquire

if gTime == totalTime:

lock.release

break

total += cur

print("{}生产了{}, 剩余{}".format(threading.current_thread, cur, total))

gTime+= 1

lock.release

time.sleep(0.5)

if __name__ == '__main__':

t1 = Producer(name="生产者")

t1.start

t2 = Consumer(name="消费者")

t2.start

总结

在目前的线程模型中,有 1:1、M:1、M:N 多种线程模型,具体采用哪种线程模型也和硬件和操作系统的支持程度有关,像诞生比较早的语言,普通采用 M:1、1:1 线程模型,像 c++、java。而新诞生不久的 go 语言,采用的是 M:N 线程模型,在多线程的支持上更加强大。

感觉了解一下线程模型还是很有必要的,如果不清楚语言层面上的线程在操作系统层面怎么映射使用,在使用过程中就会不清不楚,可能会踩一些坑,我们都知道在 java 中不同无限的创建线程,这会导致内存溢出,go 语言中对多线程支持更加强大,很多事情不需要我们再去关注了,在语言底层已经帮助我们做了。

每种语言的底层细节太多了,如果想深入研究某一个技术,还是得花精力去研究。

作者:京东零售 姜昌伟

来源:京东云开发者社区

其他相关
电脑配置推荐高性价比 电脑配置推荐

电脑配置推荐高性价比 电脑配置推荐

作者: 天天见闻 时间:2024-03-21 阅读: 31
最新的电脑配置推荐配置推荐:CPU:12代酷睿i5- 12400F散热:利民AX120主板:微星B660M-B内存:金士顿 -野兽-3200-16G固态:金士顿nv2 500G M.2一299显卡:影驰 GTX 1660S电源:影驰500W机箱:玩嘉 流光 合计:4300左右CPU:12代酷睿i5 -12400……...
Java培训:Java 20中的新特性

Java培训:Java 20中的新特性

作者: 天天见闻 时间:2023-10-08 阅读: 70
Java20版本正式指定了七个关键特性,包括虚拟线程、矢量API建议、结构化并发、作用阈值、外部函数和内存API、记录模式以及switch语句和表达式的模式匹配。想要学习Java技术的学生,可以参加Java训练,考虑全面系统的理论知识和实操项目的学习,以便你快速学习。 1。虚拟线程 虚拟线程是结构化并发的先决条件,自JDK19首次预览以来,已经进行了一些修改。这些更改包括API的一些调整以及ThreadGroup的持久性退化。当它们进入第二预览阶段时,这些轻量级线程旨在简化具有高吞吐量要求的同时应用程序的开发和维护。Oracle相信,这种扩展将带来Java应用程序扩展方式的重大变化。自JDK 19初始预览版以来的更新涉及少量API更改(目前在JDK 19中是持久的)和ThreadGroup降级(现在在JDK 19中也是持久的)。...
Java面试:Java 内存分配与回收策略及Java死锁

Java面试:Java 内存分配与回收策略及Java死锁

作者: 天天见闻 时间:2023-10-05 阅读: 62
概述java内存分配和回收策略以及Minor GC和Major GC(Full GC)。 内存分配 堆栈区域:堆栈分为java虚拟堆栈和本地方法堆栈。 堆空间:堆由所有线程共享,在虚拟机启动时创建并唯一存储对象实例。堆区域是gc的主要区域,通常分为两个块年轻一代和老龄一代。更细的年轻一代分为Eden领域,主要放置新建对象,From survivor和To survivor保存gc幸运保存的对象,默认情况下分别占8:1:1。...
什么是线程?

什么是线程?

作者: 天天见闻 时间:2023-09-26 阅读: 69
帖子是什么?线程是操作系统能够进行运算调度的最小单位,也称为轻量级进程,是进程的执行单位。在多任务处理中,线程是用户进程的一个例子,它包含一组独立的执行单元,这些执行单元称为线程。每个线程都有自己的堆栈、堆栈以及执行上下文,彼此通过共享内存进行通信。线程可以分为单线程、多线程、线程池等不同类型。单线程意味着程序只有一条执行线…...
AMD新一代线程撕裂者再展锋芒:96核心192线程

AMD新一代线程撕裂者再展锋芒:96核心192线程

作者: 天天见闻 时间:2023-07-14 阅读: 84
AMD在高端发烧友领域独占鳌头,其锐龙线程裂裂者系列无法压制Intel,Zen3架构破裂者5000系列已经拥有64核128线程、8通道DDR4和128条PCIe 4.0,而Intel在核心和性能上略逊色;AMD在第三季度发布了Zen4架构的线程撕裂者7000系列,AMD官方的OPN产品清单已经表明,下一代线程撕裂者的顶级型号至少有两种。...
进程和线程的区别

进程和线程的区别

作者: 天天见闻 时间:2023-07-05 阅读: 95
进程和线程的区别在于进程是操作系统资源分配的基本单位,每个进程都有独立的代码和数据空间程序上下文,线程之间的切换开销较小,同一类线程与代码纤维共享数据空间;每个线程都有自己的执行堆栈和程序计数器PC,可以在操作系统中同时运行多个进程程序。另一方面,同一进程程序中多个线程同时执行CPU调度,杨清杀手系统为每个进程分配不同的内存空间,系统不会向线程分配内存线程使用的资源来自其所属进程的资源。...
我来说两句

年度爆文