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

多线程编程初探:掌握基本概念与核心原理

目录

1 初识线程

1.1 线程的由来

1.2 线程的产生

1.3 进程 VS 线程

1.4 关于系统内部关于线程和进程的资源调度问题

2 页表、虚拟地址和物理地址

2.1 对物理地址的描述

2.2 对于页表设计的解析

3 线程的控制

 3.1 进程创建

 3.1.1 pthread_create

 3.2 线程退出

3.2.1 主线程退出,其余进程全部都要结束

3.2.2 pthread_exit() 线程退出函数

3.2.3 pthread_cancel() 线程取消

3.2.4 线程异常退出

3.3 进程等待

3.3.1 pthread_join()

 3.4 线程分离 

3.4.1 在创建线程时设置分离属性:

3.4.2 在创建线程后设置分离属性(使用pthread_detach函数)

3.5 线程之间通信

4 线程用户层面的线程id和Linux底层的轻量级进程的id

4.1 线程用户层面的线程id

4.1.1 pthread_self()

 4.2 Linux底层的轻量级进程的id

5 多线程的创建,以及实现多线程任务

6 进程与线程的深度比较

7.1 线程的优缺点 

7.1  线程的优点:

7.2 线程的缺点:

7.3 面试题

8 C++11封装的线程库

9 线程库的理解

9.1 用户层面的线程id(底层剖析)

9.2 线程pthread的理解

9.3 线程局部存储(TLS)


1 初识线程

线程是进程内部的一个执行分支,是CPU调度的基本单元

1.1 线程的由来

就当论前面那个线程的概念,我相信所有人都是一脸懵逼。

就是我们在进程当中运行程序的时候都是单执行流往下运行的,如果要多执行流往下运行,就需要引入多进程。

 

但是呢,多进程有一个缺点,就是创建子进程的时候需要,创建PCB和进程地址空间,页表等等内核数据结构比较浪费资源。

就是多进程成本比较高,我们需要一个低成本的方式来实现一份代码并行。

1.2 线程的产生

首先,我们就要对线程进行先描述,在组织。这样就有了线程控制块PCB概念的产生。

这样一来,我们就需要对线程重新设计一套适用于线程的系统调用、数据结构、和调度算法等。

又因为线程和进程高度相似,所以这里衍生出了两种对于线程的设计模式。

操作系统这么学科当中是有明确TCB这个概念的,但在具体的系统的实现过程中,Windows采用了对于多线程另起炉灶的想法,重新设计了TCB。而Linux中则是对于多线程这方面的底层设计,则复用了PCB的设计。

这两种方法我认为是Linux更好,这样就减少了维护成本。

这样就是三个线程在同一个进程当中,将正文代码分成三份,并行向上执行。

1.3 进程 VS 线程

以前讲述的进程就是目前理解的进程的特殊情况。

站在内核的角度上来看:进程就是承担系统资源调度的基本实体


1.4 关于系统内部关于线程和进程的资源调度问题

首先,我们明确一点,就是在Linux下线程复用了进程的代码。那么在内核调用task_struct(都是执行流)不会有任何区别。

 还就是要明确一点就是:PCB只是一个进程控制块,他不是整个进程,进程 = 内核数据结构 + 进程代码和数据。

这里的轻量级进程就是指着一个进程控制块+进程地址空间+页表+内存上的代码


2 页表、虚拟地址和物理地址

2.1 对物理地址的描述

在磁盘内容当中,每个inode的数据块都是4KB的,所以磁盘上的内容加载进入内存也是以4KB为基本单位的。

所以,我们之前提到的任何关于内存的操作都是以4KB为基本单位的,比如:new申请空间,子进程修改代码和数据发生的写时拷贝都是以4KB为基本单位的。 

2.2 对于页表设计的解析

首先,我们之前对于页表都没有一个明确的介绍,可能会让大家认为,页表就是一个普通的KV结构,我们稍微想一想就知道页表肯定不会是一个简单的KV结构,因为这样的页表需要的内存空间实在是太大了。

