Go 语言精进之路——Go 中常见并发模式总结

news2025/1/31 8:08:49

文章目录

    • 前言
    • 创建模式
    • 退出模式
      • 分离模式
      • join 模式
      • notify-and-wait模式
      • 退出模式的应用
    • 管道模式
      • 扇出与扇入模式
    • 超时与取消模式

前言

在语言层面,Go针对CSP模型提供了三种并发原语。
goroutine:对应CSP模型中的P,封装了数据的处理逻辑,是Go运行时调度的基本执行单元。
channel:对应CSP模型中的输入/输出原语,用于goroutine之间的通信和同步。
select:用于应对多路输入/输出,可以让goroutine同时协调处理多个channel操作。
通过不同的组合方式,将产生多种不同的并发模式,灵活多变。

创建模式

在函数内部创建一个 goroutine,并返回一个 channel 类型变量的函数,这样调用方与被调用方通信就打通了。
image.png

  • 创建模式是应用场景最多的一种模式,很多并发模式都是基于创建模式实现的

退出模式

有一个常驻的后台服务程序可能会对 goroutine 有着优雅退出的要求,这个时候就需要使用退出模式,退出模式有多种模式。

分离模式

这里借鉴了一些线程模型中的术语,比如分离(detached)模式。分离模式是使用最为广泛的goroutine退出模式。

对于分离的goroutine,创建它的goroutine不需要关心它的退出,这类goroutine在启动后即与其创建者彻底分离,其生命周期与其执行的主函数相关,函数返回即goroutine退出。

这类goroutine有两个常见用途:

  1. 一次性任务:顾名思义,新创建的goroutine用来执行一个简单的任务,执行后即退出。比如下面标准库中的代码:
    image.png
  2. 常驻后台执行一些特定任务,如监视(monitor)、观察(watch)等。其实现通常采用for {…}或for { select{…} }代码段形式,并多以定时器(timer)或事件(event)驱动执行。
    image.png

join 模式

