Go语言并发

Go语言并发学习目标

出色的并发性是Go语言的特色之一

  • • 理解并发与并行
  • • 理解进程和线程
  • • 掌握Go语言中的Goroutine和channel
  • • 掌握select分支语句
  • • 掌握sync包的应用

并发与并行

并发与并行的概念这里不再赘述,
可以看看之前java版写的并发实践;

进程和线程

程序、进程与线程这里也不赘述
一个进程可以包括多个线程,线程是容器中的工作单位;

协程~Goroutine

概念:

协程(Coroutine),最初在1963年被提出,又称为微线程,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程,一个线程也可以拥有多个协程;

协程是编译器级的进程和线程是操作系统级的

协程不被操作系统内核管理,而完全由程序控制,因此没有线程切换的开销。和多线程比,线程数量越多,协程的性能优势就越明显。协程的最大优势在于其轻量级,可以轻松创建上万个而不会导致系统资源衰竭。

Go语言中的协程

Go与语言中的协程由运行时调度和管理,Go会智能将协程中的任务合理的分配给每个CPU;

一开始创建的协程堆栈很小,但是可以根据需要增长和收缩;

Coroutine与Goroutine

Goroutine能并行执行,Coroutine只能顺序执行,Go中Goroutine可以在单线程中产生,也可以在多线程中产生;

Coroutine程序需要主动交出控制权,系统才能获得控制权并将控制权交给其他Coroutine.

Coroutine属于协作处理,在应用程序不使用CPU时,需要让渡CPU,否则会使得计算机失去响应或者宕机;

Goroutine属于抢占式任务处理,和现有的多线程和多进程任务处理类似;应用程序对CPU的控制最终由操作系统来管理,如果操作系统发现一个应用程序长时间占用CPU,那么用户有权终止这个任务。

在Go中开启协程

在Go中开启协程----只需要在函数前面加上关键字go,将会同时运行一个新的Goroutine;

注意:使用go关键字创建协程时,被调用的函数往往没有返回值,如果函数有返回值,那么返回值会被忽略,那我们就是要返回值时,必须使用channel,通过channel把数据从中取出来;

Go程序的执行过程

创建和启动主Goroutine,初始化操作,执行main函数,当main函数执行结束后,程序也就结束了;

代码demo

func helloworld() {
	fmt.Println("hello world")

}
func main() {
	go helloworld()
	fmt.Println("main exit")
}

运行结果:
在这里插入图片描述
但是这可能并不是我们看到的全部,如果main()的Goroutine比子Goroutine后终止,那么我们就会看到打印出的hello world;

多试几次(直接运行应该还是上面的结果,如果debug,就会出现打印hello world,这是因为协程有足够的时间反应;
在这里插入图片描述

我们来看一下如果加上睡眠时间:

func helloworld() {
	fmt.Println("hello world")

}
func main() {
	go helloworld()
	time.Sleep(10 * time.Microsecond)
	fmt.Println("main exit")
}

运行结果:
在这里插入图片描述
如果我们在fmt.Println("main exit")前加上defer 会是什么结果?会打印 hello world吗?

我试了一下,不行:

在这里插入图片描述
我们来看一下defer 关键字的释义

“defer”语句调用一个函数,该函数的执行被延迟到周围的函数返回的那一刻,要么是因为周围的函数执行了return语句,到达了它的函数体的末尾,要么是因为对应的协程出现了恐慌。

所以下面的代码运行结果是什么?

代码demo3

func helloworld() {
	fmt.Println("hello world")
}
func helloworld2() string {
	fmt.Println("hello world222222")
	return "hello world222222"
}

func main() {
	go helloworld() //要留有足够的时间,否则main()的Goroutine终止了,程序将被终止,该协程根本就没有机会表现
	defer helloworld2()
	defer fmt.Println("main exit")

}

在这里插入图片描述
我们来分析一下 程序的执行(即代码demo3):

go程序启动时,runtime默认为main函数创建一个Goroutine;

在这里插入图片描述

在main函数的Goroutine执行到 go helloworld()即加了关键字go的方法时.归属于helloworld()函数的Goroutine被创建,helloworld()函数开始在自己的Goroutine中执行,

此时main函数的Goroutine继续执行,如果helloworld()函数不能够在main函数的Goroutine执行完毕之前将任务处理完毕,那么就会发生helloworld()函数没有执行的样子 ;

下面我们来修改上面的代码:

func helloworld() {
	var i int
	for {
		i++
		fmt.Println("add", i)
		time.Sleep(time.Second)
	}
}
func main() {
	go helloworld() //要留有足够的时间,否则main()的Goroutine终止了,程序将被终止,该协程根本就没有机会表现
	var str string
	fmt.Scanln(&str) //阻塞
	fmt.Println("main exit")
}

控制台不断输出add int,同时还可以接收用户输入。两个环节同时运行。
在这里插入图片描述

此时,main()继续执行,两个Goroutine通过Go程序的调度机制同时运行。

匿名函数创建Goroutine

即我们可以在匿名函数前加go关键字实现对匿名函数创建Goroutine;

func main() {
	go func() {
		var arr int
		for {
			arr++
			fmt.Println("add", arr)
			time.Sleep(time.Second)
		}
	}() //闭包

	var str string
	fmt.Scanln(&str)
	fmt.Println("main exit")
}

在这里插入图片描述

启动多个Goroutine


func p1() {
	for i := 0; i < 10; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Print(i)
	}
}
func p2() {
	for i := 'a'; i < 'i'; i++ {
		time.Sleep(500 * time.Millisecond)
		fmt.Printf("%c", i)
	}
}

