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

Linux系统编程5(线程概念详解)

线程同进程一样都是OS中非常重要的部分,线程的应用场景非常的广泛,试想我们使用的视频软件,在网络不是很好的情况下,通常会采取下载的方式,现在你很想立即观看,又想下载,于是你点击了下载并且在线观看。学过进程的你会不会想,视频软件运行后在OS内形成一个进程,有一个执行流,但下载和在线观看是两件事情,这两件事情是如何同时进行的呢?你可能会想到CPU的时间片轮转,不过曾经提到过的时间片轮转是针对进程间的切换的,下载和在线观看这两件事本身处于同一个进程内完成,你可能还会想到在这个进程内创建一个子进程,主进程负责播放,子进程负责下载,这确实是一个解决问题的方法,但是创建一个进程所带来的开销是不小的。本篇文章将会介绍另一种更加轻便的解决方案——线程,同时我们需要重新理解CPU时间片轮转的调度单位

目录

什么是线程

深入理解页表 

理解进程和线程 

实践线程操作 

线程终止

线程等待 

分离线程 

线程取消 

TCB

线程的优缺点 

优点

缺点 

C++提供的线程库 


什么是线程

按照课本上的定义,线程就是进程内部的执行流,有多个执行流就意味着一个进程可以同时进行多个操作,比如视频软件,同时具备播放视频和下载视频的功能,如果只有一个执行流,那么在播放视频时就不能同时下载视频,因为播放视频和下载视频的代码是不同的

以前我们一直认为进程是CPU的调度单位,现在我们要改变这个看法,被CPU调度意味着被CPU执行,也就是一个执行流,一个进程里可以有多个线程,线程才是CPU的调度单位。所谓的调度单位就是CPU时间片轮转时的切换单位,以前我们解释CPU时间片轮转时说的是每个进程都被分配一定的CPU执行时间,到达时间,CPU会强制切换到下一个进程,以保证每个进程都能够被执行

此时,通过线程的概念能得知,CPU时间片轮转切换的并不是进程,而是线程。但上面的话并没有说错,一是创建一个进程时,默认只有一个执行流,也就是只有一个线程,时间片轮转时可以认为是切换进程。二是在后面我们将学习到Linux其实并没有线程的概念,所谓的线程在Linux中是轻量级进程

有些懵没有关系,后面会一一解释原因

现在线程的概念先放到一边,我们接下来再次回顾曾经学习过的进程地址空间

深入理解页表 

这是笔者曾经多次提到过的进程地址空间映射图,并且说过虚拟地址空间和物理内存之间一一映射,那么大家有没有思考过这么一个问题,假设虚拟地址空间有4G大小,物理内存也是4G大,而页表是虚拟地址空间和物理地址空间的一一映射,这意味着页表自身得有8G大小的空间才能够满足虚拟地址空间和物理内存之间一一映射,要知道,页表可也得加载到内存中才能让CPU执行,照这样的映射法,物理内存连一个页表都存不了,更何况4G物理内存空间还得留1G给OS呢

可想而知,页表的映射不会像哈希表那样一一对应,要明白页表的真实构造,我们就得从物理内存的划分开始

 ​​​

实际上物理内存是按4kb为单位进行划分的,每个大小单位被称为页框,大家知道磁盘往内存中加载数据时就是以4kb大小为单位,正好能够加载到物理内存的页框中,这看似巧妙的背后是前人无数日夜的精心设计

但是这好像并没有说明页表的真实构造,别急,接着往下看

真实的页表并不是只有一张,页表里存储的也不是虚拟地址和物理地址的一一对应,页表里正真存储的是物理内存中每个页框的起始地址,一张页表里只存储指定数量的页框,整个物理内存的页框被多张页表存储着

这多张页表被页目录记录着,通过页目录可以找到每一张页表,到这里,页表的整体结构就出来了,可见,当初我们刚了解页表时,进行了很大程度的简化。但是这就结束了吗?笔者只是把页表真实的结构给描绘出来,但是并没有解释现在的页表是如何进行映射的 

 

上图是虚拟内存中的一个虚拟地址,接下来我们刨析这个虚拟地址如何通过页表最终映射到物理内存 

虚拟地址映射到物理内存的方法就在地址本身上,通过虚拟地址的前10位可以到页目录中找到该地址对应在哪个页表,找到具体的页表之后,虚拟地址的中间10位标识着该地址在物理内存的哪个页框里,找到具体的页框之后,那么最后12位想必大家已经猜出来了

