多线程【线程概念+线程控制】

news2024/11/16 6:43:55

前置知识

在谈多进程之前,我们在谈一谈页表,在语言中:char* str=”hello world”; *str=”H”;运行时会报错,原因在于:字符串在已初始化数据区和代码区之间的,需要写的时候,我们需要对str进行虚拟地址和物理地址的转换,查到了物理地址,继续查页表,然后查到了只有R权限,然后你的操作是写操作,MMU会将当前行为终止,向硬件报错,OS识别到硬件报错,把硬件操作转换为信号(段错误,11号信号),给进程发送11号信号,进程就要处理信号,信号的默认动作是终止,所以就终止了

如何看待地址空间和页表:

  1. 地址空间是进程能看到的资源窗口
  2. 页表决定进程真正拥有资源的情况
  3. 合理的对地址空间+页表进行资源划分,我们就可以对一个进程的所有资源进行分类

页表是如何从虚拟地址到物理地址的?

在这里插入图片描述

先查找虚拟地址的前10个比特位,索引页目录找到指定的页表项,再从虚拟地址的后10个比特,索引页表项找到物理内存的启始位置,到页框以后再从虚拟地址的后12位找到页内起始地址,剩下的根据类型,从起始地址处再继续向后找

何为线程?

**线程:**线程是OS能够进行运算调度的基本单位。线程是一个进程中的一个单一执行流,通俗地说,一个程序里的一个执行路线就叫做线程。

可以知道的是,一个进程至少有一个执行线程,这个线程就是主执行流。一个进程的多个执行流是共享进程地址空间内的资源,也就是说进程的资源被合理分配给了每一个执行流,这些样就形成了线程执行流。所以说线程在进程内部运行,本质是在进程地址空间内运行

因为一个进程有多个线程,所以要对线程描述再组织,tcb,例如windows,单纯从线程角度,线程和进程有很多地方是重叠的,linux没有给线程设计专门的数据结构,而是直接服用pcb

linux下的线程

Linux下没有真正意义上的线程,线程是通过进程来模拟实现的

进程是承担分配系统资源的基本实体,即一堆的task_struct,地址空间,页表,代码和数据,而线程就是创建多个task_struct,指向父进程的地址空间

在这里插入图片描述

  1. 站在CPU视角,每一个PCB,都可以称之为轻量级进程(因为CPU内部看到PCB可能就是一个进程内部的执行流,并不能表示完整独立的一个进程)
  2. 线程是CPU调度的基本单位,而进程是承担分配系统资源的基本单位
  3. 进程用来整体申请资源 ,线程向进程要资源

没有真正的线程的好处是什么?

简单,维护成本降低

补充知识:虚拟内存决定了进程能够看到的“资源”

关系图:

在这里插入图片描述

线程创建

1.pthread_create———创建一个线程

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

参数:
thread:输出型参数,获取线程ID
attr:设置线程的属性,attr为NULL代表默认属性
start_routine:函数指针,传一个函数地址,这个函数作为线程的启动后执行的函数
arg:传给启动函数的参数(**这个是void *(start_routine) (void ),对应的参数)
返回值:
成功返回0,失败返回错误码

错误检查:

传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小

按照以前的编译是过不去的!因为linux没有真正意义上的线程,linux也就无法直接提供创建线程的系统调用接口!而只能给我们提供轻量级的进程接口!

如果我们使用的库不是语言上的接口,也不是操作系统的接口,是用户及线程库提供的,这是动静态链接所涉及的问题,-L找到动静态库在哪里, -I(大写i)找到头文件在哪里,-l(小写l)找到库名,这里的库名是phread,所以写成gcc -o #@ #^ -lpthread

实验:创建一个线程

