Golang中如何停止Goroutine下运行的fmt.Scanf进程

Golang中如何停止Goroutine下运行的fmt.Scanf进程 你好,我正在开发一个程序,如果用户在一段时间后没有在控制台输入任何内容,它会超时并移动到下一个进程。在设计我的程序时,我参考了这篇文章,然而,这并没有真正取消 fmt.Scanf 的进程,并且如果在超时后还有另一个 scanf,就会变得有问题。

我创建了一个修改后的代码版本,在超时后请求新的输入:

func main() {
	ch := make(chan int)

	go func() {
		var age int
		fmt.Printf("Your age: ")
		fmt.Scanf("%d", &age)
		fmt.Println("Age: ", age)
		ch <- 1
	}()

	select {
	case <-ch:
		fmt.Println("Exiting.")
		os.Exit(0)
	case <-time.After(3 * time.Second):
		fmt.Println("\nTimed out")

		var height int
		fmt.Printf("Your height: ")
		fmt.Scanf("%d\n", &height)
		fmt.Println("Height: ", height)
	}
}

以下是一些输入示例:

Screen Shot 2022-11-11 at 18.00.20

如你所见,当它询问身高时,它从年龄的 scanf 获取了扫描输入,而不是身高的 scanf。这是因为年龄的 scanf 仍然处于活动状态。

现在我的问题是,如何停止第一个 scanf 的进程?或者是否有其他读取用户输入的方法,不需要等待并阻塞直到用户在控制台输入内容?


更多关于Golang中如何停止Goroutine下运行的fmt.Scanf进程的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

在提出问题后,我进行了更深入的探究,发现一旦调用了 scanf(或 read)函数,就无法取消其执行过程。这篇文章对此进行了非常详细的解释。

因此,我想出了一个不同的方法。不直接调用 scanf,而是使用 syscall.Select 函数来监听控制台输入:

func Select(nfd int, r *FdSet, w *FdSet, e *FdSet, timeout *Timeval) (n int, err error)

据我所知,这个函数本质上是在给定的超时时间内监视指定的文件描述符,看它们是否准备好进行读取或写入操作。如果可读或可写,则返回 1,否则返回 0。这是一个 UNIX 特性,可能无法在所有机器上工作,尤其是在 Windows 上。你可以在这里找到关于 Select 的更多信息。

使用 select 调用后,程序如下所示:

func main() {
	fmt.Printf("Your age: ")

	fd := 0 // stdin is 0 
	fds := syscall.FdSet{}
	FD_SET(&fds, fd)
	tv := syscall.Timeval{Sec: 3, Usec: 0} // 3 seconds timeout
	n, err := syscall.Select(fd+1, &fds, nil, nil, &tv)
	if err != nil {
		fmt.Println("Error: ", err)
		return
	}
	if n == 0 {
		fmt.Println("\nTimed out")

		var height int
		fmt.Printf("Your height: ")
		fmt.Scanf("%d", &height)
		fmt.Println("Height: ", height)
	} else if n == 1 {
		var age int
		fmt.Scanf("%d", &age)
		fmt.Println("Age: ", age)
	} else {
		fmt.Println("Error!")
	}
}

// by https://go.dev/play/p/LOd7q3aawd
func FD_SET(p *syscall.FdSet, i int) {
	p.Bits[i/64] |= 1 << (uint(i) % 64)
}

按预期工作!

更多关于Golang中如何停止Goroutine下运行的fmt.Scanf进程的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Golang中,fmt.Scanf 会阻塞等待输入,且无法被直接取消。解决这个问题需要使用非阻塞的输入读取方式。以下是几种解决方案:

方案1:使用bufio.Scanner配合context

package main

import (
    "bufio"
    "context"
    "fmt"
    "os"
    "time"
)

func readInputWithTimeout(ctx context.Context, prompt string) (string, error) {
    fmt.Print(prompt)
    
    ch := make(chan string)
    errCh := make(chan error, 1)
    
    go func() {
        scanner := bufio.NewScanner(os.Stdin)
        if scanner.Scan() {
            ch <- scanner.Text()
        } else {
            errCh <- scanner.Err()
        }
    }()
    
    select {
    case input := <-ch:
        return input, nil
    case err := <-errCh:
        return "", err
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()
    
    age, err := readInputWithTimeout(ctx, "Your age: ")
    if err != nil {
        if err == context.DeadlineExceeded {
            fmt.Println("\nTimed out for age")
        } else {
            fmt.Println("Error:", err)
        }
    } else {
        fmt.Println("Age:", age)
        return
    }
    
    // 继续下一个输入
    ctx2, cancel2 := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel2()
    
    height, err := readInputWithTimeout(ctx2, "Your height: ")
    if err != nil {
        fmt.Println("\nTimed out for height")
    } else {
        fmt.Println("Height:", height)
    }
}

方案2:使用select和goroutine处理标准输入

package main

import (
    "bufio"
    "fmt"
    "os"
    "time"
)

func getInput(prompt string, timeout time.Duration) (string, bool) {
    fmt.Print(prompt)
    
    inputChan := make(chan string, 1)
    
    go func() {
        reader := bufio.NewReader(os.Stdin)
        text, _ := reader.ReadString('\n')
        inputChan <- text[:len(text)-1] // 移除换行符
    }()
    
    select {
    case input := <-inputChan:
        return input, true
    case <-time.After(timeout):
        return "", false
    }
}

func main() {
    if age, ok := getInput("Your age: ", 3*time.Second); ok {
        fmt.Println("Age:", age)
        return
    }
    
    fmt.Println("\nTimed out for age")
    
    if height, ok := getInput("Your height: ", 3*time.Second); ok {
        fmt.Println("Height:", height)
    } else {
        fmt.Println("\nTimed out for height")
    }
}

方案3:使用syscall实现非阻塞读取(Unix系统)

// +build !windows

package main

import (
    "fmt"
    "syscall"
    "time"
    "unsafe"
)

func setNonblock(fd int) error {
    _, _, err := syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), 
        syscall.F_SETFL, syscall.O_NONBLOCK)
    if err != 0 {
        return err
    }
    return nil
}

func readWithTimeout(prompt string, timeout time.Duration) (string, bool) {
    fmt.Print(prompt)
    
    // 保存原始状态
    var oldState syscall.Termios
    syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdin), 
        syscall.TCGETS, uintptr(unsafe.Pointer(&oldState)))
    
    // 设置为非阻塞
    setNonblock(syscall.Stdin)
    
    defer func() {
        // 恢复原始状态
        syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdin), 
            syscall.TCSETS, uintptr(unsafe.Pointer(&oldState)))
    }()
    
    buffer := make([]byte, 256)
    start := time.Now()
    
    for time.Since(start) < timeout {
        n, _ := syscall.Read(syscall.Stdin, buffer)
        if n > 0 {
            return string(buffer[:n-1]), true // 移除换行符
        }
        time.Sleep(50 * time.Millisecond)
    }
    
    return "", false
}

func main() {
    if age, ok := readWithTimeout("Your age: ", 3*time.Second); ok {
        fmt.Println("\nAge:", age)
        return
    }
    
    fmt.Println("\nTimed out")
    
    if height, ok := readWithTimeout("Your height: ", 3*time.Second); ok {
        fmt.Println("\nHeight:", height)
    } else {
        fmt.Println("\nTimed out for height")
    }
}

推荐方案

推荐使用方案1或方案2,它们更符合Go语言的惯用法且跨平台。方案1使用context可以更好地控制超时和取消,方案2则更简洁。这些方法都能避免fmt.Scanf的阻塞问题,并能在超时后正确清理资源。

回到顶部