在线程模型中,父线程可以通过pthread_join来等待子线程结束并获取子线程的结束状态。
在Go中,我们有时候也有类似的需求:goroutine的创建者需要等待新goroutine结束。
为这样的goroutine退出模式起名为“join模式”

  1. 等待一个goroutine退出

    package main
    
    import "time"
    
    func worker(args ...interface{}) {
    	if len(args) == 0 {
    		return
    	}
    	interval, ok := args[0].(int)
    	if !ok {
    		return
    	}
    
    	time.Sleep(time.Second * (time.Duration(interval)))
    }
    
    func spawn(f func(args ...interface{}), args ...interface{}) chan struct{} {
    	c := make(chan struct{})
    	go func() {
    		f(args...)
    		c <- struct{}{}
    	}()
    	return c
    }
    
    func main() {
    	done := spawn(worker, 5)
    	println("spawn a worker goroutine")
    	<-done
    	println("worker done")
    }
    

    spawn函数使用典型的goroutine创建模式创建了一个goroutine,main goroutine作为创建者通过spawn函数返回的channel与新goroutine建立联系,这个channel的用途就是在两个goroutine之间建立退出事件的“信号”通信机制。
    main goroutine在创建完新goroutine后便在该channel上阻塞等待,直到新goroutine退出前向该channel发送了一个信号。

  2. 获取goroutine的退出状态

    如果新goroutine的创建者不仅要等待goroutine的退出,还要精准获取其结束状态,同样可以通过自定义类型的channel来实现这一场景需求。下面是基于上面的代码改造后的示例:

    package main
    
    import (
    	"errors"
    	"fmt"
    	"time"
    )
    
    var OK = errors.New("ok")
    
    func worker(args ...interface{}) error {
    	if len(args) == 0 {
    		return errors.New("invalid args")
    	}
    	interval, ok := args[0].(int)
    	if !ok {
    		return errors.New("invalid interval arg")
    	}
    
    	time.Sleep(time.Second * (time.Duration(interval)))
    	return OK
    }
    
    func spawn(f func(args ...interface{}) error, args ...interface{}) chan error {
    	c := make(chan error)
    	go func() {
    		c <- f(args...)
    	}()
    	return c
    }
    
    func main() {
    	done := spawn(worker, 5)
    	println("spawn worker1")
    	err := <-done
    	fmt.Println("worker1 done:", err)
    	done = spawn(worker)
    	println("spawn worker2")
    	err = <-done
    	fmt.Println("worker2 done:", err)
    }
    
  3. 等待多个goroutine退出
    通过Go语言提供的sync.WaitGroup实现等待多个goroutine退出的模式:

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    func worker(args ...interface{}) {
    	if len(args) == 0 {
    		return
    	}
    
    	interval, ok := args[0].(int)
    	if !ok {
    		return
    	}
    
    	time.Sleep(time.Second * (time.Duration(interval)))
    }
    
    func spawnGroup(n int, f func(args ...interface{}), args ...interface{}) chan struct{} {
    	c := make(chan struct{})
    	var wg sync.WaitGroup
    
    	for i := 0; i < n; i++ {
    		wg.Add(1)
    		go func(i int) {
    			name := fmt.Sprintf("worker-%d:", i)
    			f(args...)
    			println(name, "done")
    			wg.Done() // worker done!
    		}(i)
    	}
    
    	go func() {
    		wg.Wait()
    		c <- struct{}{}
    	}()
    
    	return c
    }
    
    func main() {
    	done := spawnGroup(5, worker, 3)
    	println("spawn a group of workers")
    	<-done
    	println("group workers done")
    }
    
  4. 支持超时机制的等待
    通过一个定时器(time.Timer)设置了超时等待时间,并通过select原语同时监听timer.C和done这两个channel,哪个先返回数据就执行哪个case分支:

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    func worker(args ...interface{}) {
    	if len(args) == 0 {
    		return
    	}
    
    	interval, ok := args[0].(int)
    	if !ok {
    		return
    	}
    
    	time.Sleep(time.Second * (time.Duration(interval)))
    }
    
    func spawnGroup(n int, f func(args ...interface{}), args ...interface{}) chan struct{} {
    	c := make(chan struct{})
    	var wg sync.WaitGroup
    
    	for i := 0; i < n; i++ {
    		wg.Add(1)
    		go func(i int) {
    			name := fmt.Sprintf("worker-%d:", i)
    			f(args...)
    			println(name, "done")
    			wg.Done() // worker done!
    		}(i)
    	}
    
    	go func() {
    		wg.Wait()
    		c <- struct{}{}
    	}()
    
    	return c
    }
    
    func main() {
    	done := spawnGroup(5, worker, 30)
    	println("spawn a group of workers")
    
    	timer := time.NewTimer(time.Second * 5)
    	defer timer.Stop()
    	select {
    	case <-timer.C:
    		println("wait group workers exit timeout!")
    	case <-done:
    		println("group workers done")
    	}
    }
    

notify-and-wait模式

