当前位置: 首页 > news >正文

逐步学习Go-并发通道chan(channel)

概述

Go的Routines并发模型是基于CSP,如果你看过七周七并发,那么你应该了解。

什么是CSP?

"Communicating Sequential Processes"(CSP)这个词组的含义来自其英文直译以及在计算机科学中的使用环境。

CSP是 Tony Hoare 在1978年提出的,论文地址在:Communicating sequential processes | Communications of the ACM

拆字解释下

Communicating Sequential Processes(CSP)的三个单词:

  • C for Communicating: 通信,什么的通信那?进程/线程/协程的通信。

  • S for Sequenctial: 顺序的,什么的顺序?进程/线程/协程之间执行任务时应该是有顺序的,完全并行执行是理想化的,现实中就是要先指定完第一个或者第一批任务才能执行第二个或者第二批任务。

  • P for Processses: 进程,这个是进程,估计是因为这个概念提出来的时候比较早。我们这儿得抽象一下,Processes指的是进程/线程/协程。

那么我们来总结一下CSP,CSP就是多个能够进行通信,并且按照顺序执行任务的独立进程。这些进程在各自执行自己的任务的时候,还可以通过某种方式是进行通信。

在Golang中就是通过Channel进行通信。

好了,CSP解释完了,我们来看Go中的Channel,另外CSP的参与者Go Routine我在之前的文章中有提到过,大家可以去:逐步学习Go-协程goroutine

这张图就描述了CSP编程模型。

file

Go中routine代表图中的Process,Channel就是goroutine之间的连接。通道可以让一个goroutine发送信息到另一个goroutine。

Go中的channel

Go中Channel有两种类型:

  1. 无缓冲Channel(Unbuffered)
  2. 有缓冲Channel(Buffered)
    有缓冲的Channel其实就是一个环形缓冲队列;无缓冲的没有队列,因为读写都会阻塞。

Channel的定义

var channel名称 chan channel类型// 类型自动推断
channel名称 := make(chan channel类型, buffer数量(int可以为0))

COPY

比如:我们可以这样来定义:

// 定义了一个channel,还没有make,不确定是否为有缓冲和无缓冲channel
var ch chan int// 定义了一个chnnel, 容量为0,无缓冲channel
ch := make(chan int, 0)// 定义了一个channel,容量为1,有缓冲channel
ch := make(chan int, 1)

COPY

我们实际使用的时候把Channel理解为队列就可以了。

Go中的Channel有两种类型:

  1. 无缓冲channel
  2. 有缓冲channel

无缓冲和有缓冲的特性如下:

  • 无缓冲Channel
    • 无缓冲Channel没有存储数据的能力
    • 发送方向Channel中发送数据的时候,发送方会阻塞直到有接受者接受这个数据
    • 无缓冲Channel典型应用就是go协程同步通信
    • 无缓冲Channel保证通信双方都要准备好数据交换
  • 有缓冲Channel
    • 有缓冲Channel需要定义Channel的容量
    • 发送方向有缓冲Channel发送数据的时候,只有容量满的时候才会阻塞
    • 接收方只有在有缓冲Channel为空时才会阻塞
    • 有缓冲通道的典型应用场景是生产者和消费者

Channel的操作

Channel主要支持2中操作:

  1. 发送(send)
  2. 接收(recv)

这三种操作在代码中的的定义和使用:

  1. 发送和接收都使用<-

来看代码:

// 先定义一个无缓冲channel
ch := make(chan int, 0)
ch := make(chan int)// 发送数据到channel
ch <- 1// 从channel中接收数据
<- ch

COPY

我们看到发送和接收都是使用<-,差别在于:

  1. ch在<-的左边,操作为发送
  2. ch在<-的右边,操作为接收

另外,channel在使用之前都要先创建,使用完毕后要关闭,分别使用makeclose关闭。

// 创建相当于分配channel(allocation)
ch := make(chan int, 0)// 关闭channel,释放channel资源
defer close(ch)

COPY

channel创建完直接关闭了还能操作发送和接收吗?

这个问题我们通过写代码来测试,我们先来测试发送,然后再测试接收。

  • 发送数据到关闭的Channel

    func TestUnbufferedChannel_ShouldPanic_whenWriteValueToAClosedChannel(t *testing.T) {f := func() {ch := make(chan int)close(ch)ch <- 1
    }assert.Panics(t, f, "should panic")
    }
    COPY

    运行截图:

    file

