【线程概念和线程控制】

news2024/10/6 20:34:27

目录

  • 1 :peach:线程概念 :peach:
    • 1.1 :apple:什么是线程?:apple:
    • 1.2 :apple:线程的优点和缺点:apple:
    • 1.3 :apple:页表的大小:apple:
    • 1.4 :apple:线程异常和用途:apple:
    • 1.5 :apple:进程VS线程:apple:
  • 2 :peach:线程控制:peach:
    • 2.1 :apple:POSIX线程库:apple:
    • 2.2 :apple:创建线程:apple:
    • 2.3 :apple:线程退出:apple:
      • :lemon:pthread_join:lemon:
      • :lemon:pthread_exit:lemon:
      • :lemon:pthread_cancel:lemon:
    • 2.4 :apple:分离线程:apple:
    • 2.5 :apple:理解线程独立栈:apple:


1 🍑线程概念 🍑

1.1 🍎什么是线程?🍎

教材观点是这样的:线程是一个执行分支,执行力度比进程更细,调度的成本更低。
Linux内核观点:进程是系统分配资源的基本单位,线程是CPU调度的基本单位。

这两种说法都是正确的,但是我们究竟该如何理解线程呢?

在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。一切进程至少都有一个执行线程。线程在进程内部运行,本质是在进程地址空间内运行。在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

Linux中其实本质上并不存在线程,而是将轻量级进程作为线程。其本质就是OS并没有给线程创建自己独立的地址空间,而是与进程共用一套地址空间,那么这也就注定了线程中绝大部分资源是可以共享的。这样设计的好处是复用了PCB的那一套设计,线程的TCB可以用进程的PCB模拟出来,这样的设计更加简单并且维护效率更加高效,像服务器等开发选择Linux的原因就是因为Linux可以在长时间的服务中运行。Windows中的线程才算的上是一种严格的线程,Windows的线程并没有复用进程的方法,而是创造了真正意义上的线程。

我们可以用一张图来表示进程与线程的关系:
在这里插入图片描述

1.2 🍎线程的优点和缺点🍎

线程的优点

  • 1️⃣创建一个新线程的代价要比创建一个新进程小得多。
  • 2️⃣与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
  • 3️⃣线程占用的资源要比进程少很多。
  • 4️⃣能充分利用多处理器的可并行数量。
  • 5️⃣在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
  • 6️⃣计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
  • 7️⃣I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

线程的缺点:

  • 1️⃣性能损失:
    一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 2️⃣健壮性降低
    编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  • 3️⃣缺乏访问控制
    进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  • 4️⃣编程难度提高
    编写与调试一个多线程程序比单线程程序困难得多。

1.3 🍎页表的大小🍎

这里再补充一个小知识点:多线程中页表关系是怎样维护建立的呢?

我们知道,在X86的环境下,我们最多可以拥有232种虚拟地址,而将虚拟地址转化成物理地址的页表大小应该为多少呢?
如果按照每一个虚拟地址都建立一个对应映射的话,假设用一个四字节的整形变量int来维护,那么不算其他的,只算虚拟地址到物理地址的映射,那么至少得需要232 *8(大约32GB),那我们操作系统还玩不玩了,这样设计肯定是不合理的。

实际上,操作系统将每一个32比特位的虚拟地址做了如下划分:
在这里插入图片描述这样进行页表大小计算时我们用的是前20个比特位,也就是220 ,然后通过下面方式进行页表映射:
在这里插入图片描述
那么最后12位到哪里去了呢?最后12位的虚拟地址是我们将虚拟地址转化位物理地址的偏移量,这个偏移量的大小恰好是212 (4KB),这个4KB是操作系统管理物理内存的单位,相信大家对于4KB一点儿也不陌生,因为我们讲解文件系统的时候磁盘与内存进行交互的单位也是以4KB位单位。这里为什么要使用4KB的大小进行交互而不是以字节进行交互呢?因为一个很著名的原理:局部性原理。通俗的来说,局部性原理就是预测未来CPU高速缓存的命中情况来提升效率。