所以,Linux在设计的时候就对页表进行了类似于多级索引式的设计。

给不同的线程分配不同的的区域本质就是为了让不同的线程,各自看到所有页表的子集。

3 线程的控制

 3.1 进程创建

 3.1.1 pthread_create

pthread_create 是 POSIX 线程(也称为 pthreads)库中用于创建一个新线程的函数。它是多线程编程中的一个关键函数,允许你在一个进程中并发地执行多个线程

参数解释

  • pthread_t *thread:这是一个指向 pthread_t 类型变量的指针,用于存储新创建线程的线程标识符。当你调用 pthread_create 时,这个变量将被赋值为新线程的 ID。

  • const pthread_attr_t *attr:这是一个指向 pthread_attr_t 结构体的指针,用于指定线程的属性。如果你不需要设置特殊的线程属性,可以传递 NULL,表示使用默认属性。

  • void *(*start_routine) (void *):这是一个指向函数的指针,该函数是新线程的起始执行点。这个函数必须返回一个 void * 类型的值,并接受一个 void * 类型的参数。这个参数允许你将数据传递给新线程。

  • void *arg:这是传递给 start_routine 函数的参数。你可以通过这个参数将任何类型的数据传递给新线程,只需确保在 start_routine 函数中正确地解释和转换这个参数。

返回值

  • 如果函数成功,pthread_create 将返回 0
  • 如果函数失败,它将返回一个非零的错误码,表示创建线程时发生的错误。

 3.2 线程退出

3.2.1 主线程退出,其余进程全部都要结束

#include <iostream>
#include <pthread.h>
#include <unistd.h>std::string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), "0x%lx", tid);return id;
}void *handler(void *args)
{while (1){std::cout << "I am new pthread , pid : " << getpid() << std::endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, handler, nullptr);int cnt = 5;while (cnt){std::cout << "I am main pthread , pid : " << getpid() << " pthread id : " << ToHex(tid) << std::endl;sleep(1);cnt--;}std::cout << "I am main pthread ,is quited"<<  std::endl;return 0;
}


3.2.2 pthread_exit() 线程退出函数

参数

  • retval:一个指向任意类型数据的指针,该数据将被返回给调用 pthread_join() 的线程。这个返回值可以通过类型转换来匹配任何所需的数据类型。如果不需要返回值,可以将 retval 设置为 NULL


3.2.3 pthread_cancel() 线程取消

pthread_cancel是POSIX线程(pthread)库中用于取消指定线程执行的函数。 

参数说明

  • thread:指定要取消的线程的标识符(pthread_t类型)。

返回值

  • 成功时返回0。
  • 失败时返回非0值,通常用于指示错误类型。

工作机制

  • pthread_cancel函数发送一个取消请求给指定的线程,但并不会立即终止该线程的执行。
  • 线程在接收到取消请求后,会继续运行,直到到达某个取消点(Cancellation Point)。取消点是线程检查是否被取消并按照请求进行动作的一个位置。(这就类似于进程退出,处理信号,防止突然的退出,导致不可预料的错误)
  • 线程可以通过调用pthread_testcancel函数来主动检查是否存在取消请求,并在发现请求时执行相应的处理(如退出线程)。


3.2.4 线程异常退出

1. 代码跑完,结果对 

2. 代码跑完,结果不对 

3. 出异常了 --- 重点 --- 多线程中,任何一个线程出现异常(div 0, 野指针), 都会导致整个进程退出! ---- 多线程代码往往健壮性不好

#include <iostream>
#include <pthread.h>
#include <unistd.h>std::string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), "0x%lx", tid);return id;
}
// 线程退出
// // 1. 代码跑完,结果对
// // 2. 代码跑完,结果不对
// // 3. 出异常了 --- 重点 --- 多线程中,任何一个线程出现异常(div 0, 野指针), 都会导致整个进程退出! ---- 多线程代码往往健壮性不好void *handler(void *args)
{int cnt = 10;while (cnt){std::cout << "I am new pthread , pid : " << getpid() << std::endl;sleep(1);cnt--;int *p =nullptr;*p=100;}pthread_exit((void *)1234);return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, handler, nullptr);int cnt = 5;while (1){std::cout << "I am main pthread , pid : " << getpid() << " pthread id : " << ToHex(tid) << std::endl;sleep(1);cnt--;}// void *nums;// pthread_join(tid, &nums);// std::cout << "I am main pthread ,is quited" << "exit nums "<< (long long)nums <<std::endl;return 0;
}