func main() {
	go p1()
	go p2()
	var str string
	fmt.Scanln(&str)
	fmt.Println("main exit")
}

多个Goroutine随机调度,打印的结果是数字与字母交叉输出
在这里插入图片描述

并发性能优化

在Go程序运行时,go 关键字实现了一个小型的任务调度器,该调度器是使用CPU的,那么怎么为其分配CPU呢 ?

go中可以使用runtime.Gosched()来交出CPU的控制权,同时我们可以使用runtime.GOMAXPROCS()来匹配CPU核心数量

  • Go1.5版本之前,默认使用单核执行。
  • Go1.5版本开始,默认执行runtime.GOMAXPROCS(逻辑CPU数量),让代码并发执行,最大效率地利用CPU。
//Gosched生成处理器,允许其他goroutines运行。它不是挂起当前的goroutine,这个就像java中的yield(),只是让出cpu,但同时我还可以跟其他协程竞争cpu
func Gosched() {
	checkTimeouts()
	mcall(gosched_m)
}

// GOMAXPROCS设置可以执行的cpu的最大数目同时返回之前的设置。默认为 runtime.NumCPU的值。如果n < 1,则不改变当前设置。
//当调度器改进时,此调用将消失。
func GOMAXPROCS(n int) int {
	if GOARCH == "wasm" && n > 1 {
		n = 1 // WebAssembly has no threads yet, so only one CPU is possible.
	}

	lock(&sched.lock)
	ret := int(gomaxprocs)
	unlock(&sched.lock)
	if n <= 0 || n == ret {
		return ret
	}

	stopTheWorldGC("GOMAXPROCS")

	// newprocs will be processed by startTheWorld
	newprocs = int32(n)

	startTheWorldGC()
	return ret
}

Channel 通道

go中channel是协程之间的通信机制,一个channel是一个通信管道,它可以让协程通过它给另一个协程发送数据. 每个channel都需要指定数据类型, 如需要发送 int 则可以定义一个 chan int;

传统的线程之间通过共享内存进行数据交互,不同的线程共享内存的同步问题使用锁来解决,但是在go中,数据的传递使用channel来实现;

type Animal struct {
	name   string
	age    int
	weight int
}
type Dog struct {
	Animal //匿名结构体
}

func channelT() {
	//声明channel
	var ch chan int
	var c chan interface{}
	var d chan Dog
	fmt.Printf("%v \n  %v\n %v\n", ch, c, d)
	//chan类型的空值是nil,声明后需要配合make()才能使用。
	//channel是引用类型,需要使用make()进行创建
	ch = make(chan int, 1024)
	c = make(chan interface{}, 1024)
	d = make(chan Dog)
	fmt.Println("ch的size", len(ch))
	fmt.Println("c的size", len(c))
	fmt.Println("dogs的size", len(d))
}

func main() {
	channelT()
}