最后12位正是页框内的偏移地址,因为一个页框大小就是4kb,要想在某个页框内准确定位,就要知道该页框的起始地址以及在该页框内的偏移地址。至于对不对,咱们验证一下

一个地址的大小是4字节,2的12次方是4096,4096 * 4字节 = 4kb,所以验证正确

如上,真实的页表映射结构就展现在我们眼前,笔者这里并不是心血来潮讲一下页表,通过上述的过程大家能感受到地址空间是进程接触并使用资源的窗口,页表则决定了,进程拥有哪些资源,只有页表映射到的物理内存,进程才能够访问,那么通过地址空间+页表映射进行资源划分,就可以对一个进程所用的资源进行分类

理解进程和线程 

现在回到对线程的讲解上,前面说到过线程是进程内部的执行流,一个进程可以拥有多个线程,如下图,这些线程通过使用共同的地址空间和页表从而共享进程的资源,这意味着一个进程里的多个线程共享该进程的资源

前面还提到过,CPU的基本调度单位是线程,被CPU调度执行,那就得有上下文信息,那么线程就要保存好自己的上下文信息,当被CPU切换执行时,可以将上下文信息重新载入到CPU的寄存器中。线程在共享进程资源的同时也会产生自己的执行数据,也是需要保存起来的。线程是CPU调度的基本单位,这就意味着系统中会存在大量的线程等待被CPU调用,根据以往的经验,存在大量的线程时,OS要有序将其管理起来,就得给线程设计一种数据类型,设计方法还是多次提到过的先描述,再组织

给线程设计数据类型就要考虑线程的id号在系统中唯一,同时要能存储上下文信息,线程在被CPU调度时,要有自己的状态信息,同时在执行过程中要有自己的栈结构, 并且线程共享进程资源,那么文件描述符表什么的也要有,越往下举例,就能明显感受到这不就是当初学习进程时,进程的结构里所包含的内容吗?

可以发现进程结构和线程结构大量的内容都是重叠的,如果进程和线程两种结构同时存在系统中,就会造成大量的冗余,而Linux是一个非常注重效率的OS,于是聪明的Linux设计者决定不为线程设计一个独立的结构,而是采用了轻量级进程结构,也就是说在Linux系统中,进程和线程实际上使用的是同一种结构

这一点与windows有很大的不同,windows就为线程设计了一个独立的结构,这也体现了两种OS各自的设计哲学

如何理解线程就是轻量级进程呢?如何理解现在的进程概念呢?

曾经我们认为一个task_struct就是一个进程,一个task_struct有一个执行流,并且记录着该执行信息的执行状态。现在学了线程,知道进程和线程共同使用task_struct结构,对于每个进程或线程,内核都会为其分配一个唯一的task_struct结构,现在的task_struct是一种轻量级进程,也就是说一个进程里可能含有多个task_struct,不能再将一个task_struct理解成一个进程。但这并不是说曾经学的就是错误的,曾经创建一个进程,默认有一个执行流,也就是有一个主线程,该主线程是创建进程本身的执行流,task_struct就是这个主线程的结构,故而也可以将task_struct理解成进程本身,但是多线程后,有多个task_struct,再按照以前的方法理解进程就显得不严谨了

假设现在一个进程创建了三个线程,那么会有几个task_struct呢?

如果一个进程创建了三个线程,那么通常会有四个task_struct结构,在Linux中,每个进程都有一个主线程,也就是创建该进程的线程,主线程有一个对应的task_struct结构,对于每个创建的线程,也会有一个对应的task_struct结构。 故而,对于一个进程而言,如果额外创建了三个线程,那么会有一个主线程的task_struct结构,以及三个子线程的task_struct结构,共计四个task_struct结构,这四个task_struct结构共同构成了该进程的线程组成部分

站在CPU的角度上,曾经时间片轮转时切换task_struct就是切换一个进程,现在CPU时间片轮转切换一个task_struct是切换进程的一个分支,如果这个进程只有一个主线程,那就是切换进程本身

总而言之,现在一个进程有多个执行流,进程的概念不能局限于曾经只有一个执行流的task_struct,而是一个拥有多个task_struct的承担分配系统资源的基本实体

实践线程操作 

说了这么多,咱们连线程长什么样子都不知道,接下来咱们通过实践来感受线程的魅力