新线程出现了异常(除零错误),无论是新线程还是主线程的死循环都退出了 


3.3 进程等待

3.3.1 pthread_join()

pthread_join 是 POSIX 线程(pthread)库中用于等待一个特定的线程终止的函数。当你创建一个线程后,主线程(或其他线程)可能需要等待该线程完成其任务后再继续执行。

参数

  • thread:要等待的线程的标识符,通常是通过 pthread_create 函数获得的。
  • retval:一个指向指针的指针,用于接收被等待线程的返回值。如果不需要这个返回值,可以将其设置为 NULL。被等待线程的返回值应该是一个 void* 类型的指针,这允许线程返回任何类型的数据(通过类型转换)。如果 retval 不是 NULL,那么 pthread_join 会将 retval 所指向的位置设置为被等待线程的返回值。

返回值

  • 成功时,pthread_join 返回 0
  • 失败时,返回一个错误码。常见的错误码包括 ESRCH(无此线程)、EINVAL(线程不是可连接的,或者 thread 不表示一个线程),以及 EDEADLK(检测到死锁)。

#include <iostream>
#include <pthread.h>
#include <unistd.h>std::string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), "0x%lx", tid);return id;
}void *handler(void *args)
{int cnt = 10;while (cnt){std::cout << "I am new pthread , pid : " << getpid() << std::endl;sleep(1);cnt--;}pthread_exit((void *)1234);return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, handler, nullptr);int cnt = 5;while (cnt){std::cout << "I am main pthread , pid : " << getpid() << " pthread id : " << ToHex(tid) << std::endl;sleep(1);cnt--;}void *nums;pthread_join(tid, &nums);std::cout << "I am main pthread ,is quited" << "exit nums "<< (long long)nums <<std::endl;return 0;
}


 3.4 线程分离 

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

简单来讲就是,把线程分离出去,不在需要主线程进行等待了。

线程分离可以通过以下两种方法实现:

3.4.1 在创建线程时设置分离属性

使用pthread_create函数创建线程时,可以通过该函数的第二个参数(线程属性对象)来设置线程的分离属性。

3.4.2 在创建线程后设置分离属性(使用pthread_detach函数)

  • 参数thread是要设置为脱离状态的线程的ID。
  • 返回值:成功时返回0;失败时返回一个非零错误码。
#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>
#include <string.h>
#include <error.h>void *pthreadrun(void *args)
{pthread_detach(pthread_self());int cnt = 5;while (cnt){const char *name = static_cast<const char *>(args);std::cout << "I am " << name << std::endl;cnt--;sleep(1);}return nullptr;
}int main()
{const char name[64] = "new pthread-1";pthread_t tid;pthread_create(&tid, nullptr, pthreadrun, (void *)name);std::cout << "I am main pthread , begin wait" << std::endl;int n = pthread_join(tid, nullptr);std::cout << "I am main pthread" <<"n : "<< n << strerror(n) << std::endl;return 0;
}


3.5 线程之间通信

// 同一个进程内的线程,大部分资源都是共享的. 地址空间是共享的!