使用channel发送数据

通过channel发送数据需要使用 特殊的操作符 <-
channel发送的值的类型必须与channel的元素类型一致
如果接收方一直没有接收,那么发送操作将持续阻塞。此时所有的Goroutine,包括main()的Goroutine都处于等待状态

运行会提示报错:fatal error: all goroutines are asleep - deadlock!。

死锁的产生

使用channel时要考虑死锁的可能性,即一个Goroutine在channel上发送数据,但是没有人接收,那么就会出现死锁;或者一个Goroutine正在等待从channel接收数据,但是其他的Goroutine要在channel上写入数据,如果没有写入,程序也会死锁;

channel接收数据

channel收发数据在不同的两个Goroutine间进行;

// 阻塞接收数据
func main() {
	//搞一个channel
	ch := make(chan string)
	go sendData(ch) //开启协程往通道中发送数据

	//data接收channel中的数据
	//如果通道关闭,那么通道中传输的数据为个数据类型的默认值;
	for {
		data := <-ch    //接收数据,执行该语句时channel将会阻塞,直到接收到数据并赋值给data变量。
		if data == "" { //
			break
		}
		fmt.Println("从通道中取出的数据1--->>", data)
	}
	//
	for {
	//data 表示接收到的数据。未接收到数据时,data为channel类型的零值。ok(布尔类型)表示是否接收到数据。通过ok值可以判断当前channel是否被关闭。
		data, ok := <-ch
		fmt.Println(ok)
		if !ok {
			break
		}
		fmt.Println("从通道中取出的数据2-->>", data)
	}
	for i2 := range ch {
		fmt.Println("取出的数据3-->>", i2)
	}

}

func sendData(ch chan string) {

	for i := 0; i < 10; i++ {
		ch <- fmt.Sprintf("发送数据%d \n", i)
	}
	fmt.Println("send exit")
	//关闭通道
	defer close(ch)
}

以上代码是sendData的协程 与main的协程之间的数据交互;

总结就是先声明一个通道,然后 一个函数的入参是一个通道,该函数用于向通道中发送数据,然后另一个协程来取出数据;

for … range会自动判断出channel已关闭,而无须通过判断来终止循环

如果要取的数据少了,要取的多了,而且通道没有关闭—>>

func main(){
ch := make(chan string)
	go sendData(ch) //开启协程往通道中发送数据
for i := 0; i < 20; i++ {
		data := <-ch    //接收数据
		if data == "" { //
			break
		}
		fmt.Println("从通道中取出的数据1--->>", data)
	}

}
func sendData(ch chan string) {

	for i := 0; i < 10; i++ {
		ch <- fmt.Sprintf("发送数据%d \n", i)
	}
	fmt.Println("send exit")
	//关闭通道
	//close(ch)
	//defer close(ch)
}

发生了死锁;
在这里插入图片描述
如果最够关闭通道,就不会出现死锁;

阻塞

channel默认是阻塞的。当数据被发送到channel时会发生阻塞,直到有其他Goroutine从该channel中读取数据。当从channel读取数据时,读取也会被阻塞,直到其他Goroutine将数据写入该channel。

这就像java中的阻塞队列,不过go语言简化了,直接拿来用即可;

代码demo

func main() {
	var ch chan int
	ch = make(chan int)
	fmt.Printf("%T \n", ch)
	ch2 := make(chan bool)
	go func() {
		data, ok := <-ch
		if ok {
			fmt.Println("子goroutine取值", data)
		}
		ch2 <- true
	}()
	ch <- 10
	<-ch2 //   阻塞,防止代码不打印  抛弃 ---即取出后没有接收的
	defer close(ch)
	defer close(ch2)
	fmt.Println("main exit")
}

关闭channel:

发送方如果数据写入完毕,需要关闭channel,用于通知接收方数据传递完毕。

通常情况是发送方主动关闭channel。

接收方通过多重返回值判断channel是否关闭,如果返回值是false,则表示channel已经被关闭。往关闭的channel中写入数据会报错:panic: send on closed channel。但是可以从关闭后的channel中读取数据,返回数据的默认值和false。

	func main() {
	ch1 := make(chan int)
	go func() {
		//往channel中放入数据
		ch1 <- 1
		ch1 <- 2
		ch1 <- 3
		//关闭channel
		close(ch1)
		//在向通道中写入数据
		ch1 <- 4
	}()
		for i := 0; i < 4; i++ {
		data, ok := <-ch1
		if !ok {
			break
		}
		fmt.Println(data)
	}
}

