go并发和并行

进程和线程

进程(Process)就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位,进程是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间。一个进程至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。
通俗的讲进程就是一个正在执行的程序。

线程 是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位一个进程可以创建多个线程,同一个进程中的多个线程可以并发执行,一个程序要运行的话至少有一个进程。

在这里插入图片描述

在这里插入图片描述

并行和并发

并发:多个线程同时竞争一个位置,竞争到的才可以执行,每一个时间段只有一个线程在执行。
并行:多个线程可以同时执行,每一个时间段,可以有多个线程同时执行。

通俗的讲多线程程序在单核 CPU 上面运行就是并发,多线程程序在多核 CUP 上运行就是并行,如果线程数大于 CPU 核数,则多线程程序在多个 CPU 上面运行既有并行又有并发。

在这里插入图片描述
在这里插入图片描述

协程(goroutine)和主线程

golang 中的主线程:(可以理解为线程/也可以理解为进程),在一个 Golang 程序的主线程上可以起多个协程。Golang 中多协程可以实现并行或者并发。

协程:可以理解为用户级线程,这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的。Golang 的一大特色就是从语言层面原生支持协程,在函数或者方法前面加 go 关键字就可创建一个协程。可以说 Golang 中的协程就是goroutine 。

在这里插入图片描述

Golang 中的多协程有点类似其他语言中的多线程。

多协程和多线程:Golang 中每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少。OS 线程(操作系统线程)一般都有固定的栈内存(通常为 2MB 左右),一个 goroutine (协程) 占用内存非常小,只有 2KB 左右,多协程 goroutine 切换调度开销方面远比线程要少。

这也是为什么越来越多的大公司使用 Golang 的原因之一。

Goroutine 的使用

先看一个简单的并行执行需求:
在主线程(可以理解成进程)中,开启一个 goroutine, 该协程每隔 50 毫秒秒输出 “你好 golang” 在主线程中也每隔 50 毫秒输出"你好 golang", 输出 10 次后,退出程序,要求主线程和goroutine 同时执行。

实现代码:

package mainimport ("fmt""strconv""time"
)func test() {for i := 1; i <= 10; i++ {fmt.Println("test () hello,world " + strconv.Itoa(i))time.Sleep(time.Second)}
}
func main() {go test() // 开启了一个协程for i := 1; i <= 10; i++ {fmt.Println(" main() hello,golang" + strconv.Itoa(i))time.Sleep(time.Second)}
}

上面代码看上去没有问题,但是要注意主线程执行完毕后即使协程没有执行完毕,程序会退出,所以我们需要对上面代码进行改造。

在这里插入图片描述

sync.WaitGroup 可以实现主线程等待协程执行完毕。

package mainimport ("fmt""strconv""sync""time"
)var wg sync.WaitGroup //1、定义全局的 WaitGroup
func test() {for i := 1; i <= 10; i++ {fmt.Println("test () 你好 golang " + strconv.Itoa(i))time.Sleep(time.Millisecond * 50)}wg.Done() // 4、goroutine 结束就登记-1
}
func main() {wg.Add(1) //2、启动一个 goroutine 就登记+1go test()for i := 1; i <= 2; i++ {fmt.Println(" main() 你好 golang" + strconv.Itoa(i))time.Sleep(time.Millisecond * 50)}wg.Wait() // 3、等待所有登记的 goroutine 都结束
}

运行结果:

 main() 你好 golang1
test () 你好 golang 1
test () 你好 golang 2main() 你好 golang2
test () 你好 golang 3
test () 你好 golang 4
test () 你好 golang 5
test () 你好 golang 6
test () 你好 golang 7
test () 你好 golang 8
test () 你好 golang 9
test () 你好 golang 10

启动多个 Goroutine

在 Go 语言中实现并发就是这样简单,我们还可以启动多个 goroutine。让我们再来一个例子:
(这里使用了 sync.WaitGroup 来实现等待 goroutine 执行完毕)