#include <iostream>
#include <pthread.h>
#include <unistd.h>
// 同一个进程内的线程,大部分资源都是共享的. 地址空间是共享的!
int g_val = 100;std::string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), "0x%lx", tid);return id;
}
// 线程退出
// // 1. 代码跑完,结果对
// // 2. 代码跑完,结果不对
// // 3. 出异常了 --- 重点 --- 多线程中,任何一个线程出现异常(div 0, 野指针), 都会导致整个进程退出! ---- 多线程代码往往健壮性不好void *handler(void *args)
{int cnt = 10;while (cnt){std::cout << "I am new pthread , pid : " << getpid() << "  g_val : "<<g_val << std::endl;sleep(1);cnt--;// int *p =nullptr;// *p=100;}pthread_exit((void *)1234);return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, handler, nullptr);int cnt = 5;while (1){g_val++;std::cout << "I am main pthread , pid : " << getpid() << " pthread id : " << ToHex(tid) << "g_val :  "<<g_val<< std::endl;sleep(1);cnt--;}// void *nums;// pthread_join(tid, &nums);// std::cout << "I am main pthread ,is quited" << "exit nums "<< (long long)nums <<std::endl;return 0;
}


4 线程用户层面的线程id和Linux底层的轻量级进程的id

4.1 线程用户层面的线程id

#include <iostream>
#include <pthread.h>
#include <unistd.h>std::string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), "0x%lx", tid);return id;
}void *handler(void *args)
{while (1){std::cout << "I am new pthread , pid : " << getpid() << std::endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, handler, nullptr);while (1){std::cout << "I am main pthread , pid : " << getpid() << " pthread id : " << ToHex(tid) << std::endl;sleep(1);}return 0;
}

4.1.1 pthread_self()

 

功能

pthread_self 函数不需要任何参数,它返回调用线程的线程ID。这个线程ID是一个 pthread_t 类型的值,通常用于线程的管理、线程间的通信和同步等操作。

返回值

pthread_self 总是成功返回调用线程的线程ID。这个ID在线程的整个生命周期内是唯一的,但如果有多个线程,并且有一个线程完成,那么该ID就可以被重用。因此,对于所有正在运行的线程,ID在当前时刻是唯一的。


 4.2 Linux底层的轻量级进程的id

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <iostream>using namespace std;void *newthreadrun(void *args)
{while (1){cout << "I am new pthread" << endl;sleep(1);}}int main()
{pthread_t tid;pthread_create(&tid, nullptr, newthreadrun, nullptr);while (1){cout << "I am main pthread" << endl;sleep(1);}return 0;
}

这里我们看到了同一个pid(同一个进程)下的两个线程(LWP不同)。

所以,操作系统在调度的时候,是使用哪一个id进行调度呢?-- LWP


5 多线程的创建,以及实现多线程任务

#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include <cstdlib>
#include <pthread.h> // 原生线程库的头文件const int threadnum = 5;class Task
{
public:Task(){}void SetData(int x, int y){datax = x;datay = y;}int Excute(){return datax + datay;}~Task(){}
private:int datax;int datay;
};class ThreadData : public Task
{
public:ThreadData(int x, int y, const std::string &threadname):_threadname(threadname){_t.SetData(x, y);}std::string threadname(){return _threadname;}int run(){return _t.Excute();}
private:std::string _threadname;Task _t;
};class Result
{
public:Result(){}~Result(){}void SetResult(int result, const std::string &threadname){_result = result;_threadname = threadname;}void Print(){std::cout << _threadname << " : " << _result << std::endl;}
private:int _result;std::string _threadname;
};void *handlerTask(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);std::string name = td->threadname();Result *res = new Result();int result = td->run();res->SetResult(result, name);// std::cout << name << "run result : " << result << std::endl;delete td;sleep(2);return res;}
// 1. 多线程创建
// 2. 线程传参和返回值,我们可以传递级别信息,也可以传递其他对象(包括你自己定义的!)int main()
{std::vector<pthread_t> threads;for (int i = 0; i < threadnum; i++){char threadname[64];snprintf(threadname, 64, "Thread-%d", i + 1);ThreadData *td = new ThreadData(10, 20, threadname);pthread_t tid;pthread_create(&tid, nullptr, handlerTask, td);threads.push_back(tid);}std::vector<Result*> result_set;void *ret = nullptr;for (auto &tid : threads){pthread_join(tid, &ret);result_set.push_back((Result*)ret);}for(auto & res : result_set){res->Print();delete res;}
}

6 进程与线程的深度比较

