go进阶(2) -深入理解Channel实现原理

news2024/11/18 19:58:03

Go的并发模型已经在https://guisu.blog.csdn.net/article/details/129107148 详细说明。

 1、channel使用详解


 1、channel概述

Go的CSP并发模型,是通过goroutinechannel来实现的。

  • channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。
  • Go并发的核心哲学是不要通过共享内存进行通信; 相反,通过沟通分享记忆。

      channel是Go提供goroutine间的通信方式,使用channel可以使多个goroutine之间通信。channel是进程内的通信方式,通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。

如需跨进程通信,Go建议用分布式系统的方法来解决,如使用Socket或者HTTP等通信协议,Go语言在网络方面也有非常完善的支持。

主要应用场景:

  • 数据交流:当作并发的 buffer 或者 queue,解决生产者 - 消费者问题。多个 goroutine 可以并发当作生产者(Producer)和消费者(Consumer)。
  • 数据传递:一个goroutine将数据交给另一个goroutine,相当于把数据的拥有权托付出去。
  • 信号通知:一个goroutine可以将信号(closing,closed,data ready等)传递给另一个或者另一组goroutine。
  • 任务编排:可以让一组goroutine按照一定的顺序并发或者串行的执行,这就是编排功能。
  • 锁机制:利用channel实现互斥机制。

 2、channel基本语法

每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。
声明通道:var 通道变量 chan 通道类型:var channame chan ElementType
创建通道:make(chan 数据类型, [缓冲大小]):

channel跟map类似的在使用之前都需要使用make进行初始化
ch1 := make(chan int, 5) 

 未初始化的channel零值默认为nil,是一种特殊的 chan,对值是 nil 的 chan 的发送接收调用者总是会阻塞。

var ch chan int
fmt.Println(ch) // <nil>

channel的基本用法非常简单,它提供了三种类型,分别为只能接收,只能发送,既能接收也能发送这三种类型:
var channame chan <- ElementType //只写:只能发送ElementType
var channame <- chan ElementType //只读:只能从chan里接收ElementType

var channame chan ElementType //能读能写:既能接收也能发送

我们把既能发送也能接收的chan被称为双向chan,把只能接收或者只能发送的chan称为单向

而对于close方法只能是发送通道拥有。

箭头总是射向左边的,元素类型总在最右边。

如果箭头指向 chan,就表示可以往 chan 中发送(写)数据;

如果箭头远离 chan,就表示 chan 会往外吐数据,即能从chan里接收(读)数据

channel <- 1 //向channel添加一个值为1
<- channel //从channel取出一个值
a := <- channel //从channel取出一个值并赋值给a
a,b := <- channel //从channel取出一个值赋值给a,如果channel已经关闭或channel没有值,b为false

3、通信机制:

  • 成对出现:在通信过程中,传数据channel <- data和取数据<-channel必然会成对出现,因为这边传,那边取,两个goroutine之间才会实现通信。
  • 阻塞:不管传还是取,必阻塞,直到另外的goroutine传或者取为止。
  • channel仅允许被一个goroutine读写。

1)只能接收数据的chan

package main 

import "fmt"

// a 表示只能接收数据的chan
func goChanA(a <-chan int) {
  b := <-a
  fmt.Println("只能接收数据的channal[a]接收到的数据值为", b)
}

func main() {
  ch := make(chan int, 2)
  go goChanA(ch)
  // 往ch中写入数据值
  ch <- 2
  time.Sleep(time.Second)
}

结果:只能接收数据的channal[a]接收到的数据值为 2

2)只能发送数据的chan

package main 

import "fmt

func main() {
  ch := make(chan<- int, 2)
  ch <- 200
}

chan 中发送一个数据使用“ch<-”。
这里的 chchan int 类型或者是 chan <-int

3)同步,主协程和子协程之间通信:

func main(){
    ch := make(chan int)
    go func() {
        ch <- 996 //向ch添加元素
    }()
    a := <- ch
    fmt.Println(a)
    fmt.Println("程序结束!")
}

4)、两个子协程的通信

使用channel实现两个goroutine之间通信。

func two() {
    tc := make(chan string)
    ch := make(chan int)
    // 第一个协程
    go func() {
        tc <- "协程A,我在添加数据"
        ch <- 1
    }()
    // 第二个协程
    go func() {
        content := <- tc
        fmt.Printf("协程B,我在读取数据:%s\n",content)
        ch <- 2
    }()
    <- ch
    <- ch
    fmt.Println("程序结素!")
}
func main(){
    two()
}

5)、channel仅允许被一个goroutine读写。

package main
import (
    "fmt"
    "time"
)
func goRoutineA(a <-chan int) {
    val := <-a
    fmt.Println("goRoutineA received the data", val)
}
func goRoutineB(b chan int) {
    val := <-b
    fmt.Println("goRoutineB  received the data", val)
}
func main() {
    ch := make(chan int, 3)
    go goRoutineA(ch)
    go goRoutineB(ch)
    ch <- 3
    time.Sleep(time.Second * 1)
}

6)、一直阻塞的情况

  • 如果当前协程正在从一个没有任何值的通道中读取数据,那么当前协程会阻塞并且等待其他协程往此通道写入值。
  • 因此,读操作将被阻塞。类似的,如果你发送数据到一个通道,它将阻塞当前协程直到有其他协程从通道中读取数据。此时写操作将阻塞 。

主线程在进行通道操作的时候造成死锁:

package main

import "fmt"

func main() {
	fmt.Println("mainGo start")
	
	channel := make(chan string)
	
   // 给通道 channel 传入一个数据GoLang.
	channel <- "GoLang"
	//此时主线程将阻塞直到有协程接收这个数据. Go的调度器开始调度协程接收通道 channel 的数据
	// 但是由于没有协程接受,没有协程是可被调度的。所有协程都进入休眠状态,即是主程序阻塞了。

	fmt.Println("mainGo stop")
}

/*
报错
mainGo go start
fatal error: all goroutines are asleep - deadlock!  //所有协程都进入休眠状态,死锁

goroutine 1 [chan send]:
main.main()
*/

4、channel缓冲区

无缓冲通道,make(chan int),指在接收前没有能力保存任何值的通道,这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。
有缓冲通道,make(chan int, 2),指在被接收前能存储一个或者多个值的通道,这种类型的通道并不强制要求goroutine之间必须同时完成发送和接收。

 例子:

package main

import "fmt"

func main() {
	ch1 := make(chan int)
	ch1 <- 5
	rec := <-ch1
	fmt.Println("ch1被接受,程序结束:rec:,", rec)
}
//fatal error: all goroutines are asleep - deadlock!          

由于ch1没有缓冲区,channel没有缓冲区的话:

只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。同理,如果对一个无缓冲通道执行接收操作时,没有任何向通道中发送值的操作那么也会导致接收操作阻塞。

如果想要运行成功那么在发送信息前就应该有另外的协程等待着接收

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	go receive(ch1)
	ch1 <- 5
	time.Sleep(time.Second)

}
func receive(ch1 chan int) {
	for {
		select {
		case rec2 := <-ch1:
			fmt.Println("ch1被接受,程序结束:rec:,", rec2)
		}
	}
}
//ch1被接受,程序结束:rec:, 5  

 但是如果有缓冲区就能避免程序阻塞,可以将发送的channel放在缓冲区直至有接收方将它接收

向channel添加数据超过缓存,会出现死锁:

func main() {
	ch := make(chan int,3)
	ch <- 1
	//<- ch
	ch <- 1
	ch <- 1
	ch <- 1
	fmt.Println("ok")
}

  -----

5、 阻塞的 gorutinue 与资源泄露

