GoLong的学习之路(二十三)进阶,语法之并发(go最重要的特点)(锁,sync包,原子操作)

这章是我并发系列中最后的一章。这章主要讲的是锁。但是也会讲上一章channl遗留下的一些没有讲到的内容。select关键字的用法,以及错误的一些channl用法。废话不多说。。。

文章目录

  • select多路复用
  • 通道错误示例
  • 并发安全和锁
    • 问题描述
    • 互斥锁
    • 读写互斥锁
  • sync
    • sync.WaitGroup
      • 加载配置文件示例
      • 并发安全的单例模式
    • sync.Map
  • 原子操作
    • 读取操作
    • 写入操作
    • 修改操作
    • 交换操作
    • 比较并交换操作

select多路复用

使用场景:需要同时从多个通道接收数据

通道在接收数据时,如果没有数据可以被接收那么当前 goroutine 将会发生阻塞。

当然办法不是没有。遍历呗。

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 <- 10:
	//...
default:
	//默认操作
}

Select 语句具有以下特点:

  • 可处理一个或多个 channel 的发送/接收操作。
  • 如果多个 case 同时满足select随机选择一个执行。
  • 对于没有caseselect 会一直阻塞,可用于阻塞 main 函数,防止退出。
package main

import "fmt"

func main() {
	ch := make(chan int, 1)
	for i := 1; i <= 10; i++ {
		select {
		case x := <-ch:
			fmt.Println(x)
		case ch <- i:
		}
	}
}

在这里插入图片描述
代码首先是创建了一个缓冲区大小为1的通道 ch,进入 for 循环后:

  • 第一次循环时 i = 1,select 语句中包含两个 case 分支,此时由于通道中没有值可以接收,所以x := <-ch 这个 case 分支不满足,而ch <- i这个分支可以执行,会把1发送到通道中,结束本次 for 循环;
  • 第二次 for 循环时,i = 2,由于通道缓冲区已满,所以ch <- i这个分支不满足,而x := <-ch这个分支可以执行,从通道接收值1并赋值给变量 x ,所以会在终端打印出 1;
  • 后续的 for 循环以此类推会依次打印出3、5、7、9

简单而言就是,当i为偶数的时候,执行的通道里输出。

通道错误示例

示例1

// demo1 通道误用导致的bug
func demo1() {
	wg := sync.WaitGroup{}

	ch := make(chan int, 10)
	for i := 0; i < 10; i++ {
		ch <- i
	}
	close(ch)

	wg.Add(3)
	for j := 0; j < 3; j++ {
		go func() {
			for {
				task := <-ch
				// 这里假设对接收的数据执行某些操作
				fmt.Println(task)
			}
			wg.Done()
		}()
	}
	wg.Wait()
}

匿名函数所在的 goroutine 并不会按照预期在通道被关闭后退出。

因为task := <- ch的接收操作在通道被关闭后会一直接收到零值,而不会退出。此处的接收操作应该使用task, ok := <- ch ,通过判断布尔值ok为假时退出;或者使用select 来处理通道。
修改后:

for j := 0; j < 3; j++ {
		go func() {
			for {
				task, ok := <-ch
				fmt.Println(task)
				if !ok {
					break
				}
			}
			wait.Done()
		}()
	}
	wait.Wait()

其实不需要嵌套外循环的。不过为了方便观看就这样也了。

// demo2 通道误用导致的bug
func demo2() {
	ch := make(chan string)
	go func() {
		// 这里假设执行一些耗时的操作
		time.Sleep(3 * time.Second)
		ch <- "job result"
	}()

	select {
	case result := <-ch:
		fmt.Println(result)
	case <-time.After(time.Second): // 设置的超时时间
		return
	}
}

分析代码可以知道,此时有两个goroutine ,主方法走select,而另一个 goroutine 会走给通道输入值的操作。此时就有一个问题。从协程goroutine会等待三秒,而主协程,指挥等待一秒,然后按照超时操作弹出。

而这种问题的存在不是因为我们没有达到想要的结果,而是可能导致 goroutine 泄露(goroutine 并未按预期退出并销毁)

