【Linux】从零开始认识多线程 --- 线程控制

在这里插入图片描述

在这个浮躁的时代
只有自律的人才能脱颖而出
-- 《觉醒年代》

从零开始认识多线程 --- 线程控制

  • 1 知识回顾
  • 2 线程控制
    • 2.1 线程创建
    • 2.2 线程等待
    • 2.3 线程终止
  • 3 测试运行
    • 3.1 小试牛刀 --- 创建线程
    • 3.2 探幽析微 --- 理解线程参数
    • 3.3 小有心得 --- 探索线程返回
    • 3.4 求索无厌 --- 实现多线程
    • 3.5 返璞归真 --- 线程终止与线程分离
  • 4 语言层的线程封装
  • Thanks♪(・ω・)ノ谢谢阅读!!!
  • 下一篇文章见!!!

1 知识回顾

上一篇文章中,我们通过对地址空间的再次学习来认识了线程:

  1. 物理空间不是连续的,是4kb的内存块(页框)组成的。
  2. 页表映射是通过虚拟地址来索引物理地址:
    • 虚拟地址共32位:前10位用来索引页目录中的元素(页表),中间10位用来索引页表中的对应的元素(页框),后12位用来索引页框中的每一个字节
  3. 虚拟地址本质是一种资源,可以进行分配!对一个进程的数据进行分配执行,就是多线程的本质!
  4. Linux中的线程是通过进程模拟的(并没有单独设计出一个单独的线程模块)
  5. 进程中可以有多个进程(之前学习的是进程的特殊情况),他们共用一个地址空间。进程从内核来看,是承担分配系统资源的基本实体!
  6. Linux中的执行流是线程 ,CPU看到的执行流 <= 进程

进程与线程需要注意:

  1. 线程的调度成本比进程低很多,是由于硬件原因:CPU中存在一个cache会储存热点数据(进程相关数据) ,要访问数据时,会先在cache中寻找,如果命中直接访问,反之进行置换。切换进程需要更换热点数据,切换线程不需要切换。
  2. 线程的健壮性很差!一个线程出错会导致整个线程退出,而不同进程是独立的互不影响!进程和线程各有特长!
  3. 线程的本质是代码块!只使用函数的对应代码,即拿页表的一部分来执行!!!
  4. 线程的使用场景多为计算密集型和IO密集型,可以充分使用CPU的并行能力!

同一个进程中的线程虽然共享一个地址空间,但是还是有独属于自己的一些东西:

  1. 一组寄存器:在硬件中储存上下文数据,保证线程可以动态并行运行!
  2. 栈空间:线程中可以处理自己的临时变量,临时变量储存在自己独立的栈区,可以独立完成任务。
  3. 线程ID
  4. errno信号屏蔽字
  5. 调度优先级

复习的差不多了,我们了解了线程的基本概念,接下来就要开始学习如何管理线程 — 线程控制。根据我们之前学习的进程控制,大概可以估计一下线程控制的基本接口:线程创建 , 线程等待 , 线程退出…

2 线程控制

2.1 线程创建

万事开头难,我们先来看线程怎么创建:

PTHREAD_CREATE(3)                                                   Linux Programmer's Manual                                                  PTHREAD_CREATE(3)NAMEpthread_create - create a new threadSYNOPSIS#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);Compile and link with -pthread.

pthread_create是创建线程的接口,里面有4个参数:

  1. pthread_t *thread :输出型参数,线程ID。
  2. const pthread_attr_t *attr :线程属性(优先级,上下文…),默认传入nullptr
  3. void *(*start_routine) (void *) : 函数指针,线程需要执行的函数地址。
  4. void arg:想要传入到线程的信息,可以传入int,string地址或者传入一个类对象的地址。

再来看返回值:

RETURN VALUEOn success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.

pthread系列的函数的返回值是都是一样的:成功返回0,反之返回错误码!

2.2 线程等待

学习进程的时候,如果进程创建出来了,但是不进行等待,就拿不到退出信息,还会造成僵尸进程,进而造成内存泄漏。同样线程也需要进行等待。由主线程来等待新线程

PTHREAD_JOIN(3)                                                     Linux Programmer's Manual                                                    PTHREAD_JOIN(3)NAMEpthread_join - join with a terminated threadSYNOPSIS#include <pthread.h>int pthread_join(pthread_t thread, void **retval);Compile and link with -pthread.

这个函数里面有2个参数:

  1. pthread_t thread:需要进行等待的线程ID
  2. void **retval: 获取的返回信息