在 2012 年 Google I/O 大会上,Rob Pike 的 Go Concurrency Patterns 演讲讨论 Go 的几种基本并发模式,如 完整代码 中从数据集中获取第一条数据的函数:

    func First(query string, replicas []Search) Result {
        c := make(chan Result)
        replicaSearch := func(i int) { c <- replicas[i](query) }
        for i := range replicas {
            go replicaSearch(i)
        }
        return <-c
    }

在搜索重复时依旧每次都起一个 goroutine 去处理,每个 goroutine 都把它的搜索结果发送到结果 channel 中,channel 中收到的第一条数据会直接返回。

返回完第一条数据后,其他 goroutine 的搜索结果怎么处理?他们自己的协程如何处理?

在 First() 中的结果 channel 是无缓冲的,这意味着只有第一个 goroutine 能返回,由于没有 receiver,其他的 goroutine 会在发送上一直阻塞。如果你大量调用,则可能造成资源泄露。

为避免泄露,你应该确保所有的 goroutine 都能正确退出,有 2 个解决方法:

    使用带缓冲的 channel,确保能接收全部 goroutine 的返回结果:

   

func First(query string, replicas ...Search) Result {  
        c := make(chan Result,len(replicas))    
        searchReplica := func(i int) { c <- replicas[i](query) }
        for i := range replicas {
            go searchReplica(i)
        }
        return <-c
    }

使用 select 语句,配合能保存一个缓冲值的 channel default 语句:

default 的缓冲 channel 保证了即使结果 channel 收不到数据,也不会阻塞 goroutine

    func First(query string, replicas ...Search) Result {  
        c := make(chan Result,1)
        searchReplica := func(i int) {
            select {
            case c <- replicas[i](query):
            default:
            }
        }
        for i := range replicas {
            go searchReplica(i)
        }
        return <-c
    }

    使用特殊的废弃(cancellation) channel 来中断剩余 goroutine 的执行:

   

func First(query string, replicas ...Search) Result {  
        c := make(chan Result)
        done := make(chan struct{})
        defer close(done)
        searchReplica := func(i int) {
            select {
            case c <- replicas[i](query):
            case <- done:
            }
        }
        for i := range replicas {
            go searchReplica(i)
        }
     
     
        return <-c
    }

Rob Pike 为了简化演示,没有提及演讲代码中存在的这些问题。不过对于新手来说,可能会不加思考直接使用。

 二、使用Select来进行调度


select就是用来监听和channel有关的IO操作,当 IO 操作发生时,触发相应的动作。
 

Select 和 swith结构很像,但是select中的case的条件只能是I/O。

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

具体格式如下:

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

select里面case是随机执行的,如果都不满足条件,那么就执行default

select总结:

  • 每个case必须是一个I/O操作
  • case是随机执行的:如果多个 case 同时满足,select 会随机选择一个执行。
  • 如果所有case不能执行,那么会执行default
  • 如果所有case不能执行,且没有default,会出现阻塞
  • 对于没有 case 的 select 会一直阻塞,可用于阻塞 main 函数,防止退出

实现一个一直接收消息:

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

示例2:
 

package main

import (
   "fmt"
   "time"
)

func goRoutineD(ch chan int, i int) {
   time.Sleep(time.Second * 3)
   ch <- i
}
func goRoutineE(chs chan string, i string) {
   time.Sleep(time.Second * 3)
   chs <- i

}

func main() {
   ch := make(chan int, 5)
   chs := make(chan string, 5)

   go goRoutineD(ch, 5)
   go goRoutineE(chs, "ok")

	select {
	case msg := <-ch:
		fmt.Println(" received the data ", msg)
	case msgs := <-chs:
		fmt.Println(" received the data ", msgs)
	default:
		fmt.Println("no data received ")
		time.Sleep(time.Second * 1)
	}


}

运行程序,因为当前时间没有到3s,所以select 选择defult

no data received