不过在动手敲代码之前,需要明确一些事情,因为用轻量级进程来表示线程是Linux系统独特的线程处理方式。虽然这能很大提高效率,但是也带来了不通用的麻烦,很多OS,包括OS的理论基础上都是有线程这个概念的,因此并不通用Linux的轻量级进程,大家都在使用线程接口,而你Linux搞特殊提供轻量级进程接口,大家是不认的,为了解决这个问题,Linux工程师就将轻量级进程接口进行封装,适配成大家都通用的线程接口

这意味着,我们在使用Linux线程接口时,要在编译时带上线程动态库即选项 -l pthread

创建一个线程是通过接口

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

头文件:pthread.h   
参数
thread:返回线程ID (输出型参数)
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码

第一个参数是一个输出型参数,我们在主函数里创建一个pthread_t类型的变量,将其的地址传过去,创建线程后,会把该线程的id写入到这个pthread_t类型变量里

我们目前不需要关心 att r这个参数,可以看到第三个参数是一个函数指针,其所指向的函数就是创建一个线程后,该线程去执行的任务

第四个参数是对第三个参数的补充,在我们编写线程要执行的函数时,有时是需要外部给这个函数传参的,那个这个函数就会默认有一个void* 类型的参数,这个参数就是通过pthread_create的第四个参数传递过去的

下面看一个线程代码示例

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<cstring>
#include<cstdlib>
#include<cstdio>using namespace std;void* start_routine(void * arg)
{while(true){printf("%s\n", (char*)arg);sleep(1);}
}int main()
{pthread_t thread_id;char buff[64];snprintf(buff, sizeof(buff), "我是新创建的线程,我正在运行");pthread_create(&thread_id, nullptr, start_routine, (void*)buff);int counter = 10;while(counter--){printf("我是主线程,运行倒计时:%d\n", counter);sleep(1);}return 0;
}

这个示例可以看到,真的有两个执行流同时在跑

通过命令ps -aL可以查看所有进程内的线程,接下来我们让两个线程不间断运行,然后查看这两个线程的相关信息

可以发现,当test程序跑起来后,出现了两个test线程,这两个线程的PID是相同的,说明这两个线程来自同一个进程,不过两个线程的LWP不同,LWP(light weight process,即轻量级进程)LWP就是所谓的线程ID了,并且第一个线程的PID和LWP相同,这说明该线程是主线程,CPU在调度时,是以LWP为标识,表示一个特定的执行流

上面只是创建单个线程,那么如何同时创建多个线程呢?

看下面的demo,我们一次创建10个线程,并且不停打印他们的序号

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<cstring>
#include<cstdlib>using namespace std;#define MAX 10void* _start_test(void* arg){while(true){sleep(1);cout << (char*)arg << endl;}return nullptr;
}int main(){for (int i = 0; i<MAX; i++){pthread_t tid;char buff[64];snprintf(buff, sizeof(buff), "this is %d thread", i);pthread_create(&tid, nullptr, _start_test, buff);}while(true){sleep(1);cout << "我是主线程"<<endl;}return 0;
}

 

当执行结果出来后,完全超出了我们的预期,我们本想这10个线程,各自打印各自的序号,可是结果每个线程都打印序号9

出现这种情况的原因是线程被创建后的执行顺序是不确定的,当第一个被创建的进程还没来得及执行它的start_routine函数时,主线程就已经把所有的线程都创建完毕了,buff是在循环里被被创建的,出了循环后就被销毁,然后再次创建,因为都是在同一个栈里,所以每次buff的地址都不变,且buff的值不断被覆写,直到最后一个线程创建完毕,buff的值被覆写为序号9

此时循环退出,buff也被销毁了,但是由于main函数这个栈还在,也没有开其他的栈,因此原先buff指向的空间并没有被清理,导致所有的线程都打印最后一次覆写buff的内容,通过这个demo,可得知线程除了独自的PCB,独自的上下文结构,独自的栈结构,其他几乎所有内容都是共享的

每一个线程都有自己独立的栈,这是因为一个线程在执行时,可能会调用各种函数,因此需要一个独立的栈,这个栈里的内容不与其他线程共享

线程终止

会创建线程之后,自然而然的会想到,线程如何终止,导致线程终止的原因有很多

1.执行完start_routine()后,线程会自动return结束

2.使用pthread_exit()来终止当前线程,但是要注意,不要习惯性的使用exit()来终止线程,exit()是用来终止进程的,进程终止,该进程内所有的线程都会终止