由于 select 命中了超时逻辑,导致通道没有消费者(无接收操作),而其定义的通道为无缓冲通道,因此 goroutine 中的ch <- "job result"操作会一直阻塞,最终导致 goroutine 泄露。


上一章漏下的内容讲完


并发安全和锁

场景:可能会存在多个 goroutine 同时操作一个资源(临界区)的情况,这种情况下就会发生竞态问题(数据竞态)

问题描述

package main

import (
	"fmt"
	"sync"
)

var (
	x int64

	wg sync.WaitGroup // 等待组
)

// add 对全局变量x执行5000次加1操作
func add() {
	for i := 0; i < 5000; i++ {
		x = x + 1
	}
	wg.Done()
}

func main() {
	wg.Add(2)

	go add()
	go add()

	wg.Wait()
	fmt.Println(x)
}

每次执行都会生成不同的结果
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
原因:
我们开启了两个 goroutine 分别执行 add 函数,这两个 goroutine 在访问和修改全局的x变量时就会存在数据竞争,某个 goroutine 中对全局变量x的修改可能会覆盖掉另一个 goroutine 中的操作,所以导致最后的结果与预期不符

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同一时间只有一个 goroutine 可以访问共享资源。Go 语言中使用sync包中提供的Mutex类型来实现互斥锁。

sync.Mutex提供了两个方法:

方法名功能
func (m *Mutex) Lock()获取互斥锁
func (m *Mutex) Unlock()释放互斥锁

通过锁修改

package main

import (
	"fmt"
	"sync"
)

// sync.Mutex

var (
	x int64
	wg sync.WaitGroup // 等待组
	m sync.Mutex // 互斥锁
)

// add 对全局变量x执行5000次加1操作
func add() {
	for i := 0; i < 5000; i++ {
		m.Lock() // 修改x前加锁
		x = x + 1
		m.Unlock() // 改完解锁
	}
	wg.Done()
}

func main() {
	wg.Add(2)
	go add()
	go add()


	wg.Wait()
	fmt.Println(x)
}

此时就会达到我们的预期结果。

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

读写互斥锁

互斥锁是完全互斥的,但是实际上有很多场景是读多写少的。

当我们并发的去读取一个资源而不涉及资源修改的时候是没有必要加互斥锁的,这种场景下使用读写锁是更好的一种选择。

读写锁在 Go 语言中使用sync包中的RWMutex类型

方法名功能
func (rw *RWMutex) Lock()获取写锁
func (rw *RWMutex) Unlock()释放写锁
func (rw *RWMutex) RLock()获取读锁
func (rw *RWMutex) RUnlock()释放读锁
func (rw *RWMutex) RLocker() Locker返回一个实现Locker接口的读写锁

读写锁分为两种:读锁写锁

  • 当一个 goroutine 获取到读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待。
  • 当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。
package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	x       int64
	wg      sync.WaitGroup
	mutex   sync.Mutex
	rwMutex sync.RWMutex
)

// writeWithLock 使用互斥锁的写操作
func writeWithLock() {
	mutex.Lock() // 加互斥锁
	x = x + 1
	time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
	mutex.Unlock()                    // 解互斥锁
	wg.Done()
}

// readWithLock 使用互斥锁的读操作
func readWithLock() {
	mutex.Lock()                 // 加互斥锁
	time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
	mutex.Unlock()               // 释放互斥锁
	wg.Done()
}

// writeWithLock 使用读写互斥锁的写操作
func writeWithRWLock() {
	rwMutex.Lock() // 加写锁
	x = x + 1
	time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
	rwMutex.Unlock()                  // 释放写锁
	wg.Done()
}

// readWithRWLock 使用读写互斥锁的读操作
func readWithRWLock() {
	rwMutex.RLock()              // 加读锁
	time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
	rwMutex.RUnlock()            // 释放读锁
	wg.Done()
}