package mainimport ("fmt""sync"
)var wg sync.WaitGroupfunc hello(i int) {defer wg.Done() // goroutine 结束就登记-1fmt.Println("Hello Goroutine!", i)
}
func main() {for i := 0; i < 10; i++ {wg.Add(1) // 启动一个 goroutine 就登记+1go hello(i)}wg.Wait() // 等待所有登记的 goroutine 都结束
}

多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为 10 个 goroutine是并发执行的,而 goroutine 的调度是随机的。

设置 Golang 并行运行的时候占用的cpu数量

Go 运行时的调度器使用 GOMAXPROCS 参数来确定需要使用多少个 OS 线程来同时执行 Go代码。默认值是机器上的 CPU 核心数。例如在一个 8 核心的机器上,调度器会把 Go 代码同时调度到 8 个 OS 线程上。

Go 语言中可以通过 runtime.GOMAXPROCS()函数设置当前程序并发时占用的 CPU 逻辑核心数。
Go1.5 版本之前,默认使用的是单核心执行。Go1.5 版本之后,默认使用全部的 CPU 逻辑核心数。

package mainimport ("fmt""runtime"
)func main() {//获取当前计算机上面的 Cup 个数cpuNum := runtime.NumCPU()fmt.Println("cpuNum=", cpuNum)//可以自己设置使用多个 cpuruntime.GOMAXPROCS(cpuNum - 1)fmt.Println("ok")
}

Channel 管道

管道是 Golang 在语言级别上提供的 goroutine 间的通讯方式,我们可以使用 channel 在多个 goroutine 之间传递消息。如果说 goroutine 是 Go 程序并发的执行体,channel 就是它们之间的连接。channel 是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。

Golang 的并发模型是 CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。

Go 语言中的管道(channel)是一种特殊的类型。管道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个管道都是一个具体类型的导管,也就是声明 channel 的时候需要为其指定元素类型。

  1. channel 类型

channel 是一种类型,一种引用类型。声明管道类型的格式如下:

var 变量 chan 元素类型
举几个例子:
var ch1 chan int // 声明一个传递整型的管道
var ch2 chan bool // 声明一个传递布尔型的管道
var ch3 chan []int // 声明一个传递 int 切片的管道
  1. 创建channel

声明的管道后需要使用 make 函数初始化之后才能使用。
创建 channel 的格式如下:

make(chan 元素类型, 容量)

代码示例:

//创建一个能存储 10 个 int 类型数据的管道
ch1 := make(chan int, 10)
//创建一个能存储 4 个 bool 类型数据的管道
ch2 := make(chan bool, 4)
//创建一个能存储 3 个[]int 切片类型数据的管道
ch3 := make(chan []int, 3
  1. channel操作

管道有发送(send)、接收(receive)和关闭(close)三种操作。
发送和接收都使用<-符号。
现在我们先使用以下语句定义一个管道:

ch := make(chan int, 3)
  • 发送(将数据放在管道内)

将一个值发送到管道中:

ch <- 10 // 把 10 发送到 ch 中
  • 接收(从管道内取值)

从一个管道中接收值:

x := <- ch // 从 ch 中接收值并赋值给变量 x
<-ch // 从 ch 中接收值,忽略结果
  • 关闭管道

我们通过调用内置的 close 函数来关闭管道:

close(ch)

关于关闭管道需要注意的事情是,只有在通知接收方 goroutine 所有的数据都发送完毕的时候才需要关闭管道。管道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭管道不是必须的。

关闭后的管道有以下特点:
(1)对一个关闭的管道再发送值就会导致 panic。
(2)对一个关闭的管道进行接收会一直获取值直到管道为空。
(3)对一个关闭的并且没有值的管道执行接收操作会得到对应类型的零值。
(4)关闭一个已经关闭的管道会导致 panic。

  1. 管道阻塞
  • 无缓冲的管道

如果创建管道的时候没有指定容量,那么我们可以叫这个管道为无缓冲的管道
无缓冲的管道又称为阻塞的管道。我们来看一下下面的代码:

package mainimport ("fmt"
)func main() {ch := make(chan int)ch <- 10fmt.Println("发送成功")
}

上面这段代码能够通过编译,但是执行的时候会出现以下错误:

fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]:
main.main()D:/project/go-project/main.go:10 +0x28Process finished with the exit code 2
  • 有缓冲的管道

解决上面问题的方法还有一种就是使用有缓冲区的管道。我们可以在使用 make 函数初始化管道的时候为其指定管道的容量,例如:

package mainimport ("fmt"
)func main() {ch := make(chan int, 1) // 创建一个容量为 1 的有缓冲区管道ch <- 10fmt.Println("发送成功")
}

只要管道的容量大于零,那么该管道就是有缓冲的管道,管道的容量表示管道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

管道阻塞具体代码如下:

package mainimport ("fmt"
)func main() {ch := make(chan int, 1)ch <- 10ch <- 12fmt.Println("发送成功")
}

显然由于ch <-12这行代码就会报错:

fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]:
main.main()D:/project/go-project/main.go:10 +0x45Process finished with the exit code 2