我们的UT PASS了表示发生了panic,这就说明我们不能向已经关闭的channel发送数据。

  • 在已经关闭的Channel上接收

func TestUnbufferedChannel_ShouldSuccess_whenRecvValueAtAClosedChannel(t *testing.T) {ch := make(chan int)close(ch)var val = <-chassert.Equal(t, 0, val)
}func TestUnbufferedChannel_ShouldSuccess_whenRecvEmptyValueAtAClosedChannel(t *testing.T) {ch := make(chan string)close(ch)var val = <-chassert.Equal(t, "", val)
}

COPY

运行截图:

file

这两个UT都可以PASS,我只截图了一个PASS,这说明我们可以在一个关闭的channel上接收数据,只是接收到的都是0值。关于0值要特别说明一下,0值是针对不同类型的,比如:int的0值就是0,string的0值就是空字符串,指针的0值就是nil,看下面代码:

file

并非“任何后续的接收操作都将立即返回零值”,而是当channel中所有已发送的值都被接收后,接下来的接收操作会立即返回零值。

无缓冲channel

无缓冲通道顾名思义:就是没有数据缓冲能力的Channel,有goroutine向无缓冲Channel发送了数据就必须有另一个goroutine来接受,否则发送的goroutine会阻塞;反之,有goroutine从这个channel接受数据而没有另一个goroutine向这个channel发送,那么接受的goroutine也会阻塞。

应用场景:

  1. 部分任务需要同步就用无缓冲channel

来看场景代码:

有发送无接受

发送goroutine会被阻塞。


