Go_Goroutine和channel的使用、定时器、runtime、select、条件变量

2023-10-26

程序:

程序是指编译好的二进制文件,存放在磁盘上不占用系统资源(cpu、内存),当程序在计算机上运行时,它就变成了一个进程,包括程序、数据和状态信息,程序是静态的,而进程是动态的。

进程:

  • 进程是程序的实例,是资源分配的最小单位,当一个程序开始运行时,操作系统会为该程序分配一些计算机资源,例如内存、CPU 时间和输入/输出设备,并创建一个进程来管理这些资源。

  • 一每个进程都有自己的进程 ID(PID)和内存空间,可以独立运行、暂停、终止等。

  • 每个进程可以包含多个线程,每个线程可以执行不同的任务,但它们共享进程的资源。

  • 总结:程序是进程的静态描述,进程是正在运行程序的实例。

举例:

  • 使用一个计算器程序进行计算,计算器是是一个静态的可执行文件,包含一系列指令和数据。当你打开计算器程序并输入数字时,这个程序开始在计算机上运行,此时它就变成了一个进程。进程包括了程序、数据和状态信息,如当前的输入和计算结果等等。

进程的状态:

  1. 新建(New):进程刚被创建,但还没有被操作系统调度执行,在这个状态下,操作系统会为该进程分配必要的资源(如内存、I/O 设备等)
  2. 就绪(Ready):进程已经被创建并分配了所有必需的资源,正在等待分配CPU时间片。
  3. 运行(Running):CPU分配给进程,进程正在执行其指令,占用着 CPU 的时间片。
  4. 阻塞(Blocked):进程正在等待某个事件发生,比如等待IO(输入/输出操作)操作完成。
  5. 终止(Terminated):进程已经完成了它的任务,或者因为某些原因被操作系统终止。

img

进程类型:

  1. 系统进程:在系统启动时就已经存在的进程,是系统运行的基础。
  2. 用户进程:由用户创建并运行的进程,通常是执行一些特定任务的应用程序。
  3. 守护进程:在后台运行并提供某种服务的进程,通常在系统启动时自动启动并运行,例如网络服务、系统监控服务等
  4. 临时进程:只在某些特定场景下出现的临时进程,例如用于复制文件或创建临时文件等
  5. 进程组:将多个相关联的进程组合在一起,方便管理和协同工作。
  6. 僵尸进程:当一个进程完成执行后,它会向父进程发送一个信号,告诉父进程它已经完成了。如果父进程没有及时处理这个信号,就会导致子进程成为一个僵尸进程,即该进程已经完成了它的任务,但是它的进程控制块(PCB)还存在,占用系统资源,但已经不能再进行任何操作。可以通过杀死僵尸进行使僵尸进程变成孤儿进程解决。
  7. 孤儿进程:当父进程退出后,它的子进程就变成了孤儿进程。孤儿进程将被系统进程init接管,它会成为init的子进程,并且由init负责清理它的资源。因此,孤儿进程不会成为系统资源的浪费,但是它可能会影响系统的稳定性,因为它没有父进程来管理它的行为。

僵尸进程和孤儿进程都是进程的一种状态,它们分别指的是子进程和父进程之间的关系。

多进程:

多进程指的是在同一个计算机系统中同时运行多个进程,每个进程都拥有自己的独立的内存空间、寄存器集合和其他系统资源,它们之间相互独立、互不干扰。多进程能够使得计算机系统能够同时处理多个任务,提高计算机系统的并发性和吞吐量。

优点:

  • 多进程能够提高计算机系统的并发性和吞吐量,使得系统能够同时处理多个任务。
  • 多进程之间相互独立,可以防止一个进程出现问题影响到其他进程的运行。
  • 多进程能够更好地利用多核 CPU,提高系统的运行效率。

缺点:

  • 多进程的创建和销毁需要占用系统资源,因此如果同时运行的进程过多,会对系统资源造成一定的压力。
  • 多进程之间的通信和同步比较复杂,需要考虑进程间的同步、通信和资源管理等问题。

多进程与CPU:

如果进程数小于CPU数量,所有的线程都可以同时运行。

如果进程数大于CPU时并不是同时在执行,而是通过争夺CPU使用权,交替执行,不过时间较短在感觉上像是同时在执行。

  • CPU会使用时间片轮转调度算法将不同的进程交替地放到处理器上执行,处理器会为每个进程分配一个时间片,当时间片用完时,处理器就会停止执行该进程,将处理器的控制权交给下一个进程,这样就实现了多进程的同时运行。在进程调度过程中,如果一个进程在运行时需要等待某些事件的发生,比如等待输入输出完成、等待锁等,那么它就会被挂起,直到事件发生后再继续执行。被挂起的进程会进入阻塞状态,等待事件发生的同时,不会消耗CPU时间片。一旦事件发生,操作系统会将该进程从阻塞状态转换为就绪状态,重新参与进程调度。
  • 例:有三个进程 A、B、C,它们需要在单核处理器上运行。处理器会将三个进程分配到不同的时间片中。假设每个时间片的长度为 10ms,则处理器可能将进程 A 分配到前 10ms 的时间片中,将进程 B 分配到接下来的 10ms 时间片中,将进程 C 分配到再接下来的 10ms 时间片中。当处理器执行完最后一个时间片时,就会重新开始随机分配。

线程:

线程是程序执行流的最小单元,是进程中的一个实体。一个进程可以创建多个线程,每个线程都可以独立地执行不同的任务。与进程相比,线程的开销更小,切换速度更快,线程共享进程的内存空间,包括代码段、数据段和栈。这意味着多个线程可以访问相同的变量和数据结构,从而更容易实现并发编程。当一个线程修改共享内存时,其他线程也能够立即看到这些变化,因此在一些需要同时执行多个任务的场景中,多线程的应用非常广泛。

进程和线程的区别:

在于是否共享地址空间:进程(独居),线程(合租)

  • 进程:最小分配资源单位,可看成是只有一个线程的进程

  • 线程:最小的执行单位

线程同步:

在多线程编程中,多个线程可能同时访问共享资源,例如内存中的变量、文件等等。如果不进行同步,可能会出现多个线程同时修改同一资源的情况,导致数据不一致或者程序崩溃等问题。

线程同步是多线程编程中避免多个线程同时访问共享资源而导致数据不一致或者崩溃的一种机制,可以通过加锁、条件变量等方式实现。其目的是保证线程之间一致地访问共享资源,避免冲突和数据不一致的问题。

互斥锁:

