Go语言的并发:goroutine和channel

news2024/11/25 7:21:14

目录

【Go 的并发方案:goroutine】

goroutine 的基本用法

【通道channel】

创建channel:

发送与接收变量:

关闭channel:

【channel的类型】

 无缓冲channel和带缓冲channel 

无缓冲channel

带缓冲channel

 nil channel

单向channel

【多路选择:select语句】

使用select实现超时控制


并发:指的是一个时间段中有几个程序都处于已启动运行到运行完毕之间,但任一个时刻点上只有一个程序在处理机上运行。并行:指的是在同一时刻有两个或两个以上的进程在处理器(需要是多核处理器)上执行。多进程应用是通过系统调用(比如fork)创建多个子进程,共同实现应用的功能。

进程、线程、协程:

  • 进程:是一个"执行中的程序”,描述的就是程序的执行过程,是运行着的程序的代表。进程的三态模型: 运行、就绪、堵塞。
  • 线程:是进程中的一个实体,可以被视为进程中运行着的控制流,是被操作系统独立调度和分派的基本单位,一个进程至少会包含一个线程。如果一个进程只包含了一个线程,那么它里面的所有代码都只会被串行地执行;如果一个进程中包含了多个线程(其他的线程都是由已存在的线程创建出来的),那么其中的代码就可以被并发地执行。
  • 协程:是一种用户态的轻量级线程,协程的调度由用户控制。一个线程可以拥有多个协程,一个进程也可以单独拥有多个协程。协程的创建、销毁、调度、状态变更以及其中的代码和数据都完全需要程序代码去实现和处理。带来的优势就是速度会很块而且容易控制,不需要操作系统去调度。

单进程单线程:一个人在一个桌子上吃饭(单身)
单进程多线程:多个人在同一个桌子上吃饭(一个家庭里面的多个人 )
多进程单线程:多个人每个人在自己的桌子上吃饭 (多个家庭,但每个家庭都是单身)

多进程多线程:一堆人,每个桌子上都有多个人在吃饭(多个家庭,每个家庭都是多个人)

【Go 的并发方案:goroutine】

操作系统本身提供了进程和线程这两种并发执行程序的工具,而 goroutine 代表着并发编程模型中的用户级线程(也就是协程)。Go 语言实现了基于 CSP理论的并发方案

CSP:Communicating Sequential Processes,通信顺序进程,是一种并发编程模型。关于CSP的更多资料请参考 这里

主要包含两个主要组成部分:

  • 一个是 Goroutine,它是 Go 应用并发设计的基本构建与执行单元;
  • 另一个就是 channel,既可以用来实现 Goroutine 间的通信,还可以实现 Goroutine 间的同步。

goroutine 是由 Go 运行时(runtime)负责调度的、轻量的用户级线程,为并发程序设计提供原生支持。相比传统操作系统线程来说,goroutine 的优势主要是:

  • 资源占用小,每个 goroutine 的初始栈大小仅为 2k;
  • 由 Go 运行时调度(而不是操作系统调度),goroutine 上下文切换在用户层完成,开销更小;
  • 在语言层面提供(而不是通过标准库提供),goroutine 由go关键字创建,一退出就会被回收或销毁;
  • 语言内置 channel 作为 goroutine 间通信原语,为并发设计提供了强大支撑。

goroutine 的基本用法

Go语言使用 go关键词 就可以创建多个 goroutine 执行并发任务,而且还提供了 Channel 类型可以很容易的实现 goroutine 之间的数据通信。具体地说,Go 语言通过 go关键字+函数/方法 的方式创建一个 goroutine。创建后,新 goroutine 将拥有独立的代码执行流,并与创建它的 goroutine 一起被 Go 运行时调度,goroutine 的执行函数返回后就会自动退出。如果 main函数的 goroutine 退出了,那么整个 Go 应用程序也就退出了。

func main() {
	go fmt.Println("你好")
	fmt.Println("这里是main goroutine")
	time.Sleep(time.Millisecond * 500) //等待500毫秒
}

再比如下面的例子:

package main

import (
	"fmt"
	"time"
)

