Java/Go双修 - Go并发Goroutine与Java对比

Goroutine的概念

协程的概念:juejin.cn/post/746181...

Goroutine就是go语言对于协程的支持,go的并发只会用到goroutine,并不需要我们去考虑多进程或者多线程

一般OS线程栈大小为2MB-8MB,且线程在创建和上下文切换的时候是需要消耗资源的,会带来性能损耗

所以我们在使用多线程的时候,我们往往会通过池化技术,即创建线程池来管理一定数量的线程

而在Go语言中,我们用多Goroutine去执行这个函数就可以了,并不需要我们来维护类似线程池的东西

也不需要使用者去关心协程是怎么调度和切换的,因为这些都已经有Go语言内置的调度器帮我们做了

Goroutine的使用

Goroutine使用起来非常方便,通常我们会将需要并发的任务封装成一个函数,然后加上go关键字就等于开启了一个Goroutine

go 复制代码
func()
go func()

主协程概念

和Java一样,Go程序的入口也是main函数。在程序开始执行的时候Go程序会为main()函数创建一个默认的Goroutine

我们称之为主协程,我们后来认为的创建了一些Goroutine,都是在这个主Goroutine的基础上进行的

go 复制代码
package main
import "fmt"

func myGroutine() {
	fmt.Println("myGroutine")
}
func main() {
	go myGroutine()
	fmt.Println("main end")
}

输出结果:

ini 复制代码
main end

为什么只输出main end呢?明明是多协程任务,为什么只打印了主协程里的end,没有打印我们开启的协程输出的myGroutine

这是因为:当main()函数结束返回的时候主Goroutine已经结束了,当主协程退出的时候,其他剩余的协程不管是否运行完也一起退出

当主协程结束时,我们开启的协程还没有执行到fmt.Println("myGroutine"),所以我们创建的协程也就跟着退出了

多协程调用WaitGroup

就是Java里面的CountDownLatch,等待所有的协程执行完毕后主协程才能走下去

go 复制代码
package main

import (
    "fmt"
    "sync"
    "time"
)

func myGoroutine(name string, wg *sync.WaitGroup) {
    defer wg.Done()

    for i := 0; i < 5; i++ {
        fmt.Printf("myGroutine %s\n", name)
        time.Sleep(10 * time.Millisecond)
    }
}

func main() {
    var wg sync.WaitGroup
    wg.Add(2)

    go myGoroutine("goroutine1", &wg)
    go myGoroutine("goroutine2", &wg)

    wg.Wait()
  	fmt.Println("main end")
}

输出结果:

ini 复制代码
myGoroutine goroutine2
myGoroutine goroutine1
myGoroutine goroutine1
myGoroutine goroutine2
myGoroutine goroutine1
myGoroutine goroutine2
myGoroutine goroutine2
myGoroutine goroutine1
myGoroutine goroutine1
myGoroutine goroutine2
main end

Java线程池对比

  • 线程池的基本使用,直接对应的就是Go开启的Goroutine,Javaer应该一眼就能懂
java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池(5个线程)
        ExecutorService executor = Executors.newFixedThreadPool(5);
        
        // 提交一个Runnable任务
        executor.execute(() -> {
            System.out.println("任务正在执行,线程: " + Thread.currentThread().getName());
        });
        
    }
}
  • CountDownLatch多线程调用
java 复制代码
import java.util.concurrent.*;

public class ThreadPoolWithCountDownLatch {
    public static void main(String[] args) throws InterruptedException {
        // 创建线程池
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        // 创建CountDownLatch,计数为5
        CountDownLatch latch = new CountDownLatch(5);
        
        // 提交5个任务
        for (int i = 1; i <= 5; i++) {
            final int taskId = i;
            executor.execute(() -> {
                try {
                    System.out.println("任务" + taskId + "开始执行");
                    // 模拟任务执行时间
                    Thread.sleep(1000 + taskId * 200);
                    System.out.println("任务" + taskId + "执行完成");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 每个任务完成后计数器减1
                    latch.countDown();
                }
            });
        }
        
        System.out.println("主线程等待所有任务完成...");
        // 主线程等待,直到计数器减到0
        latch.await();
        System.out.println("所有任务已完成,主线程继续执行");
    }
}

·

协程池的实现

Go语言虽然有着高效的GMP调度模型,理论上支持成千上万的Goroutine,但是不代表我们完完全全不需要管理Goroutine

什么东西都是过多会导致一些问题,Goroutine过多,会对调度、GC以及系统内存都会造成压力,这样会使我们的服务性能不升反降

那Java通过线程池来管理线程,那我们也可以用池化技术,构造一个协程池,把进程中的协程控制在一定数量

我们实现一个协程池不需要像Java一样需要去管理线程的复用。协程用完就丢,不需要复用

go 复制代码
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
    "time"
)