运行结果:
在这里插入图片描述

向已经关闭的channel写入数据会导致程序崩溃。

缓冲channel

我们默认创建的channel不是用于缓冲的,即读写都是即时阻塞的; 缓冲channel自带缓冲区,如果缓冲区满了才会发生阻塞;

缓冲channel的创建


func main() {
	//channel
	ch1 := make(chan int)
	//往channel中放入数据
	go sendD(ch1)
	//再放入数据
	ch1 <- 100
}

func sendD(ch chan int) {
	for i := 0; i < 2; i++ {
		//放入数据
		ch <- i
	}
}

运行结果—发生了死锁
在这里插入图片描述

如果我们使用 缓冲channel

func main() {
	//channel
	ch1 := make(chan int, 16)
	//往channel中放入数据
	go sendD(ch1)
	//再放入数据
	ch1 <- 100

	for i := 0; i < 3; i++ {
		data, ok := <-ch1
		if !ok {
			break
		}
		fmt.Println(ok, "---", data)
	}
	 
}

func sendD(ch chan int) {
	for i := 0; i < 2; i++ {
		//放入数据
		ch <- i
	}
}

运行结果:
在这里插入图片描述

根据缓冲channel,我们就可以模拟生产者消费者问题了:


// 生产者生产蛋糕放入channel
func produce(c chan int) {
	for i := 0; i < 10; i++ {
		//放入蛋糕
		c <- i
		fmt.Println(i, "号蛋糕已放入")
		//放入需要时间的
		time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
	}
	//关闭通道
	defer close(c)
}

// 消费者消费蛋糕
// 消费的对象 单号编码 ,消费成功/失败的channel
func consumer(num int, ch chan int, b chan bool) {
	for i2 := range ch {
		fmt.Println("协程代号", num)
		fmt.Println("消费的蛋糕号码--->>", i2)
		time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
	}
	//记录消费结果
	b <- true
	defer close(b)
}

func main() {
	con := make(chan int)
	bool1 := make(chan bool)
	bool2 := make(chan bool)
	bool3 := make(chan bool)
	//生产者
	go produce(con)
	//3个消费   同时将消费结果通过通道传输
	go consumer(1, con, bool1)
	go consumer(2, con, bool2)
	go consumer(3, con, bool3)

	//取出消费情况是否成功
	for i := range bool1 {
		fmt.Println(i)
	}
	for i := range bool2 {
		fmt.Println(i)
	}
	for i := range bool3 {
		fmt.Println(i)
	}
	defer fmt.Println("main exit")

}

运行结果:
在这里插入图片描述

单向channel

我们声明的channel 如果没有特别的指定,默认是可读可写的;

定向channel也叫单向channel,即要么只读,要么只可以写;

声明只读channel

直接声明单向channel是没有意义的,通常我们需要声明一个双向channel,然后通过单向channel的方式进行函数传递;

//往通道中放数据

func normalChannel(c chan string) {
	c <- "Go语言"
	//c <- "GoWeb"
	//c <- "GoLang"
	data1 := <-c
	data2 := <-c
	fmt.Println("normal--->>", data1, data2)
}

//只读的channel

func ReadChan(c <-chan string) {
	data := <-c
	fmt.Println("只读的数据-->>", data)
	//此时我们如果往channel中放数据
	//c<-"话只说了一百遍"   //编译检查不会通过-->>Invalid operation: c<-"话只说了一百遍" (send to the receive-only type <-chan string)
}
func WriteChan(c chan<- string) {
	c <- "只能写入呀!!"

	//此时我们要读的话
	//data:=<-c  //编译检查不通过   Invalid operation: <-c (receive from the send-only type chan<- string)
	fmt.Printf("%T\n", c)
}
func main() {
	//read := make(<-chan int)//只读channel
	//write := make(chan<- int)//只写channel
	c := make(chan string)
	go normalChannel(c) //往channel中放数据
	data := <-c         //取数据 --此时去走的是剩下的那个数据
	fmt.Println("取出的数据", data)
	c <- "放入数据1"
	c <- "放入数据2"
	go WriteChan(c)
	go ReadChan(c)
	time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) //纯粹是为了复习
	fmt.Println("main exit")
}

