go中的并发

goruntine(协程)

每一个并发的执行单元叫做一个goruntine,要编写一个并发任务,可以在函数名前加go关键字,就能使这个函数以协程的方式运行,

如:go 函数名(函数参数)、

如果函数有返回值,返回值会被忽略。所以当你用go关键字后,主进程调用函数的时候函数的返回值就没有和主进程进行数据交换,而只能用channel

线程和协程的区别:

区别1:线程有固定的栈,基本都是2MB,都是固定分配的;这个栈用于保存局部变量,在函数切换时使用。对于协程来说,固定的栈可能会导致资源浪费,go采用了动态收缩扩张收缩的策略,初始化为2KB,最大可扩张到1GB

区别2:线程切换要陷入内核,进行上下文切换,而协程在用户态由协程调度器完成,不用陷入内核

demo:协程的基本使用

package mainimport ("fmt""time"
)func Task1() {for {fmt.Println(time.Now().Format("10:48:00"), "正在处理task1的任务")time.Sleep(time.Second * 3)}
}func Task2() {for {fmt.Println(time.Now().Format("10:48:00"), "正在处理task2的任务")time.Sleep(time.Second * 1)}}func main() {go Task1()go Task2()for {fmt.Println(time.Now().Format("10:48:00"), "正在处理主进程的任务")time.Sleep(time.Second * 2)}
}

demo2:

package mainimport ("fmt""time"
)/*
当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们称之为main goroutine。下面这个程序运行后不会有任何输出,因为运行go task1(),程序立刻返回到main函数,main函数后没有任何代码逻辑,程序判断为执行完毕,终止所有协程。
如果要让task1函数执行,可以在main函数加上一些等待逻辑,确定子协程执行完毕后结束main函数。如sleep()
*/func task1() {for {fmt.Println(time.Now().Format("10:48:00"), "正在处理task1的任务")time.Sleep(time.Second * 3)}
}func main() {go task1()time.Sleep(time.Second * 100)
}

demo3: 使用匿名函数创建goruntine

package mainimport ("fmt""time"
)/*
使用匿名函数创建goruntine,格式如下:
go func(参数列表){函数体
}(调用参数列表)*/func main() {go func() {for {fmt.Println(time.Now().Format("11:17:00"), "正在处理task1的任务!")time.Sleep(time.Second * 3)}}()time.Sleep(time.Second * 100)
}

channel

go中推荐使用channel作为goruntine之间同步和通信的手段

常见写法:var channelName chan T

含义:用chan关键字声明了一个channel变量,并指定了传输的数据类型T

channel的发送和接收:channel作为一个队列,收发数据时按先入先出的原则,同一时刻内只能允许一个goruntine访问channel来发送和接受数据

发送:channel<-val

含义:表示将var发送到channel中,如果channel中数据被填满之后会阻塞当前goruntine

接受:val<-channel

含义:表示从channel中读取数据赋值给var上,如果channel中没有数据,会阻塞读取的goruntine直到有数据被放到channel中。

注:可以在读取channel时立刻返回,如 var,ok:=<-channel ,此时检查ok是否为true,用来判断是否读到了有效数据。

初始化:ch:=make(chan T,sizeofChan)

注:

  • 在创建channel时要指定channel传输的数据类型,长度是可选的。
  • 如果不指定数据类型,会导致向channel发送数据的goruntine被阻塞,直到数据被读取
  • 如果指定了长度,表示是有缓冲的channel,在缓冲区未满时发送给数据不会被阻塞。
  • 无论channel是否携带缓冲区,读取的goroutine都会被阻塞,直到channel中有数据可被读取。

demo:下面通过一个例子演示goruntine和channel配合,创建一个channel用于在两个goruntine中收发数据,其中一个goruntine从命令行读取输入发送到channel中,另一个goruntine循环的从channel中读取数据并输出

