Golang 批量和异步处理任务利器:Karta 异步和批量处理任务库

5,649 阅读8分钟

我是 LEE,老李,一个在 IT 行业摸爬滚打 17 年的技术老兵。

2024 年开年的第一篇文章,不过在写文章之前先祝大家新年快乐,万事如意。写这篇文章的主要目的是分享:golang 应用中如果有批量和异步处理的函数执行任务,如何高效和轻松的应对和解决这些问题。

事件背景

在日常开发中,经常需要处理大量数据的批量任务,如批量导入、批量删除和批量更新。通常情况下,我们会使用 for 循环来遍历数据,并逐个处理每条数据。然而,当数据量很大时,这种方式效率低下,因为 for 循环是串行执行的,每个任务必须等待前一个任务完成后才能执行。这导致整个任务的执行时间变得很长。虽然可以使用 channelgoroutine 来解决这个问题,但需要处理并发控制、错误处理和超时处理等复杂的并发问题。

尽管我对这种方式不太满意,但在日常开发中,我也常常使用这种方式。这种矛盾的想法让我感到困扰,因此我决定解决这个问题。在年前的最后几周休息时间里,我开始尝试实现一个轻量级的异步和批量处理函数任务的库。这个解决方案不仅可以提高任务的执行效率,还可以提高系统的并发能力,从而提升整体性能。

痛点分析

在之前提到的批量任务处理中,当数据量很大时,使用 for 循环的方式效率低下,因为每个任务必须等待前一个任务完成后才能执行。这导致整个任务的执行时间变得很长。

需要耐心的等待

package main

import (
    "fmt"
)

// doTask 模拟一个任务
func doTask(i int) {
    fmt.Println("task", i)
}

// main 主函数
func main() {
    // 串行执行任务
    for i := 0; i < 10; i++ {
        doTask(i)
    }
}

如果我使用 channelgoroutine 的方式来解决这个问题,那么我需要自一个任务队列,然后再实现一个或多个协程来执行这些任务。

需要自己控制各种逻辑

package main

import (
    "fmt"
    "time"
)

// doTask 模拟一个任务
func doTask(i int) {
    fmt.Println("task", i)
}

// main 主函数
func main() {
    // 任务队列
    taskQueue := make(chan int, 10)

    // 任务并发控制
    go func() {
        for i := 0; i < 10; i++ {
            taskQueue <- i
        }
        close(taskQueue)
    }()

    // 任务执行
    for i := 0; i < 3; i++ {
        go func() {
            for i := range taskQueue {
                doTask(i)
            }
        }()
    }

    // 等待任务执行完成
    time.Sleep(time.Second)
}

每次都要编写这么多代码,同时还要处理大量的并发问题,实在是非常繁琐。而且这样的代码也不够易用,下一个项目还得重新写一遍。此外,还需要进行兼容性和稳定性测试,工作量非常大。

如果一个项目中的批量和异步任务代码存在错误,通过简单的复制粘贴方式,这些错误也会传递到下一个项目中。如果你想解决这个问题,请问何时才能有一个解决方案?真是令人头疼。

总结痛点如下几点

  1. 代码冗长:使用 channelgoroutine 的方式需要编写大量代码。
  2. 并发问题:需要处理并发控制、错误处理和超时处理等问题。
  3. 缺乏易用性:这样的代码不够易用,每个项目都需要重新编写。
  4. 潜在风险:复制粘贴代码可能导致潜在的兼容性和稳定性问题。

项目介绍

Karta: github.com/shengyanli1…

11112.png

Karta 是一个轻量的异步和批量处理函数任务的库,它可以帮助我们快速、高效地处理大量的任务。

Karta 支持两种工作模式,分别解决不同的问题和需求:

  • 异步模式(Pipeline):处理任务数量未知的情况。在异步模式下,Karta 将任务放入队列中,然后由一个或多个协程来执行这些任务。这种模式可以提高任务的执行效率,从而提高系统的并发能力。
  • 批量模式(Group):处理任务数量已知的情况。在批量模式下,Karta 将任务分成多个批次,然后由一个或多个协程来执行这些批次。这种模式可以提高任务的执行效率,从而提高系统的整体性能。

基本上依托这两种工作模式,Karta 可以帮助我们解决大部分的批量处理问题。

在设计过程中,我参考了一些成功的模型,以便更好地思考和解决问题。

  • 异步模式(Pipeline) 参考了 WorkerPool 模式的实现。
  • 批量模式(Group) 参考了 Python 的 concurrent.futures 模块中的 ThreadPoolExecutorProcessPoolExecutor 的实现。

尽管在 GitHub 上可以找到很多类似的库,但我发现这些库要么功能过于复杂,要么功能过于简单,要么代码过于复杂,要么代码过于简单。因此,我决定从零开始,自己动手实现一个轻量级的异步和批量处理函数任务的库。

我的设计初衷:

  1. 轻量Karta 是一个代码量非常少的轻量级库,只有几百行代码。
  2. 简单Karta 的使用非常简单,只需要几行代码即可完成任务处理。
  3. 高效Karta 的执行效率非常高,可以快速、高效地处理大量任务。