互斥锁(Mutex)是一种常用的同步机制,用于防止多线程同时访问共享资源,保证同一时间只有一个线程访问共享资源。但互斥锁也称为建议锁,在操作系统中,加锁操作通常是由编程语言或库提供的,而不是由操作系统强制要求的,如果线程不按规矩办事,依然会造成数据混乱。

互斥锁的实现通常包括两种状态:加锁、解锁。当线程加锁时,其他线程进入阻塞状态,直到锁被释放,其它线程才能访问共享资源。为了保证互斥锁的正确使用,应该在对共享资源的访问结束后及时释放锁,避免长时间占用导致死锁。

互斥锁由标准库sync中的Mutex结构体类型表示,sync.Mutex类型只有两个公开的指针方法,Lock(加锁)和Unlock(解锁)。对资源操作完成后,一定要解锁,否则会出现流程执行异常,死锁等问题,通常借助defer。锁定后,立即使用defer语句保证互斥锁及时解锁。

// 创建互斥锁,初始值 = 0(未加锁)
var mutex sync.Mutex

func printer(str string) {
	// 当有goroutine进来时就加锁
	mutex.Lock()
	for _, ch := range str {
		fmt.Printf("%c", ch)
		// 加sleep是为了能清楚的看到整个打印效果
		time.Sleep(time.Millisecond * 300)
	}
	mutex.Unlock() // 用完解锁
}

func person1() {
	printer("hello")
}

func person2() {
	printer("world")
}

func main() {
	go person1()
	go person2()
	for {

	}
}

读写锁:

互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问,这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。但是,当共享资源被频繁读取而很少被写入时,互斥锁的效率就不高了。因为在读取共享资源时,只需要保证其他goroutine不修改该资源,而不需要阻止其他goroutine进行读取操作。在这种情况下,如果使用互斥锁,那么只要有一个goroutine在读取资源,其他所有goroutine都会被阻塞。而读写锁可以解决这个问题。读写锁可以让多个读操作同时执行,但是对于写操作是完全互斥的。

读锁:共享锁,允许多个线程同时读操作

  • 读优先型读写锁:允许多个读操作同时进行,写操作需等待所有读锁释放才能操作。A线程持有读锁,B线程获取写锁时会被阻塞,直到A线程释放读锁,B线程才能获取到写锁。同时,在A线程持有读锁的情况下,其他的读操作(例如C线程)可以继续获取读锁,因为读锁是共享的。如果读操作一直占用读锁,而写操作一直无法获取写锁,则写操作就会出现饥饿现象。

写锁:独占锁,必须等线程释放锁其它线程才能操作,当写锁和读锁同时争夺CPU执行权时,写锁优先级大于读锁。

  • 写优先型读写锁:A线程持有写锁,B线程获取读锁时会被阻塞,直到A线程释放写锁。而如果C线程需要获取写锁,则B线程和其他正在等待读锁的线程都会被阻塞,直到C线程完成写操作并释放写锁。如果写操作一直占用写锁,而读操作一直无法获取读锁,则读操作就会出现饥饿现象。

饥饿现象(Starvation)是指某个进程或线程无法获得所需的资源,而导致一直无法执行或者执行受限的情况。在并发编程中,饥饿问题通常是由于某些资源被独占或者被占用时间过长而导致的。为了解决饥饿问题,可以采用一些优化策略,例如实现公平锁获取机制、设置超时时间、避免长时间持有锁等。此外,还可以根据具体的场景和需求来选择合适的锁类型和使用方式,以达到最佳的性能和资源利用效率。

GO中的读写锁由结构体类型sync.RWMutex表示

写锁定和写解锁:
func (*RWMutex)Lock()
func (*RWMutex)Unlock()

读锁定和读解锁:
func (*RWMutex)RLock()
func (*RWMutex)RUlock()

演示:

var rwMutex sync.RWMutex // 锁只有1把,但有两个属性

var value int // 模拟共享数据,代替channel

func main() {
	// 播种随机数种子
	rand.Seed(time.Now().UnixNano())

	for i := 0; i < 5; i++ {
		go TestRead(i + 1)
	}

	for i := 0; i < 5; i++ {
		go TestWrite(i + 1)
	}

	for {
	}
}

func TestRead(idx int) {
	for {
		rwMutex.RLock() // 读锁
		num := value    //从全局变量中读数据
		fmt.Println("第", idx, "个go程读到了", num)
		rwMutex.RUnlock() // 解锁
	}
}

func TestWrite(idx int) {
	for {
		// 生成随机数
		num := rand.Intn(1000)
		rwMutex.Lock() // 写锁
		value = num    //写数据到全局变量中
		fmt.Println("----第", idx, "个go程写入了", num)
		time.Sleep(time.Second) // 为了放大实验现象
		rwMutex.Unlock()        // 解锁
	}
}

输出:可以看到写锁是独占的,读锁是共享的

4go程读到了 0
----2go程写入了 6451go程读到了 6451go程读到了 6451go程读到了 6455go程读到了 6453go程读到了 645
----2go程写入了 4911go程读到了 4911go程读到了 4911go程读到了 4911go程读到了 4911go程读到了 491
----1go程写入了 794go程读到了 794go程读到了 794go程读到了 795go程读到了 791go程读到了 79

协程(Goroutine):

  • 协程也叫轻量级线程,与线程相比,协程最大的优势在于轻量级。可以在程序中创建成百上千个协程,并发地执行多个任务。

  • 一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源

  • 协程之间可以通过通道(Channel)进行通信,从而实现协程之间的数据交换和同步操作。

协程对比进程的优点:

  1. 更轻量级:协程的创建和切换开销很小,可以同时创建成千上万个协程。执行Goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。
  2. 灵活性高:协程可以手动控制调度,可以在协程之间自由切换,适合处理IO密集型任务,如网络通信、文件读写等。
  3. 更易于实现并发:协程之间可以通过通道进行通信和同步操作,更加方便实现并发编程。
  4. 更加灵活:协程可以根据需要调整自身的数量和大小,以适应不同的并发场景。
  5. 无需锁:协程的调度不需要加锁,因为同一时间只有一个协程在执行,不会出现线程安全问题。

创建Goroutine只需在函数调⽤语句前添加go关键字即可,开发⼈员无需了解任何执⾏细节,调度器会自动将其安排到合适的系统线程上执行。

func main() {
	go Test()
	fmt.Println("main fun Runtime")
}

func Test() {
	fmt.Println("Test01 fun Runtime")
}

输出:和预期先打印Test fun Runtime,再打印main fun Runtime不符。

main fun Runtime