在这里插入图片描述

上述代码的运行逻辑:
1,创建了一个通道,
2,通过 normalChannel协程往通道中放入数据
3,main的协程取出数据
4,通道中再放入数据
5.通道写入数据
6.读取通道数据
7.main exit

小结—>>
只读通道声明方式:

read := make(<-chan int)//只读channel

只写通道声明方式

write := make(chan<- int)//只写channel

time包中跟channel相关的API

我们来看一下Timer结构体:

// The Timer type represents a single event.
// When the Timer expires, the current time will be sent on C,
// unless the Timer was created by AfterFunc.
// A Timer must be created with NewTimer or AfterFunc.
//即计时器到期后后当前时间将会发送到channel
type Timer struct {
	C <-chan Time  //只读channel
	r runtimeTimer
}

计时器必须使用NewTimer()或After()创建。


func timeChan() {
	now := time.Now()
	fmt.Println(now)
	timer := time.NewTimer(time.Second)
	fmt.Printf("%T\n", timer)
	data := <-timer.C //从Timer的通道中取出数据
	fmt.Println(data)
}

func main() {
	timeChan()
}

在这里插入图片描述

newTimer()源码:

// NewTimer creates a new Timer that will send
// the current time on its channel after at least duration d.
func NewTimer(d Duration) *Timer {
	c := make(chan Time, 1)
	t := &Timer{
		C: c,
		r: runtimeTimer{
			when: when(d),
			f:    sendTime,
			arg:  c,
		},
	}
	startTimer(&t.r)
	return t
}

Afetr()源码

// After waits for the duration to elapse and then sends the current time
// on the returned channel.
// It is equivalent to NewTimer(d).C.
// The underlying Timer is not recovered by the garbage collector
// until the timer fires. If efficiency is a concern, use NewTimer
// instead and call Timer.Stop if the timer is no longer needed.
func After(d Duration) <-chan Time {
	return NewTimer(d).C
}

可以看到After返回一个只读的channel

func timeAfter() {
	now := time.Now()
	fmt.Println("前输出", now)
	after := time.After(time.Second * 2) //返回一个只读通道
//取出数据
	fmt.Println("后输出", <-after)
}
func main() {
	//timeChan()
	timeAfter()
}

在这里插入图片描述

selelct 语句

select 语句会随机挑选一个可通信的case执行,如果所有case没有到达的数据,那么会执行default,若没有default,那么就会阻塞,直到case接收到数据;


func selectChannel() {

	c := make(chan int)
	ch := make(chan int)
	go func() {
		time.Sleep(time.Duration(rand.Intn(10)) * time.Second)
		c <- 100
	}()
	go func() {
		time.Sleep(time.Duration(rand.Intn(20)) * time.Second)
		ch <- 100
	}()

	select {
	case data := <-c:
		fmt.Println("c中读到了数据", data)
	case data2 := <-ch:
		fmt.Println("c中读到了数据", data2)
	default:
		defer fmt.Println("有default执行default")
	}
	defer close(c)
	defer close(ch)
}

上述代码大部分时间是执行default的;原因在前面提到过,不再赘述;

我们修改一下上面的代码:

func selectChannel() {

	c := make(chan int)
	ch := make(chan int)
	go func() {
		time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond)
		c <- 100
	}()
	go func() {
		time.Sleep(time.Duration(rand.Intn(5)) * time.Millisecond)
		ch <- 100
	}()

	select {
	case data := <-c:
		fmt.Println("c中读到了数据", data)
	case data2 := <-ch:
		fmt.Println("ch中读到了数据", data2)
	case <-time.After(2 * time.Millisecond):
		fmt.Println("阻塞发生了")
		//default:
		//	time.Sleep(time.Duration(rand.Intn(20)) * time.Second)
		//	defer fmt.Println("有default执行default")
	}
	defer close(c)
	defer close(ch)
	fmt.Println("main exit")
}

当我们注释掉default,那么结果可能如下:
在这里插入图片描述
在这里插入图片描述

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

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