所以通过这种方式我们只用了220 量级的大小空间来完成页表的建立,最多也就几MB而已,更何况并不是所有地址都会被用到,实际用到的地址可能只有几十字节大小。所以这种方式可以解决操作系统如何为页表分配合适的空间问题。
那么实际操作系统是如何分配资源给对象的呢?比如我们使用malloc一个资源是立马就会给你开空间的吗?显然不是这样的,操作系统为了高效是不会直接立马给你开空间的,而是产生一个缺页中断,当你真正使用该空间时才会去开空间。

1.4 🍎线程异常和用途🍎

线程异常:

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

这里我们可以简单的验证一下,大家先可以先看看时如何创建线程的,后面我们会详细的讲解:
比如下面的我们让线程1出错:
Makefile:

mytest:Test.cpp
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -rf mytest

Test.cpp:


#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;

void *Run1(void *argv)
{
    int cnt = 4;
    while (true)
    {
        cout << "I am t1,is running" << endl;
        sleep(1);
        if (--cnt == 0)
        {
            char *str = "abcd";
            *str = 'Q';
        }
    }
}
void* Run2(void* argv)
{
    while(true)
    {
        cout<<"I am t2,is running"<<endl;
        sleep(1);
    }
}
int main()
{
    pthread_t t1,t2;
    pthread_create(&t1,nullptr,Run1,nullptr);
    pthread_create(&t2,nullptr,Run2,nullptr);
    while(true)
    {
         cout<<"I am main,is running"<<endl;
         sleep(1);
    }
    return 0;
}

这里面值得注意的细节:创建线程时要引入头文件<pthread.h> ;链接时为了能够找到库,在Makefile种要指定库的名称-lpthread,要查找指定进程的所有线程可以使用下面命令:ps -aL | grep 进程名称;想要显示更加详细信息可以使用下面命令:ps -aL | head -1 && ps -aL | grep 进程名称 ;为了方便观察我们可以使用下面的命令脚本:while :;do ps -aL | head -1 && ps -aL | grep mytest;echo "************************************";sleep 1;done
我们来运行下观察下结果:
在这里插入图片描述从图片中我们不难发现当其中一个线程崩溃而导致整个进程(所有线程)都挂掉了。

线程用途:

  • 合理的使用多线程,能提高CPU密集型程序的执行效率。
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

1.5 🍎进程VS线程🍎

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分数据:
    1️⃣线程ID
    2️⃣一组寄存器
    3️⃣
    4️⃣errno
    5️⃣信号屏蔽字
    6️⃣调度优先级

进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
1️⃣文件描述符表
2️⃣每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
3️⃣当前工作目录
4️⃣用户id和组id

进程与线程的关系如下图:
在这里插入图片描述


2 🍑线程控制🍑

2.1 🍎POSIX线程库🍎

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头;
  • 要使用这些函数库,要通过引入头文<pthread.h>;
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项.

2.2 🍎创建线程🍎

我们先来看看库中的基本介绍:
在这里插入图片描述

功能:创建一个新的线程
原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg)
参数:
thread:返回线程ID;
attr:设置线程的属性,attr为nullptr表示使用默认属性;
start_routine:是个函数地址,线程启动后要执行的函数;
arg:传给线程启动函数的参数;
返回值:成功返回0;失败返回错误码

这里再补充一个错误检查的知识点:

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

我们观察上面创建线程的接口中,第一个参数是线程的id,这个的数值与之前我们在用监视脚本看的线程PID不太一样,现阶段可以理解为同一线程的两种不同的身份形式(比如我们在学校的学生证和处于社会中的身份证类似);第二个参数我们一般设置为空;第三个是一个参数为void*,返回值为void的函数指针;第四个参数是一个void的对象,我们一般是将线程的信息通过该参数传递进去的。

我们根据上面介绍就可以写出如下代码:

#include<iostream>
#include<unistd.h>
using namespace std;