原因是 main() 是一个主协程(main Goroutine),Test函数被作为一个新的Goroutine启动(子协程),而main函数继续执行它的后续代码,main函数的Println语句会立即被执行,而Test函数的Println语句可能在main函数的Println语句之前或之后执行。因为Test函数的Println语句还没来得及执行,而main函数已经执行结束了,整个程序会立即退出,所以Test函数中的输出没有机会被打印出来。

总结:主Goroutine退出后,其它Goroutine也会自动退出

可以使用sleep解决,main函数输出一行文字后调用time.Sleep函数等待一秒钟。在这段等待时间内,Test函数有足够的时间执行并输出结果。在等待时间结束后,程序会继续执行并退出。输出结果可能会因为并发执行的顺序而有所不同,但Test函数的输出一定会被打印出来。

func main() {
	go Test()
	fmt.Println("main fun Runtime")
	time.Sleep(1 * time.Second)
}

func Test() {
	fmt.Println("Test01 fun Runtime")
}

输出:

main fun Runtime
Test01 fun Runtime

channel:

  • channel:管道/队列,是一种引用数据类型,默认值为nil,主要用来解决协程的同步、数据共享的问题。
  • channel默认是双向的,但也可以指定为单向,双向通道可以同时发送和接收数据。
  • channel可以建立goroutine之间的通信连接,channel的特点是:先进先出、线程安全不需要加锁
  • goroutine运行在同一个线程中,因此访问共享内存必须做好同步。goroutine奉行通过通信来共享内存,而不是共享内存来通信。
    • 传统的 goroutine 使用加锁的方式来保证并发访问的安全性,锁是加在共享内存上的。而 channel 的实现原理是基于 CSP(Communicating Sequential Processes)模型,它是一种基于通信而非共享内存的并发模型,通过channel同一时间只有一个协程可以访问数据,所以不会出现抢占CPU,从而确保并发安全。

无缓冲channel:

无缓冲的channel是指在接收前没有能力保存任何值的通道,这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作,就好像两个人打电话一样,必须有人打,同时有人接,否则通道会导致先执行发送或接收操作的 goroutine 阻塞等待。

**阻塞:**由于某种原因数据没有到达,当前协程持续处于等待状态,直到条件满足,才接触阻塞。

**同步:**在两个或多个协程(线程)间,保持数据内容一致性的机制。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SeAjlMoR-1678620655204)(/Users/itzhuzhu/Library/Application Support/typora-user-images/image-20230309004902644.png)]

无缓冲channel执行流程:

  1. 两个 goroutine 都到达通道,但哪个都没有开始执行发送或者接收数据。

  2. 左侧的 goroutine 将它的手伸进了通道,这模拟了向通道发送数据的行为。这时,这个 goroutine 会在通道中被锁住,直到交换完成才解锁。

  3. 右侧的 goroutine 将它的手放入通道,这模拟了从通道里接收数据。这个 goroutine 一样也会在通道中被锁住,直到交换完成才解锁。

  4. 4和5模拟了发送端将数据发出,接收端接收到数据。

  5. 两个 goroutine 都将它们的手从通道里拿出来,这模拟了被锁住的 goroutine 得到释放。

创建channel:

ch := make(chan int) // 创建一个int类型的channel,默认长度为0

var ch chan int

发送数据到channel:

ch <- 100 // 向channel发送int类型的数值100

从channel接收数据:

c := <- ch // 从channel中接收数据,并赋值给变量c
<- ch  //从从channel中接收数据,忽略结果

关闭channel:

close(ch) // 关闭channel,只能是写端关闭,且关闭后只能读数据不能写数据

查看channel的长度和容量:

func main() {
	ch := make(chan int)
	fmt.Println("channel中未读取数据的长度:", len(ch), "channel的容量:", cap(ch))
}

写端关闭后不能继续再写,但读端可以继续读,无缓冲的channel读到的是channel类型默认值

func main() {
	ch := make(chan int)
	go func() {
		for i := 0; i < 5; i++ {
			ch <- i
		}
		// 写完数据就关闭,否则会死锁,没有写端在写,但是读端会一直等
		close(ch)
	}()

	for {
		// 写端没关闭channel就返回true,读端可以读到数据
		num, ok := <-ch
		if ok == true {
			fmt.Println("主go程读到数据:", num)
		} else {
			// 写端关闭就返回false,读端可以继续读,但读到的是channel数据类型默认值
			n := <-ch
			fmt.Println("关闭后读到的数据:", n)
			break
		}
	}
}

输出:

go程读到数据: 0go程读到数据: 1go程读到数据: 2go程读到数据: 3go程读到数据: 4
关闭后读到的数据: 0

除了使用if判断外,还可以使用range读数据

	for num := range ch {
		fmt.Println("读到数据:", num)
	}

有缓冲channel:

有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个数据值的通道,这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收,属于异步操作。

  • 通道中没有要接收的值时,接收会阻塞。

  • 通道没有可用缓冲区存储时,发送会阻塞。

创建channel:

ch := make(chan int , 2) // 创建一个int类型的channel,设置长度为2

写端关闭后,有缓冲的channel先从缓冲读完数据,再读就是channel的默认值。

func main() {
	ch := make(chan int, 3)
	go func() {
		for i := 0; i < 5; i++ {
			fmt.Println("子go程写入数据:", i)
			// 缓冲区满时,发送会阻塞
			ch <- i
			// time.Sleep(time.Second)  // 加上sleep解决打印顺序问题
		}
		close(ch)
	}()

	for {
		// 写端没关闭channel就返回true,读端可以读到数据
		num, ok := <-ch
		if ok == true {
			fmt.Println("主go程读到数据:", num)
		} else {
			// 写端关闭就返回false,读端可以继续读,但读到的是channel数据类型默认值
			n := <-ch
			fmt.Println("关闭后读到的数据:", n)
			break
		}
	}
}

输出:可以看到创建的channel容量是3,但是写端写了5次,其实是打印结果有顺序问题,当子go程写完3次后,CPU被主go程抢走,但主go程只是把channel的值保存到了num中,还没来得及打印CPU又被子go程抢走了,这就会造成打印延迟,但其实已经读走了数据。打印需要调用电脑屏幕,而这种IO操作比较耗时,因为很多进程都在用屏幕嘛,加个sleep就没问题了

go程写入: 0go程写入: 1go程写入: 2go程写入: 3go程写入: 4go程读到: 0go程读到: 1go程读到: 2go程读到: 3go程读到: 4go程写入: 5go程写入: 6go程写入: 7go程写入: 8go程写入: 9go程读到: 5go程读到: 6go程读到: 7go程读到: 8go程读到: 9

单向channel:

使用单向 Channel 可以增加代码的可读性和安全性,因为它可以限制对数据的访问,避免多个 Goroutine 之间产生不必要的竞争条件。

单向 Channel 只允许对数据进行发送或接收操作,而不允许同时进行。可以将一个双向 Channel 转换为单向channel,但单向不能转换为双向。

创建单向channel:

	var ch1 chan int   // 双向channel,可读可写
	var ch2 chan<- int // 单向写channel
	var ch3 <-chan int // 单向读channel

模拟生产者消费者:

生产者消费者模型是一种常见的并发编程模型,其中生产者生成数据并将其放入共享的缓冲区,而消费者则从缓冲区中取出数据并进行处理。在这个模型中,生产者和消费者是独立的线程或进程,通过缓冲区进行通信和同步。其目的是解决生产者和消费者在访问共享资源时可能会出现的竞争和冲突问题,也可以降低内部耦合度,从而提高程序的效率和并发性。

func main() {
	// 先定义一个双向channel
	ch := make(chan int, 3)

	var s chan<- int = ch // 定义为写channel
	var r <-chan int = ch // 定义为读channel

	go send(s) // 子go程写,模拟生产者
	rece(r)    // 主go程读,模拟消费者
}

func send(s chan<- int) {
	defer close(s)
	for i := 0; i < 5; i++ {
		// 如果读端不读就会阻塞
		s <- i
		fmt.Println("写了数据:", i)
	}
}

func rece(c <-chan int) {
	for num := range c {
		fmt.Println("读到数据:", num)
	}
}

死锁:

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称为系统处于死锁状态或系统产生了死锁,channel至少要在2个goroutine通信,否则会出现死锁的情况。

死锁报错:

fatal error: all goroutines are asleep - deadlock!

单Goroutine导致死锁

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

	// 将789写到channel之后,因为读端没有接收,写端就会阻塞在这里,将读和写使用两个goroutine就可以解决
	c <- 789    // 写端是main函数,阻塞后main就释放了CPU执行权,整个main函数结束

	num := <-c // 不会走到这一步了
	fmt.Println(num)
}

Goroutine之间channel访问顺序导致死锁

func main() {
	ch := make(chan int)
	
	// 这里是先去读数据,但是还没有写端开始写,所以读端阻塞,读端是main函数,阻塞后main就释放了CPU执行权,整个main函数结束
	num := <-ch  // 把这个拿到匿名goroutine下面就可以解决
	fmt.Println(num) // 走不到这里了

	go func() {
		ch <- 0
	}()
}

多Goroutine多channel交叉死锁

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		for {
			select {
			// 如果读到ch1有数据,就往ch2里写同样的数据
			case num := <-ch1:
				ch2 <- num
			}
		}
	}()

	for {
		select {
		// 如果读到ch2有数据,就往ch1里写同样的数据
		case num := <-ch2:
			ch1 <- num
		}
	}
}

互斥锁和channel混用会发生死锁现象,但不报错,算是隐式死锁,解决方式就是不要混用或使用条件变量解决

// 要求先拿锁再共享数据,发生隐式死锁

var rwMutex sync.RWMutex

func main() {
	// 播种随机数种子
	rand.Seed(time.Now().UnixNano())
  // 用于传递数据的channel
	ch := make(chan int) 

	for i := 0; i < 5; i++ {
		go TestRead(ch, i+1)
	}

	for i := 0; i < 5; i++ {
		go TestWrite(ch, i+1)
	}

	for {
	}
}

func TestRead(ch <-chan int, idx int) {
	for {
		rwMutex.RLock() // 读锁,在这里开始阻塞,因为channel要求两端同时在线,当读模式加锁后没有释放,写端是无法加锁开始写数据的
		num := <-ch
		fmt.Println("第", idx, "个go程读到了", num)
		rwMutex.RUnlock() // 解锁
	}
}

func TestWrite(ch chan<- int, idx int) {
	for {
		// 生成随机数
		num := rand.Intn(1000)
    // 加写锁
		rwMutex.Lock()

		ch <- num
		fmt.Println("第", idx, "个go程写入了", num)
		time.Sleep(time.Second) // 为了放大实验现象
		rwMutex.Unlock()        // 解锁
	}
}

定时器:

time.Timer是Go语言标准库中提供的一个定时器,它可以在指定的时间间隔后触发一个事件。这个事件可以是一个函数或者一个 channel,这样就不需要在程序中使用繁琐的睡眠和轮询等方式来等待时间间隔的到来了。

源码:

type Timer struct {
	C <-chan Time // 写channel,定时结束后操作系统会往C里面写当前时间
	r runtimeTimer
}

创建:

time.NewTimer(d) // d是时间间隔,表示多长时间后触发定时器,

<-timer.C // 等待定时器触发,如果没到时间将会阻塞,当定时器触发后,程序将会执行定时器触发后的操作。

演示:

func main() {
	fmt.Println("定时前时间:", time.Now())
	timer := time.NewTimer(time.Second * 2)
	nowTime := <-timer.C
	fmt.Println("定时结束时间:", nowTime)
}

输出:

定时前时间: 2022-03-09 22:55:57.150022 +0800 CST m=+0.000191710
定时结束时间: 2022-03-09 22:55:59.15143 +0800 CST m=+2.001609835

time.After:直接返回了timer.C,和time.NewTimer效果是一样的,也可以实现定时效果

func main() {
	fmt.Println("定时前时间:", time.Now())
	after := <-time.After(time.Second * 2)
	fmt.Println("定时结束时间:", after)
}

输出:

定时前时间: 2022-03-09 23:01:56.826234 +0800 CST m=+0.000194251
定时结束时间: 2022-03-09 23:01:58.827544 +0800 CST m=+2.001514585

停止定时器:

time.Stop():停止 time.Timer 计时器的方法,取消未执行的定时任务,防止定时器在不需要时浪费系统资源,如果在定时器到期之前调用 time.Stop() 方法,则会取消该计时器。如果定时器已经到期并且任务已经执行,调用 time.Stop() 方法不会有任何影响。

func main() {
	timer := time.NewTimer(time.Second * 2)

	go func() {
		<-timer.C
		fmt.Println("定时结束")
	}()
	
	timer.Stop() // 设置定时器停止,相当于NewTimer的参数是0了
  
  // 这里会出现死锁,写端是系统往channel里写当前时间,写完以后没有close,读端还会一直去读
	for {
	}
}

重置定时器:

time.Reset():重置定时器的时间,并开始新的计时,如果定时器已经在计时,则会停止当前计时,并按照重置后的时间开始新的计时。如果定时器已经被停止,则不会有任何效果。