在前面的几个场景中,goroutine的创建者都是在被动地等待着新goroutine的退出。
但很多时候,goroutine创建者需要主动通知那些新goroutine退出,尤其是当main goroutine作为创建者时。main goroutine退出意味着Go程序的终止,而粗暴地直接让main goroutine退出的方式可能会导致业务数据损坏、不完整或丢失。
我们可以通过notify-and-wait(通知并等待)模式来满足这一场景的要求。虽然这一模式也不能完全避免损失,但是它给了各个goroutine一个挽救数据的机会,从而尽可能减少损失。

  1. 通知并等待一个goroutine退出
    通过一个双向 channel,同时向 goroutine 发送退出退出信号,并接收 goroutine 的退出状态:

    package main
    
    import "time"
    
    func worker(j int) {
    	time.Sleep(time.Second * (time.Duration(j)))
    }
    
    func spawn(f func(int)) chan string {
    	quit := make(chan string)
    	go func() {
    		var job chan int // 模拟job channel
    		for {
    			select {
    			case j := <-job:
    				f(j)
    			case <-quit:
    				quit <- "ok"
    				return
    			}
    		}
    	}()
    	return quit
    }
    
    func main() {
    	quit := spawn(worker)
    	println("spawn a worker goroutine")
    
    	time.Sleep(5 * time.Second)
    
    	// notify the child goroutine to exit
    	println("notify the worker to exit...")
    	quit <- "exit"
    
    	timer := time.NewTimer(time.Second * 10)
    	defer timer.Stop()
    	select {
    	case status := <-quit:
    		println("worker done:", status)
    	case <-timer.C:
    		println("wait worker exit timeout")
    	}
    }
    
  2. 通知并等待多个goroutine退出

    Go语言的channel有一个特性是,当使用close函数关闭channel时,所有阻塞到该channel上的goroutine都会得到通知。我们就利用这一特性实现满足这一场景的模式:

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    )
    
    func worker(j int) {
    	time.Sleep(time.Second * (time.Duration(j)))
    }
    
    func spawnGroup(n int, f func(int)) chan struct{} {
    	quit := make(chan struct{})
    	job := make(chan int)
    	var wg sync.WaitGroup
    
    	for i := 0; i < n; i++ {
    		wg.Add(1)
    		go func(i int) {
    			defer wg.Done() // 保证wg.Done在goroutine退出前被执行
    			name := fmt.Sprintf("worker-%d:", i)
    			for {
    				j, ok := <-job
    				if !ok {
    					println(name, "done")
    					return
    				}
    				// do the job
    				worker(j)
    			}
    		}(i)
    	}
    
    	go func() {
    		<-quit
    		close(job) // 广播给所有新goroutine
    		wg.Wait()
    		quit <- struct{}{}
    	}()
    
    	return quit
    }
    
    func main() {
    	quit := spawnGroup(5, worker)
    	println("spawn a group of workers")
    
    	time.Sleep(5 * time.Second)
    	// notify the worker goroutine group to exit
    	println("notify the worker group to exit...")
    	quit <- struct{}{}
    
    	timer := time.NewTimer(time.Second * 5)
    	defer timer.Stop()
    	select {
    	case <-timer.C:
    		println("wait group workers exit timeout!")
    	case <-quit:
    		println("group workers done")
    	}
    }
    

    此时各个worker goroutine监听job channel,当创建者关闭job channel时,通过“comma ok”模式获取的ok值为false,也就表明该channel已经被关闭,于是worker goroutine执行退出逻辑(退出前wg.Done()被执行)。

退出模式的应用

很多时候,我们在程序中要启动多个goroutine协作完成应用的业务逻辑,比如:
image.png
但这些goroutine的运行形态很可能不同,有的扮演服务端,有的扮演客户端,等等,因此似乎很难用一种统一的框架全面管理它们的启动、运行和退出。我们尝试将问题范围缩小,聚焦在实现一个“超时等待退出”框架,以统一解决各种运行形态goroutine的优雅退出问题。
我们来定义一个接口:
image.png
这样,凡是实现了该接口的类型均可在程序退出时得到退出的通知和调用,从而有机会做退出前的最后清理工作。这里还提供了一个类似http.HandlerFunc的类型ShutdownerFunc,用于将普通函数转化为实现了GracefullyShutdowner接口的类型实例(得益于函数在Go中为“一等公民”的特质):
image.png
一组goroutine的退出