func do(wf, rf func(), wc, rc int) {
	start := time.Now()
	// wc个并发写操作
	for i := 0; i < wc; i++ {
		wg.Add(1)
		go wf()
	}

	//  rc个并发读操作
	for i := 0; i < rc; i++ {
		wg.Add(1)
		go rf()
	}

	wg.Wait()
	cost := time.Since(start)
	fmt.Printf("x:%v cost:%v\n", x, cost)

}
func main() {
	// 使用互斥锁,10并发写,1000并发读
	do(writeWithLock, readWithLock, 10, 1000) // x:10 cost:1.466500951s

	// 使用读写互斥锁,10并发写,1000并发读
	do(writeWithRWLock, readWithRWLock, 10, 1000) // x:10 cost:117.207592ms
}

从最终的执行结果可以看出,使用读写互斥锁在读多写少的场景下能够极大地提高程序的性能。

不过需要注意的是如果一个程序中的读操作和写操作数量级差别不大,那么读写互斥锁的优势就发挥不出来

有一点要注意在在这个实验中要明确一个已经知道的属性,那就是读操作,一定比写操作快。

在并发的时候说了,GO本体的标准库有个一个专门为并发实现的包sync

sync

sync.WaitGroup

在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务同步在这里插入代码片

方法名功能
func (wg * WaitGroup) Add(delta int)计数器 +delta
func (wg *WaitGroup) Done()计数器 -1 (这个要搭配defer使用)
func (wg *WaitGroup) Wait()阻塞直到计数器变为 0

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加减少

当我们启动了 N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用 Done 方法将计数器减1。通过调用 Wait 等待并发任务执行完,当计数器值为 0 时,表示所有并发任务已经完成。几乎同时(人类看来)输出结果

需要注意:sync.WaitGroup是一个结构体,进行参数传递的时候要传递指针。## sync.Once

var wg sync.WaitGroup

func hello() {
	defer wg.Done()
	fmt.Println("Hello Goroutine!")
}
func main() {
	wg.Add(1)
	go hello() // 启动另外一个goroutine去执行hello函数
	fmt.Println("main goroutine done!")
	wg.Wait()
}

在某些场景下我们需要确保某些操作即使在高并发的场景下也只会被执行一次,例如只加载一次配置文件等。

Go语言中的sync包中提供了一个针对只执行一次场景的解决方案——sync.Oncesync.Once只有一个Do方法

func (o *Once) Do(f func())

注意:如果要执行的函数f需要传递参数就需要搭配闭包来使用。

加载配置文件示例

延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。

因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作不是必须要做的。

var icons map[string]image.Image

func loadIcons() {
	icons = map[string]image.Image{
		"left":  loadIcon("left.png"),
		"up":    loadIcon("up.png"),
		"right": loadIcon("right.png"),
		"down":  loadIcon("down.png"),
	}
}

func loadIcon(s string) image.Image {

	return nil
}

// Icon 被多个goroutine调用时不是并发安全的
func Icon(name string) image.Image {
	if icons == nil {
		loadIcons()
	}
	return icons[name]
}

多个 goroutine 并发调用Icon函数时不是并发安全的,现代的编译器和CPU可能会在保证每个 goroutine 都满足串行一致的基础上自由地重排访问内存的顺序。(指令重排序)

loadIcons函数可能会被重排为以下结果:

func loadIcons() {
	icons = make(map[string]image.Image)
	icons["left"] = loadIcon("left.png")
	icons["up"] = loadIcon("up.png")
	icons["right"] = loadIcon("right.png")
	icons["down"] = loadIcon("down.png")
}

千万别看这个顺序和前面定义的一样。那是每个goroutine的出来的结果。并不一个得出的。

在这种情况下就会出现即使判断了icons不是nil也不意味着变量初始化完成了。考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化icons的时候不会被其他的 goroutine 操作,但是这样做又会引发性能问题。

所以此时就考虑:sync.Once

import (
	"image"
	"sync"
)

var icons map[string]image.Image

var loadIconsOnce sync.Once

func loadIcons() {
	icons = map[string]image.Image{
		"left":  loadIcon("left.png"),
		"up":    loadIcon("up.png"),
		"right": loadIcon("right.png"),
		"down":  loadIcon("down.png"),
	}
}

func loadIcon(s string) image.Image {

	return nil
}