进程是资源分配的基本单位线程是调度的基本单位

线程共享进程数据,但也拥有自己的一部分数据:

  1. 线程ID
  2. 一组寄存器栈
  3. errno
  4. 信号屏蔽字调度优先级

私有:线程的硬件上下文(CPU寄存器上的值)(调度)

        线程的独立栈结构(常规运行) 

共享:代码和全局数据

        进程文件描述符表

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

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

 进程与线程的关系如下:

7.1 线程的优缺点 

7.1  线程的优点:

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

7.2 线程的缺点:

性能损失

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

健壮性降低

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

缺乏访问控制

  • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。编程难度提高
  • 编写与调试一个多线程程序比单线程程序困难得多

7.3 面试题

问:为什么与进程相比,线程切换操作系统要做的任务少很多?

1.上下文切换(非主要原因)

切换进程的时候,加载进入CPU的上下文数据全都要重新加载,因为进程地址空间,页表。。都是独立的;而线程就不需要全部重新加载,只需要重新加载一部分。

2.局部性原理 (主要问题)

CPU在调度进程的时候,当一条代码被执行的时候,CPU内部的cache就会选择性的缓存被执行代码的附近50行代码,期待这些代码能够被执行,这样就能提高效率,但是,下一条代码没有在缓存中被调度,CPU就会根据算法,删除一部分,重新写入一部分。

这样看来,如果进程切换,代码这些全部都重新加载进入内存,所以这些缓存全部都要被清除。

但是线程的话,还是执行同一份代码,可能只不过不是同一部分,但是缓存中的代码就不需要全部被清楚,或者只需要重新重新加载一部分。 

8 C++11封装的线程库

#include <iostream>
#include <thread>
#include <unistd.h>using namespace std;void threadrun(int args)
{while (true){cout << "I am new pthread" << endl;sleep(1);}
}
int main()
{thread(threadrun, 10);return 0;
}

该代码没有运用任何C原生线程库里的函数,使用的都是C++库里的函数,为什么编译的时候产生了错误 

这个错误说明了,代码编译的时候,还需要链接原生线程库,这是为什么? 

这说明了C++11,在Linux环境下对于线程的操作,都是封装的Linux下的原生线程库。

这是为了代码的跨平台性。

那么C++在Windows下,还是封装的Linux的原生线程库吗?肯定不是了,封装的是Windows下的原生线程库了。应该是相对平台进行判断,然后选择同一份代码的不同部分使用。

那么其他语言呢?我们只需要明白一点,Linux原生线程库是,Linux底层实现多线程的唯一方法。


9 线程库的理解

9.1 用户层面的线程id(底层剖析)

我们能很明显的看到,Linux底层封装的轻量级进程的编号和原生线程库层面封装的线程编号完全不是一个东西。


9.2 线程pthread的理解

首先,要使用线程就得,先让线程库加载进入内存,然后映射到进程地址空间的共享区内。 

一个进程当中会有很多线程,那么我们就需要对线程进行管理。但是在Linux操作系统方面底层没有线程的概念,只有轻量级线程的概念,所以Linux无法维护线程。Linux上的线程的概念是有线程库来提供的,所以线程的维护就应该交给线程库。

关于这方面就直接说了:用户层面的线程编号:struct_pthread的开头地址。


9.3 线程局部存储(TLS)

  1. 线程局部存储:是一种机制,允许每个线程拥有自己变量的副本,这些变量在每个线程中独立存在、互不影响。这种机制确保了线程数据的独立性,从而避免了全局变量或静态变量在并发环境下竞态条件和数据不一致的问题。(线程局部存储只能存储自定义类型)
  2. 线程局部存储的优点:数据隔离、减少了同步开销、提高了性能。
  3. __thread关键字:用于声明线程局部存储变量,使用__thread关键字声明的变量在每个线程中都有一个独立的副本,这些副本互不影响,有助于避免线程间的竞态条件或数据不一致问题,提高线程安全性。

__thread 数据类型 变量名 ;

