【Linux】线程ID与互斥、同步(锁、条件变量)
作者主页: 作者主页
本篇博客专栏:Linux
创作时间 :2024年11月24日
线程ID及进程地址空间布局
先看一下这段代码:
运行一下:
运行这个代码之后,我们看到的这个很大的数字就是线程id,然后我们可以用ps -aL命令来查看一下:
这里面的第一串数字就是pid,第二串数字就是LWP,我们可以看到LWP跟线程id是不一样的。所以给用户提供的线程id,不是内核中的lwp,而是pthread库自己维护的一个值
通过这个代码:我们把上面的数字转换成十六进制,可以看出这是一个地址:
理解库
动态库在运行时被加载,动态库没被加载前在磁盘当中
pthread库本质也是一个文件
我们刚刚写的可执行程序,他也是一个文件,他也在磁盘当中,可执行程序内部用线程库来创建多线程。
程序运行的时候,会变成一个进程,加载到内存当中,内存中就有了该进程的代码和数据。创建线程的时候,我们要先把线程库加载到内存当中,然后再映射到该进程的地址空间才能用,映射要映射到堆栈之间的共享区,如果有多个多线程进程,只需要将共享取得代码经过页表映射到已经加载到内存的库,此时多个进程就可以使用同一个库里的方法来创建线程
Linux只维护轻量级进程,linux中的pcb里与执行流相关的属性都是轻量级进程的属性,所有的属性都是围绕lwp展开的。我们在用户层的概念是线程,要的是线程的id,与线程相关的内容在Linux中是没有的,它没有维护。所以这部分属性由库来进行维护。
为了更好的管理线程,创建线程时,库会为我们的每一个线程申请一个内存块(描述线程的相关结构体字段属性)。
未来要找一个线程的所有属性,只要找到线程控制块的地址即可。所以pthread_t id就是一个地址。
pthread_t类型的线程ID,本质就是线程属性集合的起始虚拟地址 ---- 在pthread库中维护。
有这些打印出来的值可以看出,全部变量是受所有线程所控制的
但如果我们这样去写的话:
如果我们想让两个线程各自私有一份变量,g++有一个编译选项 __thread
用__thread修饰这个全局变量即可。运行后,主线程和新线程gval的地址也不一样了。这种情况叫线程的局部存储,原始代码里只看到一个gval,但是他们用的是各自的gval。
这种情况只在Linux中有效。__thread只能用来修饰内置类型
线程简单封装
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>namespace ThreadMoudle
{// 线程要执行的方法,后面我们随时调整typedef void (*func_t)(const std::string &name); // 函数指针类型class Thread{public:void Excute(){std::cout << _name << " is running" << std::endl;_isrunning = true;_func(_name);_isrunning = false;}public:Thread(const std::string &name, func_t func):_name(name), _func(func){std::cout << "create " << name << " done" << std::endl;}static void *ThreadRoutine(void *args) // 变成static,内部没有this指针,pthread_create就能匹配上了{Thread *self = static_cast<Thread*>(args); // 获得了当前对象self->Excute();return nullptr;}bool Start(){int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);//传this,线程函数才能拿到_func方法if(n != 0) return false;return true;}std::string Status(){if(_isrunning) return "running";else return "sleep";}void Stop(){if(_isrunning){::pthread_cancel(_tid);_isrunning = false;std::cout << _name << " Stop" << std::endl;}}void Join(){::pthread_join(_tid, nullptr);std::cout << _name << " Joined" << std::endl;}std::string Name(){return _name;}~Thread(){}private:std::string _name;pthread_t _tid;bool _isrunning;func_t _func; // 线程要执行的回调函数};
} // namespace ThreadModle
线程互斥
进程线程间的互斥相关背景概念
- 多线程执行流共享的资源叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任一时刻,互斥保证只有一个线程可以进入临界区去访问临界资源,通常对临界资源起保护作用
- 原子性:不会被任何调度机制所打断的操作,该操作只有两个状态,要么完成,要么未完成
下面通过抢票代码演示:
void route(const std::string &name)
{while(true){if(tickets > 0){// 抢票过程usleep(1000); // 1ms -> 抢票花费的时间printf("who: %s, get a ticket: %d\n", name.c_str(), tickets);tickets--;}else{break;}}
}int main()
{Thread t1("thread-1", route);Thread t2("thread-2", route);Thread t3("thread-3", route);Thread t4("thread-4", route);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();
}
线程就用我们前面封装的线程。每抢到一张票就--,直到没票为止。
由上面结果可知,抢票抢到负数去了。因此多线程并发访问公共资源时可能会引发异常。
- 但部分情况,线程所使用的变量都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属于单个线程,其他线程无法获得这种变量
- 但有时候,很多变量需要在线程间共享,这种变量称之为共享变量,通过数据的共享,完成线程之间的交互
- 多个线程并发的一些操作,就会带来一些问题
为什么会出现抢到负数的问题呢?
判断的过程就是计算,是由CPU来做的,判断的大致过程:
数据要先从内存移到对应的寄存器当中,然后再进行逻辑判断,然后才能得到结果,最后CPU再决定到底是进行if操作还是else操作
之前讲过,CPU内的寄存器只有一套,但是寄存器内的数据可以有多套
这里我们有四个线程进行抢票,如果一个线程在判断到一半被切换了,他需要把寄存器中的数值带走,等到被唤醒的时候,再把数值恢复过来
比如:假设票只剩一张了,线程a已经在判断完了,当他准备抢票的时候,他被切换了。此时他就把上下文数据保存。线程b被唤醒了,他也来进行票数判断,因为刚刚线程a还没来得及抢票,票数没--,所以线程b也判断成功,他也要抢票。线程a此时被唤醒,就往后执行代码进行抢票,然后--,票数就变成0。线程b又被唤醒,票数又--,就变成负数了。
如何解决上面的问题呢?加锁!
锁
pthread_mutex_t是互斥锁类型
互斥锁在任何时刻,只允许一个线程进行资源访问。
有了锁,我们往往需要初始化和销毁锁,初始化有两种做法:
- 如果定义的是全局或者静态的锁,可以只使用pthread_mutex_t 锁的名字 =PTHREAD_MUTEX_INITIALIZER
- 如果定义的这把锁是动态申请的,比如new或栈上开辟的,必须使用pthread_mutex_init函数来进行初始化。参数1就是你自己定义的锁,参数2是属性,直接设为nullptr即可
用完锁后,还需要销毁,用pthread_mutex_destroy函数,参数是锁的地址。如果锁是静态或者全局的,我们不需要destroy,全局的或者静态的变量会随着进程的运行而一直存在,进程结束他也就自动释放了。初始化和销毁的返回值,成功返回0,失败返回-1。
一旦有了锁,我们就需要对临界区进行保护, 就需要加锁和解锁。要对某个区域加锁,就要调用pthread_mutex_lock函数来加锁,参数就是你定义的锁。要解锁,就用pthread_mutex_unlock函数。
lock的情况:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量, 那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
所谓对临界区资源进行保护,本质是对临界区代码进行保护。
把前面抢票的代码改一下,加锁。运行结果发现不会再抢到负数了,而且运行时间明显比之前要长。
- 加锁的范围,粒度要尽量小,即临界区尽量小。
- 所有线程申请锁,前提是所有线程都看得到这把锁,因此锁本身也是共享资源。所以加锁的过程必须是原子的
- 原子性:要么不做,要么完成了。没有中间状态,就是原子性。
- 如果线程申请锁失败了,线程就要被阻塞
- 如果线程申请锁成功了,就继续往后运行,执行临界区代码。
在执行临界区代码期间,线程可以被切换。假设线程1正在执行临界区代码,此时线程被切换了,其他线程也无法进入临界区,因为线程1并没有释放锁。
结论:我这个线程访问临界区,对其他线程来说是原子的。因为对于其他线程,我要么没有申请锁,要么释放了锁,这样对他们才有意义。
锁简单封装
因为是临时对象,在循环结束后会自动调用析构函数销毁。
互斥量实现原理
- 经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
- 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
Linux线程同步
同步概念与竞态条件
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
- 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。
如上图,线程2一直抢到票,其他线程一直抢不到,这时候就需要线程同步
条件变量
- 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
- 例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
举例:有A,B,C三个人,一个盘子。B拿出苹果放到盘子上,另外两人就可以到盘子上拿。为了在放苹果的时候,其他人不能来拿,就要加锁,盘子就是临界区。因为另外两人想拿苹果,就一直申请锁,导致B放不了苹果。此时就需要一个铃铛。A,C两人在外面排队,当B放好苹果后就摇铃铛,此时A和C就会根据排队的顺序依次进去拿苹果。
上面的铃铛就是条件变量,人就是线程。摇铃铛后,可以规定是唤醒一个线程还是唤醒全部。
认识接口
条件变量是 pthread_cond_t 的数据类型。它的使用跟前面互斥锁一样,可以定义成局部或者全局的。如果是全局或者静态的,可以直接使用 PTHREAD_COND_INITIALIZER 初始化。
如果是局部的,就用pthread_cond_init 函数初始化,使用完了就destroy销毁掉。
线程条件不满足时,线程就要等待,要在指定的条件变量上等待。
cond:要在这个条件变量上等待
等待完成后,就要进行唤醒。
pthread_cond_signal 表示唤醒一个线程。 pthread_cond_broadcast 表示唤醒所有线程。
生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
生产者消费者模型优点:
- 解耦
- 支持并发
- 支持忙闲不均
为了方便记忆,这里有一个“321”原则:
- 一个交易场所(一段内存空间)
两种角色(生产、消费角色)
三种关系(生产和生产、消费和消费 、生产和消费)前两种是互斥关系,最后一种是互斥和同步的关系
基于BlockingQueue的生产者消费者模型
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出
BlockQueue.hpp
#pragma once#include <iostream>
#include <string>
#include <queue>
#include <pthread.h>const static int defaultcap = 5;template <typename T>
class BlockQueue
{
private:bool IsFull(){return _block_queue.size() == _max_cap;}bool IsEmpty(){return _block_queue.empty();}public:BlockQueue(int cap = defaultcap) : _max_cap(cap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_p_cond, nullptr);pthread_cond_init(&_c_cond, nullptr);}// 假设:2个消费者void Pop(T *out){pthread_mutex_lock(&_mutex);while (IsEmpty()) // while可以保证代码的鲁棒性(健壮性),不用if,因为如果只有一个生产品{ //,有两个消费者,一次性唤醒两个消费者的话,他们会竞争锁,其中一个拿完东西后,另一个才能重新拿到锁//另一个拿到锁后,不用if的话,就跳过判断了,此时队列是空的,就会异常,所以用while// 添加尚未满足,但是线程被异常唤醒的情况,叫做伪唤醒!pthread_cond_wait(&_c_cond, &_mutex); // 两个消费者都在这里等待了}// 1. 没有空 || 2. 被唤醒了*out = _block_queue.front();_block_queue.pop();// if(_block_queue.size() > hight_water)// pthread_cond_signal(&_p_cond);pthread_mutex_unlock(&_mutex);pthread_cond_signal(&_p_cond);}// 一个生产者void Equeue(const T &in){pthread_mutex_lock(&_mutex);while (IsFull()) {// 满了,生产者不能生产,必须等待// 可是在临界区里面啊!// 被调用的时候:除了让自己继续排队等待,还会自己释放传入的锁// 函数返回的时候,不就还在临界区了!// 返回时:必须先参与锁的竞争,重新加上锁,该函数才会返回!pthread_cond_wait(&_p_cond, &_mutex);}// 1. 没有满 || 2. 被唤醒了_block_queue.push(in); // 生产到阻塞队列pthread_mutex_unlock(&_mutex);// 让消费者消费pthread_cond_signal(&_c_cond);//解锁和唤醒的顺序可以交换,不影响}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_p_cond);pthread_cond_destroy(&_c_cond);}private:std::queue<T> _block_queue; // 临界资源int _max_cap;pthread_mutex_t _mutex;pthread_cond_t _p_cond; // 生产者条件变量pthread_cond_t _c_cond; // 消费者条件变量// int low_water = _max_cap/3// int hight_water _max_cap/3*2
};
Main.cc
#include "BlockQueue.hpp"
#include "Task.hpp"
#include <pthread.h>
#include <ctime>
#include <unistd.h>void *Consumer(void *args)
{BlockQueue<task_t> *bq = static_cast<BlockQueue<task_t> *>(args);while(true){// 1. 获取数据task_t t;bq->Pop(&t);// 2. 处理数据// t.Excute();t();// std::cout << "Consumer -> " << t.result() << std::endl;}
}void *Productor(void *args)
{srand(time(nullptr) ^ getpid());BlockQueue<task_t> *bq = static_cast<BlockQueue<task_t> *>(args);while(true){// 1. 构建数据/任务// int x = rand() % 10 + 1; // [1, 10]// usleep(x * 1000);// int y = rand() % 10 + 1; // [1, 10]// Task t(x, y);// 2. 生产数据bq->Equeue(Download);std::cout << "Productor -> Download" << std::endl;sleep(1);}
}int main()
{BlockQueue<task_t> *bq = new BlockQueue<task_t>();pthread_t c1,c2, p1,p2,p3;pthread_create(&c1, nullptr, Consumer, bq);pthread_create(&c2, nullptr, Consumer, bq);pthread_create(&p1, nullptr, Productor, bq);pthread_create(&p2, nullptr, Productor, bq);pthread_create(&p3, nullptr, Productor, bq);pthread_join(c1, nullptr);pthread_join(c2, nullptr);pthread_join(p1, nullptr);pthread_join(p2, nullptr);pthread_join(p3, nullptr);return 0;
}
Task.hpp
#pragma once#include<iostream>
#include<functional>// typedef std::function<void()> task_t;
using task_t = std::function<void()>;void Download()
{std::cout << "我是一个下载的任务" << std::endl;
}// // 要做加法
// class Task
// {
// public:
// Task()
// {
// }
// Task(int x, int y) : _x(x), _y(y)
// {
// }
// void Excute()
// {
// _result = _x + _y;
// }
// void operator ()()
// {
// Excute();
// }
// std::string debug()
// {
// std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=?";
// return msg;
// }
// std::string result()
// {
// std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=" + std::to_string(_result);
// return msg;
// }// private:
// int _x;
// int _y;
// int _result;
// };
多线程生产者消费者模型高效是因为:一个生产者在生产任务的时候,其他生产者在构建任务,一个消费者在获取任务的时候,其他消费者在处理任务。
为什么线程在等待的时候,都是在加锁和解锁之间等待?
答:无论是生产者还是消费者,都必须先检查资源的状态。检查就是要访问,所以检查之前就要加锁,等待必须在临界区里进行等待,因为判断结果是在临界区里的。
最后:
十分感谢你可以耐着性子把它读完和我可以坚持写到这里,送几句话,对你,也对我:
1.一个冷知识:
屏蔽力是一个人最顶级的能力,任何消耗你的人和事,多看一眼都是你的不对。
2.你不用变得很外向,内向挺好的,但需要你发言的时候,一定要勇敢。
正所谓:君子可内敛不可懦弱,面不公可起而论之。
3.成年人的世界,只筛选,不教育。
4.自律不是6点起床,7点准时学习,而是不管别人怎么说怎么看,你也会坚持去做,绝不打乱自己的节奏,是一种自我的恒心。
5.你开始炫耀自己,往往都是灾难的开始,就像老子在《道德经》里写到:光而不耀,静水流深。
最后如果觉得我写的还不错,请不要忘记点赞✌,收藏✌,加关注✌哦(。・ω・。)
愿我们一起加油,奔向更美好的未来,愿我们从懵懵懂懂的一枚菜鸟逐渐成为大佬。加油,为自己点赞!
相关文章:

【Linux】线程ID与互斥、同步(锁、条件变量)
作者主页: 作者主页 本篇博客专栏:Linux 创作时间 :2024年11月24日 线程ID及进程地址空间布局 先看一下这段代码: 运行一下: 运行这个代码之后,我们看到的这个很大的数字就是线程id,然后…...

Android 13 编译Android Studio版本的Launcher3
Android 13 Aosp源码 源码版本 Android Studio版本 Launcher3QuickStepLib (主要代码) Launcher3ResLib(主要资源) Launcher3IconLoaderLib(图片加载,冲突资源单独新建) 需要值得注意的是: SystemUISharedLib.jar 有kotlin和java下的,在 Lau…...

burp功能介绍
声明! 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下,如涉及侵权马上删除文章,笔记只是方便各位师傅的学习和探讨,文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团队无关&a…...

Android12 的 Vold梳理
1.代码位置 system/vold/ 路径下,查看bp文件,发现是编译system/vold/main.cpp编译生成可执行文件vold 2.app侧调用代码流程 2.1 整体框架 #mermaid-svg-lqO8phN62rKNW407 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#…...

[pdf,epub]162页《分析模式》漫谈合集01-35提供下载
《分析模式》漫谈合集01-35的pdf、epub文件,已上传至本号的CSDN资源。 如果CSDN资源下载有问题,可到umlchina.com/url/ap.html。 已排版成适合手机阅读,pdf的排版更好一些。 ★UMLChina为什么叒要翻译《分析模式》? ★[缝合故事…...

Vue2教程003:Vue指令之v-bind和v-for
文章目录 2.6 v-bind2.7 图片轮播案例2.8 v-for2.9 图书管理案例 2.6 v-bind 作用:动态设置html的标签属性->src、url、title…语法:v-bind:属性名"表达式" 动态设置img标签的src属性: <body> <div id"app&quo…...

Pathlib操作文件IN Python
系列文章目录 文章目录 目录 系列文章目录 文章目录 前言 一、Pathlib是什么? 二、使用步骤 前言 pathlib 是 Python 标准库中用于操作文件和目录路径的模块,自 Python 3.4 起引入。它提供了一种面向对象的方式处理路径,使路径操作更加简洁、…...

AOC显示器915Sw按键失灵维修记
大家好,我是 程序员码递夫 今天给大家分享的是自己维修老古董AOC液晶显示器按键失灵的的过程,实属DIY记录。 1、引子 家里有台老古董的19寸AOC液晶显示器(型号915Sw), 一直作为我的副显示器陪伴着左右,显示还正常&a…...

霍曼转移方法介绍
霍曼转移方法介绍 背景 在航天工程中,轨道转移是指航天器从一个轨道移动到另一个轨道的过程。为了高效利用燃料并缩短转移时间,科学家们开发了多种轨道转移方法。其中,霍曼转移(Hohmann Transfer)因其燃料效率高、计…...

我的创作之路:机缘、收获、日常与未来的憧憬
目录 前言机缘收获 日常成就一个优化后的二分查找实现 憧憬 前言 每个人的成长旅程都有它独特的轨迹,而我的这段技术创作之路,则源于一次再普通不过的项目分享。 机缘 一切的开始其实是偶然。在一次项目中,我遇到了一个棘手的问题…...

《硬件架构的艺术》笔记(六):处理字节顺序
介绍 本章主要介绍字节顺序的的基本规则。(感觉偏软件了,不知道为啥那么会放进《硬件架构的艺术》这本书)。 定义 字节顺序定义数据在计算机系统中的存储格式,描述存储器中的MSB和LSB的位置。对于数据始终以32位形式保存在存储器…...

AddIPAddress添加临时IP后,socket bind失败
问题描述 在Win10\Win11下使用addIPAddress添加临时IP成功后,立即创建socket,bind失败 if(!m_socket->bind(QHostAddress(m_localIP), listenPort)) {qCritical() << QString("bind error %1").arg(m_socket->errorString());re…...

关于IDE的相关知识之一【使用技巧】
成长路上不孤单😊😊😊😊😊😊 【14后😊///C爱好者😊///持续分享所学😊///如有需要欢迎收藏转发///😊】 今日分享关于ide使用技巧的相关内容! 关于…...

线性代数在人工智能领域中的实践
一、机器学习中的线性代数应用 在机器学习中,线性代数主要用于构建和训练各种模型,如线性回归、逻辑回归、支持向量机等。这些模型在数据的特征提取、降维处理以及分类等方面发挥了重要作用。 线性回归:线性回归是最简单的机器学习算法之一…...

图片生成视频-右进
右侧进入 ffmpeg -loop 1 -i image.jpg -f lavfi -i colorcblack:s1280x720:d20 -filter_complex "[1:v]formatrgba[bg];[0:v]formatrgba,scale1280:720[img];[bg][img]overlayxif(lt(t,3),W,if(lt(t,8),W-(t-3)*W/5,0)):y(H-h)/2:enablegte(t,3)" -c:v libx264 -t 2…...

3、集线器、交换机、路由器、ip的关系。
集线器、交换机、路由器三者的关系 1、集线器2、交换机(每个交换机是不同的广播域,ip地址起到划分广播域的作用)3、 路由器4、ip地址 所有图片和资料均来源于B站:网络安全收藏家 1、集线器 一开始两台电脑通信就需要网线就可以&a…...

w~视觉~合集25
我自己的原文哦~ https://blog.51cto.com/whaosoft/12627822 #Mean Shift 简单的介绍 Mean Shift 的数学原理和代码实现,基于均值漂移法 Mean Shift 的图像分割 Mean Shift 算法简介 从分割到聚类 对于图像分割算法,一个视角就是将图像中的某些点集分为一类&a…...

Applicaiton配置文件
server:port: 8080 # 配置 Spring Boot 启动端口,默认为 8080mybatis-plus:mapper-locations: classpath:com/xtl/mapper/xml/*.xml # 指定 MyBatis Mapper XML 文件的路径,确保 MyBatis 能够正确加载 Mapper 文件global-config:db-config:id-type: au…...

(已解决)wps无法加载此加载项程序mathpage.wll
今天,在安装Mathtype的时候遇到了点问题,如图所示 尝试了网上的方法,将C:\Users\Liai_\AppData\Roaming\Microsoft\Word\STARTUP路径中的替换为32位的Mathtype加载项。但此时,word又出现了问题 后来知道了,这是因为64位…...

ubity3D基础
Unity是一个流行的游戏开发引擎,它使用C#作为其主要的编程语言。以下是一些Unity中C#编程的基础概念: • Unity编辑器: • Unity编辑器是Unity游戏引擎的核心,提供了一个可视化界面,用于创建和管理游戏项目。 • C#脚本…...

Python2和Python3的区别
和python 2.x相比,python 3.x版本在语句输出、编码、运算和异常等方面做出了一些调整,我们这篇文章就对这些调整做一个简单的介绍。 Python3.x print函数代替了print语句 在python 2.x中,输出数据使用的是print语句,例如ÿ…...

Spring框架整合单元测试
目录 一、配置文件方式 1.导入依赖 2.编写类和方法 3.配置文件applicationContext-test.xml 4.测试类 5.运行结果 二、全注解方式 1.编写类和方法 2.配置类 3.测试类 4.运行结果 每次进行单元测试的时候,都需要编写创建工厂,加载配置文件等相关…...

docker-mysql
一、创建mysql 1、docker run --name mysql8.0-container -e MYSQL_ROOT_PASSWORDmy-secret-pw -d -p 3306:3306 mysql:8.0 参数解释: --name mysql8.0-container:指定容器的名称为mysql8.0-container。 -e MYSQL_ROOT_PASSWORDmy-secret-pw:…...

Java程序基础⑤Java数组的定义和使用+引用的概念
目录 1. Java数组的基本概念 1.1 数组的定义 1.2 数组存在的意义 1.3 数组的使用 1.4 二维数组 2. 引用类型JVM的内存分布 2.1 JVM的内存分布 2.2 基本数据类型和引用型数据类型的区别 2.3 引用注意事项 2.4 传值传递 3. 数组总结和应用场景 3.1 一维数组和二维数组…...

electron主进程和渲染进程之间的通信
主进程 (main.js) const { app, BrowserWindow, ipcMain } require("electron"); const path require("node:path"); // 导入fs模块 const fs require("fs");const createWindow () > {const win new BrowserWindow({width: 800,height…...

uniapp 安卓和ios震动方法,支持息屏和后台震动,ios和安卓均通过测试
最近使用uniapp开发震动功能,发现uniapp提供的 uni.vibrateLong()的方法震动比较弱,而且不支持息屏和后台震动。plus.ios.importClass("UIImpactFeedbackGenerator")是在网上看到的,这个震动也比较弱,ios也不支持息屏和…...

# DBeaver 连接hive数仓
前提 前提是基于hadoop的hive服务已经启动,其中hive的服务包括metastore元数据服务和hiveserver2服务已经启动。hiveserver2服务在默认端口10000启动,且通过telnet xx.xx.xx.xx 10000 能通。 满足以上要求后,再可以看以下连接文档ÿ…...

STM32H7开发笔记(2)——H7外设之多路定时器中断
STM32H7开发笔记(2)——H7外设之多路定时器中断 文章目录 STM32H7开发笔记(2)——H7外设之多路定时器中断0.引言1.CubeMX配置2.软件编写 0.引言 本文PC端采用Win11STM32CubeMX4.1.0.0Keil5.24.2的配置,硬件使用STM32H…...

Pytorch使用手册-Build the Neural Network(专题五)
在 PyTorch 中如何构建一个用于 FashionMNIST 数据集分类的神经网络模型,并解析了 PyTorch 的核心模块 torch.nn 的使用方法。以下是具体内容的讲解: 构建神经网络 在 PyTorch 中,神经网络的核心在于 torch.nn 模块,它提供了构建神经网络所需的所有工具。关键点如下: nn.…...

16. Springboot集成Tika实现文档解析
目录 1、什么是Tika 2、基本特性 3、Tika可视化提取 4、Springboot集成 4.1、maven依赖 4.2、Tika配置文件 4.3、注入tika bean 4.4、Service类 4.5、测试类TikaParserDemoTest 1、什么是Tika Tika是一款Apache开源的,跨平台,支持多品种文本类…...