// Icon 是并发安全的
func Icon(name string) image.Image {
	loadIconsOnce.Do(loadIcons)
	return icons[name]
}
func main() {

}

并发安全的单例模式

package singleton

import (
    "sync"
)

type singleton struct {}

var instance *singleton
var once sync.Once

func GetInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
    })
    return instance
}

sync.Once其实内部包含一个互斥锁一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成

这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次

sync.Map

sync.Map

Go 语言中内置的 map 不是并发安全的.

错误例子

package main

import (
	"fmt"
	"strconv"
	"sync"
)

var m = make(map[string]int)

func get(key string) int {
	return m[key]
}

func set(key string, value int) {
	m[key] = value
}

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			set(key, n)
			fmt.Printf("k=:%v,v:=%v\n", key, get(key))
			wg.Done()
		}(i)
	}
	wg.Wait()
}

在这里插入图片描述
将上面的代码编译后执行,会报出fatal error: concurrent map writes错误。我们不能在多个 goroutine 中并发对内置的 map 进行读写操作,否则会存在数据竞争问题。其实大家,自己运行一下就知道了。其实不一定会出现这个错误,但是有概率出现。所以能加锁就枷锁。

这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版 map——sync.Map

开箱即用表示其不用像内置的 map 一样使用 make 函数初始化就能直接使用。

方法名功能
func (m *Map) Store(key, value interface{})存储key-value数据
func (m *Map) Load(key interface{}) (value interface{}, ok bool)查询key对应的value
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)查询或存储key对应的value
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool)查询并删除key
func (m *Map) Delete(key interface{})删除key
func (m *Map) Range(f func(key, value interface{}) bool)对map中的每个key-value依次调用f
package main

import (
	"fmt"
	"strconv"
	"sync"
)

// 并发安全的map
var m = sync.Map{}

func main() {
	wg := sync.WaitGroup{}
	// 对m执行20个并发的读写操作
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			m.Store(key, n)         // 存储key-value
			value, _ := m.Load(key) // 根据key取值
			fmt.Printf("k=:%v,v:=%v\n", key, value)
			wg.Done()
		}(i)
	}
	wg.Wait()
}

在这里插入图片描述
此时就安全了

说到枷锁操作,就不得不说一个东西。原子性操作

原子操作

针对整数数据类型(int32、uint32、int64、uint64)我们还可以使用原子操作来保证并发安全,通常直接使用原子操作比使用锁操作效率更高。

Go语言中原子操作由内置的标准库sync/atomic提供。(具体需要的话可以去看相关文档)

读取操作

func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)

写入操作

func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

修改操作

func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

交换操作

func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)

比较并交换操作

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用

除了某些特殊的底层应用,使用通道或者 sync 包的函数/类型实现同步更好。

例子

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

type Counter interface {
	Inc()
	Load() int64
}

// 普通版
type CommonCounter struct {
	counter int64
}

func (c CommonCounter) Inc() {
	c.counter++
}

func (c CommonCounter) Load() int64 {
	return c.counter
}

// 互斥锁版
type MutexCounter struct {
	counter int64
	lock    sync.Mutex
}

func (m *MutexCounter) Inc() {
	m.lock.Lock()
	defer m.lock.Unlock()
	m.counter++
}

func (m *MutexCounter) Load() int64 {
	m.lock.Lock()
	defer m.lock.Unlock()
	return m.counter
}

// 原子操作版
type AtomicCounter struct {
	counter int64
}

func (a *AtomicCounter) Inc() {
	atomic.AddInt64(&a.counter, 1)
}

func (a *AtomicCounter) Load() int64 {
	return atomic.LoadInt64(&a.counter)
}

func test(c Counter) {
	var wg sync.WaitGroup
	start := time.Now()
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			c.Inc()
			wg.Done()
		}()
	}
	wg.Wait()
	end := time.Now()
	fmt.Println(c.Load(), end.Sub(start))
}

