linux线程 | 全面理解同步与互斥 | 同步

news2024/11/28 7:24:26

        前言:本节内容主要讲解linux下的同步问题。 同步问题是保证数据安全的情况下,让我们的线程访问具有一定的顺序性。 线程安全就规定了它必须是在加锁的场景下的!!那么, 具体什么是同步问题, 我们加下来看看吧!

        ps:建议学习了线程的互斥再来观看哦。

目录

同步与条件变量

条件变量的接口

生产消费问题

结藕 

快速实现生产消费者模型

创建文件

配置makefile        

阻塞队列的实现

主函数的实现

运行结果

生产消费者模型为什么是高效的​编辑

伪唤醒


同步与条件变量

       我们先来理解一下这句话——同步问题是保证数据安全的情况下, 让我们的线程访问资源具有一定的顺序性。

        这句话我们知道保证线程安全其实就是加锁,然后线程访问资源具有顺序性说让线程排队访问资源。 问题是, 排队访问资源就已经保证了线程安全了。 为什么还要加锁这个前提呢?这是因为, 我们这里所说的线程排队访问资源, 不是说的线程一来申请访问资源就能够自觉排队的!而这个线程先去申请访问资源访问失败, 碰壁了, 然后才过来排队的!!!——就比如一个VIP自习室, 只允许一个人进入。 现在里面已经有一个人了。 现在有一个人想进去,这个人看到旁边有一群人都在排队等着开门。 但是他不会去排队,因为他想验证一下里面有没有人, 所以他先去开一下这个门, 打不开,然后他才知道里面有人, 才来排队!!!

        然后这样做有什么用呢? 还是上面开门的例子。 如果人们不排队,那么当里面的人出来后。因为他刚出来, 他现在离着门的钥匙最近, 所以他想要再进去就非常的容易, 他这样反反复复进进出出, 那么别人是不是就会抗议, 是不是就有问题。再或者如果一个人早就来了, 它是第一个来的。 但是他腿脚不好, 那么当一个人出来后,其他后来的人抢先一步进去了, 是不是也不太公平。 但是如果排队呢? 我们规定里面的人出来后必须从队伍末尾开始排队!规定先来的就在队伍的前边!那么是不是就能服众, 就能让每一个人都能很好的进入这个自习室! ——而这,其实就是饥饿问题以及其解决方法!

        不要认为互斥是问题, 互斥是一种解决方案。 只是互斥在某些场景下有一些问题。 

        那么, 我们如何快速实现同步呢?——使用条件变量。 这是我们第一次提出这个概念, 所以我们要来认识一下linux中的条件变量。 看下面这张图:

        上图是一个vip自习室。 每个人进入这个vip自习室都需要拿到门口的钥匙。 但是这个钥匙只有一把, 也就是说一次只能有一个人能够进入这个自习室, 即vip自习室只允许一个人进入!!!

        此时, 我们提供了一个铃铛, 和一个等待队列。 当我们的新来的人去进入自习室, 找不到钥匙了, 他就要来到等待队列这里等待。 然后等到自习室的人出来, 铃铛就会“唤醒”这些等待队列的人, 让他们进入吧, 然后队列的第一个人就会去拿到钥匙进入vip自习室。 ——这里面的等待队列和这个铃铛, 就是我们口中的条件变量!条件变量至少为我们提供两件东西:第一个东西就是我们的简单的通知机制用来唤醒线程。 第二个就是等待队列, 为了线程去排队。 

        另外一个点就是问题不是我们去条件变量下排队了, 问题是为什么会去条件变量下排队。 不就是因为线程申请锁失败了,然后来到条件变量下排队吗? 所以, 条件变量的存在必须配合互斥锁!必须依赖于锁的使用!!因为我们的条件变量只是让我们的线程去等待队列, 而条件必须就绪才能唤醒我们的等待队列!

条件变量的接口

        下面我们看一下同步的接口:

        首先看一下初始化和销毁。 

        初始化的第一个参数是条件变量的类型。 这个第一个参数就是条件变量的对象。 整个函数就是要给这个对象进行初始化。 这个初始化属性由第二个参数控制, 可以不管传送nullptr。 

        然后销毁的参数就是条件变量的地址。 