2.3 线程终止

牢记:main线程结束那么进程结束,所以一定要保证main线程最后退出。

  1. 最简单的线程终止是线程函数返回return
  2. 切记不要使用exit(),我们在进程控制中学习过exit()可以退出进程,但是要注意线程是在一个进程中讨论的,新线程如果使用了exit()那整个进程就退出了!exit()不可以用来终止线程
  3. 操作系统也给我们提供了线程终止的接口:
PTHREAD_CANCEL(3)                                                   Linux Programmer's Manual                                                  PTHREAD_CANCEL(3)NAMEpthread_cancel - send a cancellation request to a threadSYNOPSIS#include <pthread.h>int pthread_cancel(pthread_t thread);Compile and link with -pthread.

通过这个参数,可以看出来这是个很简单的接口,终止对应tid的线程。只要线程存在,并且知道tid , 就可以终止线程(可以自己终止自己)。线程终止的返回值是一个整数!

3 测试运行

3.1 小试牛刀 — 创建线程

我们进行一个简单的测试,来使用这两个接口:
注意,使用线程库的接口需要动态链接g++ -o $@ $^ -std=c++11 -lpthread

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <string>// 测试 1
void *ThreadRun(void *args)
{std::cout << "name: " << *(std::string*)args << " is running"<< std::endl;sleep(1);std::string* ret = new std::string(*(std::string*)args + "finish...") ;return (void*)ret;
}int main()
{// 创建一个新线程// int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);pthread_t tid;std::string name = "thread - 1";pthread_create(&tid, nullptr, ThreadRun, &name);//进程等待//int pthread_join(pthread_t thread, void **retval);std::string *ret = nullptr;pthread_join(tid, (void**)&ret);std::cout << *(std::string*)ret << std::endl;return 0;
}

编译运行一下,我们可以看到:
在这里插入图片描述
新线程完成了任务!

问题 1 : main线程和new线程谁先运行? 不确定,和进程的调度方式一致,由具体情况来定。

问题 2 : 我们期望谁先退出?肯定是main线程,所以就有join来进行等待,阻塞等待线程退出。如果不进行join,就会造成类似僵尸进程的情况(内存泄漏)!

问题 3 :tid是什么样子的,我们可不可以看一看?当然可以:

std::string ToHex(int x)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", x);return buffer;
}

这样就可以打印出来tid的十六进制:
在这里插入图片描述
这数字好像和lwp不一致啊
在这里插入图片描述
为什么tid这么大?其实tid是一个虚拟地址!!!

3.2 探幽析微 — 理解线程参数

问题 4 : 全面看待线程函数传参。上面我们的程序传入了name变量的地址,让线程获取了对应的名字。如果想要传入多个变量或方法,可以传入类对象的地址:

class ThreadData
{
public:std::string name;int num;
};
vvoid *ThreadRun(void *args)
{ThreadData* td = static_cast<ThreadData*>(args);std::cout << "name: " << td->name << " is running" << std::endl;std::cout << "num: " << td->num << std::endl; sleep(1);std::string *ret = new std::string(*(std::string *)args + "finish...");return (void *)ret;
}std::string ToHex(int x)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", x);return buffer;
}int main()
{// 创建一个新线程// int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);pthread_t tid;// std::string name = "thread - 1";ThreadData td;td.name = "thread - 1";td.num = 100;pthread_create(&tid, nullptr, ThreadRun, &td);// 查看tidsleep(1);std::cout << "tid: " << ToHex(tid) << std::endl;// 进程等待// int pthread_join(pthread_t thread, void **retval);std::string *ret = nullptr;pthread_join(tid, (void **)&ret);std::cout << *(std::string *)ret << std::endl;return 0;
}

这样就可以传入多个变量:
在这里插入图片描述
所以这个void*的变量是可以传入任何地址的,一定要想到可以传入类对象。但是刚写的有些问题,我们上面的写法是在主线程的栈区创建变量,让新线程读取主线程的栈,不太合适(破坏了一定独立性)!如果多个变量都传入了这个变量,那么修改一个就会造成所以的线程中的数据都发生改变!!!这可不行!推荐写:

	ThreadData* td = new ThreadData();td->name = "thread - 1";td->num = "100";pthread_create(&tid, nullptr, ThreadRun, td);

这是在堆区进行开辟空间,然后将该空间交给新线程来管理!就不会出现这样的问题了!以后我们都使用这种方式来传递参数!!!