func main() {
	timer := time.NewTimer(time.Second * 10)

	timer.Reset(time.Second * 1) // 重置定时时间

	go func() {
		t := <-timer.C
		fmt.Println("定时完毕", t)
	}()

	time.Sleep(time.Second * 2)
}

周期定时器:

time.Ticker()是 Go 语言标准库中提供的一种定时触发机制,用于在指定的时间间隔内重复执行一个任务。

它会返回一个 Ticker 类型的值,Ticker 类型是一个结构体,其中包含一个 channel 和一个 time.Duration 类型的值,用于表示定时器的时间间隔。当我们通过 Ticker.C 属性读取 Ticker 结构体中的 channel 时,会每隔指定时间间隔向其中写入一个时间值。因此,我们可以通过对 Ticker.C 进行监听来实现定时触发任务的目的。

func main() {
	// 创建一个bool类型的channel用于结束ticker
	quit := make(chan bool)

	// 创建定时器,每隔2秒给channel发送一次当前时间
	ticker := time.NewTicker(time.Second * 2)

	i := 0
	// 启动一个goroutine,用于接收ticker的事件
	go func() {
		for {
			i++
			c := <-ticker.C
			fmt.Println("读到数据:", c)

			if i == 3 {
				//ticker.Stop()
				//runtime.Gosched()
				quit <- true
			}
		}
	}()

	// 主函数阻塞在这里等着读数据,读到数据后继续往下走就终止了整个程序
  fmt.Println(<-quit)
}

或使用range:

func main() {
	ticker := time.NewTicker(time.Second * 2)

	go func() {
		for t := range ticker.C {
			fmt.Println("读到数据:", t)
		}
	}()
	time.Sleep(time.Second * 3)
}

select:

select 是 Go 语言的一种控制结构,它类似于 switch 语句,不同之处在每个case语句里必须是一个IO操作select 可以用于在多个通道中等待可读或可写的操作,一旦其中一个通道准备好了,就会执行相应的代码块。如果多个通道都准备好了,select 会随机选择其中一个进行操作,如果都没准备好,就会阻塞等待。

default语句通常用于避免select语句的阻塞。当所有的case语句都没有满足条件时,就会执行default,如果没有default语句,select语句就会一直阻塞,直到某个case满足条件或者有case语句执行了超时操作。

	select {
	case <-chan1:
		// 如果chan1成功读到数据,则进行该case处理语句
	case chan2 <- 1:
		// 如果成功向chan2写入数据,则进行该case处理语句
	default:
		// 如果上面都没有成功,则进入default处理流程
	}

演示:

func main() {
	c1 := make(chan int)
	c2 := make(chan int)
	c3 := make(chan bool)

	go func() {
		c1 <- 0
		time.Sleep(time.Second * 2)
	}()

	go func() {
		c2 <- 1
		time.Sleep(time.Second * 2)
	}()

	go func() {
		for {
			select {
			case message1 := <-c1:
				fmt.Println("message1读到数据:", message1)
			case message2 := <-c2:
				fmt.Println("message2读到数据:", message2)
				// 如果2秒内一直没有case满足就退出
			case <-time.After(time.Second * 2):
				c3 <- true
				break
			}
		}
	}()
	<-c3
}

斐波那契数:

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

	// 子go程负责打印
	go count(ch, quit)

	// 先定义前两个数
	x, y := 1, 1

	// 循环用于确定计算多少个斐波那契数
	for i := 0; i < 20; i++ {
		ch <- x
		// 循环交换x、y的值
		x, y = y, x+y
	}
	quit <- true
}

func count(ch <-chan int, quit <-chan bool) {
	for {
		select {
		case num := <-ch:
			fmt.Print(num, ",")
		case <-quit:
			runtime.Gosched()
		}
	}
}

条件变量:

条件变量是一种线程同步的机制,它允许一个或多个线程等待另一个线程的某些操作完成,然后再继续执行。条件变量不是锁,在并发中不能达到同步的目的,因此条件变量总是与锁一块使用。

如果不使用条件变量完成生产者消费者模型是这样的。

func main() {
	rand.Seed(time.Now().UnixNano()) // 设置随机数种子

	ch := make(chan int, 3) // channel模拟共享资源

	for i := 0; i < 5; i++ {
		go Producer(ch, i+1)
	}

	for i := 0; i < 5; i++ {
		go Consumer(ch, i+1)
	}

	time.Sleep(time.Second)
	close(ch)
}

func Consumer(ch chan int, i int) {
	for num := range ch {
		fmt.Println("第", i, "个消费者,消费了数据:", num)
	}
}

func Producer(ch chan int, i int) {
	for {
		// 判断缓冲区是否已满
		if len(ch) == 3 {
			continue
		}

		// 不满就生成随机数并写数据到channel
		num := rand.Intn(1000)
		ch <- num
		fmt.Println("----第", i, "个生产者,生产了数据:", num)
	}
}

输出:可以看到结果是生产者生产完以后消费者并没有按照顺序去消费,还是因为读数据和打印分开执行了,读到数据刚要打印CPU执行权就被抢走了,如果是用这些数据进行计算,那么就会发生数据混乱

----2 个生产者,生产了数据: 6333 个消费者,消费了数据: 8433 个消费者,消费了数据: 7523 个消费者,消费了数据: 6333 个消费者,消费了数据: 1803 个消费者,消费了数据: 2233 个消费者,消费了数据: 6802 个消费者,消费了数据: 1634 个消费者,消费了数据: 6045 个消费者,消费了数据: 22
----4 个生产者,生产了数据: 223
----4 个生产者,生产了数据: 478
----4 个生产者,生产了数据: 606
----4 个生产者,生产了数据: 67
----4 个生产者,生产了数据: 1111 个消费者,消费了数据: 1461 个消费者,消费了数据: 423

另外一个问题,如果消费者比生产者多,仓库中就会出现没有数据的情况。我们需要不断的通过循环来判断仓库中是否有数据,这样会造成cpu的浪费。反之,如果生产者比较多,仓库很容易满,满了就不能继续添加数据,也需要循环判断仓库满这一事件,同样也会造成CPU的浪费。

我们希望当仓库满时,生产者停止生产,等待消费者消费;同理,如果仓库空了,我们希望消费者停下来等待生产者生产。为了达到这个目的,这里引入条件变量

方法 作用
func (c *Cond) Wait() 1、阻塞等待条件变量满足
2、释放已掌握的互斥锁相当于cond.L.Unlock()。 注意:1和2两步为一个原子操作(不会被线程调度机制打断的操作)
3、广播通知,给正在等待(阻塞)在该条件变量上的所有goroutine发送通知。
func (c *Cond) Signal() 单发通知,给一个正等待(阻塞)在该条件变量上的goroutine发送通知
func (c *Cond) Broadcast() 广播通知,给正在等待(阻塞)在该条件变量上的所有goroutine发送通知