然后再来看一下我们的等待和唤醒

        等待函数就是当线程申请锁失败的时候, 将线程放到等待队列里面。

        然后唤醒函数有两个, broadcast是唤醒整个条件变量下的线程, signal是唤醒一个条件变量下的线程。      

        ps:这里我们先提一下这几个接口, 先不用。 后面我们会讲生产消费模型, 然后就会带友友们实现以下生产消费模型。到时就会用到这些函数了!

生产消费问题

        生产消费问题,也叫做生产消费者模型。

        什么是生产消费者模型呢, 我们看下面这个图片:

        上面的图中有生产者,有消费者, 还有一个超市——这个就可以看成一个生产消费者模型。  其中, 我们的生产者是来生产数据的, 然后数据放到了超市里面消费者就到超市里面去拿数据。 虽然听起来很简单, 但是我们要考虑的是:超市, 是不是生产者? 

        其实, 超市并不是生产者, 但是为什么要存在超市呢?这是因为对于生产者来说, 它生产物品不需要再听用户的, 不需要听到用户要一包,他就生产一包这样了。 而是我们的超市有多大内存, 生产者就一次性生产很多包。 所以, 这个超市可以让我们的整个的商品的流动效率变高。可以节省生产者以及消费者的时间。

        因为超市的存在, 他无论对供货商来讲, 还是对于消费者来讲, 他都是可以提高效率的。超市在这里起的作用就是一个临时的缓存, 因为数据被缓存起来了。 所以我们对应的消费者拿数据的时候就不需要再从生产者那里拿, 而是直接从缓存里面直接拿我们的商品。 所以, 为什么效率高? 其实就是因为超市是一个大号的缓存!!!

        这个缓存能够支持忙闲不均, 让生产和消费的行为, 进行一定程度的结藕!什么意思? 意思就是说我们的生产者不需要在考虑消费者干什么 他自己做自己的, 消费者也一样。 

        在我们的实际的系统当中, 我们的生产者一定是我们的线程, 我们的消费者也一定是线程。 超市就是特定结构的内存空间!而中间的商品就是数据。 

        所以, 这个生产消费模型其实本质就是执行流在做通信——》 所以, 我们今天讨论的就是如何安全高效的通信!!!而这个超市的资源本质是什么资源?是不是就是一种共享资源——》所以, 他就一定有并发问题!!!而要谈并发问题的本质, 其实就是在谈生产者与生产者, 消费者与消费者,生产者与消费者。 三者之间的关系。那么三者的关系是什么?

  •         生产者VS生产者:互斥关系, 就比如生产糖果的和生产糖果的, 它们肯定只想有它们一家卖糖果的, 这不就是互斥关系? 当然只有一家卖糖果的是不可能的。
  •         消费者VS消费者:本质上其实也是互斥关系, 就比如今天这家只有一个橡皮了, 你也想买我也想买。那我俩肯定要商量一下看看这块橡皮到底是谁的了。
  •         生产者VS消费者: 本质是互斥加同步, 因为生产和消费之间应该有一定的顺序性的, 就比如应该先生产, 再消费!!!

         所以, 综上, 我们就可以总结到: 生产消费模型里面会有三种关系 两种角色一个交易场所(特定数据结构的内存空间)

        所以, 生产消费者模型就是多线程下互斥的一种场景。 它有两种角色, 三个关系, 一个特定的数据结构的交易场所。 优点是:支持忙闲不均, 生产和消费进行结藕。 

结藕 

        如果不结藕是什么情况呢? 加入今天有一个main函数, 里面有一个ADD函数, 我们一般调用函数, 都是进行传参进行实例化。 在mian函数传参add函数的时候, 这个过程其实就是生产数据交给add函数了。 而add函数拿到这个数据把这个数据做加法, 做各种运算, 这不就是叫做消费这个数据了吗? 那么我们消费完这个数据后, 结果给main函数返回, 但是当我们调用add函数时,main函数在干什么? 按照我们之前的代码, 我们调用add函数时, main函数是在那里等的。 他并没有向后执行, 他在等add函数执行完再往后运行。 因为为我们平时写的main函数和add函数都是单执行流, 是串行的。 这其实就是耦合度比较高的一种表现。 

        要解决的话该怎么办呢? 我们把main函数拆成主函数, 我们的add函数拆成工作线程, 我们的参数不通过传参的方式, 而是通过交易场所来进行。 意思就是说我们可以创建一个数据结构, 然后在这个数据结构里面缓存上一大批参数, 然后add线程定期从这里面拿数据。 这样就能让它变成两个线程, 一个生产一个消费了。 这种方式, 就是完成了一次结藕的操作!!!
 