3.某个线程执行过程中,出现错误,触发OS检查,会给当前线程的进程发送信号,进程收到信号会终止,该进程内其他所有线程都会终止

4. 一个线程可以调用pthread_ cancel()终止同一进程中的另一个线程

线程等待 

同进程一样,线程结束后其所申请的各种资源都是需要被回收的,不然会产生类似僵尸进程一样的问题,线程等待使用函数pthread_join()

int pthread_join(pthread_t thread, void **retval);

第一个参数就是被等待的线程id,第二个参数是获取该线程的返回值的,还记得start_routine有一个void* 返回值吗?这个返回值就是通过pthread_join获取的

注意:OS维护的是轻量级进程PCB,因为Linux特殊的线程方案,可以说没有线程概念。程序员日常使用习惯了线程接口,因此Linux提供了线程库,线程库负责线程接口与轻量级进程接口之间的转换,以及维护用户通过接口创建好的线程

分离线程 

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源
使用接口:int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
pthread_detach(pthread_self());   pthread_self()获取自己的线程id

 需要注意的是,一旦一个线程已经处于分离状态,那么该线程就不能被等待

线程取消 

线程取消也就是当线程跑起来后,我们通过主线程或者其他线程可以取消这个线程继续运行

也可以自己取消自己

int pthread_cancel(pthread_t thread);

返回值:成功返回0;失败返回错误码

注意:只有当该进程运行起来,有自己的线程ID时才可以被取消

TCB

PCB是Linux内核用来管理轻量级进程的内核,因为Linux没有线程的概念,程序员要使用线程的接口,因此要通过线程库进行转接,那么程序员每申请一个线程,线程库就得维护好这个线程和轻量级进程进行转换,那么TCB就是线程库维护线程的结构

  

由图中可以得知,我们接收的所谓的线程id值其实就是库中维护的该线程TCB的起始地址 

线程的优缺点 

优点

线程的使用能非常大程度上发挥多核CPU的实力,并且创建多个线程比创建多个进程的开销要小的多,为什么呢?

如果CPU执行时,要切换一个进程,那么要切换的内容至少包含页表,虚拟地址空间,PCB,上下文数据

而切换一个线程,那么只需要切换PCB,上下文数据等主要内容

CPU在执行一个进程时,会在寄存器中缓存该进程的很多热点数据,例如虚拟地址空间,页表等,一旦切换进程,这些热点数据要全部重新加载,而切换线程,这些数据不需要动

缺点 

在运行计算密集型程序时,线程需要不停的计算,持续占有CPU,切换到其它线程的时间就会延长,导致效率低下

使用多线程编程会有互斥和同步等问题,程序的编写和维护成本很高

C++提供的线程库 

虽说Linux提供了线程库,但是Linux的线程接口和Windows下的线程接口很多都是不同的,这就导致程序的可移植性很低, C++11之后,在语言层面上对Linux和windows平台下的线程接口再进行一次封装。如此以来,用C++线程库编写的多线程程序可以同时在这两个OS平台下执行,代码的可移植性大大提高

下面的demo简单演示了如何使用C++提供的线程库,这部分属于C++的知识了,笔者将在C++专栏中介绍其详细使用方法

#include<thread>
#include<iostream>
#include<unistd.h>using namespace std;void* start_routine()
{int counter = 10;while(counter--){sleep(1);cout << "我是新创建的线程,运行倒计时:" << counter <<endl;}return nullptr;
}int main()
{//创建一个线程,并把执行函数传递过去thread t1(start_routine);cout << "我是主线程" <<endl;//主线程阻塞等待回收子线程t1.join();cout << "线程回收完毕,准备退出"<<endl;return 0;
}

文章的最后,大家可以尝试自己模仿C++的线程库,对Linux的线程库再进行一次封装 

相关文章:

Linux系统编程5(线程概念详解)

线程同进程一样都是OS中非常重要的部分&#xff0c;线程的应用场景非常的广泛&#xff0c;试想我们使用的视频软件&#xff0c;在网络不是很好的情况下&#xff0c;通常会采取下载的方式&#xff0c;现在你很想立即观看&#xff0c;又想下载&#xff0c;于是你点击了下载并且在…...

leetcode645. 错误的集合(java)