总体上有两种情况:

  • 一种是并发退出,在这类退出方式下,各个goroutine的退出先后次序对数据处理无影响,因此各个goroutine可以并发执行退出逻辑;
    image.png

    • 为每个传入的GracefullyShutdowner接口实现的实例启动一个goroutine来执行退出逻辑,并将timeout参数传入每个实例的Shutdown方法中;
    • 通过sync.WaitGroup在外层等待每个goroutine的退出
    • 通过select监听一个退出通知channel和一个timer channel,决定到底是正常退出还是超时退出
  • 另一种则是串行退出,即各个goroutine之间的退出是按照一定次序逐个进行的,次序若错了可能会导致程序的状态混乱和错误。
    image.png

    • 串行退出有个问题是waitTimeout值的确定,因为这个超时时间是所有goroutine的退出时间之和。
    • 在上述代码里,将每次的left(剩余时间)传入下一个要执行的goroutine的Shutdown方法中。
    • select同样使用这个left作为timeout的值(通过timer.Reset重新设置timer定时器周期)

    完整代码

    package main
    
    import (
    	"errors"
    	"sync"
    	"time"
    )
    
    type GracefullyShutdowner interface {
    	Shutdown(waitTimeout time.Duration) error
    }
    
    type ShutdownerFunc func(time.Duration) error
    
    func (f ShutdownerFunc) Shutdown(waitTimeout time.Duration) error {
    	return f(waitTimeout)
    }
    
    func ConcurrentShutdown(waitTimeout time.Duration, shutdowners ...GracefullyShutdowner) error {
    	c := make(chan struct{})
    
    	go func() {
    		var wg sync.WaitGroup
    		for _, g := range shutdowners {
    			wg.Add(1)
    			go func(shutdowner GracefullyShutdowner) {
    				defer wg.Done()
    				shutdowner.Shutdown(waitTimeout)
    			}(g)
    		}
    		wg.Wait()
    		c <- struct{}{}
    	}()
    
    	timer := time.NewTimer(waitTimeout)
    	defer timer.Stop()
    
    	select {
    	case <-c:
    		return nil
    	case <-timer.C:
    		return errors.New("wait timeout")
    	}
    }
    
    func SequentialShutdown(waitTimeout time.Duration, shutdowners ...GracefullyShutdowner) error {
    	start := time.Now()
    	var left time.Duration
    	timer := time.NewTimer(waitTimeout)
    
    	for _, g := range shutdowners {
    		elapsed := time.Since(start)
    		left = waitTimeout - elapsed
    
    		c := make(chan struct{})
    		go func(shutdowner GracefullyShutdowner) {
    			shutdowner.Shutdown(left)
    			c <- struct{}{}
    		}(g)
    
    		timer.Reset(left)
    		select {
    		case <-c:
    			//continue
    		case <-timer.C:
    			return errors.New("wait timeout")
    		}
    	}
    
    	return nil
    }
    

管道模式

image.png
每个数据处理环节都由一组功能相同的goroutine完成。在每个数据处理环节,goroutine都要从数据输入channel获取前一个环节生产的数据,然后对这些数据进行处理,并将处理后的结果数据通过数据输出channel发往下一个环节。

package main

func newNumGenerator(start, count int) <-chan int {
	c := make(chan int)
	go func() {
		for i := start; i < start+count; i++ {
			c <- i
		}
		close(c)
	}()
	return c
}

func filterOdd(in int) (int, bool) {
	if in%2 != 0 {
		return 0, false
	}
	return in, true
}

func square(in int) (int, bool) {
	return in * in, true
}

func spawn(f func(int) (int, bool), in <-chan int) <-chan int {
	out := make(chan int)

	go func() {
		for v := range in {
			r, ok := f(v)
			if ok {
				out <- r
			}
		}
		close(out)
	}()

	return out
}

func main() {
	in := newNumGenerator(1, 20)
    // 流水线:过滤偶数 -》求平方
	out := spawn(square, spawn(filterOdd, in))

	for v := range out {
		println(v)
	}
}

扇出与扇入模式

image.png
扇出模式

  • 多个功能相同的goroutine从同一个channel读取数据并处理,直到该channel关闭,这种情况被称为“扇出”
  • 使用扇出模式可以在一组goroutine中均衡分配工作量,从而更均衡地利用CPU。

扇入模式

  • 把所有输入channel的数据汇聚到一个统一的输入channel,然后处理程序再从这个channel中读取数据并处理,直到该channel因所有输入channel关闭而关闭。