快速实现生产消费者模型

        我们首先要写一个生产者, 一个消费者。 然后它们采用一个Blockqueue作为交易场所进行资源传送。 然后这个Blockqueue(阻塞队列)需要我们自己实现。 那么什么叫做阻塞队列?——就是一定要有自己的上限, 一旦队列放满, 生产者就不能再放了, 必须去休眠。 一旦队列空了, 消费者就不能消费了, 也必须去休眠。 

创建文件

        首先创建三个文件:

配置makefile        

        然后配置我们的makefile:

main.exe:main.cpp
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -rf main.exe

阻塞队列的实现

        我们实现阻塞队列, 首先要想一下阻塞队列里面要有什么? 

        我们的阻塞队列, 首先一定要有一个队列, 而且为了这个队列能够有最大容量, 我们还要定义一个变量来作为最大的容量; 其次还要有条件变量, 我们这里就是使用的条件变量:pthread_cond_t对象。 这里我们要定义两个pthread_cond_t对象, 一个用来表示生产者的等待队列, 一个用来表示消费者的等待队列。(注意, 这里说的是等待队列, 也就是条件变量里面的等待队列。不是阻塞队列, 阻塞队列是我们用来充当交易场所的数据结构)然后为了能够让阻塞队列安全, 还要定义一把锁。 最后还要有两个水位线, 当我们高出上边的水位线, 就唤醒我们的消费者, 让消费者赶紧消费。 当低出下方的水位线, 就让生产者赶紧生产。

所以, 我们的简单的初步定义如下:

#pragma once
#include<iostream>
using namespace std;
#include<pthread.h>
#include<queue>




template<class T>
class BlockQueue
{
    static const int defaultnum = 20; //用来初始化_maxcap

public:

private:
    queue<T> _q;     //访问多线程的,共享资源
    int _maxcap;     //队列的极致, 最大能够到多少。
    
    pthread_mutex_t _mutex;  //共享资源, 那么就要保护, 所以定义一把锁来保护我们的共享资源。
    pthread_cond_t c_cond;  //为了让资源有序, 防止饥饿问题, 我们再定义两个_cond,用来同步
    pthread_cond_t p_cond;

    int low_water_;      //定义两个水位线    
    int high_water_;

}; 

        然后我们内部的方法可以有四个: 其中用来初始化和析构的构造析构函数。 另外两个是用来生产者生产数据push函数消费者用来消费数据pop函数。 

        _q成员不需要管, 会调用stl的默认构造。 然后就是_maxcap,_mutex, c_cond, p_cond, 这几个我们需要初始化。 其中锁和条件变量专门用来初始化的函数和专门用来销毁的函数。 所以我们就可以直接调用这些函数进行初始化和销毁。所以下面是构造和析构:

    BlockQueue(int maxcap = defaultnum)
        :_maxcap(maxcap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&c_cond, nullptr);
        pthread_cond_init(&p_cond, nullptr);
        low_water_ = _maxcap / 3;
        high_water_ = _maxcap * 2 / 3;    

    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex); 
        pthread_cond_destroy(&c_cond);
        pthread_cond_destroy(&p_cond);
    }

         然后就是生产函数和消费函数。 对于两个函数来说,我们的线程进入的第一件事都是先拿到锁。 然后就开始判断, 当到达最大容量不能生产, 当容量为零不能消费。同时判断成功之后才能进行_q变量的push和pop:

    //谁来唤醒呢? 
    T pop()
    {
        pthread_mutex_lock(&_mutex);
        if (_q.size() == 0)
        {
            //条件不满足, 去等待
            pthread_cond_wait(&p_cond, &_mutex); 
        }
        //

        T out = _q.front();   //你想要消费就能直接进行消费吗? 肯定不可能, 
        _q.pop();
        if (_q.size() < low_water_) pthread_cond_signal(&c_cond);

        pthread_mutex_unlock(&_mutex);   
        return out;
    }

    void push(const T& in)
    {
        pthread_mutex_lock(&_mutex);
        //先确保生产条件是否满足
        if (_q.size() == _maxcap)  //为什么这个判断不放在外边? 因为我们的判断本身就是临界资源, 所以必须放到加锁之后!!!
        {
            pthread_cond_wait(&c_cond, &_mutex); //持有锁期间让我们去等待, 我一旦被挂起就没有人能够消费了。 所以, pthread_cond_wait需要传送一个锁的参数。 这是因为他会自动给我们释放锁。
        }
        //1、队列没有满。2、被唤醒(这个的本质还是队列没有满了)
        _q.push(in);
        if (_q.size() > high_water_) pthread_cond_signal(&p_cond);

        pthread_mutex_unlock(&_mutex);
    }

        当我们判断失败时候就让线程去等待队列, 这个等待队列有两个参数, 第一个参数是等待队列的地址, 第二个参数是锁的地址。 为什么要有锁的地址? 因为我们想一下, 因为我们的线程进入等待队列的时候是拿着锁的, 如果不释放这个锁, 是不是就没人能够进入临界区了? 所以我们的等待队列拿到锁的地址就是为了能够自动释放锁!!!