func main() {
	var m = []int{1, 2, 3, 4, 5}
	for i, v := range m {
		go func(i, v int) {
			fmt.Print(i, "=>", v, ",") //4=>5,3=>4,2=>3,0=>1,1=>2,(输出顺序不确定)
		}(i, v)
	}
	time.Sleep(time.Second * 1)
	fmt.Println()
}

【通道channel】

和线程一样,一个应用内部启动的所有 goroutine 共享进程空间的资源,如果多个 goroutine 访问同一块内存数据将会存在竞争,需要进行 goroutine 间的同步。

通道类型channel 的值本身就是并发安全的,这也是 Go 语言自带的、唯一一个可以满足并发安全性的类型。goroutine 执行的函数或方法就算有返回值,Go 也会忽略这些返回值。所以如果要获取 goroutine 执行后的返回值,需要通过 goroutine 间的通信来实现:channel。goroutine 可以从 channel 获取输入数据,再将处理后得到的结果数据通过 channel 输出。

创建channel:

在声明一个channel类型变量的时候首先要确定元素类型,这决定了可以通过这个channel传递什么类型的数据。比如 chan int 表示元素类型为int的channel类型,chan string表示元素类型为string的channel类型。由于channel是引用类型,如果只声明了channel 类型的变量但没有初始化,该变量的默认值是nil。为 channel 类型变量赋初值的唯一方法是使用 make 函数。可以使用cap()函数获取channel的容量,使用len()函数获取channel的元素个数。

/* 创建channel并赋值 */
var ch chan int          //声明一个元素为int类型的channel类型变量,默认值为nil
ch1 := make(chan int)    //声明元素类型为int的channel类型变量,属于无缓冲channel
ch2 := make(chan int, 1) //声明元素类型为int的channel类型变量,并赋初值,属于带缓冲channel

fmt.Println(ch, cap(ch), len(ch))    //nil 0 0
fmt.Println(ch1, cap(ch1), len(ch1)) //0xc00001a2a0 0 0
fmt.Println(ch2, cap(ch1), len(ch1)) //0xc000078000 0 0

当容量为0时 可以称为无缓冲channel,也就是不带缓冲的通道;当容量大于0时称为 带缓冲channel,也就是带有缓冲的通道。 

ch2 := make(chan int, 1) 里面的 1 表示channel的容量,就是指channel最多可以缓存多少个元素值,这个参数是int类型的,而且是不能小于0的。 

发送与接收变量:

一个channel相当于一个先进先出(FIFO)的队列,channel中的各个元素值都是严格地按照发送的顺序排列的,先被发送到channel的元素值一定会先被接收。Go 提供了 <- 操作符用于对 channel 类型变量发送与接收。

ch1 := make(chan int, 3)
ch1 <- 2
ch1 <- 1
ch1 <- 3
data1 := <-ch1
data2 := <-ch1
data3 := <-ch1
fmt.Printf("从channel中接收的元素: %v,%v,%v \n", data1, data2, data3) // 2,1,3

【问】对channel的发送和接收操作都有哪些基本的特性?

(1)对于同一个channel,发送操作之间是互斥的,接收操作之间也是互斥的。同一时刻 同一个channel中,Go 语言的运行时系统只会执行其中一个发送操作,直到发送完成之后其他发送操作才可能被执行,接收操作也是如此。即使这些操作是并发执行的也是如此。而且对于channel中的同一个元素值来说,发送操作和接收操作之间也是互斥的。需要注意的是实际发送的元素是原来元素的副本。

(2)发送操作和接收操作中对元素值的处理都是不可分割的,类似于“事务”处理。发送元素的时候要么还没开始复制副本,要么已经复制完成,不会出现复制了一部分的情况;接收元素的时候一定会删除channel中的原值,而不会出现残留原值的情况。

(3)发送操作在完全完成之前会被阻塞,接收操作也是如此。发送操作包括了 “复制元素值”、“放置副本到channel内部” 两个步骤,这两步完成之前发送操作会一直处于阻塞状态。接收操作包含了 “复制channel内的元素值”、“放置副本到接收方”、“删掉原值” 三个步骤。如此阻塞代码就是为了实现操作的互斥和元素值的完整。

关闭channel:

调用 Go 内置的 close 函数可以关闭channel,所有的channel接收者都会在channel 关闭时立刻从阻塞等待中返回。如下是采用不同接收语法形式的语句,在 channel 被关闭后的返回值的情况:

n := <- ch // 当ch被关闭后,n将被赋值为ch元素类型的零值
m, ok := <-ch // ok为bool值,true表示正常接受,false表示通道关闭. 当ch被关闭后,m将被赋值为ch元素类型的零值, ok值为false
for v := range ch { // 当ch被关闭后,for range循环结束
    // ... ...
}

 channel 的一个使用惯例是:由发送端负责关闭 channel,原因:

  • 发送端没有像接受端那样可以安全判断 channel 是否被关闭了的方法。
  • 向一个已经关闭的 channel 执行发送操作,这个操作就会引发 panic。
  • 关闭一个已经关闭了的通道,也会引发 panic。
ch := make(chan int, 5)
close(ch)
ch <- 13 // panic: send on closed channel

下面是一个正确操作channel的例子:

package main

import "fmt"

func main() {
	ch1 := make(chan int, 2)
	// 发送方
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Printf("发送方发送数据: %v \n", i)
			ch1 <- i
		}
		fmt.Println("发送方: 关闭channel...")
		close(ch1)
	}()

	// 接收方
	for {
		elem, ok := <-ch1
		if !ok {
			fmt.Println("*接收方*: 检测到发送方已经关闭了channel")
			break
		}
		fmt.Printf("*接收方接收数据*: %v \n", elem)
	}

	fmt.Println("执行完成")
}

/*
发送方发送数据: 0
发送方发送数据: 1
发送方发送数据: 2
发送方发送数据: 3
*接收方接收数据*: 0
*接收方接收数据*: 1
*接收方接收数据*: 2
*接收方接收数据*: 3
发送方发送数据: 4
发送方发送数据: 5
发送方发送数据: 6
发送方发送数据: 7
*接收方接收数据*: 4
*接收方接收数据*: 5
*接收方接收数据*: 6
*接收方接收数据*: 7
发送方发送数据: 8
发送方发送数据: 9
发送方: 关闭channel...
*接收方接收数据*: 8
*接收方接收数据*: 9
*接收方*: 检测到发送方已经关闭了channel
执行完成
*/

【channel的类型】

 无缓冲channel和带缓冲channel 

无缓冲channel和带缓冲channel 的最大不同之处就在于它的异步性。也就是说,对一个带缓冲 channel,在缓冲区未满的情况下,对它进行发送操作的 Goroutine 不会阻塞挂起;在缓冲区有数据的情况下,对它进行接收操作的 Goroutine 也不会阻塞挂起。下面是我从网上找的无缓存channel和带缓存channel的示意图:

无缓冲channel

无缓冲channel的容量为0。对同一个无缓冲 channel,只有对它进行接收操作的 Goroutine 和对它进行发送操作的 Goroutine 都存在的情况下才可以通信,否则单方面的操作会让对应的 Goroutine 陷入挂起状态。由此可见;无缓冲channel是用同步的方式传递数据,只有收发双方对接上了数据才会被传递。

ch1 := make(chan int)    //声明元素类型为int的channel类型变量,属于无缓冲channel

// 将 13 发送到 无缓冲channel 类型变量ch1中
ch1 <- 13 //fatal error: all goroutines are asleep - deadlock!

fatal error: all goroutines are asleep - deadlock!

意思是 所有 Goroutine 都处于休眠状态,程序处于死锁状态。要想解除这种错误状态,需要将接收操作或者发送操作放到另外一个 Goroutine 中。

ch1 := make(chan int)    //声明元素类型为int的channel类型变量,属于无缓冲channel

// 将 13 发送到 无缓冲channel 类型变量ch1中
// ch1 <- 13 //fatal error: all goroutines are asleep - deadlock!
go func() {
	ch1 <- 13 // 将发送操作放入一个新goroutine中执行
}()
// 从 无缓冲channel 类型变量ch1 中接收数据 并存储到 变量n中
n := <-ch1
fmt.Println(n) // 13

无缓冲 channel 的典型应用:

  • 第一种用法:用作信号传递无缓冲 channel 用作信号传递的时候,有两种情况,分别是 1 对 1 通知信号和 1 对 n 通知信号。
  • 第二种用法:用于替代锁机制无缓冲 channel 具有同步特性,这让它在某些场合可以替代锁,让我们的程序更加清晰,可读性也更好。