package main

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

func newNumGenerator(start, count int) <-chan int {
	c := make(chan int)
	go func() {
		for i := start; i < start+count; i++ {
			c <- i
		}
		close(c)
	}()
	return c
}

func filterOdd(in int) (int, bool) {
	if in%2 != 0 {
		return 0, false
	}
	return in, true
}

func square(in int) (int, bool) {
	return in * in, true
}

func spawnGroup(name string, num int, f func(int) (int, bool), in <-chan int) <-chan int {
	groupOut := make(chan int)
	var outSlice []chan int
	for i := 0; i < num; i++ {
		out := make(chan int)
		go func(i int) {
			name := fmt.Sprintf("%s-%d:", name, i)
			fmt.Printf("%s begin to work...\n", name)

			for v := range in {
				r, ok := f(v)
				if ok {
					out <- r
				}
			}
			close(out)
			fmt.Printf("%s work done\n", name)
		}(i)
		outSlice = append(outSlice, out)
	}

	// Fan-in
	//
	// out --\
	//        \
	// out ---- --> groupOut
	//        /
	// out --/
	//
	go func() {
		var wg sync.WaitGroup
		for _, out := range outSlice {
			wg.Add(1)
			go func(out <-chan int) {
				for v := range out {
					groupOut <- v
				}
				wg.Done()
			}(out)
		}
		wg.Wait()
		close(groupOut)
	}()

	return groupOut
}

func main() {
	in := newNumGenerator(1, 20)
	out := spawnGroup("square", 2, square, spawnGroup("filterOdd", 3, filterOdd, in))

	time.Sleep(3 * time.Second)

	for v := range out {
		fmt.Println(v)
	}
}

超时与取消模式

编写一个从气象数据服务中心获取气象信息的客户端。该客户端每次会并发向三个气象数据服务中心发起数据查询请求,并以最快返回的那个响应信息作为此次请求的应答返回值。
要求在 500 ms 内返回响应结果,否则关闭请求。

package main

import (
	"context"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httptest"
	"time"
)

type result struct {
	value string
}

func first(servers ...*httptest.Server) (result, error) {
	c := make(chan result)

    // 使用 context,并且将 ctx 传进 goroutine 的请求中,保证 first 退出时,
    // goroutine 同时退出,释放资源
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
    
	queryFunc := func(i int, server *httptest.Server) {
		url := server.URL
		req, err := http.NewRequest("GET", url, nil)
		if err != nil {
			log.Printf("query goroutine-%d: http NewRequest error: %s\n", i, err)
			return
		}
		req = req.WithContext(ctx)

		log.Printf("query goroutine-%d: send request...\n", i)
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			log.Printf("query goroutine-%d: get return error: %s\n", i, err)
			return
		}
		log.Printf("query goroutine-%d: get response\n", i)
		defer resp.Body.Close()
		body, _ := ioutil.ReadAll(resp.Body)

		c <- result{
			value: string(body),
		}
		return
	}

    // 启动多个 goroutine 同时请求数据
	for i, serv := range servers {
		go queryFunc(i, serv)
	}

	select {
	case r := <-c:	// 获取响应最快的结果
		return r, nil	
	case <-time.After(500 * time.Millisecond):  // 500 ms 内没有返回数据超时退出
		return result{}, errors.New("timeout")
	}
}

// 模拟请求
func fakeWeatherServer(name string, interval int) *httptest.Server {
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Printf("%s receive a http request\n", name)
        // 模拟延时
		time.Sleep(time.Duration(interval) * time.Millisecond)
		w.Write([]byte(name + ":ok"))
	}))
}

func main() {
	result, err := first(fakeWeatherServer("open-weather-1", 200),
		fakeWeatherServer("open-weather-2", 1000),
		fakeWeatherServer("open-weather-3", 600))
	if err != nil {
		log.Println("invoke first error:", err)
		return
	}

	fmt.Println(result)
	time.Sleep(10 * time.Second)
}