修改程序,我们注释掉default,并多执行几次结果为

received the data 5

received the data ok

received the data ok

received the data ok

select语句会阻塞,直到监测到一个可以执行的IO操作为止,而这里goRoutineD和goRoutineE睡眠时间是相同的,都是3s,从输出可看出,从channel中读出数据的顺序是随机的。

再修改代码,goRoutineD睡眠时间改成4s

func goRoutineD(ch chan int, i int) {
   time.Sleep(time.Second * 4)
   ch <- i
}

此时会先执行goRoutineE,select 选择case msgs := <-chs。
 

三、死锁(deadlock)


指两个或两个以上的协程的执行过程中,由于竞争资源或由于彼此通信而造成的一种阻塞的现象。

在非缓冲信道若发生只流入不流出,或只流出不流入,就会发生死锁。

下面是一些死锁的例子

package main

func main() {
   ch := make(chan int)
   ch <- 3
}

上面情况,向非缓冲通道写数据会发生阻塞,导致死锁。解决办法创建缓冲区 ch := make(chan int,3)

package main
import (
   "fmt"
)

func main() {
   ch := make(chan int)
   fmt.Println(<-ch)
}

向非缓冲通道读取数据会发生阻塞,导致死锁。 解决办法开启缓冲区,先向channel写入数据。

package main

func main() {
   ch := make(chan int, 3)
   ch <- 3
   ch <- 4
   ch <- 5
   ch <- 6
}

写入数据超过缓冲区数量也会发生死锁。解决办法将写入数据取走。

死锁的情况有很多这里不再赘述。

还有一种情况,向关闭的channel写入数据,不会产生死锁,产生panic。

package main

func main() {
	ch := make(chan int, 3)
	ch <- 1
	close(ch)
	ch <- 2
}

解决办法别向关闭的channel写入数据。

四、channel实现原理 


1、channel数据结构

channel一个类型管道,通过它可以在goroutine之间发送消息和接收消息。它是golang在语言层面提供的goroutine间的通信方式。通过源代码分析程序执行过程,源码src/runtime/chan.go:

channel结构体hchan

type hchan struct {
  qcount uint          // 当前队列列中剩余元素个数
  dataqsiz uint        // 环形队列长度,即可以存放的元素个数即缓冲区的大小,即make(chan T,N),N.
  buf unsafe.Pointer   // 环形队列列指针
  elemsize uint16      // 每个元素的⼤⼩
  closed uint32        // 标识关闭状态:表示当前通道是否处于关闭状态。创建通道后,该字段设置为0,即通道打开; 通过调用close将其设置为1,通道关闭。
  elemtype *_type      // 元素类型:用于数据传递过程中的赋值;
  sendx uint           // 队列下标,指示元素写⼊入时存放到队列列中的位置 x
  recvx uint           // 队列下标,指示元素从队列列的该位置读出  
  recvq waitq          // 等待读消息的goroutine队列
  sendq  waitq         // 等待写消息的goroutine队列
  lock mutex           // 互斥锁,chan不允许并发读写
} 

type waitq struct {
	first *sudog
	last  *sudog
}

从数据结构可以看出channel由队列、类型信息、goroutine等待队列组成。

2、实现方式

创建channel 有两种,一种是带缓冲的channel,一种是不带缓冲的channel

// 带缓冲
ch := make(chan Task, 6)
// 不带缓冲
ch := make(chan int)

下图展示了可缓存6个元素的channel底层的数据模型如下图:

func makechan(t *chantype, size int) *hchan {

   elem := t.elem
   ...
}

  • dataqsiz:指向队列的长度为6,即可缓存6个元素
  • buf:指向队列的内存,队列中还剩余两个元素
  • qcount:当前队列中剩余的元素个数
  • sendx:指后续写入元素的位置
  • recvx:指从该位置读取数据

等待队列

从channel中读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞;向channel中写数据,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。

