go语言之时间与并发

理论部分

核心概念:时间与并发

在 Go 语言中,处理时间相关的任务通常会涉及到并发,因为我们不希望等待某个时间点的到来或者周期性地执行任务阻塞了主程序的运行。TimerTicker 正好是 Go 并发模型中处理时间事件的利器。它们都基于 Go 的 Goroutine 和 Channel 实现。

1. time.Timer:一次性定时器

原理:

time.Timer 代表一个在未来某个时刻触发的事件。当你创建一个 Timer 时,你需要指定一个延迟时间。当这个延迟时间到达后,Timer 会向其内部的 Channel 发送一个 time.Time 类型的值。之后,这个 Timer 通常就完成了它的使命(除非你显式地重置它,但这不常用)。

你可以将 Timer 想象成一个闹钟,你设置好闹钟在某个时间响起一次。

关键点:

  • 一次性触发: Timer 主要用于在指定的延迟后执行一次操作。
  • Channel 通信: 它通过自身的 Channel (C) 来通知事件的发生。你需要从这个 Channel 中接收值。
  • 需要显式停止: 如果你创建了一个 Timer 但在它触发之前就不再需要了,你应该调用它的 Stop() 方法来释放相关资源。如果不停止,这个 Goroutine 可能会一直存在。

实际应用示例:延迟执行任务

假设你想在 2 秒后打印一条消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"time"
)

func main() {
fmt.Println("程序启动...")

// 创建一个 2 秒后触发的 Timer
timer := time.NewTimer(2 * time.Second)

// 等待 Timer 触发
<-timer.C
fmt.Println("2 秒过去了,Timer 触发了!")

// 注意:timer 在触发后,其内部的 Goroutine 可能会继续尝试发送,
// 所以即使已经触发,如果不再使用,最好也 Stop() 一下(虽然在这个简单例子中不一定必要)。
timer.Stop()
}

更细致的解释:

  1. time.NewTimer(2 * time.Second) 创建了一个新的 Timer。内部会启动一个 Goroutine,这个 Goroutine 会等待 2 秒。
  2. <-timer.C 阻塞了当前的 Goroutine(主 Goroutine),直到 timer.C 这个 Channel 接收到值。当 2 秒过去后,Timer 内部的 Goroutine 会向 timer.C 发送当前的时间。
  3. 一旦 timer.C 接收到值,阻塞解除,fmt.Println("2 秒过去了,Timer 触发了!") 这行代码就会被执行。
  4. timer.Stop() 用于停止 Timer。如果 Timer 尚未触发,Stop() 会阻止它发送事件到 Channel,并返回 true。如果 Timer 已经触发或者已经被停止,Stop() 返回 false

2. time.Ticker:周期性定时器

原理:

time.Ticker 代表一个以固定时间间隔重复触发的事件。当你创建一个 Ticker 时,你需要指定一个时间间隔。之后,Ticker 会每隔这个时间间隔就向其内部的 Channel 发送一个 time.Time 类型的值。

你可以将 Ticker 想象成一个节拍器,它会按照固定的节奏发出信号。

关键点:

  • 周期性触发: Ticker 用于以固定的时间间隔重复执行操作。
  • Channel 通信: 它也通过自身的 Channel (C) 来通知每次事件的发生。你需要在一个循环中从这个 Channel 中接收值。
  • 需要显式停止: 当你不再需要 Ticker 时,必须调用它的 Stop() 方法来停止底层的 Goroutine,否则这个 Goroutine 会一直运行下去,导致资源泄漏。

实际应用示例:每隔一段时间执行任务

假设你想每隔 1 秒打印一次当前时间,持续一段时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"time"
)

func main() {
fmt.Println("程序启动,开始周期性打印时间...")

// 创建一个每隔 1 秒触发一次的 Ticker
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop() // 确保在函数退出时停止 Ticker

// 持续 5 秒打印时间
for i := 0; i < 5; i++ {
currentTime := <-ticker.C
fmt.Println("当前时间:", currentTime)
}

fmt.Println("5 秒结束,停止打印。")
}

更细致的解释:

  1. time.NewTicker(1 * time.Second) 创建了一个新的 Ticker。内部会启动一个 Goroutine,这个 Goroutine 会每隔 1 秒向 ticker.C 发送当前时间。
  2. defer ticker.Stop() 是一个良好的习惯,它确保在 main 函数退出时 ticker.Stop() 会被调用,从而释放 Ticker 占用的资源。
  3. for i := 0; i < 5; i++ { currentTime := <-ticker.C ... } 这个循环会执行 5 次。每次循环都会阻塞在 <-ticker.C,直到 Ticker 发送一个新的时间值。接收到值后,打印当前时间。

总结对比:

特性 time.Timer time.Ticker
触发次数 一次性 周期性 (重复)
主要用途 延迟执行任务 定期执行任务
停止 建议在不再需要时调用 Stop() 必须在不再需要时调用 Stop()

实际应用场景举例:

  • time.Timer:
    • 实现请求超时机制。
    • 延迟重试某个操作。
    • 在用户空闲一段时间后执行某些清理工作。
  • time.Ticker:
    • 定期上报系统状态或监控数据。
    • 实现心跳机制。
    • 周期性地刷新缓存。

实例部分

好的,我们来针对你提到的实际场景给出 time.Timertime.Ticker 的代码讲解和示例。

1. time.Timer: 实现请求超时机制

场景描述: 当向外部服务发起请求时,为了避免长时间等待无响应,我们需要设置一个超时时间。如果在指定时间内没有收到响应,就认为请求失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main

import (
"fmt"
"net/http"
"time"
)

func fetchDataFromExternalService(url string) (string, error) {
fmt.Printf("正在请求: %s\n", url)

// 创建一个结果 Channel 和一个错误 Channel
result := make(chan string, 1)
errChan := make(chan error, 1)

// 启动一个 Goroutine 来执行实际的 HTTP 请求
go func() {
resp, err := http.Get(url)
if err != nil {
errChan <- err
return
}
defer resp.Body.Close()

// 模拟读取响应
// 这里为了简化,直接发送一个成功的消息
result <- "成功获取数据"
}()

// 创建一个超时 Timer
timeout := time.NewTimer(3 * time.Second)
defer timeout.Stop()

// 使用 select 监听不同的事件
select {
case data := <-result:
fmt.Println("请求成功:", data)
return data, nil
case err := <-errChan:
fmt.Println("请求失败:", err)
return "", err
case <-timeout.C:
fmt.Println("请求超时!")
return "", fmt.Errorf("请求超时")
}
}

func main() {
_, err := fetchDataFromExternalService("https://example.com/api/data")
if err != nil {
fmt.Println("处理请求失败:", err)
}

// 模拟超时的情况
_, err = fetchDataFromExternalService("https://slow.example.com/api/data") // 假设这个地址响应很慢
if err != nil {
fmt.Println("处理慢请求:", err)
}

// 为了让慢请求的 Goroutine 有机会执行(虽然会超时),我们等待一小段时间
time.Sleep(5 * time.Second)
}

代码讲解:

  1. fetchDataFromExternalService 函数模拟向外部服务发起请求。
  2. 我们创建了两个 Channel:result 用于接收成功的数据,errChan 用于接收错误信息。
  3. 在一个新的 Goroutine 中执行实际的 HTTP 请求。
  4. timeout := time.NewTimer(3 * time.Second) 创建了一个 3 秒后触发的 Timer
  5. select 语句用于同时监听 result Channel、errChan Channel 和 timeout.C Channel。
    • 如果从 result 接收到数据,说明请求成功。
    • 如果从 errChan 接收到错误,说明请求失败。
    • 如果 timeout.C 接收到值,说明 3 秒超时时间已到,请求超时。

2. time.Timer: 延迟重试某个操作

场景描述: 当某个操作失败时,我们不立即放弃,而是等待一段时间后进行重试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"
"math/rand"
"time"
)

func attemptOperation(attempt int) error {
// 模拟一个可能失败的操作
if rand.Intn(3) != 0 { // 大约 2/3 的概率失败
fmt.Printf("尝试 %d 失败\n", attempt)
return fmt.Errorf("操作失败")
}
fmt.Printf("尝试 %d 成功\n", attempt)
return nil
}

func retryOperationWithDelay(maxRetries int, delay time.Duration) {
for i := 1; i <= maxRetries; i++ {
err := attemptOperation(i)
if err == nil {
return
}
if i < maxRetries {
fmt.Printf("等待 %s 后重试...\n", delay)
timer := time.NewTimer(delay)
<-timer.C
timer.Stop() // 记得停止 Timer
}
}
fmt.Println("达到最大重试次数,操作失败。")
}

func main() {
rand.Seed(time.Now().UnixNano())
retryOperationWithDelay(3, 2*time.Second)
}

代码讲解:

  1. attemptOperation 函数模拟一个可能失败的操作。
  2. retryOperationWithDelay 函数接收最大重试次数和延迟时间。
  3. 在一个循环中尝试执行操作。如果操作失败且未达到最大重试次数,则创建一个 time.NewTimer(delay),等待 delay 时间后继续下一次尝试。

3. time.Timer: 在用户空闲一段时间后执行某些清理工作

场景描述: 例如,在一个交互式应用中,如果用户长时间没有操作,我们可能需要清理一些资源或执行登出操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
"fmt"
"time"
)

