键盘监听与协程

تم الإنشاء في: ٢١ نوفمبر ٢٠٢٤

تمت الإجابة باستخدام OpenAI 01 Preview بواسطة OpenAI01

سؤال

package cmd

import (
"live-command-middleware/mylog/wlog"
"live-command-middleware/trigger"
"live-command-middleware/websocket/ws"
"os"
"time"

"github.com/spf13/cobra"
)

// startCmd represents the start command
var startCmd = &cobra.Command{
Use: "start",
Short: "开始服务",
Long: `会开始监听操作`,
Run: func(cmd *cobra.Command, args []string) {
wlog.Info("程序开始")

keyboardControlChan := make(chan string)

err := ws.NewWsClient(keyboardControlChan)
if err != nil {
// 重试连接
reTryNum := 100
for i := 0; i < reTryNum; i++ {
time.Sleep(1 * time.Second)
wlog.Infof("第 %d 次重试连接", i+1)
err = ws.NewWsClient(keyboardControlChan)
if err == nil {
break
}
}
wlog.Errorf("ws连接失败 Error: %v", err)
os.Exit(10002)
}
ws.WsClient.StartHeartBeat()
wlog.Infof("ws连接成功,等待接受信号...")

for {
select {
case cmd := <-keyboardControlChan:
if cmd == "start" {
wlog.Infof("主线程收到启动键盘监听指令")
trigger.StartListenKeyBoard() // 启动键盘监听
} else if cmd == "stop" {
wlog.Infof("主线程收到停止键盘监听指令")
trigger.StopListenKeyBoard() // 停止键盘监听
}
}
}
},
}

func init() {
rootCmd.AddCommand(startCmd)
}


package trigger

import (
"live-command-middleware/chrome"
"live-command-middleware/enum"
"live-command-middleware/gopool"
"live-command-middleware/instruction"
"live-command-middleware/mylog/wlog"
"live-command-middleware/setting"
"strings"
"sync"

hook "github.com/robotn/gohook"
"github.com/vcaesar/keycode"
)

var (
hookChan chan hook.Event
hookRunning bool = false
KeyBoardGlobalLock = sync.Mutex{}
)

func NewInstructionStructByOperate(operate string) instruction.Instruction {
switch operate {
case enum.OperationPreheatProduct:
return &instruction.PreheatProduct{}
case enum.OperationExplainProduct:
return &instruction.ExplainProduct{}
case enum.OperationExplainProductAll:
return &instruction.ExplainProductAll{}
case enum.OperationChangePrice:
return &instruction.ChangePrice{}
case enum.OperateCoupon:
return &instruction.Coupon{}
case enum.OperateRedBag:
return &instruction.RedBag{}
case enum.OperateLuckyBag:
return &instruction.LuckBag{}
case enum.OperationComment:
return &instruction.Comment{}
case enum.OperateWindowOpen:
return &instruction.WindowOpen{}
case enum.OperateCancel:
return &instruction.Cancel{}
case enum.Audio:
return &instruction.Audio{}
default:
return nil
}
}

var ExecMapInstructionStruct = map[string]string{
enum.OperationPreheatProduct: enum.SafeExecIfNotExists,
enum.OperationExplainProduct: enum.SafeExec,
enum.OperationExplainProductAll: enum.SafeExec,
enum.OperationChangePrice: enum.NullExec,
enum.OperateCoupon: enum.SafeExecIfNotExists,
enum.OperateRedBag: enum.SafeExec,
enum.OperateLuckyBag: enum.SafeExec,
enum.OperationComment: enum.SafeExec,
enum.OperateWindowOpen: enum.SafeExec,
enum.OperateCancel: enum.SafeExec,
enum.Audio: enum.SafeExec,
}