主函数的实现

        首先是主框架, 就是创建生产者, 消费者, 交易场所。 然后生产者去生产, 消费者去消费:

int main()
{
    //创建等待队列——交易场所
    BlockQueue<int>* bq = new BlockQueue<int>();

    //创建生产者消费者
    pthread_t c, p;

    //生产者生产, 消费者消费
    pthread_create(&c, nullptr, Comsumer, bq);
    pthread_create(&p, nullptr, Productor, bq);


    //等待释放两个线程
    pthread_join(c, nullptr);
    pthread_join(p, nullptr);

    //释放两个队列
    delete bq;
    return 0;
}

        重要的是生产者如何生产, 消费者如何消费。 这里就要定义两个线程要去执行的函数了。 

如下:


void* Comsumer(void* args)
{
    BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);
    while (true)
    {
        //消费
        // sleep(2);
        auto data = bq->pop(); 
        cout << "消费了一个数据: " << data << endl;
  
    
    }
}

void* Productor(void* args)
{
    BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);
    int data = 0;
    while (true)
    {
        //生产
        data++;
        bq->push(data);

        cout << "生产了一个数据: " << data << endl;
        sleep(1);
    } 
}

运行结果

        可以看到, 当我们的生产到了2/3的时候, 就消费了。(其实这里因为消费太快了,所以直接全部消费没了。本质其实是消费到了1/3的时候就去唤醒生产者了)

生产消费者模型为什么是高效的

        生产者对应的数据从哪来呢? 我们的生产者, 生产了产品然后放到仓库中, 这没有问题。 问题是, 我们的生产者对应需要的数据从哪来呢?要知道, 用户的数据, 网络等数据等等都是给我们的数据, 而生产者生产数据就是要获取这些数据的, 而获取这些数据也是要花时间的。 所以, 生产者在生产之前还有一个更加前置的条件, 就是获取数据!!!

        同样的, 消费者拿到数据后并不算消费了!后面拿到数据后去干了什么, 也是容易被人忽略的, 那么消费者拿到了数据要不要做一下加工? 也是要的, 所以, 生产者拿到数据也是要花时间的。 

  • 生产者要做的: 获取数据、生产数据。
  • 消费者要做的: 消费数据、加工处理数据。

所以, 为什么生产消费模型是高效的?

        虽然我们的生产者生产的时候消费者不能消费。 消费者消费的时候生产者不能生产。 但是我们的生产者拿到锁的时候去生产, 消费者有没有可能正在处理数据? 我们消费者拿到锁去消费的时候,生产者有没有可能正在接收数据? 所以, 如果我们生产消费模型当中接收数据和消费数据都要花费时间, 那么我们就有可能产生类似我们生产者正在生产, 但是我们的消费者正在处理数据这,就是并发访问吗? 说白了就是一个正在访问临界区的代码, 一个正在访问非临界区的代码。 这样的话, 两个线程就高效并发的运行起来了吗? 所以, 生产和消费模型所谓的高效性, 体现的不是生产者和生产者互磕访问临界区, 而是有概率其中一个访问临界区, 另一个访问非临界区的时候是并发的!!在这种并发条件下, 我们的生产者和消费的动作就是同时跑的。 所以我们的生产和消费才是高效的。 