#include <iostream>
#include <thread>
#include <unistd.h>using namespace std;__thread int g_val=100;void* pthreadrun1(void* args)
{while(true){cout<<"The new pthread-1 tid: "<<pthread_self()<<"g_val: "<<g_val<<endl;sleep(1);g_val++;}
}void* pthreadrun2(void* args)
{while(true){cout<<"The new pthread-2 tid: "<<pthread_self()<<"g_val: "<<g_val<<endl;sleep(1);}
}int main()
{pthread_t tid1 ;pthread_create(&tid1,nullptr,pthreadrun1,nullptr);pthread_t tid2 ;pthread_create(&tid2,nullptr,pthreadrun2,nullptr);while(1){}return 0;
}

10 自己利用原生库简单的封装成C++11的线程库

#ifndef __THREAD_HPP__
#define __THREAD_HPP__#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>namespace ThreadModule
{template<typename T>using func_t = std::function<void(T)>;// typedef std::function<void(const T&)> func_t;template<typename T>class Thread{public:void Excute(){_func(_data);}public:Thread(func_t<T> func, T data, const std::string &name="none-name"): _func(func), _data(data), _threadname(name), _stop(true){}static void *threadroutine(void *args) // 类成员函数,形参是有this指针的!!{Thread<T> *self = static_cast<Thread<T> *>(args);self->Excute();return nullptr;}bool Start(){int n = pthread_create(&_tid, nullptr, threadroutine, this);if(!n){_stop = false;return true;}else{return false;}}void Detach(){if(!_stop){pthread_detach(_tid);}}void Join(){if(!_stop){pthread_join(_tid, nullptr);}}std::string name(){return _threadname;}void Stop(){_stop = true;}~Thread() {}private:pthread_t _tid;std::string _threadname;T _data;  // 为了让所有的线程访问同一个全局变量func_t<T> _func;bool _stop;};
} // namespace ThreadModule#endif

http://www.lryc.cn/news/511234.html

相关文章:

  • 【信息系统项目管理师】第13章:项目资源管理过程详解
  • vue3封装而成的APP ,在版本更新后,页面显示空白
  • GEE云计算、多源遥感、高光谱遥感技术蓝碳储量估算;红树林植被指数计算及提取
  • 【知识】cuda检测GPU是否支持P2P通信及一些注意事项
  • 用 Python 生成功能强大的二维码工具(支持自定义颜色与 Logo)
  • RTX5 数据队列传输流程
  • 24.try块怎么用 C#例子
  • 【机器学习 | 数据挖掘】智能推荐算法
  • 120.【C语言】数据结构之快速排序(详解Hoare排序算法)
  • uniapp通过v-if进行判断时,会出现闪屏?【已解决】
  • 各种网站(学习资源、常用工具及其他,持续更新中~)
  • 网络技术-QoS策略以及如何定义 流分类,流行为,流策略
  • 线程晨考day20
  • 【ES6复习笔记】迭代器(10)
  • MySQL的TIMESTAMP类型字段非空和默认值属性的影响
  • 【Linux进程】初悉进程
  • Python学习之路(5)— 使用C扩展
  • 动态规划34:446. 等差数列划分 II - 子序列
  • PPT画图——如何设置导致图片为600dpi
  • 【模块系列】STM321.69TFT屏幕
  • 大模型辅助测试的正确打开方式?
  • 三相电的相电压、线电压、额定值、有效值,变比,零序电压,零序电流,三相三线制的三角形连接,三相四线制的星形连接
  • 电商网站的基础用户数在100万,日活跃用户数在1万左右,系统下单TPS最大支持1000,应用服务要保证高可用。请预估该网站每天的使用成本。
  • 线性代数期末总复习的点点滴滴(1)
  • python+reportlab创建PDF文件
  • 2024最新qrcode.min.js生成二维码Demo
  • 【Microi吾码】开源力量赋能低代码创新,重塑软件开发生态格局
  • Github - 如何提交一个带有“verified”标识的commit
  • HCIA笔记9--NAT、ACL与链路聚合
  • SCSA:探索空间与通道注意力之间的协同效应