#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
using namespace std;
int cnt=0;
//新线程
void* thread_routine(void* args)
{
  char* name = (char*)args;
	while(1)
	{
		// cout << "我是新线程,我正在运行! " << name << "cnt " << cnt << endl;
        printf("我是新线程,我正在运行...,name=%s,cnt=%d\n", name,cnt);
		sleep(1);
	}
}
int main()
{
	//创建线程tid
	pthread_t tid;
	int n=pthread_create(&tid,nullptr,thread_routine,(void*)"thread one ");
	assert(n==0);
	//主线程
	while(1)
	{
        cnt ++ ;
		// cout << "我是主线程,我正在运行!" <<  "cnt " << cnt << endl;
        printf("我是主线程,我正在运行...,cnt=%d\n",cnt);
		sleep(1);
	}
	return 0;
}

运行结果如下:

在这里插入图片描述
在这里插入图片描述

结论:每一个轻量级进程都有一个id,每一个轻量级进程id不同,但是pid是一样的!CPU调度的时候,以LWP为标识符标定一个特定的执行流,线程一旦被创建,几乎所有资源都是被所有线程共享的!

但同时线程也一定要有自己的私有资源,什么资源应该是私有的呢?

  1. pcb属性私有(独立的线程id,独立的优先级,独立的状态……)
  2. 要有一定私有的上下文结构(一个线程在进行切换的时候代码可能没跑完,要切换就要上下文保存)
  3. 每一个线程都要有独立的栈结构

一个线程如果出现了异常,会影响其他线程,健壮性比较差,因为信号叫做进程信号信号是整体发送给进程的

实验:创建多个线程

#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <vector>
using namespace std;
//新线程
void* thread_routine(void* args)
{
  char* name = (char*)args;
	while(1)
	{
    printf("我是新线程,我正在运行...,name=%s\n", name);
		sleep(1);
	}
}
int main()
{
	//1.创建一批线程
	vector<pthread_t> tids;
	#define NUM 10
	for(int i=0;i<NUM;++i)
	{
		char namebuffer[64];
		snprintf(namebuffer,sizeof(namebuffer),"%s:%d","thread",i);
	  pthread_t tid;
    pthread_create(&tid,nullptr,thread_routine,namebuffer);
	}
	//主线程
	while(1)
	{
        printf("我是主线程,我正在运行...\n");
		sleep(1);
	}
	return 0;
}

运行结果如下:

在这里插入图片描述

如果我们在pthread_create后面加上sleep(1);又会正常显示线程的编号,这是为什么呢?

当我们创建新线程谁先运行不确定,可能线程被创建出来了,还没来得及进行后续代码,继续跑主线程代码,主线程对缓冲区重新写入 ,你只是把缓冲区的地址传过去,但是缓冲区被下一次重新格式化显示覆盖了,所以是最新的编号

解决方法如下:

#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <vector>
using namespace std;
class ThreadData
{
public:
	pthread_t tid;
	char namebuffer[64];
};
//新线程
//这个函数被多个执行流访问,就是可重入函数!因为一个线程运行也没有影响另一个线程 
//如果在函数里定义了变量,是局部变量,具有临时性,在多线程里也一样,这也证明了每一个线程都有独立的栈结构
void* thread_routine(void* args)
{
  ThreadData* td = (ThreadData*)args;
	while(1)
	{
    //不过这里的打印有问题,这里无法解释,后面解释
    printf("我是新线程,我正在运行...,name=%s\n", td->namebuffer);
		sleep(1);
	}
	delete td;
	return nullptr;
}
int main()
{
	//1.创建一批线程
	vector<pthread_t> tids;
	#define NUM 10
	for(int i=0;i<NUM;++i)
	{
		//每一个线程创建都会new一个对象,把对象的地址传过去,不会共享buffer!
		ThreadData* td=new ThreadData();
		char namebuffer[64];
		snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i);
        pthread_create(&td->tid,nullptr,thread_routine,td);
	}
	//主线程
	while(1)
	{
        printf("我是主线程,我正在运行...\n");
		sleep(1);
	}
	return 0;
}

运行结果如下:

在这里插入图片描述

线程终止

如果在线程中使用exit,会导致整个进程结束,因为exit是用来结束进程的而不是线程!