相关文章

腾讯云服务器常用端口号大全以及端口开启方法

腾讯云服务器常用端口号如80、21、22、8080等端口&#xff0c;出于安全考虑一些常用端口默认是关闭的&#xff0c;腾讯云服务器端口如何打开呢&#xff1f;云服务器CVM在安全组中开启端口&#xff0c;轻量应用服务器在防火墙中可以打开端口&#xff0c;腾讯云百科来详细说下腾讯…

在云服务器上部署简单的聊天机器人网站(源1.0接口版)

诸神缄默不语-个人CSDN博文目录 又不是不能用.jpg http://47.113.197.198:8000/chat 集成代码可参考&#xff1a;花月与剑/scholar_ease 之所以先用源1.0&#xff0c;一是因为我API都申请了&#xff0c;不用白不用&#xff1b;二是因为源1.0可以直接用国内网络连接&#xf…

Vue登录界面精美模板分享

文章目录 &#x1f412;个人主页&#x1f3c5;Vue项目常用组件模板仓库&#x1f4d6;前言&#xff1a;&#x1f380;源码如下&#xff1a; &#x1f412;个人主页 &#x1f3c5;Vue项目常用组件模板仓库 &#x1f4d6;前言&#xff1a; 本篇博客主要提供vue组件之登陆组件源码…

idea连接Linux服务器

一、 介绍 配置idea的ssh会话和sftp可以实现对linux远程服务器的访问和文件上传下载&#xff0c;是替代Xshell的理想方式。这样我们就能在idea里面编写文件并轻松的将文件上传到linux服务器中。而且还能远程编辑linux服务器上的文件。掌握并熟练使用&#xff0c;能够大大提高我…

操作系统复习2.4.0-死锁详解

什么是死锁 各进程互相竞争对手里的资源&#xff0c;导致各进程都阻塞&#xff0c;都无法向前推进 死锁、饥饿、死循环的区别 死锁&#xff1a;各进程互相持有对方想要的资源且不释放&#xff0c;导致各进程阻塞&#xff0c;无法向前推进 饥饿&#xff1a;由于长期得不到想要…

四站精彩回顾 | Fortinet Accelerate 2023·中国区巡展火热进行中

Fortinet Accelerate 2023中国区巡展 上周&#xff0c;Fortinet Accelerate 2023中国区巡展分别走过青岛、南京、长沙、合肥四站&#xff0c;Fortinet携手太平洋电信、亚马逊云科技、中企通信等云、网、安合作伙伴&#xff0c;与各行业典型代表客户&#xff0c;就网安融合、网…

电动葫芦无法运转怎么办?

有关电动葫芦无法起动与运转故障&#xff0c;电动葫芦无法起动怎么办&#xff0c;有没有好的解决办法&#xff0c;检查电源熔丝是否烧断&#xff0c;定子绕组相间短路、接地或断路&#xff0c;以及是否负载过大或传动机械故障等。 电动葫芦无法运转故障怎么办 1、首先&#xf…

C语言基础习题讲解

C语言基础习题讲解 运算符判断简单循环 运算符 1. 设计一个程序, 输入三位数a, 分别输出个,十,百位. (0<a<1000) 样例输入: 251 样例输出: 2 5 1 #include <stdio.h> int main() {int input 0;int x 0;int y 0;int z 0;scanf("%d", &input);x …

7 种常见的路由协议

网络路由是网络通信的重要组成部分&#xff0c;通过互联网将信息从源地址移动到目的地的过程。路由发生在 OSI 模型的第 3 层&#xff08;网络层&#xff09;。实际网络中通常会将静态和动态路由结合使用。静态路由适用于小型网络&#xff0c;而动态路由适用于大型网络。 什么…

界面控件DevExpress ASP.NET新主题——Office 365暗黑主题的应用

DevExpress ASP.NET Web Forms Controls拥有针对Web表单&#xff08;包括报表&#xff09;的110种UI控件&#xff0c;DevExpress ASP.NET MVC Extensions是服务器端MVC扩展或客户端控件&#xff0c;由轻量级JavaScript小部件提供支持的70个高性能DevExpress ASP.NET Core Contr…

华为路由器 IPSec VPN 配置