import ("bufio""fmt""os"
)//从channel中读数据进行打印
func printInput(ch chan string) {for val1 := range ch {//读取到结束符合if val1 == "EOF" {break}fmt.Printf("input is: %s\n", val1)}
}func main() {//创建一个无缓冲的channelch := make(chan string)go printInput(ch)//从命令行读取输入scanner := bufio.NewScanner(os.Stdin)for scanner.Scan() {var1 := scanner.Text()//将读到的命令行发送到channelch <- var1if var1 == "EOF" {fmt.Println("End the game")break}}//关闭channeldefer close(ch)
}

运行结果:

i am cc
input is: i am cc
eof
input is: eof
EOF
End the game

上述例子的代码中,我们通过for:range语法从channel中循环读取数据:当channel中没有数据时,printInput的goroutine将会被阻塞。

代码的最后,我们还通过defer close(ch)关闭了创建的channel,需要注意的是在channel关闭后不允许再往通道中放入数据,不然会抛出panic;

而再从关闭的channel读取数据或者之前从channel读取数据并被阻塞的goroutine将会接收到零值,直接返回。

demo2:带缓冲区的channel

package mainimport ("fmt""time"
)/*
带缓冲区的channel:创建channel时指定了channel的长度,那么channel将会拥有缓冲区,goroutine在缓冲区未满时往channel发送数据将不会被阻塞。*/func consume(ch chan int) {//线程休息30s再从channel中读数据time.Sleep(time.Second * 30)<-ch
}func main() {//创建一个长度为2的channelch := make(chan int, 2)go consume(ch)ch <- 0ch <- 1//发送数据不被阻塞fmt.Println("I am free!")ch <- 2fmt.Println("I can not go there within 100s!")time.Sleep(time.Second*2)
}

运行结果:30s之内只输出I am free! ,这段时间内是阻塞的,30s之后协程从channel读数据后,再往里面发送数据2是不阻塞的,所以输出了I can not go there within 30s!

I am free!
I can not go there within 30s!

除了声明双向channel,.我们还可以声明单向的channel,即只能从channel发送数据或者只能从channel中读取数据,代码如下所示:

ch := make(chan T)
// 声明只能发送的通道
var chInput chan <- int = ch// 声明只能读取的通道
var chOutput int <- chan = ch

demo3:select使用

运行结果:

get value 0 from ch1
get value 1 from ch1 
get value 10 from ch2
get value 11 from ch2
get value 12 from ch2
get value 2 from ch1 
get value 3 from ch1 
get value 13 from ch2
get value 4 from ch1 
get value 5 from ch1 
get value 6 from ch1
get value 14 from ch2
get value 15 from ch2
get value 16 from ch2
get value 7 from ch1
get value 8 from ch1
get value 9 from ch1
get value 17 from ch2
get value 18 from ch2
get value 19 from ch2
time out

当然每次运行结果都可能不一样,因为 goroutine 调度的不确定性。

上述代码中,我们通过select多路复用分别从chl和ch2中读取数据,如果多个case语句中的ch同时到达,那么select将会运行一个伪随机算法随机选择一个case。在最后的case中我们设定可接受的最大时长为2s,如果时间超过2s,将从select中退出。

由于channel的阻塞是无法被中断的,所以这是一种有效地从阻塞的channel中超时返回的小技巧。

sync包

go中除了使用channel进行goruntine之间的同步和通信操作外,还可以使用sync包提供的并发工具类主要有7种:

  • Mutex 互斥锁
  • RWMutex 读写锁
  • WaitGroup 并发等待组
  • Map 并发安全字典
  • Cond 同步等待条件
  • Once 只执行一次
  • Pool 临时对象池

这节讲解一下互斥锁的用法:sync.Mutex能够保证同一个时间段内只有一个goruntine持有锁,这就能保证在某一个时间段内有且仅有一个goruntine访问共享资源,其他想申请锁的goruntine将会被阻塞直到锁被释放,然后重新争抢锁的持有权

demo:使用Sync.Mutex控制多个goruntine串行执行

package mainimport ("fmt""sync""time"
)func main() {var lock sync.Mutexgo func() {//加锁lock.Lock()defer lock.Unlock()fmt.Println("func1 get lock at:" + time.Now().String())time.Sleep(time.Second)fmt.Println("func1 release lock at:" + time.Now().String())}()time.Sleep(time.Second / 10)go func() {lock.Lock()defer lock.Unlock()fmt.Println("func2 get lock at:" + time.Now().String())time.Sleep(time.Second)fmt.Println("func2 release lock at:" + time.Now().String())}()//等待所有goruntine执行完毕time.Sleep(time.Second * 4)
}

读写锁的用法:

  • 为了保证同一时间段有多个goruntine访问同一资源,要满足以下条件:
  • 同一时间段只能有一个goruntine获取到写锁
  • 同一时间段可以有任意多个goruntine获取到读锁
  • 同一时间段只能存在读锁或写锁(读锁和写锁互斥)

demo:sync.RWMutex 允许多读和单写的案例

package mainimport ("fmt""strconv""sync""time"
)var rwLock sync.RWMutexfunc main() {//获取读锁for i := 0; i < 5; i++ {go func(i int) {rwLock.RLock()defer rwLock.RUnlock()fmt.Println("read func" + strconv.Itoa(i) + " get rlock at:" + time.Now().String())time.Sleep(time.Second)}(i)}time.Sleep(time.Second / 10)//获取写锁for i := 0; i < 5; i++ {go func(i int) {rwLock.Lock()defer rwLock.Unlock()fmt.Println("write func" + strconv.Itoa(i) + " get wlock at" + time.Now().String())time.Sleep(time.Second)}(i)}//保证所有的goruntine执行结束time.Sleep(time.Second * 10)
}

运行结果:

read func4 get rlock at:2023-03-14 14:24:02.5203537 +0800 CST m=+0.003223301
read func1 get rlock at:2023-03-14 14:24:02.5203537 +0800 CST m=+0.003223301
read func2 get rlock at:2023-03-14 14:24:02.5203537 +0800 CST m=+0.003223301
read func3 get rlock at:2023-03-14 14:24:02.5203537 +0800 CST m=+0.003223301
read func0 get rlock at:2023-03-14 14:24:02.5203537 +0800 CST m=+0.003223301
write func0 get wlock at2023-03-14 14:24:03.5513528 +0800 CST m=+1.034222401
write func4 get wlock at2023-03-14 14:24:04.5535884 +0800 CST m=+2.036458001
write func2 get wlock at2023-03-14 14:24:05.5637904 +0800 CST m=+3.046660001
write func1 get wlock at2023-03-14 14:24:06.5747967 +0800 CST m=+4.057666301
write func3 get wlock at2023-03-14 14:24:07.5782438 +0800 CST m=+5.061113401

从输出的结果可以看出,在写锁没有被获取时,所有read goroutine可以同时申请到读锁,如read func0到read func4几乎在同一时间点获取到读锁;

而申请写锁的goroutine必须等到没有任何的读锁和其他写锁存在时才能申请成功,如write func0需要等到readfunc最后一个goroutine释放读锁才能申请到写锁,其他申请写锁的write func需要等待前一个write func释放写锁后才能重新争抢写锁,它们获取到写锁的时间大约相差1秒,这刚好是 每个write func 持有写锁的时间

demo:sync.WaitGroup使用

package mainimport ("fmt""strconv""sync""time"
)/*
sync.WaitGroup使用:
使用sync.WaitGroup的goruntine会等预设好数量的goruntine都提交执行结束后,才会继续往下执行代码*/func main() {var waitGroup sync.WaitGroup//设置等待goruntine数量为5waitGroup.Add(5)for i := 0; i < 5; i++ {go func(i int) {fmt.Println("work" + strconv.Itoa(i) + " is done at " + time.Now().String())time.Sleep(time.Second)waitGroup.Done()}(i)}waitGroup.Wait()fmt.Println("all works are done at:" + time.Now().String())
}

demo:sync.Map的使用

package mainimport ("fmt""strconv""sync"
)/*
sync.Map的使用:
Go语言中原生的Map并不是并发安全的,在多个goroutine同时往Map中添加数据时,可能会导致部分添加数据的丢失,为了避免这种情况,
在Go语言l.9版本之前,我们需要结合sync.RWMutex和Map实现并发安全的字典。
Go语言1.9之后提供了sync.Map,相对于原生的Map,它只提供以下接口:*/var syncMap sync.Map
var waitGroup sync.WaitGroupfunc main() {routineSize := 5//设置等待goruntine数量为5waitGroup.Add(routineSize)//并发添加数据for i := 0; i < routineSize; i++ {go addNumber(i * 10)}//开始等待waitGroup.Wait()var size int//统计数量syncMap.Range(func(key, value interface{}) bool {size++fmt.Println("key-value pair is ", key, value)return true})fmt.Println("syncMap current size is " + strconv.Itoa(size))//获取键为0的值value, ok := syncMap.Load(0)if ok {fmt.Println("key 0 has value ", value, " ")}
}func addNumber(begin int) {//往syncMap中放入数据for i := begin; i < begin+3; i++ {syncMap.Store(i, i)}//通知数据已添加完毕waitGroup.Done()
}

补充学习资料:关于golang的并发同步与安全 - 9ong (可以补充学习)

并发同步之sync.WaitGroup

golang经常会有多协程并行的场景,而我们又需要这些协程全部结束后,继续完成main协程的后续行为,这个时候,我们需要一个谁来统计所有协程的运行结果,并通知main协程,不能仅仅依靠timeout、sleep的方式硬编写main协程等待其他协程时间,无法完美预估其他协程最晚完成时间。

所以就有了sync.WaitGroup,sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。比如启动了5个并发任务时,就将计数器值增加5,每个任务完成时通过调用Done()方法将计数器减1,当计数器值为0时,表示所有并发任务已经完成,通过调用Wait()来等待并发任务执行完,达到main协程知道所有协程任务完成的效果。

package mainimport ("fmt""sync"
)var wg sync.WaitGroupfunc goodjob(num int) {defer wg.Done()fmt.Println(num, "good job.")
}
func main() {for i := 0; i < 5; i++ {wg.Add(1)go goodjob(i)}fmt.Println("main goroutine do something.")wg.Wait()fmt.Println("main goroutine end.")
}

使用sync.WaitGroup后的输出:

main goroutine do something.
1 good job.
0 good job.
2 good job.
3 good job.
4 good job.
main goroutine end.

如果不使用WaitGroup,将不会看到goodjob协程的输出打印,只有main协程的输出,因为main协程并不会去等待其他协程,而是先执行并退出main协程,一旦退出main协程,其他协程也会被销毁(在销毁前还来不及执行,因为需要一些时间给GMP调度)

并发加载之sync.once

可能不同协程会加载相同的静态资源,在高并发场景下,这些资源其实只需要加载一次就可以,比如配置、关闭一次通道、连接远端资源等,sync包提供了Once解决方案,一次只执行一次的解决方案。

package mainimport ("errors""fmt""sync"
)type subConfig struct {url  stringsrc  stringdesc string
}var configs map[string]subConfig
var loadOnce sync.Oncefunc loadConfigs() {configs = map[string]subConfig{"icons":   loadIcons(),"ads":     loadAds(),"banners": loadBanners(),}
}func getConfig(key string) (res subConfig, err error) {//这里如果我们判断nil的方式来规避并发的话,是不安全的// if configs == nil {//  loadConfigs()// }//所以我们换成了sync.Once的Do方法来控制并发安全及避免通过锁的机制来控制资源并发访问带来的性能问题loadOnce.Do(loadConfigs)res, ok := configs[key]if !ok {errors.New("key invalid.")}return
}func loadIcons() subConfig {//可能从文件、缓存、数据库等资源获取icons := subConfig{src:  "http://www.9ong.com/xxx.png",url:  "http://www.9ong.com/",desc: "9ong icon desc",}return icons
}func loadBanners() subConfig {//可能从文件、缓存、数据库等资源获取banners := subConfig{src:  "http://www.9ong.com/xxx.png",url:  "http://www.9ong.com/",desc: "9ong banner desc",}return banners
}func loadAds() subConfig {//可能从文件、缓存、数据库等资源获取ads := subConfig{src:  "http://www.9ong.com/xxx.png",url:  "http://www.9ong.com/",desc: "9ong ads desc",}return ads
}func main() {//并发调用getConfigads, err := getConfig("ads")if err != nil {fmt.Println(err)} else {fmt.Println(ads)}
}

在getConfig函数中,我们通过判断configs这个全局变量是否为nil,决定是否重新加载configs数据,在这种情况下就会出现即使判断了configs不是nil,也不意味着configs变量初始化完成了。考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化加载configs全局资源变量的时候不会被其他的协程读写,但是这样做又会引发性能问题。

sync.Once内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。

所以getConfig函数改造后,通过loadOnce.Do(loadConfigs)来实现互斥加载。

解决了并发加载问题,还不会因为多次加载浪费时间和空间及引入锁等机制带来的性能问题。

并发安全与锁之sync.mutex

package mainimport ("fmt""sync"
)var love int32 = 10000 //公共资源love
var wg sync.WaitGroupfunc getLove() {defer wg.Done()for i := 0; i < 1000; i++ {love = love - 1}fmt.Println("love left:", love)
}
func main() {for children := 0; children < 10; children++ {wg.Add(1)go getLove()}wg.Wait()fmt.Println("love is:", love)
}

由于启动了10个协程,这些协程存在竞争love资源的可能(多试几次或把初始值和循环数调大点),可能会出现以下的输出,其实也就是电商里说的超卖问题,按照逻辑love最后应该为0,但实际情况是love还剩下3045:

I:\src\go\src\helo>go run test.go
love left: 7763
love left: 8763
love left: 9000
love left: 6763
love left: 6452
love left: 5452
love left: 5220
love left: 4220
love left: 4045
love left: 3045
love is: 3045

超卖的问题在于并发不安全,并发时对临界资源的读写不安全,我们需要考虑在读写临界资源时,将并发/并行转换成串行,通常采用加锁的机制。

互斥锁(完全互斥)是一种常用的控制共享资源访问的方法,它能够保证同时只有一个协程可以访问共享资源。golang中使用sync包的Mutex类型来实现互斥锁:

package mainimport ("fmt""sync"
)var love int32 = 10000 //公共资源love
var wg sync.WaitGroup
var mu sync.Mutexfunc getLove() {defer wg.Done()mu.Lock()for i := 0; i < 1000; i++ {love = love - 1}mu.Unlock()fmt.Println("love left:", love)
}
func main() {for children := 0; children < 10; children++ {wg.Add(1)go getLove()}wg.Wait()fmt.Println("love is:", love)
}

前面我们说互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当并发去读取一个资源不涉及资源修改的时候是没有必要加锁的,如果使用完全互斥锁的话,会导致读多的协程会堵塞等待锁的释放,很影响效率,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。

读写锁分为读锁和写锁,当一个协程获取读锁之后,其他的协程如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个协程获取写锁之后,其他的协程无论是获取读锁还是写锁都会等待。

也就是说,读写锁互斥,读读不会互斥,协程的读锁,不应其他协程的读锁获取,但影响写锁的获取;而写锁影响所有协程对该资源锁的获取。

编码如何处理呢?

我们只要对共享资源的读加读锁Rlock(),对共享资源的写加写锁lock()

var rwlock sync.RWMutexfunc read(){rwlock.RLock()//read love actionrwlock.RUnlock()
}func write(){rwlock.Lock()//writen love actioinrwlock.Unlock()
}

并发安全之sync.map

golang内置map不是并发安全的,很好理解,map只是一种资源的数据结构而已,所以作为共享资源,其本身当然也是并发不安全的,对于map中key、value的操作也需要做并发安全处理,比如加锁。

虽然加锁可以解决,但golang也提供了一套并发安全的map操作类:sync.Map,其内置了Store、Load、LoadOrStore、Delete、Range等操作方法,这些方法都是并发安全的,可以理解为原子操作。

package mainimport ("fmt""strconv""sync"
)var sm = sync.Map{}
var wg = sync.WaitGroup{}func setMap(i int) {key := strconv.Itoa(i)sm.Store(key, i)value, _ := sm.Load(key)fmt.Printf("k=:%v,v:=%v\n", key, value)wg.Done()}func main() {for i := 0; i < 20; i++ {wg.Add(1)go setMap(i)}wg.Wait()
}

并发安全之atomic

针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法它在用户态就可以完成,因此性能比加锁操作更好。Go语言中原子操作由内置的标准库sync/atomic提供。

package mainimport ("fmt""sync""sync/atomic"
)var love int32 = 10000 //公共资源love
var wg sync.WaitGroup
var mu sync.Mutexfunc getLove() {defer wg.Done()for i := 0; i < 1000; i++ {love = love - 1}fmt.Println("love left:", love)
}
func mutexGetLove() {defer wg.Done()mu.Lock()for i := 0; i < 1000; i++ {love = love - 1}mu.Unlock()fmt.Println("love left:", love)
}
func atomicGetLove() {defer wg.Done()for i := 0; i < 1000; i++ {atomic.AddInt32(&love, -1)}fmt.Println("love left:", love)
}
func main() {for children := 0; children < 10; children++ {wg.Add(1)// go getLove() //普通,并发不安全// go mutexGetLove() //互斥锁,并发安全,但性能开销大go atomicGetLove() //原子操作,并发安全,性能优于互斥锁机制}wg.Wait()fmt.Println("love is:", love)
}

atomic包提供了底层的原子级内存操作,虽然其对于同步算法的实现很有效果,但除了某些特殊的底层应用,我们建议使用channel或者sync.包实现同步会更好些。

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

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

相关文章

【力扣每日一题01】两数之和

开了一个新专栏&#xff0c;用来记录自己每天刷题&#xff0c;并且也是为了养成每日学习这个习惯&#xff0c;期待坚持一年后的自己&#xff01; 一、题目 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&am…

[.NET/WPF] 设置按钮, 以及其他任何包含边框的控件的圆角

在 WPF 中, 按钮包含一个 “边框”, 很多时候需要设置按钮的圆角, 但是按钮并没有提供一个属性用来设置边框圆角. 下面以按钮为例, 列举几种常用的设置圆角的方式. 通过附加属性 定义一个附加属性, 然后在各个地方就能直接方便的使用了, 下面是实际使用方式: <Button ut…

SpringBoot集成WebSocket

SpringBoot集成WebSocket 项目结构图 项目架构图 前端项目 socket.js 注意前端这里的端口是9000, 路劲是ws开头 function createScoket(token){var socket;if(typeof(WebSocket) "undefined") {console.log("您的浏览器不支持WebSocket");}else{var ho…

linux C++ 海康截图Demo

项目结构 CMakeLists.txt cmake_minimum_required(VERSION 3.7)project(CapPictureTest)include_directories(include)link_directories(${CMAKE_SOURCE_DIR}/lib ${CMAKE_SOURCE_DIR}/lib/HCNetSDKCom) add_executable(CapPictureTest ${CMAKE_SOURCE_DIR}/src/CapPictureTes…

小兔鲜儿 - 地址模块

目录 小兔鲜儿 - 地址模块 准备工作​ 静态结构​ 地址管理页​ 地址表单页​ 动态设置标题​ 新建地址页​ 接口封装​ 参考代码​ 地址管理页​ 接口调用​ 参考代码​ 修改地址页​ 数据回显​ 更新地址​ 表单校验​ 操作步骤​ 删除地址​ 侧滑组件用法…

dji uav建图导航系列()ROS中创建dji_sdk节点包(一)项目结构

文章目录 1、整体项目结构1.1、 目录launch1.2、文件CMakeLists.txt1.3、文件package.xml1.4、目录include1.4、目录srv在ROS框架下创建一个无人机的节点dji_sdk,实现必需的订阅(控制指令)、发布(无人机里程计)、服务(无人机起飞降落、控制权得很)功能,就能实现一个类似…

基于java Swing 和 mysql实现的飞机订票系统(源码+数据库+ppt+ER图+流程图+架构说明+论文+运行视频指导)

一、项目简介 本项目是一套基于java Swing 和 mysql实现的飞机订票系统&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Java学习者。 包含&#xff1a;项目源码、项目文档、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过…

myspl使用指南

mysql数据库 使用命令行工具连接数据库 mysql -h -u 用户名 -p -u表示后面是用户名-p表示后面是密码-h表示后面是主机名&#xff0c;登录当前设备可省略。 如我们要登录本机用户名为root&#xff0c;密码为123456的账户&#xff1a; mysql -u root -p按回车&#xff0c;然后…

【0901作业】QTday3 对话框、发布软件、事件处理机制,使用文件相关操作完成记事本的保存功能、处理键盘事件完成圆形的移动

目录 一、思维导图 二、作业 2.1 使用文件相关操作完成记事本的保存功能 2.2 处理键盘事件完成圆形的移动 一、思维导图 二、作业 2.1 使用文件相关操作完成记事本的保存功能 void Widget::on_saveBtn_clicked() {QString filename QFileDialog::getSaveFileName(this,&…

四川大学874考研真题00-23

22, 2022年硕士学位研究生入学考试试题回忆版 数据结构 1&#xff0e;一个时间复杂度为n2 的算法运行&#xff0c;m1算n个问题用时1秒&#xff0c;m2处理器是m1效率的64倍&#xff0c;则m2每秒能计算&#xff08;&#xff09;个问题。 A. 64n B. 8n …

目录扫描+JS文件中提取URL和子域+403状态绕过+指纹识别(dirsearch_bypass403)

dirsearch_bypass403 在安全测试时&#xff0c;安全测试人员信息收集中时可使用它进行目录枚举&#xff0c;目录进行指纹识别&#xff0c;枚举出来的403状态目录可尝试进行绕过&#xff0c;绕过403有可能获取管理员权限。不影响dirsearch原本功能使用 运行流程 dirsearch进行…

【狂神】Spring5笔记(10-19)

又是美好而努力的一天呀~ __ /|* * * * * * / * * * / * * * * / * * * * * * * happy valentines day * * * * …

自然语言处理-NLP

目录 自然语言处理-NLP 致命密码&#xff1a;一场关于语言的较量 自然语言处理的发展历程 兴起时期 符号主义时期 连接主义时期 深度学习时期 自然语言处理技术面临的挑战 语言学角度 同义词问题 情感倾向问题 歧义性问题 对话/篇章等长文本处理问题 探索自然语言…

电脑莫名其妙重启 为设备 ROOT\DISPLAY\0000 加载驱动程序 \Driver\WUDFRd 失败

卸载向日葵即可解决&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;下面是报错日志&#xff0c;估计是远程连接导致的问题

JVM工具-1. jps 虚拟机进程状态工具

文章目录 1. jps介绍2. jps命令格式3. jps工具主要选项4. jps -q5. jps -m6. jps -l7. jps -v 1. jps介绍 jps(JVM Process Status Tool)&#xff1a;虚拟机进程状态工具&#xff0c;可以列出正在运行的虚拟机进程&#xff0c;并显示虚拟机执行主类&#xff08;Main Class&…

ARM寄存器组

CM3 拥有通用寄存器 R0‐R15 以及一些特殊功能寄存器。 R0-R7&#xff0c;通用目的寄存器 R0-R7也被称为低组寄存器&#xff0c;所有指令可以访问它们&#xff0c;它们的字长为32位&#xff0c;复位后的初始值是不可预料的。 R8-R12&#xff0c;通用目的寄存器 R8-R12也被称…

汽车以太网协议栈

《大师说》栏目上线啦# 《大师说》栏目是怿星科技2023年推出的深度思考栏目&#xff0c;通过邀请内部专家&#xff0c;针对智能汽车行业发展、技术趋势等输出个性化的观点。每期一位大师&#xff0c;每位一个话题&#xff0c;本期由我们怿星的CTO虞胜伟&#xff0c;进行分享。…

Elasticsearch 优化

Elasticsearch 优化 2.1硬件选择 Elasticsearch 的基础是 Lucene &#xff0c;所有的索引和文档数据是存储在本地的磁盘中&#xff0c;具体的 路径可在 ES 的配置文件 ../config/elasticsearch.yml 中配置&#xff0c;如下&#xff1a; #----------------------------…

C#,《小白学程序》第十二课:日历的编制,时间DateTime的计算方法与代码

1 文本格式 /// <summary> /// 《小白学程序》第十二课&#xff1a;日历的编制&#xff0c;时间DateTime的计算方法与代码 /// 本课学习时间类型的数据 DateTime 的简单方法&#xff0c;并编制一个月的日历。 /// </summary> /// <param name"sender"…

zookee 安装

1、下载安装包 weget https://downloads.apache.org/zookeeper/zookeeper-3.6.3/apache-zookeeper-3.6.3-bin.tar.gz 方案1&#xff1a;wget是一个下载指令&#xff0c;后面可以跟下载连接去从服务器上下载东西。 方案2&#xff1a;也可以先下载到windows上&#xff0c;再通…