如果值想终止某个线程而不是整个进程,有三种方式:

  • 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。(如果主线程在没有循环执行的情况下,走到return相当于终止整个进程!)
  • 线程可以调用pthread_ exit终止自己。
  • 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程

1.pthread_exit函数———线程终止

void pthread_exit(void *retval);

参数:
retval:不能指向局部变量

实验:退出线程

#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <vector>
using namespace std;
class ThreadData
{
public:
	pthread_t tid;
	char namebuffer[64];
};
//新线程
void* thread_routine(void* args)
{
	sleep(1);
    ThreadData* td = (ThreadData*)args;
	while(1)
	{
        printf("我是新线程,我正在运行...,name=%s\n", td->namebuffer);
		sleep(1);
		// return nullptr;
		pthread_exit(nullptr);
	}
}
int main()
{
	//1.创建一批线程
	vector<ThreadData*> threads;
	#define NUM 10
	for(int i=0;i<NUM;++i)
	{
		//每一个线程创建都会new一个对象,把对象的地址传过去,不会共享buffer!
		ThreadData* td=new ThreadData();
		char namebuffer[64];
		snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i);
        pthread_create(&td->tid,nullptr,thread_routine,td);
		//方便后续处理
		threads.push_back(td);
	}
	//主线程查看其他线程的创建情况
	for(auto& it : threads)
	{
		cout << "create thread :" <<it->namebuffer << ": " << it->tid << "success" << endl;
	}
	//主线程
	while(1)
	{
        printf("我是主线程,我正在运行...\n");
		sleep(1);
	}
	return 0;
}

运行结果如下:

在这里插入图片描述

2.pthread_cancel函数———取消一个线程

前提是这个线程已经运行起来了!

int pthread_cancel(pthread_t thread);

参数:
thread:线程ID
返回值:
成功返回0,失败返回错误码

实验:主线程取消新线程

#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <vector>
using namespace std;
class ThreadData
{
public:
	pthread_t tid;
	char namebuffer[64];
};
class ThreadReturn
{
public:
    int exit_code;
    int exit_result;
};

void* thread_routine(void* args)
{
	int cnt=3;
	while(cnt--)
	{
		cout << "新线程" << endl;
		
		sleep(1);
	}
	return (void*)16;
}
int main()
{
	//1.创建一批线程
	vector<ThreadData*> threads;
	#define NUM 10
	for(int i=0;i<NUM;++i)
	{
		//每一个线程创建都会new一个对象,把对象的地址传过去,不会共享buffer!
		ThreadData* td=new ThreadData();
		char namebuffer[64];
		snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i);
        pthread_create(&td->tid,nullptr,thread_routine,td);
		//方便后续处理
		threads.push_back(td);
	}
	    for(int i = 0; i < threads.size()/2; i++)
    {
        pthread_cancel(threads[i]->tid);
        cout << "pthread_cancel : " << threads[i]->namebuffer << " success" << endl;
    }
	//主线程
	for(auto& it : threads)
	{
		int* ret=nullptr;
		pthread_join(it->tid,(void**)&ret);
		cout << "join :" <<it->namebuffer << "success,number"  << (long long)ret <<endl;
		delete it;
	}
	return 0;
}

运行结果如下:

在这里插入图片描述

线程的等待

线程不等待也会造成类似僵尸进程的问题———内存泄露

  • 获取线程的退出信息(可以不关心)
  • 回收新线程对应的PCB等内核资源,防止内存泄露(这个暂时无法查看)

1.pthread_join函数———等待一个线程结束(阻塞式等待)

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

参数:
thread:线程ID
retval:输出型参数,指向线程退出的返回值
返回值:
成功返回0,失败返回错误码

为什么没有所谓的退出信号的获取?因为线程出异常会全部退掉,只需要获取退出码

实验:回收线程

#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <vector>
using namespace std;
class ThreadData
{
public:
	pthread_t tid;
	char namebuffer[64];
};

