为什么当我在主程序中使用goroutine时,我的程序不能关闭?

huangapple go评论125阅读模式
英文:

Why does my program not shut down when I use goroutine in main?

问题

上下文

请仔细阅读代码中的注释,所有信息都在注释中。

如果你有使用 discordgo 的经验

完整的代码可以在这里找到:https://github.com/telephrag/kubinka/tree/bug(查看 strgmain 包)。当添加了 goroutine 命令处理程序后,停止工作的问题也会出现。与数据库交互的所有内容(在 /deploy 和 /return 上存储和从数据库中删除)都完全不起作用。用户只会收到“应用程序未响应”的消息,而不是正确的响应(查看以 cmd_ 前缀开头的包)。

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "log"
  6. "os"
  7. "os/signal"
  8. "syscall"
  9. "time"
  10. "go.etcd.io/bbolt"
  11. )
  12. /* 要重现:
  13. 启动程序,等待几秒钟,然后按下^C。
  14. 预期情况是程序在几次尝试后仍不会关闭。
  15. */
  16. func WatchExpirations(ctx context.Context, db *bbolt.DB, bkt string) error {
  17. timeout := time.After(time.Second * 5)
  18. for {
  19. select {
  20. case <-timeout:
  21. tx, err := db.Begin(true)
  22. if err != nil {
  23. return fmt.Errorf("bolt: failed to start transaction")
  24. }
  25. bkt := tx.Bucket([]byte(bkt))
  26. c := bkt.Cursor()
  27. for k, v := c.First(); k != nil; k, v = c.Next() {
  28. // 处理 bucket 的内容...
  29. fmt.Println(v) // 检查 v 是否符合条件,如果符合则删除
  30. if err := tx.Commit(); err != nil { // BUG: 在循环中提交事务
  31. tx.Rollback()
  32. return fmt.Errorf("bolt: failed to commit transaction: %w", err)
  33. }
  34. timeout = time.After(time.Second * 5)
  35. }
  36. case <-ctx.Done():
  37. return ctx.Err()
  38. }
  39. }
  40. }
  41. func main() {
  42. ctx, cancel := context.WithCancel(context.Background())
  43. db, err := bbolt.Open("kubinka.db", 0666, nil)
  44. if err != nil {
  45. log.Panicf("failed to open db %s: %v", "kubinka.db", err)
  46. }
  47. if err = db.Update(func(tx *bbolt.Tx) error {
  48. _, err := tx.CreateBucketIfNotExists([]byte("players"))
  49. if err != nil {
  50. return fmt.Errorf("failed to create bucket %s: %w", "players", err)
  51. }
  52. return nil
  53. }); err != nil {
  54. log.Panic(err)
  55. }
  56. defer func() { // BUG?: 在 defer 中发生 panic
  57. if err := db.Close(); err != nil { // 在调试模式下会正常关闭
  58. log.Panicf("error closing db conn: %v", err) // 否则会卡住
  59. }
  60. }()
  61. // 使用 `ds` 处理来自用户的命令,同时在内部存储 ctx
  62. go func() { // 添加此 goroutine 会阻止程序关闭
  63. err = WatchExpirations(ctx, db, "players")
  64. if err != nil {
  65. log.Printf("error while watching expirations in db")
  66. cancel()
  67. }
  68. }()
  69. interrupt := make(chan os.Signal, 1)
  70. signal.Notify(interrupt, syscall.SIGTERM, syscall.SIGINT)
  71. for {
  72. select {
  73. // 如调试器中所见,会进入此分支
  74. // 但程序会永远停滞不前
  75. case <-interrupt:
  76. log.Println("Execution stopped by user")
  77. cancel()
  78. return // 被调用,但程序不会停止
  79. case <-ctx.Done():
  80. log.Println("ctx cancelled")
  81. return
  82. default:
  83. time.Sleep(time.Millisecond * 200)
  84. }
  85. }
  86. }
英文:

Context

Please, read the comments in code carefully. Everything is in them.

In case you have experience using discordgo