解决办法:

package mainimport ("fmt"
)func main() {ch := make(chan int, 1)ch <- 10 //放进去<-ch     //取走ch <- 12 //放进去<-ch     //取走ch <- 17 //还可以放进去fmt.Println("发送成功")
}
  1. for range 从管道循环取值

当向管道中发送完数据时,我们可以通过 close 函数来关闭管道。
当管道被关闭时,再往该管道发送值会引发 panic,从该管道取值的操作会先取完管道中的值,再然后取到的值一直都是对应类型的零值。那如何判断一个管道是否被关闭了呢?
我们来看下面这个例子

package mainimport ("fmt"
)func main() {var ch1 = make(chan int, 5)for i := 0; i < 5; i++ {ch1 <- i + 1}close(ch1) //关闭管道//使用 for range 遍历管道,当管道被关闭的时候就会退出 for range,如果没有关闭管道 就会报个错误 fatal error: all goroutines are asleep - deadlock!//通过 for range 来遍历管道数据 管道没有 keyfor val := range ch1 {fmt.Println(val)}
}

运行结果:

1
2
3
4
5

从上面的例子中我们看到有两种方式在接收值的时候判断该管道是否被关闭,不过我们通常使用的是 for range 的方式。使用 for range 遍历管道,当管道被关闭的时候就会退出 for range。

Goroutine 结合 Channel 管道

需求 :

定义两个方法,一个方法给管道里面写数据,一个给管道里面读取数据。要求同步进行。
(1)开启一个 fn1 的的协程给向管道 inChan 中写入 100 条数据
(2)开启一个 fn2 的协程读取 inChan 中写入的数据
(3)注意:fn1 和 fn2 同时操作一个管道
(4)主线程必须等待操作完成后才可以退出

package mainimport ("fmt""sync""time"
)var wg sync.WaitGroupfunc fn1(intChan chan int) {for i := 0; i < 100; i++ {intChan <- i + 1fmt.Println("writeData 写入数据-", i+1)time.Sleep(time.Millisecond * 100)}close(intChan)wg.Done()
}
func fn2(intChan chan int) {for v := range intChan {fmt.Printf("readData 读到数据=%v\n", v)time.Sleep(time.Millisecond * 50)}wg.Done()
}
func main() {allChan := make(chan int, 100)wg.Add(1)go fn1(allChan)wg.Add(1)go fn2(allChan)wg.Wait()fmt.Println("读取完毕...")
}

单向管道

有的时候我们会将管道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用管道都会对其进行限制,比如限制管道在函数中只能发送或只能接收。

只写通道 (chan<-):只能向这个通道发送数据,不能从中接收数据。
只读通道 (<-chan):只能从这个通道接收数据,不能向其中发送数据。

package mainimport ("fmt"
)func main() {// 创建一个双向通道,并分配内存biChan := make(chan int, 3)// 只写通道var chan2 chan<- int = biChanchan2 <- 20fmt.Println("chan2=", chan2)// 向双向通道发送更多数据biChan <- 30// 只读通道var chan3 <-chan int = biChannum2 := <-chan3fmt.Println("num2", num2)
}

运行结果:

chan2= 0xc0000a6080
num2 20

select 多路复用

在某些场景下我们需要同时从多个通道接收数据。这个时候就可以用到 golang 中给我们提供的 select 多路复用。
通常情况通道在接收数据时,如果没有数据可以接收将会发生阻塞。
比如说下面代码来实现从多个通道接受数据的时候就会发生阻塞:

for{
// 尝试从 ch1 接收值
data, ok := <-ch1
// 尝试从 ch2 接收值
data, ok := <-ch2 …
}

这种方式虽然可以实现从多个管道接收值的需求,但是运行性能会差很多。为了应对这种场景,Go 内置了 select 关键字,可以同时响应多个管道的操作。

select 的使用类似于 switch 语句,它有一系列 case 分支和一个默认的分支。每个 case 会对应一个管道的通信(接收或发送)过程。select 会一直等待,直到某个 case 的通信操作完成时,就会执行 case 分支对应的语句。具体格式如下:

select{
case <-ch1:
... case data := <-ch2:
... case ch3<-data:
...
default:
默认操作
}

举个小例子来演示下 select 的使用:

package mainimport ("fmt""time"
)func main() {//1.定义一个管道 10 个数据 intintChan := make(chan int, 10)for i := 0; i < 10; i++ {intChan <- i}//2.定义一个管道 5 个数据 stringstringChan := make(chan string, 5)for i := 0; i < 5; i++ {stringChan <- "hello" + fmt.Sprintf("%d", i)}for {select {case v := <-intChan:fmt.Printf("从 intChan 读取的数据%d\n", v)case v := <-stringChan:fmt.Printf("从 stringChan 读取的数据%s\n", v)default:fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n")time.Sleep(time.Second)return}}}

Golang 并发安全和锁

  1. 互斥锁

互斥锁是传统并发编程中对共享资源进行访问控制的主要手段,它由标准库 sync 中的 Mutex结构体类型表示。sync.Mutex 类型只有两个公开的指针方法,Lock 和 Unlock。Lock 锁定当前的共享资源,Unlock 进行解锁。

我们先看一段有问题的代码:

package mainimport ("fmt""time"
)var count = 0func test() {count++fmt.Println("the count is : ", count)time.Sleep(time.Millisecond)
}
func main() {for r := 0; r < 100; r++ {go test()}time.Sleep(time.Second)
}

这段代码的主要问题是它涉及到并发编程中的竞态条件(race condition)。具体来说,多个goroutine同时访问和修改共享变量count,而没有适当的同步机制来保护对这个变量的访问。这会导致不可预测的行为,因为不同的goroutine可能会在同一时间尝试读取和写入count,从而导致数据竞争。

互斥锁解决这个问题:

package mainimport ("fmt""sync""time"
)var count = 0
var mu sync.Mutex // 声明一个互斥锁func test(wg *sync.WaitGroup) {defer wg.Done() // 确保在函数退出时调用Done()mu.Lock()       // 获取锁count++fmt.Println("the count is : ", count)mu.Unlock() // 释放锁time.Sleep(time.Millisecond)
}func main() {var wg sync.WaitGroup // 声明一个WaitGroup用于等待所有goroutine完成for r := 0; r < 100; r++ {wg.Add(1) // 每启动一个goroutine就增加计数go test(&wg)}wg.Wait() // 等待所有goroutine完成
}

这时我们可以看到打印的结果就正常了。

使用互斥锁能够保证同一时间有且只有一个 goroutine 进入临界区,其他的 goroutine 则在等待锁;当互斥锁释放后,等待的 goroutine 才可以获取锁进入临界区,多个 goroutine 同时等待一个锁时,唤醒的策略是随机的。

虽然使用互斥锁能解决资源争夺问题,但是并不完美,通过全局变量加锁同步来实现通讯,并不利于多个协程对全局变量的读写操作。这个时候我们也可以通过另一种方式来实现上面的功能管道(Channel)。

  1. 读写锁

互斥锁的本质是当一个 goroutine 访问的时候,其他 goroutine 都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。

其实,当我们对一个不会变化的数据只做“读”操作的话,是不存在资源竞争的问题的。因为数据是不变的,不管怎么读取,多少 goroutine 同时读取,都是可以的。

所以问题不是出在“读”上,主要是修改,也就是“写”。修改的数据要同步,这样其他goroutine 才可以感知到。所以真正的互斥应该是读取和修改、修改和修改之间,读和读是没有互斥操作的必要的。

因此,衍生出另外一种锁,叫做读写锁。

读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个 goroutine 进行写操作的时候,其他 goroutine 既不能进行读操作,也不能进行写操作。

GO 中的读写锁由结构体类型 sync.RWMutex 表示。此类型的方法集合中包含两对方法:

一组是对写操作的锁定和解锁,简称“写锁定”和“写解锁”

func (*RWMutex)Lock()
func (*RWMutex)Unlock()

另一组表示对读操作的锁定和解锁,简称为“读锁定”与“读解锁”:

func (*RWMutex)RLock()
func (*RWMutex)RUnlock()

读写锁示例代码:

package mainimport ("fmt""sync""time"
)var count int
var mutex sync.RWMutex
var wg sync.WaitGroup// 写的方法
func write() {mutex.Lock()fmt.Println("执行写操作")time.Sleep(time.Second * 3)mutex.Unlock()wg.Done()
}// 读的方法
func read() {mutex.RLock()fmt.Println("执行读操作")time.Sleep(time.Second * 3)mutex.RUnlock()wg.Done()
}
func main() {// 开启 10 个协程执行写操作for i := 0; i < 10; i++ {wg.Add(1)go read()}//开启 10 个协程执行读操作for i := 0; i < 10; i++ {wg.Add(1)go write()}wg.Wait()
}