func TestUnbufferedChannel_ShouldWriteTimeout_WhenNoRoutineReadTheChannel(t *testing.T) {// 创建无缓冲channelc := make(chan int)is_timeout := falsetry_to_write_value := 1// whenselect {// 直接向channel中发送case c <- try_to_write_value:case <-time.After(3 * time.Second):// shouldis_timeout = true}assert.True(t, is_timeout)}

COPY

file

有接受无发送

接收goroutine会被阻塞


func TestUnbufferedChannel_ShouldReadTimeout_WhenNoValueWriteToChannel(t *testing.T) {// 创建无缓冲channelc := make(chan int)is_timeout := falseselect {// 直接接受channel中的数据case <-c:case <-time.After(3 * time.Second):// shouldis_timeout = true}// 三秒后超时assert.True(t, is_timeout)}

COPY

有发送有接受

有发送有接收,一切正常。

func sum(s []int, c chan int) {sum := 0for _, v := range s {sum += v}// 将累加结果发送到channelc <- sum
}func TestUnbufferedChannel_ShouldRecvValues_WhenWriteValueToChannel(t *testing.T) {// 创建无缓冲channelc := make(chan int)// givens := []int{1, 2, 3, 4, 5, 6}// when// 执行数组累加go sum(s[:], c)ret1 := <-c// should// 和应该是21assert.Equal(t, 21, ret1)
}

COPY

file

使用无缓冲Channel控制并发

// 先定义一个worker函数
// worker函数从无缓冲channel中接收
// 可以接到到数据就执行后面的打印内容
// 打印完成后退出
func worker(id int, lock chan bool) {var shouldRun = <-lockif shouldRun {fmt.Printf("time: %v Worker %d is working\n", time.Now(), id)time.Sleep(time.Second)fmt.Printf("time: %v Worker %d has finished\n", time.Now(), id)}
}func TestUnbufferedChannel_ShouldRunOneByOne_When(t *testing.T) {lock := make(chan bool, 1)// 启动5个goroutine等待释放接收for i := 0; i < 5; i++ {go worker(i, lock)}// 发送5个true到channelfor i := 0; i < 5; i++ {lock <- truetime.Sleep(time.Second)}close(lock)time.Sleep(10 * time.Second)
}

COPY

file

使用无缓冲Channel实现CompleteFuture.anyOf()

CompleteFuture.anyOf() 是 Java 中的一个函数,它返回一个新的 CompletableFuture,当给定的任何 CompletableFuture 完成时,返回的 CompletableFuture 也完成,并带有完成的 CompletableFuture 的结果。


// future函数使用time.Sleep模拟实际业务处理延迟
// 业务处理完成后将业务数据写入无缓冲Channel
func future(id int, delay time.Duration, resChan chan int) {time.Sleep(delay)fmt.Printf("Hi, I have finished my task, my id is %d\n", id)resChan <- id
}// 接收一系列上面的future, 然后使用go routine启动这些future函数并将结果写入到result channel,最后再返回result channel。
func anyOf(futures ...<-chan int) <-chan int {result := make(chan int)for _, future := range futures {go func(f <-chan int) {result <- <-f}(future)}return result
}func TestAnyOf_ShouldSuccess(t *testing.T) {// 创建无缓冲的 channelresChan1 := make(chan int)resChan2 := make(chan int)resChan3 := make(chan int)// 启动 goroutinesgo future(1, 3*time.Second, resChan1)go future(2, 2*time.Second, resChan2)go future(3, 5*time.Second, resChan3)result := anyOf(resChan1, resChan2, resChan3)assert.Equal(t, 2, <-result)
}

COPY

上面有两个比较让人纠结的语法:

  1. <-chan int
  2. result <- <-f
  • <-chan int表示只读通道,anyOf只能读取通道内的数据;有了只读就有只写,只写通道chan<- int
  • result <- <-f表示从通道f中接收数据并将数据写入到result通道。这一行相当于执行了
    v := <-f
    result <- v
    COPY

    file

有缓冲channel

有缓冲channel就是你可以暂时把数据发送到channel,如果channel的缓冲区没有被占用完就不会阻塞,缓冲区被占用完了就被阻塞了。

特性:

  1. 发送goroutine在缓冲区没有用完之前不会阻塞,缓冲区被使用完了之后发送goroutine就会被阻塞
  2. 接受goroutine在缓冲区有数据时,不会阻塞,缓冲区没有数据时会被阻塞

有缓冲channel应用场景是什么?

  1. 任务队列就是最典型的场景,生产者消费者模型
  2. 其他无缓冲channel搞不定的就用有缓冲channel

实现一个有缓冲channel的RateLimiter

import ("sync""sync/atomic""testing""fmt""time""github.com/stretchr/testify/assert"
)type RateLimiter struct {tokens       chan struct{}refillTicker *time.TickercloseCh      chan struct{}
}func NewRateLimiter(rate int) *RateLimiter {r := &RateLimiter{tokens:       make(chan struct{}, rate),refillTicker: time.NewTicker(time.Second / time.Duration(rate)),closeCh:      make(chan struct{}),}go r.refill()return r
}func (r *RateLimiter) refill() {for {select {case <-r.refillTicker.C:select {case r.tokens <- struct{}{}:default:}case <-r.closeCh:r.refillTicker.Stop()return}}
}func (r *RateLimiter) Acquire() {<-r.tokens
}func (r *RateLimiter) TryAcquire() bool {select {case <-r.tokens:return truedefault:return false}
}func (r *RateLimiter) Close() {close(r.closeCh)
}func myTask(id int) {fmt.Printf("time: %v workder %d is working\n", time.Now(), id)time.Sleep(20 * time.Millisecond)fmt.Printf("time: %v workder %d has finished\n", time.Now(), id)
}func TestRateLimiter_ShouldPermitWithBlocking_WhenRequestOnce(t *testing.T) {rateLimiter := NewRateLimiter(100)startTime := time.Now()for i := 0; i < 1; i++ {rateLimiter.TryAcquire()myTask(i)}endTime := time.Now()elapsedTime := endTime.Sub(startTime)fmt.Printf("elapsed time: %v\n", elapsedTime)fmt.Printf("explect time: %v\n", 300*time.Millisecond)assert.True(t, elapsedTime < 300*time.Millisecond)
}func TestRateLimiter_ShouldLimitPermits_WhenGivenLimitedResource(t *testing.T) {var counter int32 = 0rateLimiter := NewRateLimiter(100)wg := sync.WaitGroup{}startTime := time.Now()for i := range 1000 {wg.Add(1)go func() {rateLimiter.Acquire()myTask(i)atomic.AddInt32(&counter, 1)wg.Done()}()}wg.Wait()endTime := time.Now()elapsedTime := endTime.Sub(startTime)fmt.Printf("elapsed time: %v\n", elapsedTime)fmt.Printf("should greater than explect time: %v\n", 10*time.Second)assert.Equal(t, counter, int32(1000))assert.True(t, 10*time.Second < elapsedTime)
}

COPY

file

实现无缓冲Channel实现Java中的CyclicBarrier

CyclicBarrier 是一个同步工具,它允许一组线程互相等待,直到他们都到达了一个共同的屏障点。在涉及固定大小的线程团队必须偶尔相互等待的程序中,CyclicBarriers 非常有用。之所以称之为“循环”屏障,是因为在等待的线程被释放之后,它可以被重复使用。

  • await() 所有的参与者都调用了wait方法后返回或者被中断
    我们就实现这个await方法,暂时不支持中断,代码如下:

package mainimport ("fmt""sync""sync/atomic""time"
)// CyclicBarrier 让一组goroutine在到达某个点之后才能继续执行
type CyclicBarrier struct {// 总goroutine数量participant int// 用于等待所有goroutine准备好waitGroup sync.WaitGroup// 无缓冲channel,用于goroutine间同步barrierChan chan struct{}running     int32
}// NewCyclicBarrier 创建一个新的CyclicBarrier
func NewCyclicBarrier(participant int) *CyclicBarrier {b := &CyclicBarrier{participant: participant,barrierChan: make(chan struct{}),running:     int32(participant),}// 设置等待的goroutine数b.waitGroup.Add(participant)return b
}// 当一个goroutine调用Wait时,
// 它将在屏障处等待,
// 直到所有goroutine都到达这里
func (b *CyclicBarrier) Wait() {// 一个goroutine准备好了b.waitGroup.Done()// 等待所有goroutine都准备好b.waitGroup.Wait()// 当所有goroutine都准备好了,关闭channel进行广播通知if atomic.AddInt32(&b.running, -1) == 0 {close(b.barrierChan)} else {// 等待通知<-b.barrierChan}}// 阻塞调用goroutine直到所有goroutine都调用了Wait方法,
// 屏障开放后,重新置为待关闭状态
func (b *CyclicBarrier) Await() {// 等待屏障开放的信号<-b.barrierChan// 重置屏障状态b.barrierChan = make(chan struct{})b.waitGroup.Add(b.participant)
}func (b *CyclicBarrier) Close() {close(b.barrierChan)
}func main() {// 这里我们设置3个goroutine参与barrier := NewCyclicBarrier(100)for i := 0; i < 100; i++ {go func(i int) {fmt.Printf("Goroutine %d is working...\n", i)// 模拟工作time.Sleep(time.Duration(i+1) * time.Second)fmt.Printf("Goroutine %d reached the barrier.\n", i)barrier.Wait()fmt.Printf("Goroutine %d passed the barrier.\n", i)}(i)}// 主goroutine等待所有goroutine都到达屏障barrier.Await()fmt.Println("All goroutines have passed the barrier")
}

COPY

参考

  1. go/src/runtime/chan.go at master · golang/go · GitHub
  2. 逐步学习Go-并发通道chan(channel) – FOF编程网

编写不易,如有问题请评论告知

相关文章:

逐步学习Go-并发通道chan(channel)

概述 Go的Routines并发模型是基于CSP&#xff0c;如果你看过七周七并发&#xff0c;那么你应该了解。 什么是CSP&#xff1f; "Communicating Sequential Processes"&#xff08;CSP&#xff09;这个词组的含义来自其英文直译以及在计算机科学中的使用环境。 CSP…...

鸿蒙HarmonyOS应用开发之Node-API开发规范

当传入napi_get_cb_info的argv不为nullptr时&#xff0c;argv的长度必须大于等于传入argc声明的大小。 当argv不为nullptr时&#xff0c;napi_get_cb_info会根据argc声明的数量将JS实际传入的参数写入argv。如果argc小于等于实际JS传入参数的数量&#xff0c;该接口仅会将声明…...

单例模式

文章目录 单例模式特殊类的设计单例模式饿汉模式懒汉模式懒汉VS饿汉懒汉的线程安全单例对象的释放 单例模式 认识单例模式之前先认识一下几种常见的特殊类的设计。 特殊类的设计 设计一个类 只能再堆上创建对象 只能再堆上创建&#xff0c;则通过new来创建对象。 将类的构造函…...

Android OpenMAX - 开篇

Android Media是一块非常庞大的内容&#xff0c;上到APP的书写&#xff0c;中到播放器的实现、封装格式的了解&#xff0c;下到OMX IL层的实现、Decoder的封装&#xff0c;每一块都需要我们下很大的功夫学习。除此之外&#xff0c;我们还要对一些相关的模块进行了解&#xff0c…...

ubuntu开启ssh服务

1.安装openssh-server sudo apt-get install openssh-server 2.开启服务 sudo servive ssh start 3.设置开机自启动 sudo systemctl enable ssh 4.检查是否开启成功 ps -ef | grep ssh 如使用xshell等连接时失败&#xff0c;可以尝试关闭防火墙&#xff1a; sudo sys…...

测试缺陷定位的基本方法

前后端bug特征 后端&#xff1a; 业务逻辑问题&#xff1a;如任务状态未扭转成功&#xff0c;创建任务失败等数据类问题&#xff1a;如新增的任务在页面没有展示出来等性能类问题&#xff1a;提交任务一直显示创建中、批量操作等待耗时长超时等 前端&#xff1a; 页面显示类…...

【数字图像处理matlab系列】数组索引

【数字图像处理matlab系列】数组索引 【先赞后看养成习惯】【求点赞+关注+收藏】 MATLAB 支持大量功能强大的索引方案,这些索引方案不仅简化了数组操作,而且提高了程序的运行效率。 1. 向量索引 维数为1xN的数组称为行向量。行向量中元素的存取是使用一维索引进行的。因此…...

【2024系统架构设计】案例分析- 3 数据库

目录 一 基础知识 二 真题 一 基础知识 1 ORM ORM(Object—Relationl Mapping...

vue基础——java程序员版(总集)

前言&#xff1a; ​ 这是一个java程序员的vue学习记录。 ​ vue是前端的主流框架&#xff0c;按照如今的就业形式作为后端开发的java程序员也是要有所了解的&#xff0c;下面是本人的vue学习记录&#xff0c;包括vue2的基本使用以及引入element-ui&#xff0c;使用的开发工具…...

Rancher(v2.6.3)——Rancher配置Harbor镜像仓库

Rancher配置Harbor镜像仓库详细说明文档&#xff1a;https://gitee.com/WilliamWangmy/snail-knowledge/blob/master/Rancher/Rancher%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3.md#8rancher%E9%85%8D%E7%BD%AEharbor ps&#xff1a;如果觉得作者写的还行&#xff0c;能够满足您的需…...

C++类和对象、面向对象编程 (OOP)

文章目录 一、封装1.抽象、封装2.类和对象(0)学习视频(1)类的构成(2)三种访问权限(3)struct和class的区别(4)私有的成员变量、共有的成员函数(5)类内可以直接访问私有成员&#xff0c;不需要经过对象 二、继承三、多态1.概念2.多态的满足条件3.多态的使用条件4.多态原理剖析5.纯…...

Introduction to Data Mining 数据挖掘

Why Data Mining? • The Explosive Growth of Data: from terabytes to petabytes — Data collection and data availability ◦ Automated data collection tools, database systems, Web, computerized society — Major sources of abundant data ◦ Business: Web, e-co…...

常用的 Git 命令

初始化一个新的仓库&#xff1a; git init 克隆一个仓库&#xff1a; git clone <仓库地址> 查看文件状态&#xff1a; git status 添加文件到暂存区&#xff1a; git add <文件名> 提交文件到仓库&#xff1a; git commit -m "提交说明" 查看提交历…...

jackson:JSON字符串(String)类型的成员序列化和反序列化

对于如下类型定义TestTaskInfo&#xff0c;props字段为JSON字符串(这在数据库经常用到),可以自由保存各种类型的数据 Data public class TestTaskInfo {private String id;private String props;public TestTaskInfo() {}public TestTaskInfo(String id, String props) {super…...

使用docker的好处???(docker的优势)

标准化环境&#xff1a; Docker通过容器技术封装应用程序及其依赖&#xff08;如库、配置文件、运行时环境等&#xff09;&#xff0c;确保应用程序在任何环境中都能以一致的方式运行。这种标准化消除了“在我机器上能运行”的问题&#xff0c;因为容器化应用能在开发、测试、生…...

Google AI 肺癌筛查的计算机辅助诊断

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…...

【字符串算法题记录】反转字符串中的单词(leetcode),右旋字符串(kama)——双指针以及反转的奇思妙用

反转字符串中的单词 题目链接 思考 这题的思路顺序是&#xff1a;移除多余空格&#xff08;双指针法&#xff09;——》反转整个字符串&#xff09;——》反转字符串中每个单词。 移除多余空格&#xff08;双指针法&#xff09; 因为字符串开头也可能有多个字符&#xff0…...

基于springboot+vue调用百度ai实现车牌号识别功能

百度车牌号识别官方文档 结果视频演示 后端代码 private String getCarNumber(String imagePath, int count) {// 请求urlString url "https://aip.baidubce.com/rest/2.0/ocr/v1/license_plate";try {byte[] imgData FileUtil.readFileByBytes(imagePath);Stri…...

【NTN 卫星通信】 TN和多NTN配合的应用场景

1 场景描述 此场景描述了农村环境&#xff0c;其中MNO (运营商TerrA)仅在城市附近提供本地地面覆盖&#xff0c;而MNO (SatA)提供广泛的NTN覆盖。SatA使用GSO轨道和NGSO轨道上的卫星。SatA与TerrA有漫游协议&#xff0c;允许:   所有TerrA用户的连接&#xff0c;当这些用户不…...

健康餐饮必备!油烟净化器超强洁净餐饮环境

我最近分析了餐饮市场的油烟净化器等产品报告&#xff0c;解决了餐饮业厨房油腻的难题&#xff0c;更加方便了在餐饮业和商业场所有需求的小伙伴们。 ​在如今注重健康生活的时代&#xff0c;餐饮业不仅需要美味佳肴&#xff0c;更需要一个清洁、舒适的用餐环境。油烟净化器作…...

XCTF-web-easyupload

试了试php&#xff0c;php7&#xff0c;pht&#xff0c;phtml等&#xff0c;都没有用 尝试.user.ini 抓包修改将.user.ini修改为jpg图片 在上传一个123.jpg 用蚁剑连接&#xff0c;得到flag...

通过Wrangler CLI在worker中创建数据库和表

官方使用文档&#xff1a;Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后&#xff0c;会在本地和远程创建数据库&#xff1a; npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库&#xff1a; 现在&#xff0c;您的Cloudfla…...

线程同步:确保多线程程序的安全与高效!

全文目录&#xff1a; 开篇语前序前言第一部分&#xff1a;线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分&#xff1a;synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分&#xff…...

Docker 运行 Kafka 带 SASL 认证教程

Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明&#xff1a;server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...

可靠性+灵活性:电力载波技术在楼宇自控中的核心价值

可靠性灵活性&#xff1a;电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中&#xff0c;电力载波技术&#xff08;PLC&#xff09;凭借其独特的优势&#xff0c;正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据&#xff0c;无需额外布…...

蓝牙 BLE 扫描面试题大全(2):进阶面试题与实战演练

前文覆盖了 BLE 扫描的基础概念与经典问题蓝牙 BLE 扫描面试题大全(1)&#xff1a;从基础到实战的深度解析-CSDN博客&#xff0c;但实际面试中&#xff0c;企业更关注候选人对复杂场景的应对能力&#xff08;如多设备并发扫描、低功耗与高发现率的平衡&#xff09;和前沿技术的…...

Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级

在互联网的快速发展中&#xff0c;高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司&#xff0c;近期做出了一个重大技术决策&#xff1a;弃用长期使用的 Nginx&#xff0c;转而采用其内部开发…...

相机从app启动流程

一、流程框架图 二、具体流程分析 1、得到cameralist和对应的静态信息 目录如下: 重点代码分析: 启动相机前,先要通过getCameraIdList获取camera的个数以及id,然后可以通过getCameraCharacteristics获取对应id camera的capabilities(静态信息)进行一些openCamera前的…...

Java入门学习详细版(一)

大家好&#xff0c;Java 学习是一个系统学习的过程&#xff0c;核心原则就是“理论 实践 坚持”&#xff0c;并且需循序渐进&#xff0c;不可过于着急&#xff0c;本篇文章推出的这份详细入门学习资料将带大家从零基础开始&#xff0c;逐步掌握 Java 的核心概念和编程技能。 …...

DeepSeek 技术赋能无人农场协同作业:用 AI 重构农田管理 “神经网”

目录 一、引言二、DeepSeek 技术大揭秘2.1 核心架构解析2.2 关键技术剖析 三、智能农业无人农场协同作业现状3.1 发展现状概述3.2 协同作业模式介绍 四、DeepSeek 的 “农场奇妙游”4.1 数据处理与分析4.2 作物生长监测与预测4.3 病虫害防治4.4 农机协同作业调度 五、实际案例大…...