每日一图,赏心悦目
在这里插入图片描述

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

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

相关文章

数字化赋能大健康实体行业迈入发展新阶段,大健康招商加盟系统优势有哪些?

数字经济的发展&#xff0c;正推动大健康实体行业迈入高质量发展新阶段。大健康实体行业应如何在数字化浪潮中抢占先机&#xff1f;大健康实体行业招商加盟平台应如何开发设计&#xff0c;才能帮助大健康企业主取得营收突破&#xff1f; 围绕蚓链大健康招商加盟系统&#xff0c…

ppt制作相关内容小结

ppt制作是天大的事&#xff01;是讲清一件事&#xff0c;表达自己的最好方式 1.删除ppt中的所有备注信息2.ppt制作中的快捷键3.精美的ppt收集 这里还是要提醒自己一下&#xff0c;做好ppt是外在的事情&#xff0c;把道理吃透才是根本&#xff01; 但是ppt外在也是表达的一种方式…

闭包实现函数柯里化,js实现

闭包实现函数柯里化&#xff0c;js实现 函数柯里化定义代码实现 函数柯里化定义 柯里化&#xff08;Currying&#xff09;是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数&#xff0c;并且返回接受余下的参数且返回结果的新函数的技术 即函数可以接…

.NET 8 Preview 4 中的 ASP.NET Core 更新

作者&#xff1a;Daniel Roth - Principal Program Manager, ASP.NET 翻译&#xff1a;Alan Wang 排版&#xff1a;Alan Wang .NET 8 Preview 4 现已可用&#xff0c;并包括了许多对 ASP.NET Core 的新改进。 以下是本预览版本中的新内容摘要&#xff1a; Blazor 使用 Blazor …

图片:前端展示图像(img 、picture、svg、canvas )及常用图片格式(PNG、JPG、JPEG、WebP、GIF、SVG、AVIF等)

一、浏览器网页展示图片方法 1.1、HTML <img> 标签 <!DOCTYPE html> <html><head><title>图片展示</title></head><body><h1>图片展示</h1><img src"example.jpg" alt"Example Image" w…

项目——学生信息管理系统3

目录 班级添加的界面实现 创建班级的实体类 在org.xingyun.dao 包下 编写 ClassDao 创建 AddStudentClassFrm 添加班级页面 注意创建成 JInternalFrame 类型 给控件起个名字 注释掉main方法 给提交按钮绑定事件 回到 MainFrm.java 给添加班级按钮绑定事件 启动测试 班…

chatgpt赋能python:Python重写父类的方法

Python重写父类的方法 在Python中&#xff0c;继承是一个常见的概念。通过继承&#xff0c;子类可以使用父类中定义的属性和方法。有时候&#xff0c;子类需要改变父类中的行为。这时候&#xff0c;可以通过重写父类的方法来实现这一目的。 什么是重写方法&#xff1f; 当一…

区块链生态发展

文章目录 前言以太坊的到来什么是图灵完备&#xff1f;什么是智能合约&#xff1f; 以太坊的应用去中心化应用 DApp代币发行 公有链&联盟链区块链应用总结 前言 前面的区块链文章有介绍区块链的诞生以及底层运行原理&#xff0c; 本文主要介绍一下区块链应用的发展&#x…

(八)灌溉系统-将nodejs部署到云服务器(未实现,思路供参考)

这里之后我就是升级优化了 如果不想再学习的话&#xff0c;前面的就足够用了 Node后端部署上线详细步骤及踩坑记录&#xff08;使用宝塔面板&#xff09;&#xff1a;参考文章 Xshelll7下载&#xff1a;教程 这里是成功连上了

如何使用 Terraform 和 Git 分支有效管理多环境?

作者&#xff5c;Sumeet Ninawe 翻译&#xff5c;Seal软件 链接&#xff5c;https://spacelift.io/blog/terraform-environments 通常我们使用 Terraform 将我们的基础设施定义为代码&#xff0c;然后用 Terraform CLI 在我们选择的云平台中创建制定的基础设施组件。从表面上看…