Goroutine Recover 解决协程中出现的 Panic

package mainimport ("fmt""time"
)// 函数
func sayHello() {for i := 0; i < 10; i++ {time.Sleep(time.Second)fmt.Println("hello,world")}
}// 函数
func test() {//这里我们可以使用 defer + recoverdefer func() {//捕获 test 抛出的 panicif err := recover(); err != nil {fmt.Println("test() 发生错误", err)}}()//定义了一个 mapvar myMap map[int]stringmyMap[0] = "golang" //error
}func main() {sayHello()test()
}

参考文献

https://gobyexample.com/

https://www.w3schools.com/go/

https://go.dev/doc/tutorial/

https://www.geeksforgeeks.org/golang-tutorial-learn-go-programming-language/

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

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

相关文章

element-ui rate 组件源码分享

评分组件&#xff0c;从三个方面分享&#xff1a; 1、页面结构。 2、组件属性。 3、组件方法。 一、页面结构&#xff1a; 主要有图标的、图标(默认或自定义图标)文字的、图标分数的。 二、属性。 2.1 value 2.2 max 最大分数。 2.3 disabled 是否只读 2.4 allow-half 是…

python学opencv|读取图像(五十六)使用cv2.GaussianBlur()函数实现图像像素高斯滤波处理

【1】引言 前序学习了均值滤波和中值滤波&#xff0c;对图像的滤波处理有了基础认知&#xff0c;相关文章链接为&#xff1a; python学opencv|读取图像&#xff08;五十四&#xff09;使用cv2.blur()函数实现图像像素均值处理-CSDN博客 python学opencv|读取图像&#xff08;…

HIVE如何注册UDF函数

如果注册UDF函数的时候报了上面的错误&#xff0c;说明hdfs上传的路径不正确&#xff0c; 一定要用下面的命令 hadoop fs -put /tmp/hive/111.jar /user/hive/warehouse 一定要上传到上面路径&#xff0c;这样在创建函数时&#xff0c;引用下面的地址就可以创建成功

紧跟潮流,将 DeepSeek 集成到 VSCode

Visual Studio Code&#xff08;简称 VSCode&#xff09;是一款由微软开发的免费开源代码编辑器&#xff0c;自 2015 年发布以来&#xff0c;凭借其轻便、强大、且拥有丰富扩展生态的特点&#xff0c;迅速成为了全球开发者的首选工具。VSCode 支持多平台操作系统&#xff0c;包…

HAL库 Systick定时器 基于STM32F103EZT6 野火霸道,可做参考

目录 1.时钟选择(这里选择高速外部时钟) ​编辑 2.调试模式和时基源选择: 3.LED的GPIO配置 这里用板子的红灯PB5 4.工程配置 5.1ms的systick中断实现led闪烁 源码: 6.修改systick的中断频率 7.systick定时原理 SysTick 定时器的工作原理 中断触发机制 HAL_SYSTICK_Co…

DeepSeek与llama本地部署(含WebUI)

DeepSeek从2025年1月起开始火爆&#xff0c;成为全球最炙手可热的大模型&#xff0c;各大媒体争相报道。我们可以和文心一言一样去官网进行DeepSeek的使用&#xff0c;那如果有读者希望将大模型部署在本地应该怎么做呢&#xff1f;本篇文章将会教你如何在本地傻瓜式的部署我们的…

【重新认识C语言----文件管理篇】

目录 ​编辑 -----------------------------------------begin------------------------------------- 引言 1. 文件的基本概念 2. 文件指针 3. 文件的打开与关闭 3.1 打开文件 3.2 关闭文件 4. 文件的读写操作 4.1 读取文件 4.1.1 使用fgetc()读取文件 4.1.2 使用fg…

全面解析String类

一、String 类初相识 在 C 语言的世界里&#xff0c;字符串是以\0结尾的字符集合&#xff0c;为了方便操作&#xff0c;C 标准库提供了一系列str系列的库函数&#xff0c;如strcpy、strcat、strlen等。虽然这些库函数在一定程度上满足了我们对字符串的操作需求&#xff0c;但是…

pycharm 中的 Mark Directory As 的作用是什么?

文章目录 Mark Directory As 的作用PYTHONPATH 是什么PYTHONPATH 作用注意事项 Mark Directory As 的作用 可以查看官网&#xff1a;https://www.jetbrains.com/help/pycharm/project-structure-dialog.html#-9p9rve_3 我们这里以 Mark Directory As Sources 为例进行介绍。 这…