被阻塞的goroutine将会被挂在channel的等待队列中:

  • 因读阻塞的goroutine会被向channel写入数据的goroutine唤醒
  • 因写阻塞的goroutine会被从channel读数据的goroutine唤醒

下面展示了一个没有缓冲区的channel,有几个goroutine阻塞等待数据:

注意,一般情况下recvq和sendq至少有一个为空。只有一个例外,那就是同一个goroutine使用select语句向channel一边写数据一边读数据。

3、向channel写数据

ch := make(chan int, 3)

创建通道后的缓冲通道结构:

hchan struct {
	qcount uint : 0 
	dataqsiz uint : 3 
	buf unsafe.Pointer : 0xc00007e0e0 
	elemsize uint16 : 8 
	closed uint32 : 0 
	elemtype *runtime._type : &{
		size:8 
		ptrdata:0 
		hash:4149441018 
		tflag:7 
		align:8 
		fieldalign:8 
		kind:130 
		alg:0x55cdf0 
		gcdata:0x4d61b4 
		str:1055 
		ptrToThis:45152
		}
	sendx uint : 0 
	recvx uint : 0 
	recvq runtime.waitq : 
		{first:<nil> last:<nil>}
	sendq runtime.waitq : 
		{first:<nil> last:<nil>}
	lock runtime.mutex : 
		{key:0}
}
 

 写入数据:ch <- 3,底层hchan数据流程如图

 1、锁定整个通道结构。

2、确定写入:如果recvq队列不为空,说明缓冲区没有数据或者没有缓冲区,此时直接从recvq等待队列中取出一个G(goroutine),并把数据写入,最后把该G唤醒,结束发送过程;

3、如果recvq为Empty,则确定缓冲区是否可用。如果可用,从当前goroutine复制数据写入缓冲区,结束发送过程。

4、如果缓冲区已满,则要写入的元素将保存在当前正在执行的goroutine的结构中,并且当前goroutine将在sendq中排队并从运行时挂起(进入休眠,等待被读goroutine唤醒)。

5、写入完成释放锁。

这里我们要注意几个属性buf、sendx、lock的变化。

3、从channel读取操作

几乎和写入操作相同

func goRoutineA(a <-chan int) {
   val := <-a
   fmt.Println("goRoutineA received the data", val)
}

底层hchan数据流程如图:

1、先获取channel全局锁

2、如果等待发送队列sendq不为空(有等待的goroutine):

       1)若没有缓冲区,直接从sendq队列中取出G(goroutine),直接取出goroutine并读取数据,然后唤醒这个goroutine,结束读取释放锁,结束读取过程;

       2)若有缓冲区(说明此时缓冲区已满),从缓冲队列中首部读取数据,再从sendq等待发送队列中取出G,把G中的数据写入缓冲区buf队尾,结束读取释放锁;

3、如果等待发送队列sendq为空(没有等待的goroutine):

      1)若缓冲区有数据,直接读取缓冲区数据,结束读取释放锁。

      2)没有缓冲区或缓冲区为空,将当前的goroutine加入recvq排队,进入睡眠,等待被写goroutine唤醒。结束读取释放锁。

流程图:

ecvq和

recvq和sendq 结构

recvq和sendq基本上是链表,看起来基本如下:

 

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

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

相关文章

通用信息抽取技术UIE产业案例解析,Prompt 范式落地经验分享!

想了解用户的评价究竟是“真心夸赞”还是“阴阳怪气”&#xff1f;想快速从多角色多事件的繁杂信息中剥茧抽丝提取核心内容&#xff1f;想通过聚合相似事件准确地归纳出特征标签&#xff1f;……想了解UIE技术在产业中的实战落地经验&#xff1f;通用信息抽取技术 UIE 产业案例…

FPGA基础知识