错误的集合 题目描述优化空间代码演示 题目描述 难度 - 简单 LC645 - 错误的集合 集合 s 包含从 1 到 n 的整数。不幸的是&#xff0c;因为数据错误&#xff0c;导致集合里面某一个数字复制了成了集合里面的另外一个数字的值&#xff0c;导致集合 丢失了一个数字 并且 有一个数…...

Pytest参数详解 — 基于命令行模式

1、--collect-only 查看在给定的配置下哪些测试用例会被执行 2、-k 使用表达式来指定希望运行的测试用例。如果测试名是唯一的或者多个测试名的前缀或者后缀相同&#xff0c;可以使用表达式来快速定位&#xff0c;例如&#xff1a; 命令行-k参数.png 3、-m 标记&#xff0…...

【python爬虫】3.爬虫初体验(BeautifulSoup解析)

文章目录 前言BeautifulSoup是什么BeautifulSoup怎么用解析数据提取数据 对象的变化过程总结 前言 上一关&#xff0c;我们学习了HTML基础知识&#xff0c;知道了HTML是一种用来描述网页的语言&#xff0c;又了解了HTML的基本结构。 认识了HTML中的常见标签和常见属性&#x…...

【Three.js + Vue 构建三维地球-Part One】

Three.js Vue 构建三维地球-Part One Vue 初始化部分Vue-cli 安装初始化 Vue 项目调整目录结构 Three.js 简介Three.js 安装与开始使用 实习的第一个任务是完成一个三维地球的首屏搭建&#xff0c;看了很多的案例&#xff0c;也尝试了用 Echarts 3D地球的模型进行构建&#xf…...

Power View

界面 切换可视化效果 对于已经上传到透视表的数据&#xff0c;选择power view&#xff0c;形成表格后。...

SQL查询本年每月的数据

--一、以一行数据的形式&#xff0c;显示本年的12月的数据&#xff0c;本示例以2017年为例&#xff0c;根据统计日期字段判断&#xff0c;计算总和&#xff0c;查询语句如下&#xff1a;selectsum(case when datepart(month,统计日期)1 then 支付金额 else 0 end) as 1月, sum…...

C++之struct和union对比介绍

C之struct和union对比介绍 在C中&#xff0c;struct和union都是用来定义自定义数据类型的关键字&#xff0c;但它们的作用略有不同。 首先了解一下它们的基本概念&#xff1a; struct&#xff08;结构体&#xff09;&#xff1a;struct 是一个用户自定义的数据类型&#xff…...

微服务--SkayWalking(链路追踪:国产开源框架)

SkayWalking&#xff1a;分布式系统的应用程序性能监视工具 作用&#xff1a;分布式追踪、性能指标分析、应用、服务依赖分析&#xff1b; SkayWalking性能剖析&#xff1a; 我操&#xff0c;能够定位到某一个方法会有多慢。。。 通过Tid查看全局所有的日志信息&#xff08…...

在Windows 10上部署ChatGLM2-6B:掌握信息时代的智能对话

在Windows 10上部署ChatGLM2-6B&#xff1a;掌握信息时代的智能对话 硬件环境ChatGLM2-6B的量化模型最低GPU配置说明准备工作ChatGLM2-6B安装部署ChatGLM2-6B运行模式解决问题总结 随着当代科技的快速发展&#xff0c;我们进入了一个数字化时代&#xff0c;其中信息以前所未有的…...

LRU和LFU算法的简单实现

LRU #include <iostream> #include <unordered_map> #include <list> struct Node{int key;int value;Node(int key, int value):key(key),value(value){} }; class LruCache{ private:int maxCapacity;// 最大容量std::list<Node>CacheList;// 缓存链…...

OCR多语言识别模型构建资料收集

OCR多语言识别模型构建 构建多语言识别模型方案 合合&#xff0c;百度&#xff0c;腾讯&#xff0c;阿里这四家的不错 调研多家&#xff0c;发现有两种方案&#xff0c;但是大多数厂商都是将多语言放在一个字典里&#xff0c;构建1w~2W的字典&#xff0c;训练一个可识别多种语…...

倍增的经典题目:扩大区间、st表

1. 扩大区间 P4155 [SCOI2015] 国旗计划例题1&#xff1a;P4155 [SCOI2015] 国旗计划 计算能覆盖整个圆圈的最少区间&#xff0c;题目给定的所有区间互相不包含&#xff0c;按区间左端点排序后&#xff0c;区间的右端点也是单增的。 我们首先需要化圆为线&#xff0c;然后贪…...