带缓冲channel

带缓冲channel是用异步的方式传递数据,带缓冲channel会作为收发双方的中间件,元素值会先从发送方复制到缓冲channel,之后再由缓冲channel复制给接收方。但是当发送操作在执行的时候发现空的通道中正好有等待的接收操作,那么它会直接把元素值复制给接收方。

对一个带缓冲 channel 来说有下面的情况:

  • 在缓冲区未满的情况下,对它进行发送操作的 Goroutine 并不会阻塞挂起;
  • 在缓冲区有数据的情况下,对它进行接收操作的 Goroutine 也不会阻塞挂起。
  • 当缓冲区满了的情况下,对它进行发送操作的 Goroutine 就会阻塞挂起,直到通道中有元素值被接收走;
  • 当缓冲区为空的情况下,对它进行接收操作的 Goroutine 也会阻塞挂起,直到通道中有新的元素值出现。
ch2 := make(chan int, 1) //声明元素类型为int的channel类型变量,并赋初值,属于带缓冲channel
// s := <-ch2 // 由于此时ch2的缓冲区中无数据,因此对其进行接收操作将导致goroutine挂起

ch2 <- 17 // 将 17 发送到 带缓冲channel 类型变量ch2中, OK
// ch2 <- 18 // 由于此时ch2中缓冲区已满,再向ch2发送数据将导致goroutine挂起.引发fatal error: all goroutines are asleep - deadlock!

fmt.Printf("ch2的容量:%v,元素个数:%v \n", cap(ch2), len(ch2)) //ch2的容量:1,元素个数:1
fmt.Println(<-ch2) // 17
fmt.Printf("ch2的容量:%v,元素个数:%v \n", cap(ch2), len(ch2)) //ch2的容量:1,元素个数:0

对于值为nil的channel,不论它的具体类型是什么,对它的发送操作和接收操作都会永久地处于阻塞状态,它们所属的 goroutine 中的任何代码都不再会被执行。

带缓冲 channel 的典型应用:

  • 第一种用法:用作消息队列,channel 的原生特性与消息队列十分相似,包括 Goroutine 安全、有 FIFO(first-in, first out)保证等。但是 Go 支持 channel 的初衷是将它作为 Goroutine 间的通信手段,它并不是专门用于消息队列场景的。 
  • 第二种用法:用作计数信号量(counting semaphore),带缓冲 channel 中的当前数据个数代表的是,当前同时处于活动状态(处理业务)的 Goroutine 的数量,而带缓冲 channel 的容量(capacity),就代表了允许同时处于活动状态的 Goroutine 的最大数量。

更多 Go语言中 无缓冲 channel 和带缓冲 channel 的用法请参考这里。 

 nil channel

如果一个 channel 类型变量的值为 nil,我们称它为 nil channel。nil channel 有一个特性,那就是对 nil channel 的读写都会发生阻塞。

func main() {
  var c chan int
  <-c //阻塞
}

或者:

func main() {
  var c chan int
  c<-1  //阻塞
}

单向channel

上面说的 channel 都是指的双向通道:既可以发送数据也可以接收数据。另外Go语言中还有单向channel,就是 只能发送数据而不能接收数据,或者只能接收数据而不能发送数据
如果把操作符 <-  用在通道的类型字面量中,那么它代表的就不是“发送”或“接收”的动作了,而是表示通道的方向。(什么意思?有点绕?懵圈?)看个例子对比一下:

chA := make(chan int, 1) //双向channel
ch3 := make(chan<- int, 1) // 单向channel,只能发送数据
ch4 := make(<-chan int, 1) // 单向channel,只能接收数据

只能发送数据的channel叫做:只发送 channel 类型,只能接收数据的channel叫做:只接收 channel 类型。如果从一个 只发送 channel 类型 变量中接收数据,或者向一个 只接收 channel 类型 发送数据,都会导致编译错误。