//新线程
//这个函数被多个执行流访问,就是可重入函数!因为一个线程运行也没有影响另一个线程 
//如果在函数里定义了变量,是局部变量,具有临时性,在多线程里也一样,这也证明了每一个线程都有独立的栈结构
void* thread_routine(void* args)
{
	sleep(1);
  ThreadData* td = (ThreadData*)args;
	while(1)
	{
        printf("我是新线程,我正在运行...,name=%s\n", td->namebuffer);
		sleep(1);
		return nullptr;
	}
}
int main()
{
	//1.创建一批线程
	vector<ThreadData*> threads;
	#define NUM 10
	for(int i=0;i<NUM;++i)
	{
		//每一个线程创建都会new一个对象,把对象的地址传过去,不会共享buffer!
		ThreadData* td=new ThreadData();
		char namebuffer[64];
		snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i);
        pthread_create(&td->tid,nullptr,thread_routine,td);
		//方便后续处理
		threads.push_back(td);
	}
	//主线程查看其他线程的创建情况
	for(auto& it : threads)
	{
		cout << "create thread :" <<it->namebuffer << ": " << it->tid << "success" << endl;
	}
	//主线程
	for(auto& it : threads)
	{
		pthread_join(it->tid,nullptr);
		cout << "join :" << it->namebuffer << "success" <<endl;
		//不能在新线程里delete,因为主线程和新线程的执行是随机的,可能已经被删除了,但是主线程才访问,会造成段错误
		delete it;
	}
	return 0;
}

运行结果如下:

在这里插入图片描述

实验:线程等待获得退出结果

#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <vector>
using namespace std;
class ThreadData
{
public:
	pthread_t tid;
	char namebuffer[64];
};

void* thread_routine(void* args)
{
	//一样的效果
	// return (void*)16;
	pthread_exit((void*)16);
}
int main()
{
	//1.创建一批线程
	vector<ThreadData*> threads;
	#define NUM 10
	for(int i=0;i<NUM;++i)
	{
		//每一个线程创建都会new一个对象,把对象的地址传过去,不会共享buffer!
		ThreadData* td=new ThreadData();
		char namebuffer[64];
		snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i);
        pthread_create(&td->tid,nullptr,thread_routine,td);
		//方便后续处理
		threads.push_back(td);
	}
	//主线程
	for(auto& it : threads)
	{
		void* ret=nullptr;
		pthread_join(it->tid,&ret);
		cout << "join :" << it->namebuffer << "success,number"  << (long long)ret <<endl;
	}
	return 0;
}

运行结果如下:

在这里插入图片描述

为什么二级指针可以这样使用?如图所示

在这里插入图片描述

线程分离

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

如果设置了默认分离状态,不能够进行等待了!

1.pthread_detach函数———对一个线程进行分离

int pthread_detach(pthread_t thread);

参数:
thread:线程ID
返回值:
成功返回0,失败返回错误码

为了获得线程id我们还需要学一个函数

2.pthread_self()———获得线程id

pthread_t pthread_self(void);

实验:分离线程

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* pthreadrun(void* arg)
{
  int count = 0;
  pthread_detach(pthread_self());
  while (1){
    printf("new threaad is running, pid is %d, thread id is %p\n", getpid(), pthread_self());
    sleep(1);
    if (++count == 3){
      pthread_exit(NULL);
    }
  }
}

int main()
{
  //pthread_t pthread[5];
  pthread_t thread;
  pthread_create(&thread, NULL, pthreadrun, NULL);
  sleep(1);// 让线程先分离
  if (pthread_join(thread, NULL) == 0){
    printf("wait success\n");
  }else{
    printf("wait failed\n");
  }
  return 0;
}

运行结果如下:

在这里插入图片描述

线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

1.进程切换:页表&&虚拟地址空间&&切换PCB&&上下文切换
2.线程(轻量级进程)切换:切换PCB&&上下文切换
3.CPU除了寄存器,还存在cache(CPU内的硬件级缓存),软件存在一个属性:局部性原理——当前正在访问的代码和数据附近的代码有较大的概率被访问到,如果当前进程内部在进行处理的数据的时候,访问到的代码和数据被预先访问到cache中,CPU在读取时先读取cache,如果cache没有命中,再去内存中读取,读取先缓冲到cache再读取,一个运行很久的进程,cache缓存了很多热点数据,线程在切换时,cache不用被切换,但进程切换,需要重新缓存,线程切换cache不用太更新,但是进程切换重新更新

  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用(使用CPU资源,例如加密,解密),为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用(使用外设资源,例如磁盘,网络),为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