func StartListenKeyBoard() {
KeyBoardGlobalLock.Lock()
defer KeyBoardGlobalLock.Unlock()
wlog.Infof("--- Please press ctrl + shift + q to stop hook ---")

// 加载键盘配置,加载不到就用默认配置
keyboardSettings := setting.GetUserKeyBoardSettingMap()
wlog.Infod("加载键盘配置", keyboardSettings, true)

// TODO,不用GoHook库,调试的时候太卡了,自己实现一个
for bindKey, boardSetting := range keyboardSettings {
// 在循环内部创建新的变量
bindKeyCopy := bindKey
newSettingCopy := boardSetting
str1 := strings.Split(bindKeyCopy, ",")
if len(str1) == 0 {
wlog.Warnf("bindKey %s split error", bindKeyCopy)
continue
}
// 如果keycode.Keycode[str1[0]]存在
if _, ok := keycode.Keycode[str1[0]]; !ok {
// NOTE:hook 钩子函数有BUG,传入不存在的按键会导致任何按键都触发
// 使用mod vendor之后,有可能会重新加载老版本的keycode,会少很多键值
wlog.Warnf("bindKey %s not exist", bindKeyCopy)
continue
}

func(bindKeyCopy string, newSettingCopy setting.UserKeyBoardSetting, str1 []string) {
hook.Register(hook.KeyDown, str1, func(e hook.Event) {
// 检查上次按键时间
// 当前时间 - 上次按键时间 < keyDownInterval
if chrome.ChildCtx == nil || chrome.ChildCtx.Err() != nil {
wlog.Infof("chrome 上下文已关闭,忽略按键,关闭事件监听")
hook.End()
return
}
ii := NewInstructionStructByOperate(newSettingCopy.Operate)
if ii == nil {
wlog.Infof("bind %s Operate %s no config", bindKeyCopy, newSettingCopy.Operate)
} else {
ii.Instance(newSettingCopy.Config.OperateNo, newSettingCopy.Config.OperateNos)
ii.SetBindKey(newSettingCopy.BindKey)
wlog.Infof("load instruction %s", newSettingCopy.BindKey)
execType, ok := ExecMapInstructionStruct[newSettingCopy.Operate]
if !ok {
wlog.Infof("execType %s Operate %s no config", newSettingCopy.BindKey, newSettingCopy.Operate)
} else {
if execType == enum.SafeExec {
instruction.SafeExec(newSettingCopy.Operate, ii)
} else if execType == enum.SafeExecIfNotExists {
instruction.SafeExecIfNotExists(newSettingCopy.Operate, ii)
}
}
}
})
}(bindKeyCopy, newSettingCopy, str1) // 立即调用,传入当前值的副本
}

//关闭程序 内置,不做修改
hook.Register(hook.KeyDown, []string{"q", "ctrl", "shift"}, func(e hook.Event) {
wlog.Infof("ctrl-shift-q")
hook.End()
})

wlog.Infof("启动键盘成功,等待键盘输入指令...")
wlog.Infof("当前Goroutine数量: %d", gopool.GlobalPool.Running())
if gopool.GlobalPool.Running() != 0 {
// 有一个是监听关闭状态的goroutine,在allocator.go中
wlog.Errorf("当前Gopool中Goroutine数量异常,请检查")
}

hookChan = hook.Start()
hookRunning = true
go func() {
defer func() {
if r := recover(); r != nil {
wlog.Panicf("键盘监听发生 panic: %v", r)
}
}()
<-hook.Process(hookChan)
wlog.Infof("键盘监听结束")
hookRunning = false
}()
}

// 停止键盘监听
func StopListenKeyBoard() {
if !hookRunning {
wlog.Infof("键盘监听未在运行")
return
}
wlog.Infof("停止键盘监听")
hook.End()
}


如果以这种方式启动,StartListenKeyBoard是在主线程里吗,还是goroutine里

الفكرة

理解代码结构

用户提供了Go代码,其中main程序和trigger程序的代码。这让我想到,用户需要了解StartListenKeyBoard的运行线程是主线程还是另一个协程。

监听键盘事件

主线程通过for-select循环监听keyboardControlChan,接收到"start"指令时调用trigger.StartListenKeyBoard(),除非StartListenKeyBoard自身启动goroutine,否则会在相同goroutine中执行。

启动键盘监听

在StartListenKeyBoard()的底部函数中,启动了一个新的goroutine来处理hookChan。主代码运行在调用的goroutine中,这就引起了我的注意。

الإجابة

In the provided code, the StartListenKeyBoard function is called directly within the main loop of the startCmd command handler. Specifically, it's invoked inside the Run function of the startCmd variable, which is part of the cobra.Command setup.

Here's the relevant part of the main function:

for { select { case cmd := <-keyboardControlChan: if cmd == "start" { wlog.Infof("主线程收到启动键盘监听指令") trigger.StartListenKeyBoard() // Starts keyboard listening } else if cmd == "stop" { wlog.Infof("主线程收到停止键盘监听指令") trigger.StopListenKeyBoard() // Stops keyboard listening } } }

In this loop, when a "start" command is received from the keyboardControlChan, the StartListenKeyBoard function is called directly. This means that the function runs synchronously in the same goroutine (the main goroutine) that is executing the loop.

Within the StartListenKeyBoard function, most of the setup and registration code runs in the calling goroutine (the main goroutine). The function only spawns a new goroutine at the very end, where it starts listening for keyboard events:

hookChan = hook.Start() hookRunning = true go func() { defer func() { if r := recover(); r != nil { wlog.Panicf("键盘监听发生 panic: %v", r) } }() <-hook.Process(hookChan) wlog.Infof("键盘监听结束") hookRunning = false }()

In this snippet:

  • hook.Start() is called in the main goroutine, which initializes the keyboard hook and returns a channel (hookChan).
  • A new goroutine is started with go func() { ... }(). This goroutine waits for events from hookChan by calling <-hook.Process(hookChan) and handles the processing of keyboard events.

Answer:

When started this way, StartListenKeyBoard runs in the main goroutine (thread) that calls it—not in a separate goroutine—except for specific parts where it explicitly creates new goroutines internally. So, most of StartListenKeyBoard executes synchronously in the main thread.

سؤال

在goroutine中主线程和其他的goroutine有区别吗,他们不都是goroutine会参与GMP的调度吗。用中文回答

الفكرة

探讨goroutines的差异

用户正在探讨主协程与其他协程之间的区别,并且提到在Go中,所有协程都是在G-M-P模型下进行调度的。izungumza

الإجابة

在Go语言中,所有的goroutine,包括主goroutine和其他goroutine,都是由Go运行时的调度器按照GMP(Goroutine、OS线程、处理器)模型进行调度的。从调度和执行的角度来看,主goroutine和其他goroutine之间并没有本质的区别。

主goroutine的概念:

  • 当Go程序启动时,Go运行时会创建一个主goroutine,用于执行main.main()函数,这就是程序的入口点。
  • 除了这个初始的区别之外,主goroutine和其他goroutine在运行时的管理和调度上是平等的,都由Go调度器进行调度。

在你的代码中:

  • 当你在for循环中调用trigger.StartListenKeyBoard()时,如果没有使用go关键字,那么StartListenKeyBoard()函数将在当前goroutine中同步执行。
  • 如果当前的goroutine是主goroutine,那么StartListenKeyBoard()的执行也就在主goroutine中进行。
  • StartListenKeyBoard()函数内部,只有显式使用go关键字的地方(如go func() { ... }()),才会创建新的goroutine,其余的代码都是在调用它的goroutine中同步执行的。

GMP调度模型:

  • G:代表goroutine,是Go中的轻量级线程。
  • M:代表OS Thread,即操作系统线程。
  • P:代表Processor,执行goroutine的上下文,包含运行队列等信息。

Go运行时通过GMP模型高效地调度goroutine,将它们映射到操作系统线程上执行。所有的goroutine都由调度器统一管理,没有优先级或特权的区别。

回答你的问题:

是的,在Go语言中,主goroutine和其他goroutine在调度和执行上没有区别,都是由Go运行时的调度器统一管理的。因此,当我之前提到StartListenKeyBoard()在主goroutine中执行时,主要是为了说明它是在调用它的goroutine中同步执行的,而不是在新的goroutine中异步执行。

总结:

  • 主线程和其他goroutine没有本质区别:在Go中,所有的goroutine都是由运行时调度器管理,主goroutine只是程序的起点。
  • 同步和异步执行取决于是否使用go关键字:如果直接调用函数,它会在当前goroutine中同步执行;如果使用go关键字,则会在新的goroutine中异步执行。
  • GMP模型负责调度所有的goroutine:Go运行时通过GMP模型高效地调度所有的goroutine,使其在多个OS线程上运行。

希望这能解答你的疑问。

مشاركة هذا السؤال والجواب