The full code can be found here: https://github.com/telephrag/kubinka/tree/bug (see packages strg and main) With addition of goroutine command handlers stop working properly as well. Everything related to interaction with database (storing and removing from database on /deploy and /return respectively) is not working at all. Users receive only "The application did not respond" message instead of proper response (see packages starting with cmd_ prefix).

  1. package main
  2. import (
  3. &quot;context&quot;
  4. &quot;fmt&quot;
  5. &quot;log&quot;
  6. &quot;os&quot;
  7. &quot;os/signal&quot;
  8. &quot;syscall&quot;
  9. &quot;time&quot;
  10. &quot;go.etcd.io/bbolt&quot;
  11. )
  12. /* TO REPRODUCE:
  13. Start the program wait a few seconds and press ^C.
  14. Expect the case of program not shutting down after few attempts.
  15. */
  16. func WatchExpirations(ctx context.Context, db *bbolt.DB, bkt string) error {
  17. timeout := time.After(time.Second * 5)
  18. for {
  19. select {
  20. case &lt;-timeout:
  21. tx, err := db.Begin(true)
  22. if err != nil {
  23. return fmt.Errorf(&quot;bolt: failed to start transaction&quot;)
  24. }
  25. bkt := tx.Bucket([]byte(bkt))
  26. c := bkt.Cursor()
  27. for k, v := c.First(); k != nil; k, v = c.Next() {
  28. // do stuff with bucket...
  29. fmt.Println(v) // check if v matches condition, delete if does
  30. if err := tx.Commit(); err != nil { // BUG: commiting transaction in a loop
  31. tx.Rollback()
  32. return fmt.Errorf(&quot;bolt: failed to commit transaction: %w&quot;, err)
  33. }
  34. timeout = time.After(time.Second * 5)
  35. }
  36. case &lt;-ctx.Done():
  37. return ctx.Err()
  38. }
  39. }
  40. }
  41. func main() {
  42. ctx, cancel := context.WithCancel(context.Background())
  43. db, err := bbolt.Open(&quot;kubinka.db&quot;, 0666, nil)
  44. if err != nil {
  45. log.Panicf(&quot;failed to open db %s: %v&quot;, &quot;kubinka.db&quot;, err)
  46. }
  47. if err = db.Update(func(tx *bbolt.Tx) error {
  48. _, err := tx.CreateBucketIfNotExists([]byte(&quot;players&quot;))
  49. if err != nil {
  50. return fmt.Errorf(&quot;failed to create bucket %s: %w&quot;, &quot;players&quot;, err)
  51. }
  52. return nil
  53. }); err != nil {
  54. log.Panic(err)
  55. }
  56. defer func() { // BUG?: Panicing inside defer
  57. if err := db.Close(); err != nil { // will close normally in debug mode
  58. log.Panicf(&quot;error closing db conn: %v&quot;, err) // will stuck otherwise
  59. }
  60. }()
  61. // use `ds` to handle commands from user while storing ctx internally
  62. go func() { // addition of this goroutine prevents program from shutting down
  63. err = WatchExpirations(ctx, db, &quot;players&quot;)
  64. if err != nil {
  65. log.Printf(&quot;error while watching expirations in db&quot;)
  66. cancel()
  67. }
  68. }()
  69. interrupt := make(chan os.Signal, 1)
  70. signal.Notify(interrupt, syscall.SIGTERM, syscall.SIGINT)
  71. for {
  72. select {
  73. // as was seen in the debugger this branch is being reached
  74. // however than program stalls eternally
  75. case &lt;-interrupt:
  76. log.Println(&quot;Execution stopped by user&quot;)
  77. cancel()
  78. return // is called but program doesn&#39;t stop
  79. case &lt;-ctx.Done():
  80. log.Println(&quot;ctx cancelled&quot;)
  81. return
  82. default:
  83. time.Sleep(time.Millisecond * 200)
  84. }
  85. }
  86. }

答案1

得分: 0

根据您在存储库中的评论,问题似乎出现在这里:

  1. tx, err := db.Begin(true)
  2. if err != nil {
  3. return fmt.Errorf("bolt: failed to start transaction")
  4. }
  5. bkt := tx.Bucket([]byte(bkt))
  6. c := bkt.Cursor()
  7. for k, v := c.First(); k != nil; k, v = c.Next() {
  8. // 处理存储桶...
  9. fmt.Println(v) // 检查 v 是否符合条件,如果符合则删除
  10. if err := tx.Commit(); err != nil { // BUG: 在循环中提交事务
  11. tx.Rollback()
  12. return fmt.Errorf("bolt: failed to commit transaction: %w", err)
  13. }
  14. timeout = time.After(time.Second * 5)
  15. }

循环可能会执行0到多次。

  • 如果没有迭代 - tx 不会被提交,timeout 不会被重置(因此 case <-timeout: 将不会再次触发)。
  • 如果有多次迭代 - 您将尝试多次 tx.Commit()(会出错)。

这可能导致您看到的问题;boltClose 函数

Close 释放所有数据库资源。在关闭数据库之前,必须关闭所有事务。

因此,如果有一个正在运行的事务,Close 会阻塞直到其完成(在内部,bolt 在事务开始时锁定 mutex,并在完成时 释放)。

解决方案是确保事务始终关闭(并且只关闭一次)。

英文:

As per the comment in your repo the issue appears to have been here:

  1. tx, err := db.Begin(true)
  2. if err != nil {
  3. return fmt.Errorf(&quot;bolt: failed to start transaction&quot;)
  4. }
  5. bkt := tx.Bucket([]byte(bkt))
  6. c := bkt.Cursor()
  7. for k, v := c.First(); k != nil; k, v = c.Next() {
  8. // do stuff with bucket...
  9. fmt.Println(v) // check if v matches condition, delete if does
  10. if err := tx.Commit(); err != nil { // BUG: commiting transaction in a loop
  11. tx.Rollback()
  12. return fmt.Errorf(&quot;bolt: failed to commit transaction: %w&quot;, err)
  13. }
  14. timeout = time.After(time.Second * 5)
  15. }

The loop could iterate 0-many times.

  • If there are no iterations - tx is not committed and timeout not reset (so case &lt;-timeout: will not be triggered again).
  • If there are more than one iterations - you will attempt to tx.Commit() multiple times (an error).

This probably led to the issue you saw; the bolt Close function:

>Close releases all database resources. All transactions must be closed before closing the database.

So if there is a transaction running Close blocks until is completes (internally bolt locks a mutex when the transaction begins and releases it when done).

The solution is to ensure that the transaction is always closed (and only closed once).

huangapple
  • 本文由 发表于 2022年7月24日 23:54:26
  • 转载请务必保留本文链接:https://go.coder-hub.com/73099912.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定