线程的缺点

  • 性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型
线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的
同步和调度开销,而可用的资源不变

  • 健壮性降低

编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了
不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的

  • 缺乏访问控制

进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响

  • 编程难度提高

编写与调试一个多线程程序比单线程程序困难得多

线程的共享与独立

共享:

进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

独立:

  • 线程ID
  • 一组寄存器
  • errno
  • 信号屏蔽字
  • 调度优先级

线程的深层理解

原生线程库中可能要存在多个线程,我们需要对线程进行管理,先描述(线程的属性比较少),再组织,每一次创建线程都要在pthread库中创建结构体,每一个结构体都会对应到一个轻量级进程的执行流

至于线程有独立的栈结构,具体如何实现的?如下图:

在这里插入图片描述

主线程用主线程的栈,其他线程用的栈在共享区,更准确点是在线程库中

如果我们就想再全局区创建变量,但是要求在线程独立的栈中我们可以在类型前加_thread,例如_thread int g_val=100;

封装用户级线程库

虽然这个是用户级线程库,但是还是不够简单,所以我们可以封装起来

.hpp开源代码,类的生命和实现放在一起不用像之前.c .h分开写

Thread.hpp

#include <iostream>
#include <vector>
#include <cstdio>
#include <cstring>
#include <string>
#include <unistd.h>
#include <functional>
#include <cassert>
#include <pthread.h>
using namespace std;
class Thread;
class context
{
public:
    Thread* this_;
    void *args_;
    context()
    :this_(nullptr)
    ,args_(nullptr)
    {}
};
class Thread
{
public:
    typedef function<void*(void*)> func_t;
    const int num=1024;
    Thread(func_t func,void* args,int number)
            :func_(func)
            ,args_(args)
    {
        char buffer[num];
        snprintf(buffer,sizeof(buffer),"thread-%d",number);
        name_=buffer;
        context *ctx=new context();
        ctx->this_= this;
        ctx->args_=args_;
        int n=pthread_create(&tid_,nullptr,start_routine,ctx);
        assert(n==0);
        (void)n;
    }
    static void *start_routine(void* args) //类内成员,使用static没有this指针了
    {
        //把对应的方法再做一下包   装
        context* ctx=(context*)args;
        void* ret=ctx->this_->run(ctx->args_);
        delete ctx;
        return ret;
//        静态方法不能调用成员方法,把成员变量变量变为static可以,但不是好方法!
//        return func_(args);
    }
    void* run(void* args)
    {
        return func_(args);
    }
    void join()
    {
        int n=pthread_join(tid_,nullptr);
        assert(n==0);
        (void)n;
    }
    ~Thread()
    {
        //do nothing
    }
private:
    string name_;
    pthread_t tid_;
    func_t func_;
    void* args_;
};

void *getTicket(void* args)
{
    string work_type=(char*) args;
    while(true)
    {
        cout << "我是一个新线程,我正在做:" << work_type << endl;
        sleep(1);
    }
}

test.cpp

#include"Thread.hpp"
int main()
{
    Thread* thread1=new Thread(getTicket,(void*)"user1",1);
    Thread* thread2=new Thread(getTicket,(void*)"user2",2);
    Thread* thread3=new Thread(getTicket,(void*)"user3",3);
    Thread* thread4=new Thread(getTicket,(void*)"user4",4);
    // thread1->join();
    // thread2->join();
    // thread3->join();
    // thread4->join();
    while(1)
    {
        ;
    }
}

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

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

相关文章

springboot第17集:Spring我的春天

