Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go语言 多线程定时

Go语言中实现多线程定时任务的示例代码

作者:苏苏苏苏大霖

本文主要介绍了Go语言中实现多线程定时任务的示例代码,使用goroutine和channel实现轻量级线程及通信,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

简介:Go语言的并发模型基于CSP理论,使用goroutine和channel实现轻量级线程和线程间通信,非常适合处理定时任务。本文介绍如何利用Go的 time 包中的 Ticker 创建周期性任务,并通过goroutine实现多线程定时任务。示例代码演示了如何为不同任务设置不同的执行间隔,并独立执行。同时,提到了更高级的定时调度库,如 github.com/robfig/cron ,其提供更灵活的定时任务调度。

1. Go并发模型与CSP理论

1.1 Go并发模型简介

Go语言的并发模型建立在CSP(Communicating Sequential Processes)理论之上,这是一种描述并发进程间通信的数学理论。在Go语言中,每一个并发执行的单元被称为goroutine,它比传统操作系统的线程更加轻量级。CSP模型在Go中的具体实现是通过goroutine和channel(通道)进行进程间通信,即所谓的“不要通过共享内存来通信,而应通过通信来实现共享”。

1.2 CSP理论核心概念

CSP的核心思想是将数据的处理过程分散到一系列顺序执行的进程(goroutine)中,进程间通过通道(channel)进行数据交换。这样设计的目的是简化并发程序的编写和推理,因为每个进程都可以认为是独立运行的,无需担心其他进程的内部状态,仅需要关注自己与通道的交互。CSP模型中的通信是同步的,意味着发送和接收操作都是阻塞的,只有当通信双方都准备就绪时,信息才能成功传递。

1.3 Go并发与传统并发模型对比

与传统基于线程的并发模型相比,Go语言的并发模型更倾向于利用多核处理器的能力,而不是依赖于多线程。Goroutine的轻量级以及其与系统线程的映射机制使得开发者能够以极低的成本创建成百上千个并发任务。此外,通道(channel)为goroutine间的同步和数据传递提供了安全的通信机制,这降低了锁的使用,进一步提升了并发代码的可靠性和性能。

2. goroutine的使用和管理

2.1 goroutine的启动和生命周期

2.1.1 启动goroutine的基本语法

在Go语言中,启动一个 goroutine 非常简单,只需要在函数调用前加上 go 关键字。这种方式被称为协程,它是一种轻量级的线程,由Go运行时管理。每一个 goroutine 在逻辑上都有自己的调用栈,它们运行在共享同一个地址空间的多个线程上。

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

在这个例子中, say("world") 运行在一个新的 goroutine 中,而 say("hello") 在主 goroutine 中运行。运行程序会看到两个消息交织在一起打印出来,表明两个函数在并发执行。

2.1.2 goroutine的同步执行和异步执行

goroutine 可以同步和异步执行。同步执行意味着主 goroutine 会等待一个 goroutine 完成后才继续执行后续代码,通常用于依赖关系明确的任务。而异步执行则允许 goroutine 在后台 独立运行,不会阻塞主函数的执行。

同步执行的一个常见方式是使用 sync.WaitGroup ,它允许主 goroutine 等待一个或多个 goroutine 完成执行:

package main

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

func worker(wg *sync.WaitGroup, i int) {
    defer wg.Done()
    fmt.Printf("worker %d starting\n", i)
    time.Sleep(time.Second)
    fmt.Printf("worker %d done\n", i)
}

func main() {
    var wg sync.WaitGroup
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(&wg, i)
    }
    wg.Wait()
    fmt.Println("all workers done")
}

在这个例子中, WaitGroup 用于等待所有的工作协程完成它们的工作。

2.1.3 理解goroutine的调度模型

Go语言的调度器是基于 M:N 调度模型,这个模型下,M个 goroutine 被映射到N个操作系统线程上执行。这种设计允许在有限的系统线程上高效地并发执行大量 goroutine