架构设计

为了让 Karta 简单易用且高效执行,它的架构设计必须简洁可靠。尽管这样的设计可能导致某些功能的缺失,但我认为这是一个合理的权衡。

批量模式(Group)

模式特征

  • 任务数量已知
  • 任务分为多个批次
  • 每个批次的任务数量相同(最后一批可能不足)

流程图

karta-1.png

异步模式(Pipeline)

模式特征

  • 任务数量未知
  • 任务放入队列
  • 由一个或多个协程执行这些任务

流程图

karta-2.png

接口设计

Karta 的接口设计也非常简洁,只有几个接口,但这些接口可以帮助我们完成大部分批量处理任务。Karta 的属性控制通过 Config 结构体来实现,通过 WithXXX 方法来设置属性。

配置选项

  • WithWorkerNumber: 设置工作协程的数量
  • WithHandleFunc: 设置处理函数(默认处理函数),如果未设置处理函数,则执行指定函数。
  • WithResult: 设置是否返回结果,仅对**批量模式(Group)**有效
  • WithCallback: 设置回调函数

方法接口

批量模式(Group)

  • Stop: 停止任务
  • Map: 执行任务(类似 Python 的 ThreadPoolExecutor 中的 map 函数)

异步模式(Pipeline)

  • Stop: 停止任务
  • GetWorkerNumber: 获取工作协程的数量
  • Submit: 提交任务
  • SubmitWithFunc: 提交任务(带有处理函数)
  • SubmitAfter: 提交任务(延迟执行)
  • SubmitAfterWithFunc: 提交任务(延迟执行,带有处理函数)

Callback

  • OnBefore: 在消息处理前调用
  • OnAfter: 在消息处理后调用,将处理结果和执行错误传递给回调函数

使用示例

下面通过简单的示例来演示 Karta 的使用方法。

批量模式(Group)

在这段代码中,我定义了一个处理函数 handleFunc,然后使用 KartaGroup 方法来执行该处理函数。在示例中,将工作协程的数量设置为 2,并执行了一个批量任务。最后打印任务的执行结果。

package main

import (
	"time"

	k "github.com/shengyanli1982/karta"
)

func handleFunc(msg any) (any, error) {
	time.Sleep(time.Duration(msg.(int)) * time.Millisecond * 100)
	return msg, nil
}

func main() {
	c := k.NewConfig()
	c.WithHandleFunc(handleFunc).WithWorkerNumber(2).WithResult()

	g := k.NewGroup(c)
	defer g.Stop()

	r0 := g.Map([]any{3, 5, 2})
	println(r0[0].(int))
}

异步模式(Pipeline)

在这个示例中,我定义了一个处理函数 handleFunc,然后使用 KartaPipeline 方法来执行该处理函数。将工作协程的数量设置为 2,并执行了一个使用默认处理函数的任务以及一个使用自定义处理函数的任务。最后打印任务的执行结果。

特别提示:

  1. NewPipeline 中的 queue 参数是一个接口,你只需要实现该接口即可。NewPipeline 可以接受你的实现。
  2. NewPipeline 中的 config 参数是配置选项,你可以使用 WithXXX 方法来设置配置选项。
  3. NewFakeDelayingQueue 是一个包装器,用于实现队列的延迟功能兼容性,当队列本身不提供延迟功能时使用。
  4. NewPipeline 中的 goroutine 数量是动态的,根据队列中的任务数量进行动态调整。WithWorkerNumber 只是一个最大值。
  5. NewPipeline 在创建 goroutine 时使用限速器,防止一次性创建过多的 goroutine,从而避免系统资源耗尽。
package main

import (
	"fmt"
	"time"

	k "github.com/shengyanli1982/karta"
	"github.com/shengyanli1982/workqueue"
)

func handleFunc(msg any) (any, error) {
	fmt.Println("default:", msg)
	return msg, nil
}

func main() {
	c := k.NewConfig()
	c.WithHandleFunc(handleFunc).WithWorkerNumber(2)
	queue := k.NewFakeDelayingQueue(workqueue.NewSimpleQueue(nil))
	pl := k.NewPipeline(queue, c)

	defer func() {
		pl.Stop()
		queue.Stop()
	}()

	_ = pl.Submit("foo")
	_ = pl.SubmitWithFunc(func(msg any) (any, error) {
		fmt.Println("SpecFunc:", msg)
		return msg, nil
	}, "bar")

	time.Sleep(time.Second)
}

最后

Karta 是一个轻量级的库,用于异步和批量处理函数任务。它能够高效地处理大量任务。

Karta 支持两种工作模式:异步模式(Pipeline)批量模式(Group)。通过这两种模式,Karta 可以解决大部分批量处理问题。

通过设计和实现 Karta,我们标准化了批量和异步操作过程,实现了逻辑代码的多处复用。这大大减少了重复编写代码的时间,提高了开发效率。

最后,如果您有任何问题或建议,请在 KartaGitHub 上提出 issue。我将尽快回复您的问题。