伪唤醒

        这里我们要讲解一下什么是伪唤醒, 这个是为了后面讲解信号量做准备。 首先我们要知道伪唤醒其实是对应的多个生产者和多个消费者的情况。 

        如果只有一个生产者, 有三个消费者。 假设此时生产者只生产了一条数据, 也就是说现在数据结构里面只有一条数据。 但是我们唤醒机制使用的是broadcast, 所以三个线程就全部被唤醒了。全部被唤醒之后, 三个线程就对这个锁展开了竞争假设第一个线程把锁持有了, 然后他就消费, 然后他消费完了之后, 就解锁了。 可是解锁之后, 我们的生产者没有拿到这个锁, 而是另一个消费线程拿到了这个锁。 可是此时数据结构种已经空了,注意,此时消费者已经处于临界区里面了, 所以消费者拿到锁后继续向后执行pop就发生错误了所以就产生了一种伪唤醒或者叫做误唤醒的情况。 

——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!    

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

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

相关文章

基于Matlab车牌识别课程设计报告模板(附源代码)

目 录 一&#xff0e;课程设计目的……………………………………………3 二&#xff0e;设计原理…………………………………………………3 三&#xff0e;详细设计步骤……………………………………………3 四. 设计结果及分析…………………………………………18 五. …

NC 单据模板自定义项 设置参照(自定义参照)

NC 单据模板自定义项 设置参照&#xff08;自定义参照&#xff09; 如图下图&#xff0c;NC 单据模板自定义项 设置参照&#xff1a; 1、选择需要设置参照的自定义字段&#xff0c;选择高级属性页签&#xff0c;在类型设置中&#xff0c;数据类型选择参照信息&#xff0c;即bd…

DART: Implicit Doppler Tomography for Radar Novel View Synthesis 笔记

Link&#xff1a;https://wiselabcmu.github.io/dart/ Publish&#xff1a; 2024CVPR Abstract DART主要任务就是用来合成雷达距离多普勒图像range-droppler&#xff0c;可用于生成高质量的断层扫描图像。 Related Work 1 Radar Simulation 基于模型的方法 任务&#xff…

通信工程学习:什么是VPN虚拟私人网络

VPN&#xff1a;虚拟私人网络 VPN&#xff0c;即虚拟私人网络&#xff08;Virtual Private Network&#xff09;&#xff0c;是一种通过公共网络&#xff08;如互联网&#xff09;建立的加密连接&#xff0c;用于保护用户的网络连接和数据传输的安全与隐私。以下是关于VPN的详细…

【数学二】一元函数积分学-定积分的应用-平面图形面积、旋转体体积、函数的平均值、平面曲线的弧长、旋转曲面面积

考试要求 1、理解原函数的概念&#xff0c;理解不定积分和定积分的概念. 2、掌握不定积分的基本公式&#xff0c;掌握不定积分和定积分的性质及定积分中值定理&#xff0c;掌握换元积分法与分部积分法. 3、会求有理函数、三角函数有理式和简单无理函数的积分. 4、理解积分上限…

动态内存管理 (上)

目录 1. 为什么要有动态内存分配 2. malloc和free 2.1 malloc 2.1 1 malloc 申请空间和数组的空间有什么区别呢&#xff1f; 2.2 free 3. calloc和realloc 3.1 calloc 3.2 realloc 4. 常⻅的动态内存的错误 4.1 对NULL指针的解引⽤操作 4.2 对动态开辟空间的越界访问 4…

CSS面试真题 part2

CSS面试真题 part2 11、css3新增了哪些新特性&#xff1f;12、css3动画有哪些&#xff1f;13、介绍一下grid网格布局14、说说flexbox&#xff08;弹性盒布局模型&#xff09;&#xff0c;以及使用场景&#xff1f;15、说说设备像素、css像素、设备独立像素、dpr、ppi之间的区别…

分机绑定线路和线路组(mod_cti基于FreeSWITCH)

文章目录 前言相关问题&#xff1a; 联系我们解决方案1. 创建线路2. 创建线路组3. 分机绑定线路组 前言 顶顶通呼叫中心中间件如果想要能外呼到手机上的话&#xff0c;那就必须对接能外呼的线路&#xff0c;这才可以实现分机与手机的通话。 相关问题&#xff1a; 如何设置一…

开发工具(上)