3.3 小有心得 — 探索线程返回

问题 5 :线程的返回值输出型参数void** retval,他需要我们传递一个void*变量,然后返回值就交给了void*变量!这个过程就是对一个指针进行改变其指向的内容的操作。

下面是一个让新线程进行加法工作的程序

void *ThreadRun(void *args)
{ThreadData* td = static_cast<ThreadData*>(args);std::cout << "name: " << td->name << " is running" << std::endl;std::cout << "num: " << td->num << std::endl; sleep(1);delete td;//返回值std::string *ret = new std::string(*(std::string *)args + "finish...");return (void *)ret;
}

这就将void*变量返回给&(void* ret)变量,让ret指向对应的堆区。这就类似int a放入int * 中就可以改变a的值

问题 5 :如何全面的看待线程的返回。我们知道如果一个线程出现问题,整个进程就会退出。所以线程的返回只有正常的返回,没有异常的返回,出现异常整个进程会直接退出,根本没有返回错误信息的机会!和传入参数音参数一样,我们也可以返回一个类对象来传递多个变量。

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <string>// 测试 1
class ThreadData
{
public:std::string name;int num1;int num2;
};class ThreadResult
{
public:std::string name;int num1;int num2;int ans;
};void *ThreadRun(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);std::cout << "name: " << td->name << " is running" << std::endl;std::cout << "num1: " << td->num1 << " num2: " << td->num2 << std::endl;sleep(1);ThreadResult *ret = new ThreadResult();ret->name = td->name;ret->num1 = td->num1;ret->num2 = td->num2;ret->ans = td->num2 + td->num1;delete td;return (void *)ret;
}std::string ToHex(int x)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", x);return buffer;
}int main()
{// 创建一个新线程// int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);pthread_t tid;// std::string name = "thread - 1";ThreadData *td = new ThreadData();td->name = "thread - 1";td->num1 = 100;td->num2 = 88;pthread_create(&tid, nullptr, ThreadRun, td);// 查看tidsleep(1);std::cout << "tid: " << ToHex(tid) << std::endl;// 进程等待// int pthread_join(pthread_t thread, void **retval);ThreadResult *ret = nullptr;pthread_join(tid, (void **)&ret);std::cout << ret->num1 << " + " << ret->num2 << " = " << ret->ans << std::endl;return 0;
}

来看返回值:
在这里插入图片描述
我们成功获取了新线程中设置的返回值!非常nice!

3.4 求索无厌 — 实现多线程

问题 6 :上面只是创建了单独的一个线程,那如何创建多线程呢?
可以通过维护一个vector数组来对tid进行统一管理