void* Run(void* args)
{
    const char* name=static_cast<char*> (args);

    while(true)
    {
        cout<<name<<"is running"<<endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t pids[5];

    for(int i=0;i<5;++i)
    {
        char name[26];
        snprintf(name,sizeof(name),"pthread%d:",i+1);
        pthread_create(pids+i,nullptr,Run,name);
    }

    while(true)
    {
        cout<<"I am is main thread,is running"<<endl;
        sleep(1);
    }
    return 0;
}

当我们运行时:
在这里插入图片描述
为啥跟我们预计的不太一样呀?我们想要的是打印pthread1 pthread2……这样的数据呀,为啥打印出来的都是pthread5呢?
这其实与我们传入的数组有关:
在这里插入图片描述
我们在这里传入的是数组名,也就是首元素地址,我们传给线程创建的参数并不是一个缓冲区而是一个数组的地址,由于创建线程是先将线程创建出来,并不会立马去执行线程中的代码,而我们每次传入的地址(数组名)是相同的,所以最后一个线程的数据就被保存到了数组中,当我们并发执行线程中的代码时读到的就是数组中的数据(也就是最后一次修改数组中的数据)。那我们如何解决这种现象呢?
我们可以在堆上开辟空间,这样我们每次new出来的地址是不同的,所以就不会出现覆盖的情况了。

比如我们可以这样修改:
在这里插入图片描述
当我们再次运行时:
在这里插入图片描述
这里面打印顺序并不是1 2 3 4 5那样的原因是因为线程的调度也是不确定的,谁先调度完全是由调度器所决定的。

其实上面传入的对象大家可以更具需求设置的更加完善一些,我们可以封装一个类,让多线程帮助我们完成不同的任务,我这里就不再多写了,大家有兴趣可以根据自己的需求下去完善。

线程ID及进程地址空间布局:

  • pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
  • 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID.

2.3 🍎线程退出🍎

🍋pthread_join🍋

但是其实上面的代码中还存在一个很严重的问题,我们在学习进程中知道,父进程会wait子进程,否则就可能造成了内存泄漏。线程也是一样的,已经退出的线程,其空间没有被释放,仍然在进程的地址空间内;创建新的线程不会复用刚才退出线程的地址空间。主线程必须要回收其他线程的资源,否则就会造成内存泄漏,那回收其他线程的接口是啥呢?
我们来看看官网对pthread_join的介绍:
在这里插入图片描述

功能:等待线程结束
原型:
int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

第一个参数比较好理解,那么第二个参数是一个二级指针,接受的是一个线程的返回值,那么我们知道创建线程的参数里面有一个函数指针,该函数指针的返回值为void*,而这个返回值就可以传递给join的第二个参数使用,比如我们看看下面的代码:

void *Run(void *args)
{
    const char *name = static_cast<char *>(args);

    cout << "thread1 is running" << endl;
    sleep(2);
    return (void *)11;
}

int main()
{
    pthread_t p1;
    pthread_create(&p1, nullptr, Run, nullptr);
    cout << "I am is main thread,is running" << endl;
    sleep(2);
    void* ret=nullptr;
    pthread_join(p1,&ret);
    cout<<"new pthread exit   "<<ret<<endl;
    return 0;
}

当我们运行时:
在这里插入图片描述
我们发现我们通过返回值返回的11被join给接收到了。

🍋pthread_exit🍋

除了使用return 这种方式,我们还可以使用哪种方式终止线程呢?我们还可以使用pthread_exit接口来处理:
在这里插入图片描述

功能:线程终止
原型:
void pthread_exit(void *value_ptr);
参数:
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

当我们这样使用时:
在这里插入图片描述
我们可以来观察下运行结果:
在这里插入图片描述

🍋pthread_cancel🍋

除了上面我们讲解的这两种方式外,我们还可以使用pthread_cancel取消一个执行中的线程:

功能:取消一个执行中的线程
原型:
int pthread_cancel(pthread_t thread);
参数:
thread:线程ID
返回值:成功返回0;失败返回错误码

假如我们想要取消自己呢?我们如何得到自己的pid,我们可以使用pthread_self():
在这里插入图片描述
我们下面来看看线程取消的基本用法:

void *threadRun(void* args)
{
    const char*name = static_cast<const char *>(args);

    int cnt = 5;
    while(cnt)
    {
        cout << name << " is running: " << cnt-- << " obtain self id: " << pthread_self() << endl;
        sleep(1);
    }

    pthread_exit((void*)11); 

    // PTHREAD_CANCELED; #define PTHREAD_CANCELED ((void *) -1)
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
    sleep(3);

    pthread_cancel(tid);

    void *ret = nullptr;
    pthread_join(tid, &ret);
    cout << " new thread exit : " << (int64_t)ret << "quit thread: " << tid << endl;
    return 0;
}

当我们直接运行时:
在这里插入图片描述

不难发现程序3s后直接退出了,其实也很好理解,因为我们退出的是主线程,所以肯定会直接退出的。

所以我们可以总结线程退出有三种方式:

  • 1️⃣从线程函数return,这种方法对主线程不适用,从main函数return相当于调用exit。
  • 2️⃣线程可以调用pthread_ exit终止自己。
  • 3️⃣一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

调用pthread_join函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  • 1️⃣如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  • 2️⃣ 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED(-1)。
  • 3️⃣ 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  • 4️⃣ 如果对thread线程的终止状态不感兴趣,可以传nullptr给value_ ptr参数。

2.4 🍎分离线程🍎

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

所以此时我们可以使用pthread_detach:
在这里插入图片描述
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

pthread_detach(pthread_self());

joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

我们来验证下:

void* Run(void* args)
{
    pthread_detach(pthread_self());
    const char* name=static_cast<char*> (args);

    cout<<name<<" is running"<<endl;
    return nullptr;
}

int main()
{
    pthread_t p1;
    pthread_create(&p1,nullptr,Run,(void*)"thread1");
   
    int ret=pthread_join(p1,nullptr);

    if(ret==0)
        cout<<"wait success"<<endl;
    else
        cout<<"wait fail "<<endl;

    return 0;
}

当我们运行时:
在这里插入图片描述
为什么是运行success呀?不是说joinable和分离是冲突的吗?按道理这里应该会join失败的呀。
这是由于执行时是先执行的join,此时线程还没有被分离,自然就能够join成功了,我们可以像下面这样写,就会join失败:
在这里插入图片描述
当我们再次运行时:
在这里插入图片描述

2.5 🍎理解线程独立栈🍎

首先我们来看看一张图:
在这里插入图片描述通过之前动静态库的知识我们知道,pthread库是加载到共享区的,那么也就决定了进程中所有线程都是可以访问得到该库的。但是从上图我们看见了有一个主线程栈的空间,这个空间又是为谁准备的呢?
其实这个空间是为主线程准备的,我们之前讲过其余线程中的栈是相互独立的,而这个独立栈的空间就开辟在共享区中,也就是独立栈的空间其实是由库帮助我们开辟的。上图右边第一个struct_pthread又是什么鬼呢?这个是管理共享区中线程的一种数据结构,类似于进程中的PCB。至于什么是局部存储,我们可以来写一个程序看看:

int g_val=20;

void* Run(void* args)
{
     const char* name=static_cast<char*> (args);

     while(true)
     {
        cout<<"g_val:"<<g_val<<"&g_val:"<<&g_val<<endl;
        sleep(1);
     }
}

int main()
{
    pthread_t pids[5];
    for(int i=0;i<5;++i)
    {
        char* name=new char[32];
        snprintf(name,32,"pthread%d:",i+1);

        pthread_create(pids+i,nullptr,Run,name);
    }

    for(int i=0;i<5;++i)
    {
        pthread_join(pids[i],nullptr);
    }

    return 0;
}

当我们运行时:
在这里插入图片描述
这也符合我们的预期,因为全局变量是所有线程共享的,但是当我们在全局变量前加上了__pthread后:
在这里插入图片描述当我们运行时:
在这里插入图片描述我们惊奇的发现居然地址不一样了,这其实就是将g_val分别保存了一份在各自的独立栈中。至于为什么打印出来的数据无规律是因为多线程并发访问的问题,我们后面在详细讲解。


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

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

相关文章

tp6的runtime/Logs目录下产生大量日记文件,怎么取消自动生成?

一开始查了好多网上提供的&#xff0c;很幸运都是抄袭别人的&#xff0c;没一个成功&#xff0c;最后无奈只能自己解决方法 其实很简单&#xff0c;不用修改config/log.php文件&#xff0c;没用因为只要有登入错误&#xff0c;警告&#xff0c;消息或者sql错误都会写入 解决方…

windows编译poco c++库

背景 最近有了解到poco c库&#xff0c;这里记录下编译及使用过程。 最开始使用的vs studio 2022 和 poco 1.12.4版本编译&#xff0c;不管openssl 使用哪种版本&#xff0c;都会编译报错&#xff0c;最后妥协了。 参考&#xff1a;https://www.bilibili.com/read/cv1416565…

《语文新读写》期刊简介及投稿邮箱

《语文新读写》期刊简介&#xff1a; 《语文新读写》是“国家期刊奖”获奖期刊&#xff0c;中国期刊方阵双效期刊&#xff0c;中国核心期刊&#xff08;遴选&#xff09;数据库收录期刊&#xff0c;被授予上海市期刊优秀编辑部。杂志的主要读者对象为各大院校、中小学各科教师…

安全帽佩戴检测算法模型训练详细流程

一、任务描述 实际施工现场需要对每个进出的人员进行安全帽监测&#xff0c;对未佩戴安全帽的人员平台进行风险告警&#xff0c;通知工作人员并记录下来。 主要包括三类目标物体&#xff1a;头盔&#xff08;helmet&#xff09;&#xff0c;人&#xff08;person&#xff09;…

100种思维模型之耗散结构理论思维模型-96

避免熵死的方法之一就是建立 耗散结构 。 那么&#xff0c;何谓耗散结构理论&#xff1f; 01、何谓耗散结构思维模型‍‍‍ 一、耗散结构理论 1969年&#xff0c;比利时学者 伊里亚普利高津 在对热力学第二定律研究的基础上&#xff0c;提出了 “耗散结构理论”。 他认…

【导航电子地图(MAP)模块功能】

Map功能&#xff1a;提供导航系统中地图描画、地图操作、地图检索的功能。 地图操作功能&#xff1a;地图滚动等。 地图检索功能&#xff1a;附近名称取得、View内检索等。 地图描画功能&#xff1a;是指从地图Data base中读出地点情报&#xff0c;然后按照一定的风格&#…

Meta提出全新参数高效微调方案,仅需一个RNN,Transformer模型GPU使用量减少84%!

近来&#xff0c;随着ChatGPT和GPT-4模型的不断发展&#xff0c;国内外互联网大厂纷纷推出了自家的大语言模型&#xff0c;例如谷歌的PaLM系列&#xff0c;MetaAI的LLaMA系列&#xff0c;还有国内公司和高校推出的一些大模型&#xff0c;例如百度的文心一言&#xff0c;清华的C…

vue3中通过vue-i18n实现国际化

效果图 前言 突然想在vue3项目中使用国际化功能&#xff0c;查阅相关资料后发现和vue2的用法有些出入&#xff0c;记录一下 使用 下载vue-i18n npm i vue-i18n2、准备语言文件 目前我的项目只支持中英文切换&#xff0c;故准备一份中文文件和一份对应的英译文件 创建langur…

七大排序算法——直接插入排序,通俗易懂的思路讲解与图解(完整Java代码)

文章目录 一、排序的概念排序的概念排序的稳定性七大排序算法 二、直接插入排序核心思想代码实现 三、性能分析四、七大排序算法性能对比 一、排序的概念 排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#…

【Java进阶之路】NIO基础

一、NIO基础 Java New IO是从Java1.4版本开始引入的一个新的IO api&#xff0c;可以替代以往的标准IO&#xff0c;NIO相比原来的IO有同样的作用和目的&#xff0c;但是使用的方式完全不一样&#xff0c;NIO是面向缓冲区的&#xff0c;基于通道的IO操作&#xff0c;这也让它比传…

美团外卖智能陪伴型导购的探索与实践

相比于其他电商场景&#xff0c;外卖场景对于实时发现和反馈用户兴趣的能力有着更高的要求。近年来&#xff0c;美团外卖算法团队摸索出了一套适用于外卖场景的智能陪伴型导购架构和策略。这一举措已经取得了显著成效&#xff0c;本文将详细介绍外卖搜索技术团队搭建智能陪伴型…

Redis数据结构 — IntSet

目录 整数集合IntSet结构设计 IntSet的升级操作 升级具体过程 升级具体源码 小结 IntSet是Redis中set集合的一种实现方式&#xff0c;基于整数数组来实现&#xff0c;并且具备长度可变、有序等特征。 整数集合IntSet结构设计 整数集合本质上是一块连续内存空间&#xff…

Ubuntu22.04安装飞书

通过以下教程可以快速的安装飞书。 安装包下载 进入飞书下载官网下载飞书Linux客户端 选择deb格式安装包下载 安装方式 方式一&#xff1a;运行安装包安装 双击deb文件&#xff0c;点击install进行安装 方式二&#xff1a;终端命令安装 到安装目录&#xff0c;然后dpkg你的安…

光速吟唱,Clibor ,批量多次复制依次粘贴工具 快捷输入软件教程

批量多次复制依次粘贴工具 批量复制粘贴工具0.81.exe https://www.aliyundrive.com/s/3sbBaGmHkb8 点击链接保存&#xff0c;或者复制本段内容&#xff0c;打开「阿里云盘」APP &#xff0c;无需下载极速在线查看&#xff0c;视频原画倍速播放。 青县solidworks钣金设计培训 …

Web3.0:重新定义数字资产的所有权和交易方式

随着区块链技术的发展和应用&#xff0c;数字资产的概念已经逐渐深入人心。数字资产不仅包括加密货币&#xff0c;还包括数字艺术品、虚拟土地、游戏道具等各种形式的数字物品。然而&#xff0c;在传统的互联网环境下&#xff0c;数字资产的所有权和交易方式往往受到限制和约束…

二极管总结

目录 1.2.2二极管的伏安特性 1.2.3二极管的主要参数 1.2.4二极管的等效电路 1.2.5稳压二极管 1.2.6其它类型二极管 1.2.2二极管的伏安特性 二极管和PN结伏安特性区别&#xff1a; 相同点&#xff1a;同样具有单向导电性 不同点&#xff1a;二极管存在半导体体电阻和引线电阻…

Android JNI线程的创建 (十二)

🔥 Android Studio 版本 🔥 🔥 创建JNI 🔥 package com.cmake.ndk1.jni;public class JNIThread {static {System.loadLibrary("thread-lib");}public native void createNativeThread();public native void createNativeThreadWithArgs();public native v…

金科威GoldWayUT4000监护仪协议对接

通过网口&#xff0c;成功对接到参数&#xff1a; Sys、Dia、Map、Temp、HR、SPO2、Resp、RR、EtCO2、InCO2等数值。

玩玩两个简单的python的web框架 flask、fastapi

IDEA连接远程解释器&#xff0c;本地代码编辑无法代码提示 一、Flask入门使用 官网 其它参考 注意 1.这里使用linux 192.168.72.126上远程解释器,需要/usr/bin/pip3 install flask&#xff0c;host参数不要使用localhost/127.0.0.1,即只监听本地的访问&#xff0c;会导致wind…

electron-updater 自动更新升级应用

electron 内置了 autoUpdater 自动更新功能&#xff0c;但是服务配置有些复杂&#xff0c;最后选择了 electron-updater 工具插件&#xff0c;这里就讲讲如何配置 electron-updater 来自动更新升级应用。 一、项目依赖和 scripts 安装 electron-updater 和 electron-log pnpm…