func main() {
	c1 := CommonCounter{} // 非并发安全
	test(c1)
	c2 := MutexCounter{} // 使用互斥锁实现并发安全
	test(&c2)
	c3 := AtomicCounter{} // 并发安全且比互斥锁效率更高
	test(&c3)
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Device Partner 平台合作伙伴认证和数据安全保护

Device Partner 平台是面向 AIoT 产业链伙伴的一站式服务平台&#xff0c;伙伴可以通过平台获取最新的产品、服务与解决方案&#xff0c;实现智能硬件产品的开发、认证、量产和推广等全生命周期的管理&#xff0c;加入 HarmonyOS Connect 生态&#xff0c;共同提升消费者的智慧…

基于减法平均算法的无人机航迹规划-附代码

基于减法平均算法的无人机航迹规划 文章目录 基于减法平均算法的无人机航迹规划1.减法平均搜索算法2.无人机飞行环境建模3.无人机航迹规划建模4.实验结果4.1地图创建4.2 航迹规划 5.参考文献6.Matlab代码 摘要&#xff1a;本文主要介绍利用减法平均算法来优化无人机航迹规划。 …

85.x的平方根(力扣)

目录 问题描述 代码解决以及思想 知识点 问题描述 代码解决以及思想 class Solution { public:int mySqrt(int x) {int left 0; // 定义左边界int right x; // 定义右边界&#xff0c;初始值取 xwhile (left < right) { // 当左边界小于或等于…

HiSilicon352 android9.0 适配红外遥控器

海思Android解决方案在原生Android基础上&#xff0c;基于传统电视用户使用习惯&#xff0c;增加了对红外遥控器和按键板的支持&#xff0c;使传统电视用户能更好适应智能电视方案。 一.功能描述&#xff1a; 在系统启动时&#xff0c;会先启动android_ir_user&#xff1b;vinp…

SOLIDWORKS软件提供了哪些特征造型方法?硕迪科技

SOLIDWORKS作为一款三维设计软件&#xff0c;为用户提供了多种特征造型方法&#xff0c;以下是其中几种常用的&#xff1a; 实体建模特征&#xff1a;SOLIDWORKS使用实体建模技术来创建和编辑三维几何体。通过使用基本几何体&#xff08;如立方体、圆柱体、圆锥体等&#xff09…

四川芸鹰蓬飞商务信息咨询有限公司电商带货可信吗

今天&#xff0c;我们要向大家介绍的是四川芸鹰蓬飞商务信息咨询有限公司的电商带货服务&#xff0c;一个在电商领域独树一帜的服务项目。它的出现&#xff0c;不仅为电商行业注入了新的活力&#xff0c;也引领了行业发展的新趋势。 一、背景介绍 四川芸鹰蓬飞商务信息咨询有限…

【1++的Linux】之线程(三)含生产者消费者模型

&#x1f44d;作者主页&#xff1a;进击的1 &#x1f929; 专栏链接&#xff1a;【1的Linux】 文章目录 一&#xff0c;可重入与线程安全二&#xff0c;死锁三&#xff0c;线程同步什么是线程同步&#xff1f;怎么实现线程同步条件变量 四&#xff0c;生产者与消费者模型1&…

MySQL的安装使用(入学篇)

目录 1 MySQL安装 1.1 安装epel源 1.2 安装MySQL Repository 1.3 安装MySQL官方yum源 1.4 安装服务端、客户端 1.5 启动MySQL服务 2 MySQL 使用 2.1 获取初始登录密码 2.2 登录MySQL数据库 2.3 修改密码 2.4 退出数据库 2.5 使用新密码登录数据库 2.6 重启数据库 2.7 创建数据…

【分享贴】需求变更、项目延误,项目经理应该如何应对?

案例分享&#xff1a; 项目经理小李跟进了一年半的项目&#xff0c;眼看着要到交付验收的阶段了&#xff0c;甲方的对接人却临时更换了&#xff0c;现在面临着一系列他无法处理的问题&#xff0c;项目目前推进困难。 案例背景&#xff1a; 小李在跟原甲方对接人合作时&#x…

Qt 事件循环

引出 UI程序之所叫UI程序&#xff0c;是因为需要与用户有交互&#xff0c;用户交互一般是通过鼠标键盘等的输入设备&#xff0c;那UI程序就需要有能随时响应用户交互的能力。 一个C程序的main函数大概是下面这样&#xff1a; int main() {...return 0; } 我们如何使程序能随…

ECharts中rich的使用

ECharts官方rich介绍 label: {// 在文本中&#xff0c;可以对部分文本采用 rich 中定义样式。// 这里需要在文本中使用标记符号&#xff1a;// {styleName|text content text content} 标记样式名。// 注意&#xff0c;换行仍是使用 \n。formatter: [{a|这段文本采用样式a},{b…

使用Nginx和Spring Gateway为SkyWalking的增加登录认证功能

文章目录 1、使用Nginx增加认证。2、使用Spring Gateway增加认证 SkyWalking的可视化后台是没有用户认证功能的&#xff0c;默认下所有知道地址的用户都能访问&#xff0c;官网是建议通过网关增加认证。 本文介绍通过Nginx和Spring Gateway两种方式 1、使用Nginx增加认证。 生…

大模型+人形机器人,用AI唤起钢筋铁骨

《经济参考报》11月8日刊发文章《多方布局人形机器人赛道,智能应用前景广》。文章称&#xff0c;工信部日前印发的《人形机器人创新发展指导意见》&#xff0c;按照谋划三年、展望五年的时间安排&#xff0c;对人形机器人创新发展作了战略部署。 从开发基于人工智能大模型的人…

原型制作神器ProtoPie的使用Unity与网页跨端交互

什么是ProtoPie&#xff1f; ProtoPie是一款面向设计师的软件原型设计工具&#xff0c;例如制作App界面交互展示&#xff0c;制作好的原型可以一键发布到Web服务器&#xff0c;就可以浏览器访问。由于其内置了大量常用交互类型&#xff0c;以及"程序化"模块&#xf…

【Mac开发环境搭建】Node.js安装(多版本切换)、Maven安装

文章目录 Node安装安装多个Node Maven安装下载配置环境变量修改配置文件settings.xml配置maven的本地仓库地址配置阿里云镜像仓库 IDEA使用 Node安装 https://nodejs.org/download/release/v16.20.1/ 如果对安装位置有要求&#xff0c;可以更改安装位置&#xff0c;不然直接点…

【开源分享】国内可用的免费安卓GPT语音助手 - 可音量键唤起,可联网

写在前面&#xff1a;这是一个我写的开源GPT语音助手&#xff0c;不收钱&#xff0c;只求Star! 简要介绍 这是一个基于ChatGPT的安卓端语音助手&#xff0c;允许用户通过手机音量键从任意界面唤起并直接进行语音交流&#xff0c;用最快捷的方式询问并获取回复 使用效果 一、基…

【干货】132道最新K8S面试题汇总~

k8s全称kubernetes&#xff0c;这个名字大家应该都不陌生&#xff0c;k8s是为容器服务而生的一个可移植容器的编排管理工具&#xff0c;越来越多的公司正在拥抱k8s&#xff0c;并且当前k8s已经主导了云业务流程&#xff0c;推动了微服务架构等热门技术的普及和落地&#xff0c;…

ChatGPT:如何安装使用插件?超详细的教程!

1.最简单的方法 直接使用油猴&#xff0c;里边能搜索到的插件都可以用 2.官方插件使用 ChatGPT Plus引入插件后&#xff0c;功能暴强许多&#xff0c;比如可以联网、可以生成图表、可以分析视频、可以与PDF交谈等。但有不少小伙伴还不知道怎么安装使用ChatGPT插件&#xff0c;所…

Python比较2个json数据是否相等

大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 如果有什么疑惑/资料需要的可以点击文章末尾名片领取源码 1、json数据转换成字典 dict1 json.load(load_f1) dict2 json.load(load_f2)2、将两个字典按key排好序&#xff0c;然后使用zip()函数将两个字典对应的key打包成元组。 …

MySQL 批量修改表的列名为小写

1、获取脚本 SELECT concat( alter table , TABLE_NAME, change column , COLUMN_NAME, , lower( COLUMN_NAME ), , COLUMN_TYPE, comment \, COLUMN_COMMENT, \; ) AS 脚本 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA 数据库名 and TABLE_NAME表名-- 大写是up…