C++primer(第五版)第五章(语句)

5.1简单语句 一个语句在末尾加上分号;就变成了表达式语句.表达式语句的作用是执行表达式并丢弃掉求值结果(也可以用赋值运算符将求值结果存起来),不存起来的话,大多数表达式语句是没有什么实际用处的表达式语句,例如: int a10; a5; //这就是没什么用的表达式语句 ; …

《Linux操作系统编程》第四章 屏幕编程器vi : 了解屏幕编辑器vi的概述和基本操作命令

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

基于单片机自动控制的电动模型汽车

摘 要 本文研究了一种基于单片机自动控制的电动模型汽车。主要论述了自动循迹、避障、测距跟随等自动驾驶相关技术在模型车上的应用。 模型汽车以STM32为主控芯片&#xff0c;采用了多种传感器、驱动电机、控制舵机等检测与控制模块&#xff0c;实现了路径循迹行驶和检测避障的…

【python】numpy的array数组与pandas的DataFrame表格互相转换(图文代码超详细)

目录 0.环境 1.array数组和DataFrame表格的简单介绍 2.转换方式详解&#xff08;代码&#xff09; 0&#xff09;前提&#xff1a;【需注意】 1&#xff09;array转化为DataFrame 2&#xff09;DataFrame转化为array 3&#xff09;完整代码 0.环境 windows jupyter note…

英语统考错题集_作文题---网络教育统考工作笔记003

scholar 学者 下面是关于统考中的作文的如何书写,要打个照面,不能到时候蒙了 sincere 真诚的 cover 覆盖 excited 激动的 兴奋的 sincerely 真诚的 absent 缺勤的 citizen 公民 居民 每种题型都接触一下,然后后面有时间继续扩充中.. 152下上

ansible 变量与事实变量

Ansible变量与事实变量&#xff1a; 自定义变量&#xff1a; 变量可以在定义任务前进行定义&#xff0c;也可以从其他文件中调用。 下面我写了一个在任务前定义的变量&#xff0c;并用循环将其打印。 内部变量&#xff1a; 剧本如下&#xff1a; --- - hosts: localhostva…

基于51单片机开发的步进电机远程控制系统

摘 要 电机是日常生活中必不可少的一部分&#xff0c;同时也是一种常用的机电元件。步进电机是一种特殊的电机&#xff0c;相较于其他类型的电机&#xff0c;步进电机的优点更加突出、应用优势更加明显&#xff0c;广泛应用于各个领域。 本设计是基于单片机开发的步进电机远程…

力扣算法刷题Day47|休息日总结:动态规划之背包问题

背包问题 〉题型分类 解题套路 〉动规五部曲 确定dp数组&#xff08;dp table&#xff09;以及下标的含义确定递推公式dp数组如何初始化确定遍历顺序举例推导dp数组 解题技巧 〉递推公式 问背包装满后的最大价值&#xff1a;dp[j] max(dp[j], dp[j - weight[i]] value[i]) …

JMeter之简单控制线程组(Thread Group)组件的执行顺序

jmeter的线程类型一共有3种分别是setUp线程组、tearDown线程组和线程组 他们的执行优先级为 setUp线程组 > 线程组(Thread Group) > tearDown线程组 当存在多个线程组(Thread Group)&#xff0c;jmeter默认是同时执行的&#xff0c;也就是说是无序的&#xff0c;此时如果…

蓝桥杯单片机赛点数据包模块文件使用的注意事项

目录 蓝桥杯单片机赛点数据包模块文件使用的注意事项 前言&#xff1a; 正文&#xff1a; DS1302 IIC onewire 2023年赛点资源包数据包下载地址&#xff1a;https://download.csdn.net/download/qq_25218501/87965408?spm1001.2014.3001.5503 蓝桥杯单片机赛点数据包模块…