Spring是一个开源免费的框架和容器&#xff0c;具有轻量级和非侵入式的特点。它支持控制反转(IoC)和面向切面(AOP)&#xff0c;同时提供了对事务和其他框架的支持。因此&#xff0c;简单来说&#xff0c;Spring就是一个轻量级的IoC和AOP容器框架。 假设有一个应用程序需要使用数…

【Golang】多线程爬虫的实现

〇、前言 Golang 是一种并发友好的语言&#xff0c;使用 goroutines 和 channels 可以轻松地实现多线程爬虫。具体地说&#xff0c;实现的是多协程。协程是一种比线程更轻量化的最小逻辑可运行单位&#xff0c;它不受操作系统调度&#xff0c;由用户调度。因此对于协程并发的控…

突发!ChatGPT王炸级更新!支持GPT-4联网 Code Interpreter!

4月30日&#xff0c;OpenAI官方悄悄发布了联网版GPT-3.5。虽然名字变了&#xff0c;但使用体验却是换汤不换药&#xff0c;还是那套。 然而&#xff0c;万万没想到的是&#xff0c;刚过去没几天&#xff0c;昨天5月4日&#xff0c;鱼哥发现自己的Plus账号竟然多了一些能力&…

AI 视频编辑革新:GEN-1 / GEN-2 引领新风潮

在早先的一篇文章中《AI 学习心得速览&#xff08;3月&#xff09;》&#xff0c;提到过一家AI视频公司RunWay&#xff0c;公司专注于 AI 视频处理&#xff0c;在二月份发布了第一个人工智能视频编辑模型 Gen-1&#xff0c;对视频素材进行转换成相应的风格。 RunwayML 今天来聊…

C#学习系列之throw new ApplicationException

C#学习系列之throw new ApplicationException 啰嗦问题解决总结 啰嗦 在项目的解码过程中使用到throw new ApplicationException语句&#xff0c;之前一致没有意识到这句话会带来很多问题。项目中使用这句话来捕捉解码过程中的解码异常问题。 问题 在使用throw new Applicati…

(二)【平衡小车制作】电机驱动(超详解)

一、硬件设计 1.直流减速电机   直流减速电机&#xff0c;即齿轮减速电机&#xff0c;是在普通直流电机的基础上&#xff0c;加上配套齿轮减速箱。齿轮减速箱的作用是&#xff0c;提供较低的转速&#xff0c;较大的力矩。  简单的来说&#xff0c;STM32分配两个IO口给一个…

LeetCoed 2, 23, 25, 112, 113

文章目录 1. 两数相加2. K 个一组翻转链表3. 合并 K 个升序链表4. 路径总和I5. 路径总和II 1. 两数相加 题目详情见: LeetCode2. 两数相加 题目描述相对来说比较绕, 我们可以直接理解为两个多位的整数相加, 只不过整数的每一位都是通过链表进行存储; 比如, 整数 342, 通过链表…

使用Webpack搭建项目(vue篇)

本篇承接使用Webpack搭建项目&#xff08;react篇&#xff09; 由于大部分配置一样&#xff0c;我们从上一篇react项目中&#xff0c;复制webpack.dev.js以及webpack.prod.js 开发模式 1.删除ReactRefreshWebpackPlugin 2.自动补充拓展名修改为.vue文件&#xff0c;同时处理…

每天一道算法练习题--Day21 第一章 --算法专题 --- ----------位运算

我这里总结了几道位运算的题目分享给大家&#xff0c;分别是 136 和 137&#xff0c; 260 和 645&#xff0c; 总共加起来四道题。 四道题全部都是位运算的套路&#xff0c;如果你想练习位运算的话&#xff0c;不要错过哦&#xff5e;&#xff5e; 前菜 开始之前我们先了解下…

【linux的学习与软件安装】

文章目录 linux的学习一、工具安装与联网&#xff1f;二、Linux软件安装1.安装jdk2.安装MySQL安装redis linux的学习 一、工具安装与联网&#xff1f; 1.1安装好VM后 进入vi /etc/sysconfig/network-scripts/ifcfg-ens33 然后ip addr 查看ip 1.2打开IDEA的tools 二、Linux软…