演示:

// 创建全局变量
var cond sync.Cond

func main() {
	rand.Seed(time.Now().UnixNano()) // 设置随机数种子
	ch := make(chan int, 3)          // channel模拟共享资源

	// 创建互斥锁和条件变量 ,cond.L:给公共区加锁
	cond.L = new(sync.Mutex)

	for i := 0; i < 5; i++ {
		go Producer(ch, i+1)
	}

	for i := 0; i < 5; i++ {
		go Consumer(ch, i+1)
	}

	// 加上死循环,可以看到终端已经不打印了,隐式死锁出现了
	for {

	}
}

func Consumer(ch chan int, i int) {
	for {
		// 先加锁
		cond.L.Lock()

		// 判断缓冲区是否为空
		if len(ch) == 0 {
			cond.Wait()
		}

		num := <-ch
		fmt.Println("第", i, "个消费者,消费了数据:", num)

		// 访问公共区结束,并打印结束再解锁
		cond.L.Unlock()

		// 唤醒阻塞在条件变量上的对端
		cond.Signal()
		cond.Broadcast()
	}
}

func Producer(ch chan int, i int) {
	for {
		// 先加锁
		cond.L.Lock()

		// 判断缓冲区是否已满
		if len(ch) == 3 {
			cond.Wait()
		}

		// 不满就生成随机数并写数据到channel
		num := rand.Intn(1000)
		ch <- num
		fmt.Println("----第", i, "个生产者,生产了数据:", num)

		// 访问公共区结束,并打印结束再解锁,否则会有几率出现,刚把num写到了channel,正准备Println时CPU使用权被抢
		cond.L.Unlock()

		// 唤醒阻塞在条件变量上的对端
		cond.Signal()
	}
}

在这段代码中,可能会出现生产者和消费者同时处于等待条件变量的状态,导致死锁。因为在生产者或消费者获取锁之后,仅仅通过 Wait() 来等待条件变量的唤醒。但当所有生产者和消费者都在等待时,没有任何一个可以对条件变量进行操作,这会导致所有goroutine一直处于等待状态,无法继续执行。

为了避免这种情况,我们需要更改 Wait() 的使用方式。在 Wait() 之前,需要用 for 循环判断条件变量的状态,避免虚假唤醒。当一个goroutine被唤醒时,需要重新检查条件变量的状态,如果不符合要求,则需要再次等待条件变量的唤醒。

// 创建全局变量
var cond sync.Cond

func main() {
	rand.Seed(time.Now().UnixNano()) // 设置随机数种子
	ch := make(chan int, 3)          // channel模拟共享资源

	// 创建互斥锁和条件变量 ,cond.L:给公共区加锁
	cond.L = new(sync.Mutex)

	for i := 0; i < 5; i++ {
		go Producer(ch, i+1)
	}

	for i := 0; i < 5; i++ {
		go Consumer(ch, i+1)
	}

	time.Sleep(time.Second)
	close(ch)
}

func Consumer(ch chan int, i int) {
	for {
		// 先加锁
		cond.L.Lock()

		// 判断缓冲区是否为空
		for len(ch) == 0 {
			cond.Wait()
		}

		num := <-ch
		fmt.Println("第", i, "个消费者,消费了数据:", num)

		// 访问公共区结束,并打印结束再解锁
		cond.L.Unlock()

		// 唤醒阻塞在条件变量上的对端
		cond.Signal()
		cond.Broadcast()
	}
}

func Producer(ch chan int, i int) {
	for {
		// 先加锁
		cond.L.Lock()

		// 判断缓冲区是否已满
		for len(ch) == 3 {
			cond.Wait()
		}

		// 不满就生成随机数并写数据到channel
		num := rand.Intn(1000)
		ch <- num
		fmt.Println("----第", i, "个生产者,生产了数据:", num)

		// 访问公共区结束,并打印结束再解锁,否则会有几率出现,刚把num写到了channel,正准备Println时CPU使用权被抢
		cond.L.Unlock()

		// 唤醒阻塞在条件变量上的对端
		cond.Signal()
	}
}

输出:

----4 个生产者,生产了数据: 412
----4 个生产者,生产了数据: 797
----4 个生产者,生产了数据: 1884 个消费者,消费了数据: 4124 个消费者,消费了数据: 7974 个消费者,消费了数据: 188
----4 个生产者,生产了数据: 722
----4 个生产者,生产了数据: 899
----4 个生产者,生产了数据: 4674 个消费者,消费了数据: 7224 个消费者,消费了数据: 8994 个消费者,消费了数据: 467
----4 个生产者,生产了数据: 11
----4 个生产者,生产了数据: 752
----4 个生产者,生产了数据: 2164 个消费者,消费了数据: 114 个消费者,消费了数据: 7524 个消费者,消费了数据: 216

runtime包:

Gosched():

用于让当前的Go协程暂停,让出 CPU 时间片给其他协程执行的函数。它的作用是协程调度,避免某些协程长时间占用 CPU 资源,导致其他协程无法得到执行的机会。并在下次再获得cpu时间轮片的时候,从该出让cpu的位置恢复执行。

下面的程序执行结果是,main和test交替执行,基本上打印数量是差不多的

func main() {
	go Test()

	// 循环次数太少main抢到CPU一口气就打印完了,要多一点才能看到效果
	for i := 0; i < 500; i++ {
		fmt.Println("main fun Runtime")
	}
}

func Test() {
	for i := 0; i < 500; i++ {
		fmt.Println("Test fun Runtime")
	}
}

使用runtime.Gosched后可以看到Test协程打印的非常少了。如果循环次数太低,main执行完毕Test就不会被执行了,直接终止程序了

func main() {
	go Test()
	for i := 0; i < 500; i++ {
		fmt.Println("main fun Runtime")
	}
}

func Test() {
	for i := 0; i < 500; i++ {
		/*
			流程:
			1. 调用Test()
			2. 遇到Gosched(),开始让出时间片,不向下继续执行l,此时main开始拿到执行权
			3. 直到main函数失去CPU执行权,Test和main协程开始抢占CPU执行权
			4. 假设Test抢到,接着让出时间片的位置继续执行代码,打印Test fun Runtime,下一次循环碰到Gosched又让出时间片
		*/
		runtime.Gosched()
		fmt.Println("Test fun Runtime")
	}
}

Goexit:

Goexit() 函数被调用时,该协程中正在执行的函数将立即停止执行,任何延迟函数将被忽略,并且该协程将从调用 Go() 函数启动的堆栈中返回。如果该协程是在 main() 函数中创建的,则整个程序将终止。这个函数通常用于在协程中发现错误或完成任务时,用于优雅地终止协程并释放相关资源,避免出现意外的行为。

func main() {
	fmt.Println("A")

	go func() {
		defer fmt.Println("B")
		runtime.Goexit() // 在这直接跳出匿名函数了,下面的通通不执行,即使是defer
		defer fmt.Println("C") // 不会被执行
	}()

	fmt.Println("D")

	for {

	}
}

输出:

A
D
B

GOMAXPROCS:

用来设置可以并行计算的CPU核数的最大值,并返回上一次设置的核心数。

func main() {
	// 设置为单核,可以看到go程抢占CPU的效果
	num := runtime.GOMAXPROCS(1)
	fmt.Println("上一次设置CPU核数", num)

	for i := 0; i < 20; i++ {
		time.Sleep(time.Second)
		// 主go程和子go程抢占CPU执行权,可以看到熬01交替效果
		go fmt.Print(0)
		fmt.Print(1)
	}
}

输出:

上一次设置CPU核数 10
101010101010101010101010101010101010101
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Go_Goroutine和channel的使用、定时器、runtime、select、条件变量 的相关文章

  • 如何在IE中使用键盘选择多个选择的非连续选项

    仅使用键盘 如何选择多选元素的非连续选项 在 Firefox 或 Windows 资源管理器上 我只需按 ctrl 箭头即可移动焦点 然后按空格键进行选择 但我没有看到任何方法让它在 IE 上工作 Thanks 今天我一直在寻找这个 事实证
  • Jquery-nice-select 插件无法正常工作

    我正在使用 Jquery nice select 插件 http hernansartorio com jquery nice select http hernansartorio com jquery nice select 我有两个选择
  • 从单个表中选择同一列两次但条件不同

    我想显示员工的姓名和号码以及老板的号码和姓名 如下所示 只有一张表 到目前为止我尝试过 SELECT ID Name Boss SELECT Name FROM Employees WHERE ID IN SELECT Boss FROM
  • LINQ:从列表中选择项目(分组/选择/求和和最大值!)

    我只是沉浸在 Linq 中并享受很多乐趣 任何人都可以帮助我查询以下内容 我有一个数据列表 Key Value Aaa 12 AaA 10 AAa 5 BBB 2 Bbb 1 1 I want to group by Key ToUpper
  • 简单的 MySQL 语法错误 [关闭]

    Closed 这个问题是无法重现或由拼写错误引起 help closed questions 目前不接受答案 我正在运行以下代码 但找不到错误是什么 谁能建议我应该检查什么 result mysql query SELECT FROM ta
  • 在 PHP 中对逗号分隔值列表运行选择

    我在数据库上运行选择查询时遇到一些问题 一些数据以逗号分隔值的列表形式保存 例如 Table example tbl Id People Children 1 1 2 3 8 10 3 2 7 6 12 18 19 2 我正在尝试运行的示例
  • 如果 1 行满足条件,则排除 ID 的所有行

    我正在尝试从联系人表中选择某些客户 如果他们没有列出监护人 ClientId ContactId Guardian 123 1 Y 123 2 N 123 3 N 456 4 N 456 5 N 456 6 N 期望的输出 ClientId
  • 从两个表中选择最大值、最小值

    我有两张桌子 不同之处在于 归档是一个表 另一个保存当前记录 这些是记录公司销售额的表格 在这两个字段中 我们都有其他字段 id 名称 销售价格 我需要从两个表中选择给定名称的最高价格和最低价格 我尝试处理查询 select name ma
  • 将 SELECT DISTINCT ON 查询从 Postgresql 转换为 MySQL

    我一直在使用PostgreSQL现在迁移到MySQL 在我的查询中 我正在使用PostgreSQL s SELECT DISTINCT ON col1 col2 col3 我想知道这句话是否有对应的内容MySQL 没有完全等效的方法可以将使
  • jQuery - 选择选择框中的选项[重复]

    这个问题在这里已经有答案了 可能的重复 jQuery 以编程方式选择选择框中的选项 https stackoverflow com questions 1573930 jquery programmatically select an op
  • Oracle 相当于 information_schema.tables

    当我尝试在 Oracle 中执行以下查询时 出现 表或视图不存在 错误 SQL查询 SELECT table type table name FROM information schema tables WHERE table rows g
  • MySQL 返回用户排名最高的事件

    我目前使用以下查询来获取每个用户的详细信息 SELECT u sums total votes sums no of events FROM user u LEFT JOIN SELECT us user uid count ev even
  • sync.WaitGroup 的示例正确吗?

    这个例子的用法是sync WaitGroup正确的 它给出了预期的结果 但我不确定wg Add 4 和位置wg Done 一次添加四个 goroutine 是否有意义wg Add http play golang org p ecvYHii
  • 确定用winsock(FIONWRITE)可以发送多少字节?

    通过 select 我可以确定是否可以在不阻塞的情况下接收或发送任何字节 通过这个函数 我可以确定可以接收多少字节 function BytesAvailable S TSocket Integer begin if ioctlsocket
  • MySQL ORDER BY rand(),名称 ASC

    我想获取一个包含 1000 个用户的数据库并随机选择 20 个用户 ORDER BY rand LIMIT 20 然后按名称对结果集进行排序 我想出了以下查询not像我希望的那样工作 SELECT FROM users WHERE 1 OR
  • SQL select通常是如何实现的

    我有两节课 class PopulationMember public void operationOnThisMember1 void operationOnThisMember2 private Population populalti
  • PostgreSQL 选择结果大小

    我在 PostgreSQL 数据库中有一个表 并从该表中进行一些限制的选择 并且我想知道该选择有多少磁盘空间 我知道有一个postgres函数pg total relation size这给了我数据库中某个表的大小 但是如何找到 子表 的大
  • 当Where子句中的术语不在数据库中时,如何从MySQL数据库返回0?

    如果 WHERE 子句中的邻域不存在 如何让 mysql 数据库返回 0 因此 在下面的示例中 旧城区不在数据库中 我希望数据库返回 0 个事件而不是空结果 SELECT incidents neighborhoods FROM myTab
  • 当涉及多个渠道时,select 如何工作?

    我发现在多个非缓冲通道上使用 select 时 例如 select case lt chana case lt chanb 即使两个通道都有数据 但在处理此选择时 case chana 和 case chanb 的跟注不平衡 package
  • 每行中非空列的计数

    我有一个包含 4 列的表 在第 5 列中我想存储前 4 列中有多少个非空列的计数 例如 其中 X 是任意值 Column1 Column2 Column3 Column4 Count X X NULL X 3 NULL NULL X X 2