FPGA是在PAL、PLA和CPLD等可编程器件的基础上进一步发展起来的一种更复杂的可编程逻辑器件。它是ASIC领域中的一种半定制电路&#xff0c;既解决了定制电路的不足&#xff0c;又克服了原有可编程器件门电路有限的缺点。 由于FPGA需要被反复烧写&#xff0c;它实现组合逻辑的基…

【强化学习】强化学习数学基础:贝尔曼公式

强化学习数学基础&#xff1a;贝尔曼公式强化学习的数学原理课程总览贝尔曼公式&#xff08;Bellman Equation&#xff09;一个示例状态值贝尔曼公式&#xff1a;推导过程贝尔曼公式&#xff1a;矩阵-向量形式&#xff08;Matrix-vector form&#xff09;贝尔曼公式&#xff1a…

(四)应变度量

本文主要内容包括&#xff1a;1. Hill 应变度量 与 Seth 应变度量2. Hill -Seth 应变度量的 Lagrange 描述2.1. Green-Lagrange 应变张量2.2. 物质 Biot 应变张量/工程应变2.3. 右 Henkey 应变张量/Lagrange 型对数应变2.4. Piola 应变张量3. Hill -Seth 应变度量的 Euler 描述…

Tesla都使用什么编程语言?

作者 | 初光 出品 | 车端 备注 | 转载请阅读文中版权声明 知圈 | 进“汽车电子与AutoSAR开发”群&#xff0c;请加微“cloud2sunshine” 总目录链接>> AutoSAR入门和实战系列总目录 带着对更美好未来的愿景&#xff0c;特斯拉不仅成为有史以来最有价值的汽车公司&…

乐友商城学习笔记(五)

什么是Nginx Nginx是一个高性能的web和反向代理服务器 作为web服务器作为负载均衡服务器作为邮件代理服务 树组件的用法 跨域问题 跨域&#xff1a;浏览器对javastript的同源策略的限制。 以下情况都属于跨域&#xff1a; 域名不同域名相同&#xff0c;端口不同二级域名不…

Python每日一练(20230225)

目录 1. 整数反转 2. 求最大公约数和最小公倍数 最大公约数 最小公倍数 3. 单词搜索 II 附录&#xff1a; DFS 深度优先搜索算法 BFS 广度优先搜索算法 BFS 和 DFS 的区别 1. 整数反转 给你一个 32 位的有符号整数 x &#xff0c;返回将 x 中的数字部分反转后的结果。…

大型旋转设备滑动轴承X、Y测点振动值说明(转载的)

滑动轴承支撑的大型旋转设备&#xff0c;绝大部分的故障都表现为不平衡引起的1倍频振动&#xff0c;诊断故障原因要根据振动随转速、负荷、温度、时间的变化情况来具体判断。滑动轴承设备的诊断主要依据电涡流传感器测量轴和轴瓦间的相对振动&#xff0c;判断转子相关的各种问题…

基于SpringBoot的共享汽车管理系统

文末获取源码 开发语言&#xff1a;Java 框架&#xff1a;springboot JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7/8.0 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;Maven3.3.9 浏…

Orin安装ssh、vnc教程

文章目录一&#xff1a;ssh远程终端的配置PC的配置MobaXterm的下载二&#xff1a;VNC Viewer远程图形界面终端配置&#xff1a;PC配置&#xff1a;一&#xff1a;ssh远程 终端的配置 1.ifconfig查看终端ip地址 其中的eth是网口&#xff0c;我们需要看的是wlan0下的inet&#…

5M1270ZT144A5N CPLD 980MC 6.2NS 144TQFP /5M1270ZT144C5N

【产品介绍】MAX V系列低成本和低功耗CPLD提供更大的密度和每占地面积的I/O。MAX V器件的密度从40到2210个逻辑元件(32到1700个等效宏单元)和多达271个I/O&#xff0c;为I/O扩展、总线和协议桥接、电源监控和控制、FPGA配置和模拟IC接口等应用提供可编程解决方案。MAX V器件具有…