需求&#xff1a; 通过 IPSecVPN 实现上海与成都内网互通 拓扑图如下&#xff1a; 一、首先完成网络配置 1、R1 路由器设置 <Huawei>sys [Huawei]sys R1 [R1]un in en# 开启DHCP [R1]dhcp enable# 设置内网接口 [R1]int g0/0/0 [R1-GigabitEthernet0/0/0]ip addr 10.…

Git日常使用技巧 - 笔记

Git日常使用技巧 - 笔记 Git是目前世界上最先进的分布式版本控制系统 学习资料 廖雪峰 学习视频 https://www.bilibili.com/video/BV1pX4y1S7Dq/?spm_id_from333.337.search-card.all.click&vd_source2ac127043ccd79c92d5b966fd4a54cd7 Git 命令在线练习工具 https://l…

国内可用 ChatGPT 网页版

前言 ChatGPT持续火热&#xff0c;然鹅国内很多人还是不会使用。 2023年6月1日消息&#xff0c;ChatGPT 聊天机器人可以根据用户的输入生成各种各样的文本&#xff0c;包括代码。但是&#xff0c;加拿大魁北克大学的四位研究人员发现&#xff0c;ChatGPT 生成的代码往往存在严…

项目开发-依赖倒置、里式替换、接口隔离的应用深入理解

文章目录 前言依赖倒置定义不符合依赖倒置原则是什么样子&#x1f604;完善 里式替换定义具体应用 接口隔离定义具体应用 前言 最近在做.net项目和学习这个设计模式中的依赖倒置和工厂方法&#xff0c;这个过程当中发现在开发这个.net项目中有很多不合理的地方&#xff0c;就是…

cplex基础入门(一)

这边文章会以纯新手小白的视角&#xff0c;教会大家如何快速的搭建自己的cplex模型&#xff0c;做到求解模型不求人。 目录 一、引言 1、掌握数据类型及数据结构 2、常规Cplex编程方法 3、Cplex编程步骤 4、cplex 程序框架 5、创建模型 二、规划建模的入门求解案例 1、…

Python3数据分析与挖掘建模(3)探索性数据分析

1. 概述 探索性数据分析&#xff08;Exploratory Data Analysis&#xff0c;EDA&#xff09;是一种数据分析的方法&#xff0c;用于探索和理解数据集的特征、关系和分布等。EDA旨在揭示数据中的模式、异常值、缺失值等信息&#xff0c;并为后续的分析和建模提供基础。以下是关…

CISCN 2023 初赛 pwn——Shellwego 题解

这是一个用go语言写的elf程序&#xff0c;没有PIE。这也是本蒟蒻第一次解go pwn题&#xff0c;故在此记录以便参考。 而且&#xff0c;这还是一个全部符号表被抠的go elf&#xff0c;直接面对一堆不知名的函数实在有些应付不来&#xff0c;因此在比赛时委托逆向的队友把符号表…

​【指针与数组的恩怨情仇】

指针和数组的关系 指针指的是指针变量&#xff0c;不是数组&#xff0c;指针变量的大小是4/8个字节&#xff0c;是专门来存放地址的。数组也不是指针&#xff0c;数组是一块连续的空间&#xff0c;存放一组相同类型的数据的。 没有关系&#xff0c;但是它们之间有比较相似的地方…

【Netty】一行简单的writeAndFlush都做了哪些事(十八)

文章目录 前言一、源码分析1.1 ctx.writeAndFlush 的逻辑1.2 writeAndFlush 源码1.3 ChannelOutBoundBuff 类1.4 addMessage 方法1.5 addFlush 方法1.6 AbstractNioByteChannel 类 总结 前言 回顾Netty系列文章&#xff1a; Netty 概述&#xff08;一&#xff09;Netty 架构设…

好用的Chrome浏览器插件推荐(不定期更新)

好用的Chrome浏览器插件推荐 1.1 CSDN-浏览器助手1.2 Google 翻译1.3 JSON Viewer1.4 ModHeader - Modify HTTP headers1.5 Octotree - GitHub code tree 1.1 CSDN-浏览器助手 CSDN-浏览器助手 是一款集成本地书签、历史记录与 CSDN搜索(so.csdn.net) 的搜索工具 推荐&#x…