ch3 := make(chan<- int, 1) // 只发送channel类型
ch4 := make(<-chan int, 1) // 只接收channel类型
// 这里打印的是可以分别代表两个通道的指针的16进制表示
fmt.Printf("单向channel的指针: %v, %v \n", ch3, ch4) //单向channel的指针: 0xc00001c4d0, 0xc00001c540

<-ch3                      // invalid operation: cannot receive from send-only channel ch3 (variable of type chan<- int)
ch4 <- 13                  // invalid operation: cannot send to receive-only channel ch4 (variable of type <-chan int)

从表面上看,声明一个只有一端(发送端或者接收端)能用的通道没有任何意义,因为channel就是为了传递数据而存在的。那么单向 channel 有什么用处?

Go 设计单向channel 最主要的用途就是约束其他代码的行为。通常 只发送 channel 类型 和 只接收 channel 类型 会被用作函数的参数类型或返回值,用于限制对 channel 内的操作,或者是明确可对 channel 进行的操作的类型。比如下面的例子中,声明一个接口类型的方法时使用了单向channel类型,在函数声明的结果列表中也使用单向channel,就对传入的参数起到了约束作用。

package main

import (
	"fmt"
	"math/rand"
)

// 声明一个接口类型的方法时使用单向channel类型
type Constraint interface {
	SendInt(ch chan<- int)
}

// 约束此方法传入的参数只能是channel
func SendInt(ch chan<- int) {
	ch <- rand.Intn(1000)
}

// 在函数声明的结果列表中也使用单向channel
// 返回一个<-chan int类型的channel,约束得到该channel的程序只能从channel中接收元素值
func getIntChan() <-chan int {
	num := 5
	ch := make(chan int, num)
	for i := 0; i < num; i++ {
		ch <- i
	}
	close(ch)
	return ch
}

type GetIntChan func() <-chan int

func main() {
	intChan1 := make(chan int, 3)
	SendInt(intChan1)

	intChan2 := getIntChan()
	for elem := range intChan2 {
		fmt.Printf("intChan2中的元素值: %v\n", elem)
	}

	_ = GetIntChan(getIntChan)
}

/*
intChan2中的元素值: 0
intChan2中的元素值: 1
intChan2中的元素值: 2
intChan2中的元素值: 3
intChan2中的元素值: 4
*/

上面代码的细节分析:

  • 这里的 for range 语句会不断地尝试从 intChan2 中取出元素值,即使intChan2被关闭,它也会在取出所有剩余的元素值之后再结束执行。
  • 当intChan2中没有元素值时,它会被阻塞在有for关键字的那一行,直到有新的元素值可取。
  • 假设intChan2的值为nil,那么它会被永远阻塞在有for关键字的那一行。

再比如下面的代码使用单向channel类型实现了一个简单的消息队列的功能:

package main

import (
	"sync"
	"time"
)

/*
生产者,使用 只发送channel类型 作为参数类型
*/
func produce(ch chan<- int) {
	for i := 0; i < 10; i++ {
		ch <- i + 1
		time.Sleep(time.Second)
	}
	close(ch)  //关闭channel
}

/*
消费者,从channel中接收数据
*/
func consume(ch <-chan int) {
	// for range 会阻塞在channel的接收操作上,直到channel中有数据可接收 或 channel被关闭循环,然后才会继续向下执行。
	for n := range ch {
		print(n, " ")
	}
}
func main() {
	ch := make(chan int, 5)
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		produce(ch)
		wg.Done()
	}()

	go func() {
		consume(ch)
		wg.Done()
	}()
	wg.Wait()
}

运行后,每隔1秒钟会分别输出 1-10

【多路选择:select语句】

select 语句只能与channel 一起使用,它由若干个分支组成,每次执行select语句的时候只有一个分支中的代码会被运行,类似switch语句,只不过每个case表达式中都只能包含操作channel的表达式。比如下面的例子:

func test1() {
	// 定义数组类型的channel
	intChannels := [3]chan int{
		make(chan int, 1),
		make(chan int, 1),
		make(chan int, 1),
	}
	// 随机选择一个channel 并发送元素
	// index := rand.Intn(3) //使用一个随机数
	index := 2
	fmt.Printf("索引是 %d,", index)
	intChannels[index] <- index

	// 哪一个通道中有可取的元素值,哪个对应的分支就会被执行
	select {
	case <-intChannels[0]:
		fmt.Println("执行分支0")
	case <-intChannels[1]:
		fmt.Println("执行分支1")
	case element := <-intChannels[2]: //可以把channel元素的值赋给一个普通变量
		fmt.Printf("执行分支2,对应的元素是: %d \n", element)
	default:
		fmt.Println("没有分支被执行")
	}

	//输出: 索引是 2,执行分支2,对应的元素是: 2
}