void *ThreadRun(void *args)
{std::string name = static_cast<const char *>(args);while (true){std::cout << name << "is running ..." << std::endl;sleep(1);}return (void *)0;
}std::string ToHex(int x)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", x);return buffer;
}const int num = 10;int main()
{std::vector<pthread_t> tids;for (int i = 0; i < num; i++){// 1. 线程IDpthread_t tid;// 2. 线程名字char* name = new char[128];snprintf(name, 128, "thread - %d", i + 1);pthread_create(&tid, nullptr, ThreadRun, name);//保存所有线程的IDtids.push_back(tid) ;}//joinsleep(100);return 0;
}

在这里插入图片描述
这样就创建出了10个新线程,但是我们看这些新线程的的名字好像不太对:
在这里插入图片描述
怎么不是1 - 10???完全是乱的!因为线程谁先被调度运行不确定!而我们传入的名字是在主线程的栈区域,可能在新线程还没有调度,name就已经在主线程中被覆盖了!解决办法很简单,我们创建在堆区就可以了

for (int i = 0; i < num; i++){// 1. 线程IDpthread_t tid;// 2. 线程名字//在堆区进行创建。防止被重写覆盖char* name = new char[128];snprintf(name, 128, "thread - %d", i + 1);pthread_create(&tid, nullptr, ThreadRun, name);pids.push_back(tid) ;}

在这里插入图片描述
这样就整齐多了!

接下来就要进行等待:
我们已经通过vector容器来维护了创建所有线程的tid,所以只需要对所有的tid进行join就好了!

void *ThreadRun(void *args)
{std::string name = static_cast<const char *>(args);while (true){std::cout << name << "is running ..." << std::endl;sleep(3);break;}return nullptr;
}std::string ToHex(int x)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", x);return buffer;
}const int num = 10;int main()
{std::vector<pthread_t> tids;for (int i = 0; i < num; i++){// 1. 线程IDpthread_t tid;// 2. 线程名字char* name = new char[128];snprintf(name, 128, "thread - %d", i + 1);pthread_create(&tid, nullptr, ThreadRun, name);//保存所有线程的IDtids.push_back(tid) ;}//joinfor (auto tid : tids){pthread_join(tid , nullptr);std::cout << ToHex(tid) << " quit..." << std::endl;}
} 

来看运行效果:
在这里插入图片描述
非常好!!!

我们也可以通过返回值来获取线程的名字:

    for (auto tid : tids){void* name = nullptr;pthread_join(tid , &name);std::cout << (const char*)name<< " quit..." << std::endl;delete (const char*)name;}

非常优雅!
在这里插入图片描述

3.5 返璞归真 — 线程终止与线程分离

问题 7 :线程终止的返回值
我们来看看通过线程终止接口终止的线程返回值是什么样的:

void *ThreadRun(void *args)
{std::string name = static_cast<const char *>(args);while (true){std::cout << name << "is running ..." << std::endl;sleep(3);//break;}return args;
}std::string ToHex(int x)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", x);return buffer;
}const int num = 10;int main()
{std::vector<pthread_t> tids;for (int i = 0; i < num; i++){// 1. 线程IDpthread_t tid;// 2. 线程名字char* name = new char[128];snprintf(name, 128, "thread - %d", i + 1);pthread_create(&tid, nullptr, ThreadRun, name);//保存所有线程的IDtids.push_back(tid) ;}//joinsleep(3);for (auto tid : tids){pthread_cancel(tid);std::cout <<  " cancel: " << ToHex(tid) << std::endl;void* ret= nullptr;pthread_join(tid , &ret);std::cout << (long long int)ret << " quit..." << std::endl;}return 0;
}

在这里插入图片描述
可以看的,被phread_cancel()终止的线程的返回值是 -1!这个 -1其实是宏定义#define PTHREAD_CANCELED ((void *) -1)。线程终止的方式有三种:

  1. 线程函数 return
  2. pthread_cancel 新线程退出结果为-1
  3. pthread_exit

问题 8 :可不可以不通过join线程,让他执行完就退出呢,当然可以!
这里需要线程分离接口:

PTHREAD_DETACH(3)                                                   Linux Programmer's Manual                                                  PTHREAD_DETACH(3)NAMEpthread_detach - detach a threadSYNOPSIS#include <pthread.h>int pthread_detach(pthread_t thread);Compile and link with -pthread.

通过这个接口,分离出去的线程依然属于进程内部,但不需要被等待了。举个例子,之前再讲线程与进程的关系时,我们把不同的线程比作家庭成员,做好自己分内的事情,既可以让家庭幸福,即进程成功运行。而进程分离就好比你长大了,自己搬出去住,不受父母管了,但是依旧属于这个家庭。这种状态就是线程分离。

当然,如果想要将自己分离出去,就要知道自己的tid,这里需要接口:

PTHREAD_SELF(3)                                                     Linux Programmer's Manual                                                    PTHREAD_SELF(3)NAMEpthread_self - obtain ID of the calling threadSYNOPSIS#include <pthread.h>pthread_t pthread_self(void);Compile and link with -pthread.

这个接口会返回调用它的线程的ID。如同getpid()

void *ThreadRun(void *args)
{// 线程分离pthread_detach(pthread_self());std::string name = static_cast<const char *>(args);while (true){std::cout << name << "is running ..." << std::endl;sleep(3);break;}return args;
}std::string ToHex(int x)
{char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", x);return buffer;
}const int num = 10;int main()
{std::vector<pthread_t> tids;for (int i = 0; i < num; i++){// 1. 线程IDpthread_t tid;// 2. 线程名字char *name = new char[128];snprintf(name, 128, "thread - %d", i + 1);pthread_create(&tid, nullptr, ThreadRun, name);// 保存所有线程的IDtids.push_back(tid);}sleep(3);for (auto tid : tids){pthread_cancel(tid);std::cout << " cancel: " << ToHex(tid) << std::endl;void *ret = nullptr;int n = pthread_join(tid, &ret);std::cout << (long long int)ret << " quit... , n: " << n << std::endl;}return 0;
}

可以看到,如果我们等待一个已经分离出去的线程,会得到22号错误信息!所以不能 join 一个分离的线程!
在这里插入图片描述
所以主线程就可以不管新线程,可以继续做自己的事情,不用阻塞在join!

但是注意:线程分离了,依然是同一个进程!一个线程出异常,会导致整个进程退出!

上面是自己分离自己。也可以通过主线程分离新进程:

    for (auto tid : tids){pthread_detach(tid);//主线程分离新线程}

4 语言层的线程封装

上面讲的是Linux系统提供给我们的系统调用,帮助我们可以进行线程控制,也叫做原生线程库。我们熟悉了底层的原生线程库,就会方便很多。
我们来看C++11中的线程

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <vector>
#include <string>
#include <thread>void threadrun( int num)
{while (num){std::cout << " num: " << num << std::endl;}
}// C++中线程库
int main()
{std::thread mythread(threadrun, 10);while (true){std::cout << "main thread..." << std::endl;sleep(1);}mythread.join();return 0;
}

注意,虽然是使用的语言层的线程库,但是依旧要连接thread动态库,因为语言层线程库的本质是对原生线程库接口的封装!!!无论是java还是python都要与原生线程库产生联系!

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!

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

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

相关文章

vuepress 配置文件分类管理

背景 在.vuepress的config.js配置文件中&#xff0c;我们需要设置head, plugins, nav三项主要配置。 如果都写在config.js就会显得很臃肿&#xff0c;不便于维护。 代码 config.js const headConf require("./config/headConf"); const pluginsConf require(&q…

数据结构_顺序表专题

何为数据结构&#xff1f; 咱今天也来说道说道...... 数据结构介绍 准确概念 数据结构就是计算机存储、组织数据的方式 概念分析 从上句分析&#xff0c;数据结构是一种方式。一种管理数据的方式。为了做什么&#xff1f;为的就是计算机存储数据&#xff0c;组织数据。 …

企业数字化转型成刚需 协同管理行业步入黄金发展期

随着全球经济的数字化进程加速推进&#xff0c;企业管理模式正经历着前所未有的变革。其中&#xff0c;协同管理作为一种关键的商业实践&#xff0c;正处于黄金发展期。本文将探讨企业数字化转型对协同管理行业的影响和未来发展趋势。 数字化转型的驱动因素 企业数字化转型的推…

元器件基础学习笔记——磁珠

一、磁珠的作用及构造 1.1 磁珠的作用 磁珠是一种用于抑制高频噪声的被动电子组件&#xff0c;通常由铁氧体材料制成&#xff0c;这种材料具有高电阻率和高磁导率&#xff0c;使其能够在高频下有效地将干扰信号以热能的形式消耗掉。在电路设计中&#xff0c;磁珠被广泛用于信号…

Java Lombok 使用记录

一、什么是 Lombok Lombok 是一个 Java 库&#xff0c;它通过注解的方式&#xff0c;帮助开发者消除一些样板代码&#xff0c;从而简化 Java 代码的编写。Lombok 提供了许多注解&#xff0c;用于自动生成构造函数、getter、setter、equals、hashCode 等方法&#xff0c;以及简…

单元测试有什么好处呢?

测试工作就是模拟真实环境&#xff0c;在代码正式上线前进行验证的工作&#xff0c;即使没有任何工具和方法&#xff0c;这项工作也能够通过人工操作来手动完成。那么单元测试有什么好处呢&#xff1f; 提高软件质量&#xff1a;通过测试代码的各个小部分&#xff08;通常是函数…

LeetCode 441, 57, 79

目录 441. 排列硬币题目链接标签思路代码 57. 插入区间题目链接标签思路两个区间的情况对每个区间的处理最终的处理 代码 79. 单词搜索题目链接标签原理思路代码 优化思路代码 441. 排列硬币 题目链接 441. 排列硬币 标签 数学 二分查找 思路 由于本题所返回的 答案在区间…

解决PyCharm中的文件格式关联错误:终极指南

解决PyCharm中的文件格式关联错误&#xff1a;终极指南 PyCharm是一个功能强大的集成开发环境&#xff08;IDE&#xff09;&#xff0c;专为Python开发设计。然而&#xff0c;在使用过程中&#xff0c;用户可能会遇到文件格式关联错误的问题&#xff0c;这可能会导致IDE无法正…

engine.addImportPath()用于向 QML 引擎添加新的模块搜索路径

engine.addImportPath() 是 QQmlApplicationEngine 类中的一个方法&#xff0c;用于向 QML 引擎添加新的模块搜索路径。这在需要加载自定义模块或从非标准位置加载 QML 文件时非常有用。通过使用 addImportPath() 方法&#xff0c;可以让 QML 引擎在额外的路径中查找 QML 模块。…

JavaScript的学习(二)

今天继续学习JavaScript的第二天&#xff0c;还是打基础 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title…

3d复制的模型怎么渲染不出来?----模大狮模型网

在展览3D模型设计领域&#xff0c;技术的进步和创新使得模型的复杂性和精细度有了显著提升。然而&#xff0c;有时设计师们在尝试渲染复杂的3D复制模型时&#xff0c;却面临着无法正确呈现的问题。模大狮将探讨这一现象的可能原因&#xff0c;并提供相应的解决方案和建议&#…

知识图谱和 LLM:利用Neo4j驾驭大型语言模型(探索真实用例)

这是关于 Neo4j 的 NaLLM 项目的一篇博客文章。这个项目是为了探索、开发和展示这些 LLM 与 Neo4j 结合的实际用途。 2023 年,ChatGPT 等大型语言模型 (LLM) 因其理解和生成类似人类的文本的能力而风靡全球。它们能够适应不同的对话环境、回答各种主题的问题,甚至模拟创意写…

3d导入模型后墙体变成黑色?---模大狮模型网

在展览3D模型设计领域&#xff0c;技术和设计的融合通常是创意和实现之间的桥梁。然而&#xff0c;有时设计师们会遇到一些技术上的挑战&#xff0c;如导入3D模型后&#xff0c;墙体却突然变成了黑色。这种问题不仅影响了设计的视觉效果&#xff0c;也反映了技术应用中的一些复…

【UE5.3】笔记10-时间轴的使用

时间轴 右键--Add Timeline(在最下面) --> 双击进入时间轴的编辑界面&#xff1a; 左上角可以添加不同类型的轨道&#xff0c;可以自定义轨道的长度&#xff0c;单位秒&#xff0c;一次可以添加多个 可以通过右键添加关键帧&#xff0c;快捷键&#xff1a;shift鼠标左键按…

Perl 语言开发(十三):网络编程

目录 1. 概述 2. 网络编程基础 2.1 网络协议概述 2.2 Perl 网络编程基础模块 3. TCP 网络编程 3.1 创建 TCP 服务器 3.2 创建 TCP 客户端 4. UDP 网络编程 4.1 创建 UDP 服务器 4.2 创建 UDP 客户端 5. 高级网络编程 5.1 非阻塞 I/O 5.2 多线程网络编程 6. 简单的…

C++基础语法:链表和数据结构

前言 "打牢基础,万事不愁" .C的基础语法的学习 引入 链表是最基础的数据集合,对标数组.数组是固定长度,随机访问,链表是非固定长度,不能随机访问.数组查找快,插入慢;链表是插入快,查找慢. 前面推导过"数据结构算法数据集合".想建立一个数据集合,就要设计数…

前端时间格式传入后端负载里面没有东西

我是因为没有将时间值格式化&#xff0c;所有负载没有东西 <el-col :md"6"><el-form-item label"创建时间" prop"createTime"><el-date-picker v-model"queryParams.createTime" type"date" change"ha…

【MATLAB库函数系列】MATLAB库函数pwelch之功率谱估计的详解及实现

功率谱估计 由于实际信号通常是非定常的,我们只能假设其在10ms的时间段内是定常的,并在此基础上对短的定常信号求PSD或者能谱。 窗函数的作用就是将原始的信号分割成一段段可以计算PSD和能谱的短信号,并且保证了周期结构的连续性、避免了频谱泄漏。不同的窗函数具有不同的…

鸿蒙 画布来了 我不允许你不会

前言: 作者:徐庆 团队:坚果派 公众号:“大前端之旅” 润开鸿生态技术专家,华为HDE,CSDN博客专家,CSDN超级个体,CSDN特邀嘉宾,InfoQ签约作者,OpenHarmony布道师,电子发烧友专家博客,51CTO博客专家,擅长HarmonyOS/OpenHarmony应用开发、熟悉服务卡片开发。欢迎合作…

WPF+MvvmLight 项目入门完整教程(一)

WPF+MvvmLight入门完整教程一 创建项目MvvmLight框架安装完善整个项目的目录结构创建自定义的字体资源下载更新和使用字体资源创建项目 打开VS2022,点击创建新项目,选择**WPF应用(.NET Framework)** 创建一个名称为 CommonProject_DeskTop 的项目,如下图所示:MvvmLight框架…