前面我们在Linux部分了解文件权限&#xff0c;和基本指令的内容&#xff0c;但对于开发工具还是没有很多的接触&#xff0c;现在这一篇就是主要讲基础的工具&#xff1b;如yum&#xff0c;yum源&#xff0c;包管理器等等&#xff1b; Linux中的安装软件&#xff1a; 源码安装 …

第23章 - Elasticsearch 洞悉你的查询:如何在上线前发现潜在问题!

文章目录 1. 前言2. Profile API - 查询优化2.1 Profile API 简单介绍2.2 查询结果图形化2.3 Profile 注意事项 3. Explain API - 解释查询 1. 前言 在第 21 章中&#xff0c;我介绍了 Elasticsearch 的读优化&#xff0c;但你是否曾疑惑&#xff1a;如何在上线前判断查询的耗…

Java项目-基于Springboot的农机电招平台项目(源码+说明).zip

作者&#xff1a;计算机学长阿伟 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、ElementUI等&#xff0c;“文末源码”。 开发运行环境 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBoot、Vue、Mybaits Plus、ELementUI工具&#xff1a;IDEA/…

基于因果推理的强对流降水临近预报问题研究

我国地域辽阔&#xff0c;自然条件复杂&#xff0c;灾害性天气种类繁多&#xff0c;地区差异性大。雷雨大风、冰雹、短时强降水等强对流天气是造成经济损失、危害生命安全最严重的一类灾害性天气。由于强对流降水具有高强度、小空间尺度等特点&#xff0c;一直是气象预报领域的…

前端js html css 基础巩固6

这样可以当做一个字典 来使用 每次 点击 键盘上的字母或数字 就可以获得 keyCode 这个 在实际应用中还是有可能使到的 所以大家可以练习一下 直接上代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta…

精选的四款强大视频压缩工具的整理:

大家好&#xff01;今天我来跟大家分享一下我使用过的几款视频压缩软件的体验感受&#xff0c;以及它们各自的好用之处&#xff1b;在这个信息爆炸的时代&#xff0c;视频文件越来越大&#xff0c;如何快速有效地压缩视频&#xff0c;同时还能保持较好的画质&#xff0c;是很多…

html+css+js实现Badge 标记

实现效果&#xff1a; 代码实现&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Badge…

【黑马Redis原理篇】Redis网络模型

来源视频 [16,27] 文章目录 1.用户空间和内核空间空间划分缓冲区 2.IO模型2.1 阻塞IO2.2 非阻塞IO2.3 IO多路复用2.3.1 阻塞和非阻塞的对比2.3.2 IO多路复用2.3.3 监听FD方式、通知的方式&#xff0c;有多种实现 2.4 信号驱动IO2.5 异步IO2.6 真正的同步和异步 1.用户空间和内…

opencv环境配置-适配b站阿童木的opencv教程

首先&#xff0c;opencv作为一个库文件&#xff0c;目的是为了让更多人不需要学习底层像素操作就能上手视觉技术&#xff0c;所以他适配很多环境&#xff0c;目前电脑端我知道的就可以适配C语言 C Python MCU端就是openmv跟他最类似&#xff0c;还有个k210 canmv 阿童木教的…

考研前所学的c语言01(2024/10/15)

1.变量由字母数字下划线组成&#xff0c;但是首字母只能是字母和下划线 2.基本函数01 3.基本代码02&#xff08;符号常量&#xff09; 4. A 是字符常量&#xff08;character constant&#xff09;。它表示单个字符&#xff0c;并且它的类型是 char&#xff0c;一个字节 "…

mysql connect -- C api编译链接问题,接口介绍(初始化和销毁,连接,执行sql语句,获取结果集的元数据和数据,设置编码格式)

目录 mysql connect 介绍 开发环境 编译链接问题 编译 链接 接口介绍 初始化和销毁 mysql_init() 句柄 mysql_close() 链接数据库 mysql_real_connect() 参数 返回值 show processlist 给mysql下达命令 mysql_query() 参数 返回值 查询结果的获取 引入 …

HarmonyOS NEXT 应用开发实战(七、知乎日报轮播图的完整实现)

在今天的博文中&#xff0c;我们将深入探讨如何在 HarmonyOS NEXT 中使用 ArkUI 实现一个轮播图组件。我们将通过一个示例代码来演示这个完整的过程&#xff0c;其中包含获取数据、管理数据源以及渲染组件等多个部分。 先来看下最终实现效果&#xff1a; 项目准备 首先&#…