使用 select 语句的注意事项:

  • 如果加入了default分支,那么涉及channel操作的表达式不管是否有阻塞,此时的select语句都不会被阻塞。如果所有case都没有满足条件,那么default分支就会执行;相反的如果没有default分支,当所有case都没有满足条件的情况下,select语句就会阻塞,直到至少有一个case表达式满足条件为止。
  • select语句如果在for循环中,在select语句中使用break语句只能结束当前的select语句的执行,而并不会对外层的for语句产生作用。
func test2() {
	intChan := make(chan int, 1)
	time.AfterFunc(time.Second, func() { // 一秒后关闭channel
		close(intChan)
	})
	select {
	case _, ok := <-intChan:
		if !ok {
			fmt.Println("channel已关闭")
			break
		}
		fmt.Println("已选择当前分支")
	}

	//输出: channel已关闭
}

select语句的分支选择规则:

  • 每一个case表达式至少会包含一个代表 发送操作或者接收操作 的表达式,同时也可能会包含其他的表达式。
  • 在select语句开始执行时,排在最上边的候选分支中最左边的表达式会最先被求值,然后是它右边的表达式。
  • 对于每一个case表达式,如果其中的发送/接收表达式在被求值时,相应的操作正处于阻塞状态,那么这个case表达式所在的分支是不满足选择条件的。
  • 只有select语句中所有case表达式都被求值完毕后,它才会开始选择候选分支。此时只会挑选满足条件的分支执行;如果所有的分支都不满足条件,那么会执行default 分支;如果没有default分支,select语句就会立即进入阻塞状态,直到至少有一个候选分支满足条件为止。
  • 如果select语句发现同时有多个分支满足条件,那么它会用一种伪随机的算法在这些分支中选择一个并执行。
  • 一条select语句中只能够有一个default分支,并且default分支只会在无分支可选时才被执行,这与它的编写位置无关。
  • select语句的每次执行(包括case表达式求值和分支选择)都是独立的,但至于它的执行是否是并发安全的,还要看其中的case表达式以及分支中是否包含并发不安全的代码。

以上定论可以用下面的代码来验证:

package main

import "fmt"

var channels = [3]chan int{
	nil,
	make(chan int),
	nil,
}

var numbers = []int{1, 2, 3}

func getNumber(i int) int {
	fmt.Printf("numbers[%d]\n", i)
	return numbers[i]
}

func getChan(i int) chan int {
	fmt.Printf("channels[%d]\n", i)
	return channels[i]
}

func main() {
	select {
	case getChan(0) <- getNumber(0):
		fmt.Println("执行第0条分支")
	case getChan(1) <- getNumber(1):
		fmt.Println("执行第1条分支")
	case getChan(2) <- getNumber(2):
		fmt.Println("执行第2条分支")
	default:
		fmt.Println("没有分支被执行")
	}
}

/* 输出:
channels[0]
numbers[0]
channels[1]
numbers[1]
channels[2]
numbers[2]
没有分支被执行
*/

使用select实现超时控制

假设某个goroutine运行非常耗时,可以在select的case分支中设定超时时间,如果超过了预设的时间就执行指定的操作。看下面的代码:

package main

import (
	"fmt"
	"time"
)

func service() string {
	time.Sleep(time.Millisecond * 500) //休息500毫秒
	return "处理完成"
}

func AsyncService() chan string {
	retCh := make(chan string, 1)
	go func() {
		ret := service()
		fmt.Println("returned result.")
		retCh <- ret
		fmt.Println("service exited.")
	}()
	return retCh
}

func main() {
	select {
	case ret := <-AsyncService():
		fmt.Println(ret)
	case <-time.After(time.Millisecond * 100): //设定超时时间为100毫秒
		fmt.Println("超时")
	}
}

 源代码:https://gitee.com/rxbook/go-demo-2023/tree/master/basic/go03/goroutine1

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/534706.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