随机推荐

  • Android 系统设置中显示设置之休眠和屏保设置篇

    Android 系统设置中显示设置之休眠和屏保设置篇 在上一篇中我们学习了Android系统设置中字体大小和屏幕旋转设置基本内容 在这一篇中我们继续学习显示设置中的休眠和屏保设置 1 休眠设置 首先我们来看一下休眠设置在界面中的定义 1
  • js--for循环99乘法表的四种样式

  • Eclipse/Code blocks/PyCharm连接MySQL数据库初尝试

    第一次使用MySQL 在此罗列我搭配环境的一些路程 我最终Code blocks和Eclipse成功了 Pycharm一直因为版本不合适未成功 我也把我试过的未成功的方法罗列在此 希望可以得到最终的解决 Code blocks 我在用cod
  • Cocos Creator组件化开发之——地图类缩放拖动点击组件

    好记性不如烂笔头 记录开发过程中的点点滴滴 xshu 书写是对思维的缓存 佚名 进入正题 在游戏开发过程中经常会遇到渲染元素的尺寸大于你所能展示的区域 需要玩家自行进行操作 拖动以及点击 比如地图通常会超出玩家所示区域 这个时候需要一个能够
  • STM32F103ZET6+IIC+SGP30气体传感

    STM32F103ZET6 IIC SGP30气体传感 为了加深对IIC协议的理解和应用 接下来 使用STM32驱动SGP30气体传感传感器 准备IIC协议 用的还是之前的那些代码 只是把GPIO引脚口改了一下而已 1 声明GPIO和IIC
  • 虚拟地址空间和物理地址空间

    1 概念 物理地址 物理地址空间是实在的存在于计算机中的一个实体 在每一台计算机中保持唯一独立性 我们可以称它为物理内存 如在32位的机器上 物理空间的大小理论上可以达到2 32字节 4GB 但如果实际装了512的内存 那么其物理地址真正的
  • 如何让一句话木马绕过waf的检测 ?

    一 什么是一句话木马 一句话木马就是只需要一行代码的木马 短短一行代码 就能做到和大马相当的功能 为了绕过waf的检测 一句话木马出现了无数中变形 但本质是不变的 木马的函数执行了我们发送的命令 二 我们如何发送命令 发送的命令如何执行 我
  • 爬虫逆向实战(33)-某联社数据(webpack)

    一 数据接口分析 主页地址 某联社 1 抓包 通过抓包可以发现数据接口是 nodeapi telegraphList 2 判断是否有加密参数 请求参数是否加密 通过查看 载荷 模块可以发现有一个sign加密参数 请求头是否加密 无 响应是否
  • CSS盒子模型

    CSS盒子模型 课程目标 1 常用样式 2 盒子模型 3 CSS3常见效果 盒子模型介绍 引用百度百科 层叠样式表其实就是对静态页面进行装饰 但是 特别要注意的点是 通常建议表现形式与页面内容分离 例如 人就是内容 穿着打扮就是表现形式 常
  • 使用MQTT客户端连接阿里云MQTT服务器

    本文是转载的 原文请戳这里查看 摘要 物联网全栈教程 从云端到设备 八 一 这一篇文章零妖老哥将给你展示两个电脑软件的使用方法 将极大地方便你调试与MQTT有关的物联网项目 一个叫MQTT客户端用来模拟设备向云端发送数据和接收云端的数据 另
  • linux常用命令

    Linux目录结构简介 目录结构 说明 bin 是Binary的缩写 这个目录存放着最经常使用的命令 sbin s就是Super User的意思 这里存的系统管理员使用的系统管理程序 home 存放普通用户的主目录 在Linux中每个用户都
  • 使用Symchk来离线需要的Symbol文件

    我们在工作中会经常遇到离线环境 这时候就需要准备好离线符号 我们使用安装windbg时候自带的工具SymChk exe 微软的官方说明在下面 使用起来也很简单 下面是一个使用示例 使用 SymChk exe 实用程序下载 Windows S
  • python怎么打开npz文件_numpy的文件存储.npy .npz 文件详解

    Numpy能够读写磁盘上的文本数据或二进制数据 将数组以二进制格式保存到磁盘 np load和np save是读写磁盘数组数据的两个主要函数 默认情况下 数组是以未压缩的原始二进制格式保存在扩展名为 npy的文件中 import numpy
  • matlab_线性规划

    求解线性规划问题 min z f x s t Ax b Aeqx beq lb x ub 其中 f x b beq lb ub为向量 A Aeq为矩阵 linprog函数 x fval exitflag output lambda linp
  • linux解压rar包的命令

    压缩文件 rar a r test rar file 解压文件 unrar x test rar
  • QT信号槽connect的第五个参数

    用过QT的小伙伴都知道连接信号槽的connect方法 但是这个方法有第五个参数 一般都是用的默认的 1 connect th SIGNAL started tmpmyobject SLOT showID 今天给大家讲讲第五个参数的用法 一
  • Python 爬虫运行时如何忽略错误,并继续运行

    我在第一次写Python爬虫的时候 没过多久Python就会报错 然后停止采集 总是没办法从头到尾把数据采集下来 每一下次报错我都要去找原因 随着一次又一次的报错问题越来越少 比如我要采集的标签里面没有URL URL为空就会报错 或者页面没
  • 03虚幻4【UE4】蓝图通信

    UE蓝图通信 目录 UE蓝图通信 情景一 方法一 直接拾取 简单粗暴 局限性一对一 方法二 获取类的actor 调用 自定义函数 变量 1 获取类的actor 调用 自定义函数 2 获取类的actor 调用 变量 方法三 蓝图接口 情景二
  • 预训练语言模型

    模型参数不再是随机初始化的 而是通过一些任务进行预先训练 得到一套模型参数 然后用这套参数对模型进行初始化 再进行训练 从模型的角度 模型参数不再是随机初始化 而是通过一些任务 如语言模型 进行预训练 从数据的角度 将训练任务拆解成共性学习
  • Go_Goroutine和channel的使用、定时器、runtime、select、条件变量

    程序 程序是指编译好的二进制文件 存放在磁盘上不占用系统资源 cpu 内存 当程序在计算机上运行时 它就变成了一个进程 包括程序 数据和状态信息 程序是静态的 而进程是动态的 进程 进程是程序的实例 是资源分配的最小单位 当一个程序开始运行