网络编程 | 多进程多线程并发服务器代码实现

欢迎关注博主 Mindtechnist 或加入【Linux C/C/Python社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和…

5.5 Mybatis Update标签实战,返回值是什么? 教你通常处理做法

本文目录 前言一、update标签实战① 在UserMapper接口中新增update方法② MybatisX插件生成update标签③ 写update SQL 语句 二、update sql返回值是什么?三、Mybatis update标签返回值是什么?四、实现简易的修改密码API1. dal层2. service层3. web层自测通过 五、Git提交最后…

vue - 常见的移动端rem适配方案

移动端rem适配方案 rem适配原理方案1&#xff1a;rem媒体查询方案2&#xff1a;jsrem方案3&#xff1a;vwrem&#xff08;不用查询屏幕宽度&#xff09; 移动端适配经常使用的就是 rem; 主要有以下几种方案&#xff1a; 1&#xff1a;rem 媒体查询&#xff08;media&#xff0…

蓝桥杯最后一战

目录 分巧克力_二分 题目描述 输入格式 输出格式 输入输出样例 说明/提示 代码&#xff1a; 巧克力 - 优先队列 题目描述 输入格式 输出格式 输入输出样例 说明/提示 代码&#xff1a; 思路分析&#xff1a; 秘密行动_dp 蓝桥杯算法提高-秘密行动 题目描述 …

Unity之OpenXR+XR Interaction Toolkit 安装和配置

前言 XR Interaction Toolkit 是Unity基于OpenXR标准&#xff0c;发布的一套XR工具&#xff0c;目的是方便我们快速接入XR相关的SDK&#xff0c;并且做到兼容不同VR设备的目的&#xff0c;目前流行的VR设备如Oculus&#xff0c;Metal&#xff0c;HTC Vive&#xff0c;Pico等统…

改进YOLOv8 | 主干网络篇 | YOLOv8 更换骨干网络之 MobileNetV3 | 《搜寻 MobileNetV3》

论文地址:https://arxiv.org/abs/1905.02244 代码地址:https://github.com/xiaolai-sqlai/mobilenetv3 我们展示了基于互补搜索技术和新颖架构设计相结合的下一代 MobileNets。MobileNetV3通过结合硬件感知网络架构搜索(NAS)和 NetAdapt算法对移动设计如何协同工作,利用互…

【天秤座区块链】元宇宙知识普以及简单解读清华研究报告

本节目录 温馨提示关于分栏【天秤座区块链】由来提前感受元宇宙区块链的两个注意点区块链革命简单认识清华大学报告解读&#xff08;元宇宙&#xff09;前传《雪崩》元宇宙具体是什么&#xff1f;元宇宙不是什么&#xff1f;那为什么要冲击元宇宙呢&#xff1f; 小补充及感谢 温…

前端搭建打字通游戏(内附源码)

The sand accumulates to form a pagoda ✨ 写在前面✨ 打字通功能介绍✨ 页面搭建✨ 样式代码✨ 功能实现 ✨ 写在前面 上周我们实通过前端基础实现了名言生成器&#xff0c;当然很多伙伴再评论区提出了想法&#xff0c;后续我们会考虑实现的&#xff0c;今天还是继续按照我们…

java基础入门-06-【面向对象进阶(多态包final权限修饰符代码块)】

Java基础入门-06-【面向对象进阶&#xff08;多态&包&final&权限修饰符&代码块&#xff09;】 14、面向对象进阶&#xff08;多态&包&final&权限修饰符&代码块&#xff09;1.1 多态的形式1.2 多态的使用场景1.3 多态的定义和前提1.4 多态的运行…

嵌入式设备逆向所需的工具链

导语&#xff1a;本文介绍了嵌入式设备逆向所需的工具链。 相关的应用程序或工具有&#xff1a; UART(Universal Asynchronous Receiver Transmitter&#xff0c;通用异步收发器)&#xff1a; UBoot&#xff1b; Depthcharge&#xff1b; SPI (Serial Peripheral Interface…