LeetCode——和为K的子数组(中等)

题目 给你一个整数数组 nums 和一个整数 k &#xff0c;请你统计并返回 该数组中和为 k 的连续子数组的个数 。 示例 1&#xff1a; 输入&#xff1a;nums [1,1,1], k 2 输出&#xff1a;2示例 2&#xff1a; 输入&#xff1a;nums [1,2,3], k 3 输出&#xff1a;2 题解 …...

Truncation Sampling as Language Model Desmoothing

本文是LLM系列文章&#xff0c;针对《Truncation Sampling as Language Model Desmoothing》的翻译。 截断采样作为语言模型的去平滑性 摘要1 引言2 背景3 截断作为去平滑性4 方法5 实验与结果6 相关工作7 结论8 不足 摘要 来自神经语言模型的长文本样本可能质量较差。截断采…...

docker安装jenkins

运行jenkins docker run -d \--name jenkins \ --hostname jenkins \-u root \-p 29090:8080 \--restart always \-v D:\springcloud\学习\jekins\jenkins\jks_home:/var/jenkins_home \ jenkins/jenkins获取root登录密码 密码在jekins_home/secrets/initalAdminPassword文件…...

学习pytorch8 土堆说卷积操作

土堆说卷积操作 官网debug torch版本只有nn 没有nn.functional代码执行结果 B站小土堆视频学习笔记 官网 https://pytorch.org/docs/stable/nn.html#convolution-layers 常用torch.nn, nn是对nn.functional的封装&#xff0c;使函数更易用。 卷积核从输入图像左上角&#xf…...

pytest自动化测试两种执行环境切换的解决方案

目录 一、痛点分析 方法一&#xff1a;Hook方法pytest_addoption注册命令行参数 1、Hook方法注解 2、使用方法 方法二&#xff1a;使用插件pytest-base-url进行命令行传参 一、痛点分析 在实际企业的项目中&#xff0c;自动化测试的代码往往需要在不同的环境中进行切换&am…...

说说TIME_WAIT和CLOSE_WAIT区别

分析&回答 TCP协议规定&#xff0c;对于已经建立的连接&#xff0c;网络双方要进行四次握手才能成功断开连接&#xff0c;如果缺少了其中某个步骤&#xff0c;将会使连接处于假死状态&#xff0c;连接本身占用的资源不会被释放。网络服务器程序要同时管理大量连接&#xf…...

Docker的优势

Docker是一种开源的容器化平台&#xff0c;提供了一种将应用程序、库和其它依赖项封装在容器中的方法。以下是Docker的基本概念和优势&#xff1a; 基本概念&#xff1a; 镜像&#xff1a;一个Docker镜像是一个可运行的软件包&#xff0c;包括应用程序、库和其它依赖项。它是D…...

在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能

下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能&#xff0c;包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...

多场景 OkHttpClient 管理器 - Android 网络通信解决方案

下面是一个完整的 Android 实现&#xff0c;展示如何创建和管理多个 OkHttpClient 实例&#xff0c;分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...

2.Vue编写一个app

1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...

Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具

文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...

linux arm系统烧录

1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 &#xff08;忘了有没有这步了 估计有&#xff09; 刷机程序 和 镜像 就不提供了。要刷的时…...

C# SqlSugar:依赖注入与仓储模式实践

C# SqlSugar&#xff1a;依赖注入与仓储模式实践 在 C# 的应用开发中&#xff0c;数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护&#xff0c;许多开发者会选择成熟的 ORM&#xff08;对象关系映射&#xff09;框架&#xff0c;SqlSugar 就是其中备受…...

CRMEB 框架中 PHP 上传扩展开发:涵盖本地上传及阿里云 OSS、腾讯云 COS、七牛云

目前已有本地上传、阿里云OSS上传、腾讯云COS上传、七牛云上传扩展 扩展入口文件 文件目录 crmeb\services\upload\Upload.php namespace crmeb\services\upload;use crmeb\basic\BaseManager; use think\facade\Config;/*** Class Upload* package crmeb\services\upload* …...

CMake控制VS2022项目文件分组

我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...

蓝桥杯3498 01串的熵

问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798&#xff0c; 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...

Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)

目录 一、&#x1f44b;&#x1f3fb;前言 二、&#x1f608;sinx波动的基本原理 三、&#x1f608;波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、&#x1f30a;波动优化…...