MYSQL 索引失效的十个场景(二)

六、对索引列运算&#xff08;如&#xff0c;、-、*、/、%等&#xff09;&#xff0c;索引失效 CREATE TABLE student (id bigint(20) NOT NULL AUTO_INCREMENT,name varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL,score decimal(10,2) DEFAULT NULL,subject varchar(…

【华为OD机试模拟题】用 C++ 实现 - 绘图机器(2023.Q1)

最近更新的博客 华为OD机试 - 入栈出栈(C++) | 附带编码思路 【2023】 华为OD机试 - 箱子之形摆放(C++) | 附带编码思路 【2023】 华为OD机试 - 简易内存池 2(C++) | 附带编码思路 【2023】 华为OD机试 - 第 N 个排列(C++) | 附带编码思路 【2023】 华为OD机试 - 考古…

【Leedcode】环形链表必备的面试题和证明题(附图解)

环形链表必备的面试题和证明题&#xff08;附图解&#xff09; 文章目录环形链表必备的面试题和证明题&#xff08;附图解&#xff09;前言一、第一题1.题目2.思路3.代码4.延伸问题(1)证明题一&#xff1a;(2)证明题二&#xff1a;二、第二题1.题目2.思路延伸的证明题总结前言 …

【网络原理8】HTTP请求篇

在上一篇文章当中&#xff0c;我们也提到了什么是HTTP。 每一个HTTP请求&#xff0c;都会对应一个HTTP响应。 下面这一篇文章&#xff0c;将聊一下HTTP请求的一些内容 目录 一、URL 第一部分&#xff1a;协议名称 第二部分:认证信息(新的版本已经没有了) 第三部分&#xf…

这款 Python 调试神器推荐收藏

大家好&#xff0c;对于每个程序开发者来说&#xff0c;调试几乎是必备技能。 代码写到一半卡住了&#xff0c;不知道这个函数执行完的返回结果是怎样的&#xff1f;调试一下看看 代码运行到一半报错了&#xff0c;什么情况&#xff1f;怎么跟预期的不一样&#xff1f;调试一…

【教学类-10-03】python单线程下载哔哩哔哩网址(中间字母不同,前面后面相同)的视频

背景需求&#xff1a;最近测试以前的多线程&#xff08;同时下载5个视频&#xff09;&#xff0c;结果30个视频只下到了3个&#xff0c;于是把“单个下载&#xff08;单线程下载&#xff09;”的一个代码进行拓展研究。前一篇介绍了网址尾数递增的遍历程序&#xff0c;本篇介绍…

【华为OD机试模拟题】用 C++ 实现 - 最大报酬(2023.Q1)

最近更新的博客 华为OD机试 - 入栈出栈(C++) | 附带编码思路 【2023】 华为OD机试 - 箱子之形摆放(C++) | 附带编码思路 【2023】 华为OD机试 - 简易内存池 2(C++) | 附带编码思路 【2023】 华为OD机试 - 第 N 个排列(C++) | 附带编码思路 【2023】 华为OD机试 - 考古…

数据结构与算法之[把数字翻译成字符串]动态规划

前言&#xff1a;最近在刷动态规划的算法题目&#xff0c;感觉这一类题目还是有一点难度的&#xff0c;但是不放弃也还是能学好的&#xff0c;今天给大家分享的是牛客网中的编程题目[把数字翻译成字符串]&#xff0c;这是一道经典的面试题目&#xff0c;快手&#xff0c;字节跳…

CleanMyMac是什么清理软件?及使用教程

你知道CleanMyMac是什么吗&#xff1f;它的字面意思为“清理我的Mac”&#xff0c;作为软件&#xff0c;那就是一款Mac清理工具&#xff0c;Mac OS X 系统下知名系统清理软件&#xff0c;是数以万计的Mac用户的选择。它可以流畅地与系统性能相结合&#xff0c;只需简单的步骤就…