随便聊聊 顺便晒一下我的听歌设备

平时最大的爱好就是听歌了&#xff0c;我平时的听歌类型挺多元化的&#xff0c;硬要说的话更偏向 Jpop、ACG、女声、轻音乐、大编制、交响乐&#xff0c;当然好听的都听不局限于类型。 又是30天一天不落O(∩_∩)O&#x1f604; 作为一个音乐爱好者&#xff0c;在听歌设备上面花…

Liunx压缩命令 - zip

zip命令 – 压缩文件 zip命令的功能是用于压缩文件&#xff0c;解压命令为unzip。通过zip命令可以将文件打包成.zip格式的压缩包&#xff0c;里面会包含文件的名称、路径、创建时间、上次修改时间等等信息&#xff0c;与tar命令相似。 语法格式&#xff1a;zip 参数 目标文件…

MySQL高级_第04章_逻辑架构

MySQL高级_第04章_逻辑架构 1. 逻辑架构剖析 1.1 服务器处理客户端请求 那服务器进程对客户端进程发送的请求做了什么处理&#xff0c;才能产生最后的处理结果呢&#xff1f;这里以查询请求为例展示&#xff1a; 下面具体展开看一下&#xff1a; 1.2 Connectors 1.3 第1…

Go 本地缓存 bigcache

​本地缓存已经算是数据库的常规设计了&#xff0c;因为数据直接缓存在机器内存中&#xff0c;避免昂贵的IO开销&#xff0c;数据的读取和写入快到不可思议。本地缓存常驻在内存中&#xff0c;就好像业务逻辑中声明的全局变量一样&#xff0c;不会被垃圾回收。但本地内存也会导…

JavaScript:new操作符

一、new操作符的作用 用于创建一个给定构造函数的实例对象 new操作符创建一个用户定义的对象类型的实例 或 具有构造函数的内置对象的实例。二、new一个构造函数的执行过程 2.1、创建一个空对象obj 2.2、将空对象的原型与构造函数的原型连接起来 2.3、将构造函数中的this绑定…

CPU的功能和组成

CPU的功能和组成 CPU是控制计算机自动完成取指令和执行指令任务的部件&#xff0c;是计算机的核心部件、简称CPU 功能&#xff1a; 指令控制&#xff1a;对程序的顺序控制也是对指令执行的顺序控制&#xff08;PC、JMP&#xff09;操作控制&#xff1a;产生各种操作信号&…

2023.05.17-使用Vizzy进行音乐的可视化

文章目录 1. 简介2. 官网3. 使用3.1. 进行音乐可视化 1. 简介 Vizzy是MusicVid创作者的另一个在线音乐可视化工具。虽然这款应用还处于Alpha版本&#xff0c;但Vizzy相当令人印象深刻&#xff0c;绝对值得一试。Vizzy支持动画文本对象、频谱、图像和效果。工具集的最突出的功能…

基础篇007. 串行通信(二)--中断方式接收数据

目录 1. 实验任务 2. 硬件原理 3. 利用STM32CubeMX创建MDK工程 3.1 STM32CubeMX工程创建 3.2 配置调试方式 3.3 配置时钟电路 3.4 配置GPIO 3.5 配置串口参数 3.6 配置时钟 3.7 项目配置 4. 串行通信实验 4.1 UART串口printf&#xff0c;scanf函数串口重定向 4.2 …

数组(C语言程序设计)

一、一维数组 数组是相同类型的有序数据的集合 1、一维数组的定义 形式&#xff1a;类型名 数组名[常量表达式] 2、一维数组元素的引用 形式&#xff1a;数组名[下标] 3、一维数组的初始化 形式&#xff1a;类型名 数组名[数组长度]{初值表} 4、一维数组程序设计示例 【例6.…

如何画类图

是为了写论文才回头看的,已经忘光了 在类图中&#xff0c;我们用一个矩形来表示一个类。这个矩形通常分为三个部分&#xff1a; 顶部&#xff1a;写类的名字。 中间&#xff1a;写类的特性&#xff0c;比如一个"狗"类可能有"颜色"&#xff0c;“品种"…