func simulateUserActivity() {
for i := 0; i < 5; i++ {
fmt.Println("用户正在操作...")
// 模拟用户操作的间隔
sleepTime := time.Duration(rand.Intn(3)) * time.Second
time.Sleep(sleepTime)
resetIdleTimer() // 用户活动后重置空闲计时器
}
fmt.Println("用户停止活动。")
// 即使用户停止活动,空闲清理的 Goroutine 仍然可能在运行,需要考虑如何优雅地关闭
time.Sleep(5 * time.Second)
stopIdleCheck()
}

var idleTimer *time.Timer
var idleDuration = 5 * time.Second
var idleStop = make(chan bool)

func idleCleanup() {
fmt.Println("启动空闲状态检测...")
idleTimer = time.NewTimer(idleDuration)
defer idleTimer.Stop()

for {
select {
case <-idleTimer.C:
fmt.Println("用户长时间未活动,执行清理操作...")
// 在这里执行清理逻辑
return
case <-idleStop:
fmt.Println("停止空闲状态检测。")
return
}
}
}

func resetIdleTimer() {
if !idleTimer.Stop() && len(idleTimer.C) > 0 {
<-idleTimer.C
}
idleTimer.Reset(idleDuration)
fmt.Println("空闲计时器已重置。")
}

func stopIdleCheck() {
close(idleStop)
}

func main() {
rand.Seed(time.Now().UnixNano())
go idleCleanup()
simulateUserActivity()
}

代码讲解:

  1. idleCleanup 函数启动一个 Goroutine,使用 time.NewTimer 检测用户是否空闲超过 idleDuration
  2. resetIdleTimer 函数在用户活动时被调用,它会停止之前的 Timer 并重新启动一个新的 Timer。
  3. simulateUserActivity 模拟用户的操作,并在每次操作后调用 resetIdleTimer
  4. stopIdleCheck 用于停止空闲检测的 Goroutine。

4. time.Ticker: 定期上报系统状态或监控数据

场景描述: 系统需要定期向监控中心发送自身的运行状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"runtime"
"time"
)

func reportSystemStats(interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()

for range ticker.C {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
fmt.Printf("[%s] Goroutines: %d, HeapAlloc: %d bytes\n",
time.Now().Format(time.RFC3339), runtime.NumGoroutine(), mem.HeapAlloc)
// 在这里可以将这些数据发送到监控系统
}
}

func main() {
fmt.Println("开始定期上报系统状态...")
reportSystemStats(2 * time.Second)

// 为了让程序运行一段时间,我们 Sleep 一下
time.Sleep(10 * time.Second)
fmt.Println("停止上报。")
}

代码讲解:

  1. reportSystemStats 函数接收一个时间间隔。
  2. ticker := time.NewTicker(interval) 创建一个每隔 interval 时间触发一次的 Ticker
  3. for range ticker.C 循环会持续从 ticker.C 接收时间,并在每次接收到时获取并打印当前的 Goroutine 数量和堆内存分配情况。

5. time.Ticker: 实现心跳机制

场景描述: 在分布式系统中,服务之间需要定期发送心跳包,以告知对方自己仍然存活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"fmt"
"time"
)

func sendHeartbeat(serviceName string, interval time.Duration, quit <-chan bool) {
ticker := time.NewTicker(interval)
defer ticker.Stop()

for {
select {
case <-ticker.C:
fmt.Printf("[%s] 发送心跳: %s\n", time.Now().Format(time.RFC3339), serviceName)
// 在这里可以将心跳发送给其他服务
case <-quit:
fmt.Printf("[%s] 停止发送心跳: %s\n", time.Now().Format(time.RFC3339), serviceName)
return
}
}
}

func main() {
quit := make(chan bool)
go sendHeartbeat("ServiceA", 1*time.Second, quit)
go sendHeartbeat("ServiceB", 2*time.Second, quit)

// 模拟运行一段时间
time.Sleep(5 * time.Second)
fmt.Println("停止发送心跳。")
close(quit) // 通知心跳 Goroutine 停止

// 等待心跳 Goroutine 退出
time.Sleep(1 * time.Second)
}

代码讲解:

  1. sendHeartbeat 函数接收服务名称、心跳间隔和一个退出 Channel。
  2. ticker := time.NewTicker(interval) 创建一个定期触发的 Ticker
  3. select 语句用于监听 ticker.C(发送心跳)和 quit Channel(停止心跳)。

6. time.Ticker: 周期性地刷新缓存

场景描述: 为了保证数据的时效性,我们需要定期地从数据源加载最新的数据到缓存中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
"fmt"
"time"
)

// 模拟缓存
var cache map[string]string

func loadDataFromSource() map[string]string {
fmt.Println("从数据源加载最新数据...")
// 实际场景中会连接数据库或 API
return map[string]string{
"user:1": fmt.Sprintf("User 1 - Updated at %s", time.Now().Format(time.RFC3339)),
"item:1": fmt.Sprintf("Item 1 - Updated at %s", time.Now().Format(time.RFC3339)),
}
}