type Task struct {
    f func() error // 具体的任务逻辑
}

func NewTask(funcArg func() error) *Task {
    return &Task{
        f: funcArg,
    }
}

type Pool struct {
    RunningWorkers int64    // 运行着的worker数量
    Capacity       int64    // 协程池worker容量
    JobCh          chan *Task // 用于worker取任务
    sync.Mutex
}

func NewPool(capacity int64, taskNum int) *Pool {
    return &Pool{
        Capacity: capacity,
        JobCh:    make(chan *Task, taskNum),
    }
}

func (p *Pool) GetCap() int64 {
    return p.Capacity
}

func (p *Pool) incRunning() { // runningWorkers + 1
    atomic.AddInt64(&p.RunningWorkers, 1)
}

func (p *Pool) decRunning() { // runningWorkers - 1
    atomic.AddInt64(&p.RunningWorkers, -1)
}

func (p *Pool) GetRunningWorkers() int64 {
    return atomic.LoadInt64(&p.RunningWorkers)
}

func (p *Pool) run() {
    p.incRunning()
    go func() {
        defer func() {
            p.decRunning()
        }()
        for task := range p.JobCh {
            task.f()
        }
    }()
}

// AddTask 往协程池添加任务
func (p *Pool) AddTask(task *Task) {
    // 加锁防止启动多个 worker
    p.Lock()
    defer p.Unlock()

    if p.GetRunningWorkers() < p.GetCap() { // 如果任务池满,则不再创建 worker
        // 创建启动一个 worker
        p.run()
    }

    // 将任务推入队列,等待消费
    p.JobCh <- task
}

func main() {
    // 创建任务池
    pool := NewPool(3, 10)

    for i := 0; i < 20; i++ {
        // 任务放入池中
        pool.AddTask(NewTask(func() error {
            fmt.Printf("I am Task\n")
            return nil
        }))
    }

    time.Sleep(1e9) // 等待执行
}

拓展 - 有栈协程和无栈协程

协程的实现方式分为有栈协程和无栈协程两种。有栈协程指每个协程会保存单独的上下文(执行栈、寄存器等)

有栈协程的唤醒和挂起就是拷贝、切换上下文,无栈协程指单个线程内的所有协程都共享一个执行栈,协程的切换就是简单的函数返回

  • 有栈协程

    函数运行在调用栈上,把函数作为一个协程,那么协程的上下文就是这个函数及其嵌套函数的(连续的)栈帧和寄存器的值

    如果我们进行协程的调度,也就是保存当前正在运行的协程上下文,然后恢复下一个将要运行的协程的上下文

    因为保存上下文和普通函数执行的上下文是一样的,所以有栈协程可以在任意嵌套函数中挂起 (无栈协程不行)

    有栈协程的优点在易用性上,通常只需要调用对应的方法,就可以切换上下文挂起协程

    在有栈协程调度的时候,需要频繁的切换上下文,开销比较大

    从实现上看,有栈协程更接近于内核级线程,都需要为每个线程保存单独的上下文

    区别在于有栈协程的调度是由应用程序自行实现的,对内核是透明的,而内核级线程的调度由系统内核完成的

  • 无栈协程

    相比于有栈协程直接切换栈帧的思路,无栈协程在不改变函数调用栈的情况下,采用类似生成器的思路实现了上下文切换

    通过编译器将生成器改写为对应的迭代器类型 (内部是一个状态机)

    无栈协程需要在编译器将代码编译为对应的状态机代码,挂起的位置在编译器确定

    无栈协程的优点在于不需要保存单独的上下文,内存占用低,切换成本也低,性能高

    缺点就是需要编译器提供语义支持,无栈协程的实现是通过编译器对语法糖做了支持

相关推荐
汇匠源1 小时前
Spring Boot + +小程序, 快速开发零工市场小程序
spring boot·后端·小程序
小邓是个人才呀1 小时前
第二章:Android常用UI控件
android·java·ui
68岁扶墙肾透2 小时前
Java安全-Servlet内存马
java·安全·web安全·网络安全·系统安全·网络攻击模型
码农爱java2 小时前
Elasticsearch 深入分析三种分页查询【Elasticsearch 深度分页】
java·大数据·spring boot·后端·elasticsearch·全文检索
_extraordinary_2 小时前
Java 继承
java·开发语言·继承
黄暄2 小时前
Spring Boot 登录实现:JWT 与 Session 全面对比与实战讲解
javascript·网络·spring boot·后端
小鹭同学_2 小时前
Java基础 Day17
java·开发语言
设计师小聂!3 小时前
Spring ---IOC容器和DI的具体应用
java·后端·spring
徐子宸3 小时前
docker面试题(4)
java·spring cloud·docker
潇凝子潇3 小时前
IntelliJ IDEA设置编码集
java·ide·intellij-idea