Go语言的错误和异常处理:error、panic和recover

目录 【error类型】 error的基本用法 error.Is 用法 封装自定义错误结构体 error.As 用法 错误行为特征检视策略 【异常panic和recover】 panic recover panic 和 os.Exit 如何正确应对panic 【error类型】 error的基本用法 在Go语言中&#xff0c;一般使用 error …

【P1003 [NOIP2011 提高组] 铺地毯】

[NOIP2011 提高组] 铺地毯 题目描述 为了准备一个独特的颁奖典礼&#xff0c;组织者在会场的一片矩形区域&#xff08;可看做是平面直角坐标系的第一象限&#xff09;铺上一些矩形地毯。一共有 n n n 张地毯&#xff0c;编号从 1 1 1 到 n n n。现在将这些地毯按照编号从小…

Redis单线程 Vs 多线程

Redis单线程 Vs 多线程 一 面试题引入1.1 Redis到底是单线程还是多线程&#xff1f;1.2 IO多路复用1.3 Redis为什么快&#xff1f;1.4 Subtopic 二 Redis为什么选择单线程&#xff1f;2.1 这种问法其实并不严谨&#xff0c;为啥这么说&#xff1f;2.2 Redis是单线程究竟何意&am…

什么是JavaScript?为什么需要学习它?

JavaScript是一种广泛使用的编程语言&#xff0c;它被用于开发Web应用程序、桌面应用程序和移动应用程序。它的出现可以追溯到1995年&#xff0c;由瑞典计算机科学家Tim Bergling和美国计算机科学家John Resig共同开发。 JavaScript的历史可以追溯到20世纪90年代&#xff0c;当…

完美解决:由于找不到MSVR100.dll ,无法继续执行代码

当我们在运行某一个软件时&#xff0c;突然提示找不到MSVCR100.dll&#xff0c;我相信有不少用户都遇到过这种情况&#xff0c;并且在重新安装软件后还是无法解决。那么电脑提示找不到MSVCR100.dll该怎办呢? MSVCR100.dll是什么&#xff1f; 在解决找不到MSVCR100.dll这个问…

RabbitMQ之交换机详解

1 Exchages ​ 我们假设的是工作队列背后&#xff0c;每个任务都恰好交付给一个消费者(工作进程)。在这一部分中&#xff0c;我们将做一些完全不同的事情&#xff0c;我们将消息传达给多个消费者。这种模式 称为 ”发布/订阅“。 ​ 为了说明这种模式&#xff0c;我们将构建一…

vi和vim编辑器介绍与使用

VI 和 VIM 编辑器是 Unix 和 Linux 操作系统中最常用的文本编辑工具之一。虽然它们都用于编辑文本文件&#xff0c;但它们有一些不同之处。本文将对 VI 和 VIM 编辑器进行介绍&#xff0c;帮助你更好地了解编辑器的特性和优点。 Linux下常见的文本编辑器有&#xff1a; emacsp…

Unity解决在摄像机上面设置了TargetTexture后获取屏幕坐标不准的问题

大家好&#xff0c;我是阿赵 这里来分享一个最近遇到的小问题。 一、发现问题 如果我们想将3D模型放在UI上&#xff0c;一个比较普遍的做法是&#xff1a; 用一个单独的摄像机&#xff0c;把3D模型拍下来&#xff0c;并转成RenderTexture&#xff0c;贴到RawImage上。 那么如…

枚举类型enum详解

概述 enum是C语言中的一个关键字&#xff0c;enum叫枚举数据类型&#xff0c;枚举数据类型描述的是一组整型值的集合&#xff08;这句话其实不太妥当&#xff09;&#xff0c;因为枚举类型是一种基本数据类型&#xff0c;而不是一种构造类型&#xff0c;它不能再分解成什么基本…

架构设计如何绘图?

大家好&#xff0c;我是易安&#xff01; 很多同学技术能力很强&#xff0c;架构设计也做得很好&#xff0c;但是在给别人讲解的时候&#xff0c;总感觉像是“茶壶里煮饺子&#xff0c;有货倒不出”。 其实&#xff0c;在为新员工培训系统架构、给领导汇报技术规划、上技术大会…