func refreshCache(interval time.Duration, quit <-chan bool) {
ticker := time.NewTicker(interval)
defer ticker.Stop()

for {
select {
case <-ticker.C:
fmt.Println("开始刷新缓存...")
cache = loadDataFromSource()
fmt.Println("缓存刷新完成:", cache)
case <-quit:
fmt.Println("停止缓存刷新。")
return
}
}
}

func main() {
cache = make(map[string]string)
quit := make(chan bool)

go refreshCache(5*time.Second, quit)

// 模拟一段时间的应用运行
time.Sleep(15 * time.Second)
fmt.Println("停止缓存刷新。")
close(quit)
time.Sleep(1 * time.Second)
}

代码讲解:

  1. loadDataFromSource 函数模拟从数据源加载最新数据。
  2. refreshCache 函数使用 time.NewTicker 定期触发缓存刷新操作,调用 loadDataFromSource 更新全局的 cache 变量。

在 Go 语言的 time 包中,time.Tick()time.NewTicker() 都用于以固定的时间间隔产生事件,但它们之间存在一些关键的区别。

time.Tick()

  • 功能: time.Tick(d Duration) 是一个便利的封装函数,它返回一个只读的 <-chan Time。这个 channel 会以 d 指定的时间间隔发送当时的时间。
  • 用法: 它通常在 for range 循环中使用,以便在每个时间间隔执行某些操作。
  • 资源管理: 在 Go 1.23 之前,由 time.Tick() 创建的底层的 Ticker 不会被垃圾回收,除非程序结束。因此,如果不再需要这个 ticker,在 Go 1.23 之前推荐使用 time.NewTicker() 并调用其 Stop() 方法来释放资源。从 Go 1.23 开始,垃圾回收器可以回收不再被引用的 tickers,即使它们没有被停止。因此,现在 time.Tick() 在不需要显式停止 ticker 的场景下也更方便。
  • 无法停止: 你无法显式地停止由 time.Tick() 创建的 ticker。它会一直发送 tick,直到包含它的 goroutine 结束。
  • 返回值: 返回一个 <-chan Time。如果 d <= 0time.Tick() 会返回 nil

time.NewTicker()

  • 功能: time.NewTicker(d Duration) 创建并返回一个新的 *TickerTicker 类型包含一个 channel C (也是 <-chan Time),它会以 d 指定的时间间隔发送时间。Ticker 类型还包含一个 Stop() 方法,用于显式停止 ticker 并释放相关资源.
  • 用法: 你需要创建一个 Ticker 实例,然后通过访问其 C 字段来接收 tick。当不再需要时,应该调用 Stop() 方法。
  • 资源管理: 使用 time.NewTicker() 可以让你显式地控制 ticker 的生命周期,通过调用 Stop() 来释放资源,这在长时间运行的程序中很重要,尤其是在 Go 1.23 之前
  • 可以停止: 你可以通过调用 ticker.Stop() 来停止 NewTicker() 创建的 ticker。
  • 返回值: 返回一个指向 Ticker 类型的指针 *Ticker。如果 d <= 0time.NewTicker() 会 panic。

总结:

特性 time.Tick() time.NewTicker()
返回值 <-chan Time *Ticker (包含 C 字段和 Stop() 方法)
停止 无法显式停止 可以通过 ticker.Stop() 停止
资源管理 Go 1.23 之前: 可能导致资源泄漏,不适合长时间运行且需要停止的场景。Go 1.23 之后: GC 可以回收。 需要显式调用 Stop() 来释放资源 (在 Go 1.23 之前更重要)
用途 简单的周期性任务,不需要停止 ticker 的场景 需要更精细控制 ticker 生命周期和资源管理的场景
d <= 0 返回 nil panic

用法示例:

time.Tick() 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"time"
)

func main() {
// 每秒 tick 一次
ticker := time.Tick(1 * time.Second)
done := time.After(5 * time.Second) // 5 秒后结束

for {
select {
case t := <-ticker:
fmt.Println("Tick at", t)
case <-done:
fmt.Println("Done!")
return
}
}
}

time.NewTicker() 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"time"
)

func main() {
// 每 500 毫秒 tick 一次
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop() // 确保在函数退出时停止 ticker

done := time.After(3 * time.Second) // 3 秒后结束

for {
select {
case t := <-ticker.C:
fmt.Println("Ticker at", t)
case <-done:
fmt.Println("Done!")
return
}
}
}

在上面的 time.NewTicker() 示例中,我们使用了 defer ticker.Stop() 来确保在 main 函数结束时调用 Stop() 方法,释放 ticker 占用的资源。这在长时间运行的程序中是一个良好的实践。