MySQL - 字段内分组

1、MySQL 5.7及之前版本 SELECT A.要显示的字段名称,FIRST_VALUE : A.分组字段名称,last :IF(FIRST_VALUE A.分组字段名称, last 1, 1 ) AS rn,FROM 表1 A,(SELECT last : 0, FIRST_VALUE : NULL ) BORDER BY A.排序字段例&#xff1a;SELECT A.DLR_CODE,A.VAILD_CARD_NO,A.L…

瞬态分析中的时域分析与频域分析:原理、对比与应用指南

目录 一、核心概念区分 二、时域分析&#xff1a;时间维度直接求解 1. 基本原理 2. 关键特点 3. 典型算法 4. 应用案例 三、频域分析&#xff1a;频率维度的等效映射 1. 基本原理 2. 关键特点 3. 典型方法 4. 应用案例 四、对比与选择依据 1. 方法论对比 2. 工程…

【DeepSeek】DeepSeek小模型蒸馏与本地部署深度解析DeepSeek小模型蒸馏与本地部署深度解析

一、引言与背景 在人工智能领域&#xff0c;大型语言模型&#xff08;LLM&#xff09;如DeepSeek以其卓越的自然语言理解和生成能力&#xff0c;推动了众多应用场景的发展。然而&#xff0c;大型模型的高昂计算和存储成本&#xff0c;以及潜在的数据隐私风险&#xff0c;限制了…

安卓/ios脚本开发按键精灵经验小分享

1. 程序的切换 我们经常碰到这样的需求&#xff1a;打开最近的应用列表&#xff0c;选取我们想要的程序。但是每个手机为了自己的风格&#xff0c;样式都有区别&#xff0c;甚至连列表的滑动方向都不一样&#xff0c;我们很难通过模拟操作来识别点击&#xff0c;那么我们做的只…

camera光心检测算法

1.概要 光心检测算法&#xff0c;基于opencv c实现&#xff0c;便于模组厂快速集成到软件工具中&#xff0c;适用于camera模组厂算法评估组装制程镜头与sensor的偏心程度&#xff0c;便于工程师了解制程的问题找出改善方向。 2.技术介绍 下图为camera模组厂抓取的bayer-raw经过…

OpenCV:特征检测总结

目录 一、什么是特征检测&#xff1f; 二、OpenCV 中的常见特征检测方法 1. Harris 角点检测 2. Shi-Tomasi 角点检测 3. Canny 边缘检测 4. SIFT&#xff08;尺度不变特征变换&#xff09; 5. ORB 三、特征检测的应用场景 1. 图像匹配 2. 运动检测 3. 自动驾驶 4.…

Docker安装pypiserver私服

Docker安装pypiserver私服 1 简介 Python开源包管理工具有pypiserver、devpi和Nexus等&#xff0c;pypiserver安装部署比较简单&#xff0c;性能也不错。 搭建pypiserver私服&#xff0c;可以自己构建镜像&#xff0c;也可以使用官网的docker镜像。 # Github地址 https://g…

什么是自动化测试?自动化测试的作用

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、自动化测试 所谓的自动化测试简单来说就是有计算机代替 人来单击被测软件的界面&#xff0c;执行一系列操作并进行验证。 自动化有点&#xff1a;通过执行…

AI驱动的智能流程自动化是什么

在当今快速发展的数字化时代&#xff0c;企业正在寻找更高效、更智能的方式来管理日常运营和复杂任务。其中&#xff0c;“AI驱动的智能流程自动化”&#xff08;Intelligent Process Automation, IPA&#xff09;成为了一个热门趋势。通过结合人工智能&#xff08;AI&#xff…

集合类不安全问题

ArrayList不是线程安全类&#xff0c;在多线程同时写的情况下&#xff0c;会抛出java.util.ConcurrentModificationException异常 解决办法&#xff1a; 1.使用Vector&#xff08;ArrayList所有方法加synchronized&#xff0c;太重&#xff09; 2.使用Collections.synchronized…

私有化部署DeepSeek并SpringBoot集成使用(附UI界面使用教程-支持语音、图片)

私有化部署DeepSeek并SpringBoot集成使用&#xff08;附UI界面使用教程-支持语音、图片&#xff09; windows部署ollama Ollama 是一个开源框架&#xff0c;专为在本地机器上便捷部署和运行大型语言模型&#xff08;LLM&#xff09;而设计 下载ollama 下载地址&#xff08;m…