调度器负责在适当的时机将 goroutine 调度到线程上执行。它使用了多种策略,包括协作式调度和抢占式调度。协作式调度依赖于 goroutine 主动让出CPU时间,而抢占式调度则允许调度器在满足一定条件时强制切换 goroutine

2.2 goroutine的并发控制

2.2.1 使用通道(Chan)同步goroutine

通道(Chan)是Go中用于协程之间通讯的一个核心机制。通过通道,可以安全地传递数据和同步执行多个协程。

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // send sum to c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}
    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // receive from c
    fmt.Println(x, y, x+y)
}

在这个例子中, sum 函数计算数组的一半并将其结果发送到通道。主 goroutine 从通道接收数据并打印出来。

2.2.2 利用WaitGroup等待goroutine完成

sync.WaitGroup 可以确保主 goroutine 等待一个或多个后台 goroutine 完成它们的工作。

package main

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

func process(i int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Processing %d\n", i)
    time.Sleep(time.Second)
    fmt.Printf("Done processing %d\n", i)
}

func main() {
    var wg sync.WaitGroup
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go process(i, &wg)
    }
    wg.Wait()
    fmt.Println("All processes finished")
}

2.2.3 使用Context控制goroutine的退出

context 包提供了控制goroutine退出和传递请求作用域值的机制。它常被用于控制一组相关 goroutine 的生命周期。

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context) {
loop:
    for {
        select {
        case <-ctx.Done():
            fmt.Println("worker is told to stop")
            break loop
        default:
            fmt.Println("worker is working...")
        }
        time.Sleep(time.Second)
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go worker(ctx)
    time.Sleep(time.Second * 3)
    cancel() // worker will be stopped now
    time.Sleep(time.Second * 2)
    fmt.Println("Main completed")
}

在这个例子中,通过调用 cancel() ,我们可以向 worker 发送取消信号,使其停止工作。

以上章节详细介绍了如何启动和管理 goroutine ,确保在并发执行时各部分能够正确协同工作。这为后面章节中关于定时任务的并发执行和管理打下了坚实的基础。

3.time包中的Ticker工具

3.1Ticker的基本使用方法

time 包中的 Ticker 工具提供了一种基于时间周期的定时任务触发机制。开发者可以利用它来周期性地执行任务,如每秒钟执行一次日志刷新或每分钟检查一次特定资源的状态。 Ticker 的使用非常适合那些不需要严格到毫秒级别定时的场景。

3.1.1 创建Ticker实例

创建一个 Ticker 实例非常简单,只需要调用 time.NewTicker 函数,并传入期望的定时周期即可。例如,创建一个每5秒触发一次的 Ticker 实例:

ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop() // 使用完毕后停止Ticker,防止goroutine泄漏

3.1.2 使用Ticker进行定时任务

一旦有了 Ticker 实例,我们就可以在需要的地方使用它进行定时任务。通常,我们会在一个单独的goroutine中启动一个循环,在循环中使用 <-ticker.C 来获取定时器的时间信号。

go func() {
    for range ticker.C {
        fmt.Println("Tick", time.Now())
    }
}()

这段代码会持续打印出当前的时间,每5秒更新一次。 ticker.C 是一个chan time.Time类型的channel,它会按时发送当前时间。

3.2Ticker与goroutine的结合使用

3.2.1 理解Ticker与goroutine的协作

Ticker 与goroutine的协作是Go语言并发模型的一个应用实例。 Ticker 通过发送时间信号来协调多个goroutine执行周期性任务。这样,开发者可以通过goroutine来处理各种并发的业务逻辑,而 Ticker 则负责触发这些goroutine的执行。

3.2.2 避免Ticker使用中的常见错误

在使用 Ticker 时,开发者需要避免一些常见错误:

  1. 未停止 Ticker :在goroutine完成其任务后,应适时停止 Ticker 。否则,一旦创建的 Ticker 不再被使用,就会造成资源泄漏,同时未停止的 Ticker 也会继续占用系统资源。
    go ticker := time.NewTicker(1 * time.Second) defer ticker.Stop()

  2. 忘记读取 Ticker 的channel :如果未从 ticker.C 中读取数据,该channel将会阻塞。如果读取操作被阻塞,那么发送到该channel的新数据项也会被阻塞。为了避免这个问题,开发者应该确保从 ticker.C 中读取数据,并处理可能出现的错误。

  3. 使用不恰当的时间间隔 :如果时间间隔设置得太短,可能会导致程序中的goroutine来不及完成任务就被再次触发。相反,如果时间间隔过长,可能又会导致任务的执行效率不达标。因此,选择合适的定时周期对于任务的性能至关重要。

  4. 多个goroutine共享 Ticker : Ticker 不能被多个goroutine共享,一旦多个goroutine尝试从同一个 Ticker 的channel中读取数据,就会发生竞态条件,可能导致程序行为不可预测。如果需要在多个goroutine中使用定时器,应该为每个goroutine创建独立的 Ticker 实例。

本章节介绍了 time 包中的 Ticker 工具,包括它的基本使用方法以及如何结合goroutine进行高效的周期性任务处理。下一章节,我们将探讨如何将 Ticker 与goroutine更深层次地结合使用,以及在使用过程中需要注意的一些常见错误。

4. 多线程定时任务的实现方法

在本章中,我们将探讨多线程定时任务的实现方法,特别是在Go语言的环境中。我们将分析多线程定时任务的理论基础,然后深入讲解如何使用Go语言中的goroutine和相关工具来设计和实现多线程安全的定时任务机制。

4.1 多线程定时任务的理论基础

多线程定时任务涉及到并发编程的核心概念。它们在很多场景中非常有用,例如在处理时间敏感任务、调度周期性操作或实现异步事件处理等方面。

4.1.1 多线程与并发的关系

在现代编程语言中,多线程是实现并发的一种机制。它允许在单一进程中同时执行多个线程,理论上可以提升程序的执行效率和响应能力。然而,实现多线程编程也带来了挑战,比如线程安全问题、资源竞争和死锁等。

在Go语言中,goroutine提供了更简单的并发模型。与传统的线程不同,goroutine是由Go运行时(runtime)管理的轻量级线程,启动goroutine的成本远低于传统线程。goroutine的并发控制也更加方便,通过channel、WaitGroup和Context等工具可以轻松实现线程间的协作和通信。

4.1.2 定时任务在并发环境中的挑战

在并发环境中实现定时任务会带来额外的挑战。定时任务需要在指定的时间点或时间间隔执行,但并发环境中的线程调度可能会导致任务执行时间上的偏差。例如,CPU的调度延迟、线程阻塞和IO操作等都可能导致定时任务的执行被延迟。

为了确保定时任务的准确性,设计时需要考虑到任务的调度策略、时间精度和错误处理。在Go语言中,我们可以利用 time 包中的 Ticker Timer 类型来帮助我们实现定时任务,并且通过合理设计goroutine来处理定时任务中的并发问题。

4.2 使用Go实现多线程定时任务

Go语言提供了一套强大的并发工具集,可以让我们实现高效且线程安全的定时任务。

4.2.1 goroutine在定时任务中的作用

在Go中,goroutine是实现定时任务的基石。通过goroutine,我们可以轻松地并发执行多个定时任务而不会相互干扰。以下是一个简单的例子,展示了如何启动一个goroutine来周期性地执行任务。

package main

import (
    "fmt"
    "time"
)

func main() {
    // 每隔5秒执行一次任务
 ticker := time.NewTicker(5 * time.Second)
 defer ticker.Stop()
 for {
    select {
    case <-ticker.C:
        // 在这里执行定时任务
        fmt.Println("执行周期性任务")
    }
 }
}

在这个例子中, time.NewTicker 用于创建一个新的 Ticker 实例,它会在指定的时间间隔后触发 time.Ticker 类型的 C 通道。goroutine会周期性地从 C 通道中接收时间信息,并在接收到信息时执行定时任务。

4.2.2 设计多线程安全的定时任务机制

为了设计一个多线程安全的定时任务机制,我们需要确保任务执行过程中的数据访问是同步的。在Go语言中,通常可以利用channel来实现线程安全的消息传递。

package main

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

func timedTask(ch chan<- string) {
    // 模拟定时任务执行
    time.Sleep(2 * time.Second)
    ch <- "任务执行完毕"
}

func main() {
    var wg sync.WaitGroup
    ch := make(chan string)
    // 启动goroutine执行定时任务
    wg.Add(1)
    go func() {
        defer wg.Done()
        timedTask(ch)
    }()

    // 使用WaitGroup等待任务执行完成
    go func() {
        wg.Wait()
        close(ch)
    }()

    // 等待定时任务的结果
    for result := range ch {
        fmt.Println(result)
        break
    }
}

在这个例子中,我们创建了一个channel ch 用于传递任务结果。通过 sync.WaitGroup 来等待goroutine完成任务执行。这样设计的好处是即使有多个goroutine执行定时任务,它们也不会相互干扰,因为数据的交换是通过channel同步的。

以上章节内容展示了如何利用Go语言的并发模型来实现多线程安全的定时任务。在实际开发中,我们还需要考虑定时任务的异常处理、资源管理等问题,以确保定时任务的健壮性和可靠性。

5.time.Ticker的周期性任务触发机制

5.1time.Ticker的工作原理

5.1.1 内部机制剖析

time.Ticker 是Go语言标准库 time 包中实现周期性任务触发的一个工具。它利用了Go语言的并发模型,可以高效地在goroutine之间同步和定时执行任务。

time.Ticker 内部通过一个通道(channel)来实现定时任务的周期性触发。当你创建一个 Ticker 实例时,你可以指定一个时间间隔, Ticker 会以这个间隔向通道中发送时间值。这些时间值都是 time.Time 类型的值,可以用来判断当前时间与任务执行的时间差。

在后台, Ticker 内部使用了一个循环的计时器( time.Timer )。计时器到达设定的时间间隔后,会将当前时间发送到通道中。然后 Ticker 会自动重置这个计时器,继续等待下一个时间间隔到来。

5.1.2 周期性触发任务的实现逻辑

要使用 Ticker 触发周期性任务,你首先需要创建一个 Ticker 实例,并且在一个goroutine中等待通道中的时间值。

ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()

for {
    select {
    case <-ticker.C:
        // 这里执行周期性任务
        fmt.Println("周期性任务执行:", time.Now())
    }
}

在上述代码中, time.NewTicker(5 * time.Second) 创建了一个 Ticker 实例,其任务执行间隔设置为5秒。 ticker.C 是一个通道,用来接收定时发送的时间值。通过 select 语句,我们可以等待并接收这些时间值。只要 Ticker 没有停止,它就会不断向通道中发送时间值,从而周期性地触发任务。

这种方式非常适用于需要周期性执行的任务,比如监控任务、定时检查等。

5.2time.Ticker与定时器

5.2.1 定时器的创建和管理

除了 time.Ticker 之外,Go的 time 包还提供了 time.Timer ,这是一个可以被复用的定时器。它和 time.Ticker 的主要区别在于, time.Timer 是单次触发,而 time.Ticker 是周期性触发。

创建一个 time.Timer 的方式如下:

timer := time.NewTimer(5 * time.Second)
defer timer.Stop()

// 等待定时器到期
<-timer.C

// 如果需要,可以重置定时器
timer.Reset(5 * time.Second)

time.Timer 创建后,默认是处于停止状态,需要调用 timer.Reset() 或者 timer.Stop() 来启用它。如果定时器已经到期,调用 timer.Reset() 将会清除已经到期的事件,并开始倒计时新的时间间隔。

5.2.2Ticker与定时器的配合使用

time.Ticker time.Timer 在实际应用中可以相互配合,实现更复杂的定时逻辑。

一个场景是在执行周期性任务的同时,需要在特定时刻执行一次任务。这时我们可以使用 time.Timer 来实现一次性任务的触发,同时 time.Ticker 继续周期性地触发任务。

ticker := time.NewTicker(5 * time.Second)
timer := time.NewTimer(15 * time.Second)
defer ticker.Stop()
defer timer.Stop()

for {
    select {
    case <-ticker.C:
        fmt.Println("周期性任务执行:", time.Now())
    case <-timer.C:
        fmt.Println("一次性任务执行:", time.Now())
        // 重置定时器,让它在20秒后再次触发
        timer.Reset(20 * time.Second)
    }
}

在这个例子中,每5秒执行一次周期性任务,同时在程序启动后15秒执行一次一次性任务。通过 timer.Reset() ,我们让一次性任务在20秒后再次触发。

需要注意的是, time.Timer time.Ticker 在使用时都要确保及时停止,防止goroutine泄露。在goroutine中调用 defer ticker.Stop() defer timer.Stop() 是一种良好的实践。

6. 定时任务的独立执行与并行管理

在分布式系统中,定时任务的独立执行与并行管理是保持系统效率与稳定性的重要策略。这涉及到任务的隔离、资源管理以及高效的任务调度。在本章中,我们将深入探讨如何设计支持独立执行的定时任务系统,并提出并行管理的策略,以期达到最佳的性能表现。

6.1 独立执行定时任务的设计思路

6.1.1 任务独立性的重要性和实现方式

任务的独立性是提高定时任务系统稳定性和可维护性的关键。如果任务之间相互依赖,那么任何一个任务的失败都可能导致整个系统出错,或者需要复杂的错误恢复机制。独立的任务设计可以确保单个任务的问题不会蔓延到其他任务,从而提高系统的鲁棒性。

实现独立执行的一个关键策略是任务队列,它允许将任务进行排队,并且控制它们的执行顺序。Go语言中可以使用通道(channel)来实现一个任务队列,每个任务在通道中都作为一个独立的消息存在。这样可以确保任务在执行时的独立性,每个任务的执行不会受到其他任务的影响。

6.1.2 设计任务队列和调度策略

为了实现任务的独立性,我们需要设计一个任务队列,该队列可以用来存储待执行的任务,并控制任务的执行顺序。以下是一个简单的任务队列的Go语言实现示例:

type Task struct {
    // 定义任务所需的数据结构
}

type TaskQueue struct {
    // 用于存储任务的通道
    queue chan Task
}

func NewTaskQueue(size int) *TaskQueue {
    return &TaskQueue{
        queue: make(chan Task, size),
    }
}

func (tq *TaskQueue) AddTask(task Task) {
    // 添加任务到队列
    tq.queue <- task
}

func (tq *TaskQueue) Run() {
    // 运行任务队列,从队列中取出任务并执行
    for task := range tq.queue {
        // 独立执行每个任务
        go func(t Task) {
            // 执行任务的代码逻辑
        }(task)
    }
}

我们定义了一个 Task 结构体来表示一个任务,一个 TaskQueue 结构体来管理任务队列。 AddTask 函数用于向队列中添加新的任务,而 Run 方法则启动了一个goroutine,它会从队列中取出任务并使用另一个goroutine独立地执行每个任务。

任务调度策略的实现可以根据不同的需求进行设计。一个简单的策略可以是先进先出(FIFO)原则,即先添加到队列的任务优先执行。而在复杂的场景中,调度策略可能需要考虑任务的优先级、执行时间、依赖关系等因素。

6.2 并行管理定时任务的策略

6.2.1 并行与并发的区别和联系

并行(Parallelism)和并发(Concurrency)是两个密切相关的概念,但它们并不相同。并发指的是系统能够处理多个任务的能力,而并行则是这些任务在同一时刻实际同时运行的能力。简单来说,并发是系统设计的概念,而并行是运行时的现象。

在Go语言中,goroutine是实现并发的方式,而通过多核处理器则可以实现真正的并行执行。定时任务的并行管理指的是让定时任务可以在不同的goroutine中执行,从而提升整个系统的执行效率。

6.2.2 利用Go协程进行高效并行管理

利用Go的并发机制,我们可以通过创建多个goroutine来并行执行定时任务。以下是一个并行管理定时任务的简单示例:

func parallelTaskExecution() {
    numTasks := 10
    taskQueue := NewTaskQueue(numTasks)

    // 启动并行任务执行
    var wg sync.WaitGroup
    wg.Add(numTasks)
    for i := 0; i < numTasks; i++ {
        go func(i int) {
            defer wg.Done()
            task := Task{ID: i}
            taskQueue.AddTask(task)
            // 执行任务,这里可以是调用第三方服务,或者进行计算等
        }(i)
    }

    // 等待所有任务完成
    wg.Wait()
}

func main() {
    // 开启并行执行
    parallelTaskExecution()
}

在这个例子中,我们创建了一个 sync.WaitGroup 来等待所有的goroutine执行完毕。每个goroutine都会从任务队列中获取一个任务并执行。通过这种方式,我们可以同时执行多个定时任务,提高系统的执行效率。

通过Go的并发控制和同步机制,如通道(channel)、WaitGroup等,我们可以设计出能够高效执行定时任务的系统,并确保任务之间的独立性和并行性。这不仅提升了系统的整体性能,也提高了任务处理的可靠性。

7.github.com/robfig/cron库在定时任务中的应用

在Go语言的生态中, github.com/robfig/cron 库提供了一个强大且易于使用的定时任务调度器,它基于 cron 表达式,允许用户以类似于 Unix/Linux 系统中的 cron 守护进程的方式安排定时任务。本章将重点介绍 robfig/cron 库的基本使用和高级特性。

7.1robfig/cron库简介

robfig/cron 库支持复杂的定时规则,可以用于周期性的执行后台任务,如定时发送邮件、清理临时文件、更新数据缓存等。

7.1.1robfig/cron库的主要功能

7.1.2 库的安装和基本使用

安装库非常简单,通过以下命令安装:

go get github.com/robfig/cron/v3

下面是 robfig/cron 的基本使用示例:

package main

import (
    "fmt"
    "github.com/robfig/cron/v3"
    "time"
)

func main() {
    // 创建一个cron实例
    c := cron.New()

    // 定义一个定时任务,每分钟执行一次
    _, err := c.AddFunc("@every 1m", func() {
        fmt.Println("Job executed at:", time.Now())
    })

    if err != nil {
        fmt.Println(err)
        return
    }

    // 启动定时任务
    c.Start()

    // 运行一段时间后关闭
    time.Sleep(5 * time.Minute)
    c.Stop()
}

7.2robfig/cron库高级特性应用

7.2.1 定义复杂的定时规则

robfig/cron 支持自定义和预定义的许多特殊的 cron 规则,例如,定义一个每月的第一个工作日:

_, err = c.AddFunc("0 12 1W * fri", func() {
    fmt.Println("First Friday of every month at 12PM")
})

7.2.2 错过任务的处理和重试机制

当由于各种原因(如程序停止)导致任务错过时,可以配置重试策略来确保任务最终执行:

// 定义一个定时任务,每秒检查一次
job := c.AddJob("@every 1s", NewCheckJob())

// 增加重试机制
jobEntry, err := c.AddJob("@every 1s", NewCheckJob())
if err != nil {
    log.Fatal(err)
}
jobEntry.SetMaxRetry(3) // 最多重试3次
jobEntry.SetRetryDelay(5 * time.Minute) // 重试间隔为5分钟

func NewCheckJob() cron.Job {
    return &jobImpl{}
}

type jobImpl struct{}

func (ji *jobImpl) Run() {
    // 模拟检查工作
}

这样,即使程序由于意外原因停止运行,再次启动时会自动重试未执行的任务,直到成功执行。

robfig/cron 是一个功能强大的定时任务调度器,通过使用它的基本功能,可以满足日常的定时任务需求;而其高级特性则为处理复杂场景提供了强大的支持。在实际开发中,合理运用这些特性能够极大地提高系统的稳定性和可靠性。

到此这篇关于Go语言中实现多线程定时任务的示例代码的文章就介绍到这了,更多相关Go语言 多线程定时内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文