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

从零开始学习Linux(13)---多线程

目录

1.线程

1.线程的概念

2.线程的理解(Linux系统为例)---一般系统

3.进程vs线程

4.线程的控制

5.线程的等待

6.线程的终止

7.线程的分离

2.线程的互斥

1.互斥锁

2.条件变量

3.生产消费模型

4.阻塞队列

5.信号量

6.唤醒队列


1.线程

1.线程的概念

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

        加载到内存中的程序,叫做进程。修正=内核数据结构+进程代码和数据。

2.线程的理解(Linux系统为例)---一般系统

        地址空间是计算机内存管理中的一个基本概念,它指的是一个程序在运行时可以访问的内存地址的集合。地址空间为程序提供了一个抽象层,使得程序可以独立于其他程序和物理内存的实际情况来访问内存。

  • 物理地址空间:指的是实际的物理内存地址,即内存条上的每一个存储单元都有一个唯一的物理地址。物理地址空间由硬件直接管理。

  • 虚拟地址空间:是相对于物理地址空间的一个抽象层,它由操作系统创建和管理。程序通常使用虚拟地址来访问内存,然后由操作系统和硬件将这些虚拟地址映射到物理地址。

        虚拟地址到物理地址的映射通常通过页表来实现。当程序访问一个虚拟地址时,内存管理单元会查找页表,找到对应的物理地址,然后进行实际的内存访问。

        地址空间和地址空间上的虚拟地址,本质是一种资源。

3.进程vs线程

        进程是操作系统进行资源分配和调度的基本单位。每个进程都拥有独立的地址空间、执行堆栈、程序计数器、寄存器集合以及系统资源。

        线程是进程内的一个执行流,是CPU调度和分派的基本单位。同一进程中的线程共享地址空间和其他资源。

4.线程的控制

        pthread_create是 POSIX 线程(pthread)库中的一个函数,用于在 POSIX 兼容的操作系统中创建新的线程。以下是关于pthread_create 的详细信息:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
  • thread:这是一个输出参数,用于存储新创建线程的线程 ID。
  • attr:这是一个输入参数,用于设置新线程的属性。如果传递NULL,则新线程将使用默认属性。
  • start_routine:这是一个函数指针,指向新线程将要执行的函数。该函数接受一个void*类型的参数,并返回一个 void* 类型的值。
  • arg:这是传递给start_routine函数的参数。
  • 成功:返回 0。
  • 失败:返回错误编号,可以通过pthread_error获取。
#include<iostream>      // 包含标准输入输出流库
#include<pthread.h>     // 包含 POSIX 线程库
#include<unistd.h>      // 包含 UNIX 标准函数库,如 sleep()using namespace std;   // 使用标准命名空间// 线程函数
void *newthreadrun(void *args) {while (true) {    // 无限循环// 输出新线程的信息和进程 IDcout << "I am new thread, pid:" << getpid() << endl;sleep(1);     // 休眠 1 秒}// 这个函数不会退出,因此不需要 return 语句
}int main() {pthread_t tid;    // 声明线程 ID 变量// 创建新线程,传入线程函数和参数pthread_create(&tid, nullptr, newthreadrun, nullptr);while (true) {    // 无限循环// 输出主线程的信息和进程 IDcout << "I am main thread, pid:" << getpid() << endl;sleep(1);     // 休眠 1 秒}// 主循环也不会退出,因此不会执行到这里// 如果需要正常退出程序,应该添加适当的退出条件或信号处理
}

5.线程的等待

        pthread_join是 POSIX 线程(pthread)库中的一个函数,用于等待线程结束并获取其返回值。以下是关于pthread_join的详细信息:

int pthread_join(pthread_t thread, void **retval);
  • thread:这是要等待的线程的线程 ID。
  • retval:这是一个输出参数,用于存储线程结束时返回的值。如果线程没有返回值,则将其设置为NULL。
  • 成功:返回 0。
  • 失败:返回错误编号,可以通过pthread_error获取。
#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>
//同一个进程内的线程,大部分资源都是共享的,地址空间是共享的
std::string ToHex(pthread_t tid)
{char id[64];snprintf(id,sizeof(id),"0x%lx",tid);return id;
}using namespace std; void *threadrun(void *args)
{string threadname=(char*)args;int cnt=5; while(cnt){cout<<threadname<<"is running:"<<cnt<<",pid:"<<getpid()<<"mythread id:"<<ToHex(pthread_self())<<endl;sleep(1);cnt--;}return (void*)123;
}
//主线程退出==进程退出==所以线程都要退出
//1.往往我们需要main thread最后结束
//2.线程也要被"wait",不然可能内存泄漏
int main()
{pthread_t tid;pthread_create(&tid,nullptr,threadrun,(void*)"thread-1");void* ret=nullptr;int n=pthread_join(tid,&ret);cout<<"main thread quit,n="<<n<<"main thread get a ret:"<<(int)ret<<endl;sleep(5);// int cnt=10;// while(cnt)// {//     cout<<"main thread is running:"<<cnt<<",pid:"//     <<getpid()<<"new thread id:"<<ToHex(tid)<<" "//     <<"main thread id:"<<ToHex(pthread_self())<<endl;//     sleep(1);//     cnt--;// }return 0;
}

6.线程的终止

        pthread_exit是 POSIX 线程(pthread)库中的一个函数,用于使当前线程立即终止并返回指定的值。以下是关于pthread_exit的详细信息:

void pthread_exit(void *retval);

        retval:这是线程退出时返回的值。这个值会被传递给调用pthread_join的线程

        pthread_cancel是 POSIX 线程(pthread)库中的一个函数,用于请求取消另一个线程。当一个线程被取消时,它会收到一个取消请求,并可以选择如何响应这个请求。以下是关于pthread_cancel的详细信息:

int pthread_cancel(pthread_t thread);

        thread:这是要取消的线程的线程 ID。

const int threadnum = 5; // 定义线程数量为5// Task类,表示一个任务
class Task {
public:Task() {} // 构造函数void SetData(int x, int y) { // 设置任务数据datax = x;datay = y;}int Excute() { // 执行任务,返回数据x和数据y的和return datax + datay;}~Task() {} // 析构函数
private:int datax; // 任务数据xint datay; // 任务数据y
};// ThreadData类,继承自Task类,用于包装任务和线程名称
class ThreadData : public Task {
public:ThreadData(int x, int y, const string &threadname): _threadname(threadname) { // 构造函数,设置线程名称_t.SetData(x, y); // 设置任务数据}string threadname() { // 返回线程名称return _threadname;}int run() { // 执行任务return _t.Excute();}
private:string _threadname; // 线程名称Task _t; // 任务数据
};// Result类,用于存储线程执行结果
class Result {
public:Result() {} // 构造函数~Result() {} // 析构函数void SetResult(int result, const string &threadname) { // 设置结果和线程名称_result = result;_threadname = threadname;}void Print() { // 打印结果cout << _threadname << ":" << _result << endl;}
private:int _result; // 结果string _threadname; // 线程名称
};void *handlerTask(void *args) {ThreadData *td = static_cast<ThreadData*>(args); // 获取线程数据string name = td->threadname(); // 获取线程名称Result* res = new Result(); // 创建结果对象int result = td->run(); // 执行任务res->SetResult(result, name); // 设置结果和线程名称cout << name << "run result:" << result << endl; // 打印结果delete td; // 删除线程数据sleep(2); // 线程休眠2秒return res; // 返回结果对象
}int main() {vector<pthread_t> threads; // 线程ID向量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; // 线程IDpthread_create(&tid, nullptr, handlerTask, td); // 创建线程threads.push_back(tid); // 添加线程ID到向量}vector<Result*> result_set; // 结果集合void *ret = nullptr;for (auto& tid : threads) { // 等待每个线程完成pthread_join(tid, &ret); // 等待线程完成Result* res = static_cast<Result*>(ret); // 获取返回的结果对象result_set.push_back(res); // 添加结果对象到集合}for (auto& res : result_set) { // 打印每个线程的结果res->Print(); // 打印结果delete res; // 删除结果对象}
}

7.线程的分离

        在Linux环境中,pthread_detach函数用于设置线程的分离属性。分离线程是指线程在完成其工作后立即退出,而不等待其他线程来收集它的返回值或清理它的资源。以下是关于pthread_detach的详细信息:

int pthread_detach(pthread_t thread);

        当一个线程被设置为分离状态并完成其工作后,它会立即退出,并且不会返回任何值给调用pthread_join的线程。这意味着调用pthread_join的线程不会接收到任何值,也不会被阻塞。

2.线程的同步和互斥

        线程互斥是指操作系统提供的一种机制,用于确保多个线程在访问共享资源时能够互不干扰,从而避免数据竞争和不一致性。线程互斥通常通过以下几种方式实现:

下面是封装了一个简单的线程:

#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&)>; // 使用引用传递,允许修改传入的数据// 线程类模板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); // 将void*指针转换为Thread<T>指针self->Excute(); // 调用Excute函数执行传入的函数对象return nullptr;}// 启动线程bool Start(){int n = pthread_create(&_tid, nullptr, threadroutine, this); // 创建线程if(!n) // 如果创建成功{_stop = false; // 设置停止标志为falsereturn 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; // 线程IDstd::string _threadname; // 线程名称T &_data;  // 引用传递的数据,所有线程可以访问同一个数据func_t<T> _func; // 函数对象,线程执行的函数bool _stop; // 线程停止标志};
} // namespace ThreadModule#endif

1.互斥锁

        互斥锁(Mutex)是一种同步机制,用于保护共享资源,防止多个线程同时访问。以下是关于互斥锁的详细信息:

  • 程序在访问共享资源之前,必须先获得互斥锁。
  • 只有当线程持有互斥锁时,它才能访问共享资源。
  • 线程在完成对共享资源的访问后,必须释放互斥锁,以便其他线程可以获取它。
  • 互斥锁通常用于保护临界区,即那些只允许一个线程访问的代码段。
  • 在多线程编程中,互斥锁是确保数据一致性和防止竞态条件的关键。

临界区的主要特点是:

  1. 共享资源:临界区访问的是共享资源,即多个线程可以访问的资源。

  2. 互斥访问:为了避免竞态条件和其他同步问题,临界区内的代码必须保证在同一时刻只有一个线程可以执行。

  3. 执行时间:临界区的执行时间应该尽可能短,以减少线程的阻塞和等待时间。

        pthread_mutex_lock是 POSIX 线程(pthread)库中的一个函数,用于获取互斥锁。当一个线程尝试访问一个共享资源时,它必须首先获取互斥锁。以下是关于pthread_mutex_lock的详细信息:

int pthread_mutex_lock(pthread_mutex_t *mutex);

mutex这是指向pthread_mutex_t类型的指针,用于指定要获取的互斥锁。

        pthread_mutex_unlock是 POSIX 线程(pthread)库中的一个函数,用于释放互斥锁。当一个线程完成对共享资源的访问后,它必须释放互斥锁,以便其他线程可以获取它。以下是关于pthread_mutex_unlock的详细信息:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

2.条件变量

        条件变量(Condition Variable)是操作系统提供的一种同步机制,用于线程间的通信。它通常与互斥锁一起使用,以确保线程在等待某些条件满足时不会竞争共享资源。以下是关于条件变量的详细信息:

  • 当线程需要等待某些条件满足时,它会释放当前持有的互斥锁,然后等待条件变量。
  • 当其他线程满足这些条件时,它会通知等待的线程,然后等待的线程会重新获取互斥锁并继续执行。

        pthread_cond_init是 POSIX 线程(pthread)库中的一个函数,用于初始化一个条件变量。条件变量是线程间通信的同步机制,它允许线程在等待某些条件满足时不会竞争共享资源。

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *cond_attr);
  • cond:这是指向pthread_cond_t类型的指针,用于指定要初始化的条件变量。
  • cond_attr:这是一个可选参数,指向pthread_condattr_t类型的指针,用于指定条件变量的属性。如果传递NULL,则使用默认属性。

        pthread_cond_destory是 POSIX 线程(pthread)库中的一个函数,用于清理条件变量。当一个条件变量不再需要时,可以使用pthread_cond_destory函数来释放它所占用的资源。

int pthread_cond_destroy(pthread_cond_t *cond);

        pthread_cond_wait是 POSIX 线程(pthread)库中的一个函数,用于线程在等待条件变量时释放互斥锁。当一个线程需要等待某个条件满足时,它会释放当前持有的互斥锁,然后等待条件变量。

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
  • cond:这是指向pthread_cond_t类型的指针,用于指定要等待的条件变量。
  • mutex:这是指向pthread_mutex_t类型的指针,用于指定当前持有的互斥锁。

        pthread_cond_signal是 POSIX 线程(pthread)库中的一个函数,用于唤醒一个等待条件变量的线程。当一个线程满足某些条件时,它可以使用pthread_cond_signa函数来通知其他等待的线程。

int pthread_cond_signal(pthread_cond_t *cond);
  • cond:这是指向pthread_cond_t类型的指针,用于指定要发送信号的条件变量。

        pthread_cond_broadcast是 POSIX 线程(pthread)库中的一个函数,用于唤醒所有等待特定条件变量的线程。当一个线程满足某些条件时,它可以使用pthread_cond_broadcast函数来通知所有等待的线程。

int pthread_cond_broadcast(pthread_cond_t *cond);

下面是一段测试代码:

#include<iostream>
#include<string>
#include<pthread.h>
#include<vector>
#include<unistd.h>pthread_cond_t gcond=PTHREAD_COND_INITIALIZER;
pthread_mutex_t gmutex=PTHREAD_MUTEX_INITIALIZER;void *SlaverCore(void *args)
{std::string name=static_cast<const char*>(args);while(true){pthread_mutex_lock(&gmutex);pthread_cond_wait(&gcond,&gmutex);std::cout<<"当前被叫醒的线程是:"<<name<<std::endl;pthread_mutex_unlock(&gmutex);}
}void *MasterCore(void *args)
{sleep(3);std::cout<<"master开始工作..."<<std::endl;std::string name=static_cast<const char*>(args);while(true){pthread_cond_signal(&gcond);std::cout<<"master唤醒一个线程..."<<std::endl;sleep(1);}
}void StartMaster(std::vector<pthread_t> *tidsptr)
{pthread_t tid;int n=pthread_create(&tid,nullptr,MasterCore,(void*)"Master Thread");if(n==0){std::cout<<"create success"<<std::endl;}tidsptr->emplace_back(tid);
}void StartSlaver(std::vector<pthread_t> *tidsptr,int threadnum=3)
{for(int i=0;i<threadnum;i++){char *name=new char[64];snprintf(name,64,"slaver-%d",i+1);pthread_t tid;int n=pthread_create(&tid,nullptr,SlaverCore,name);if(n==0){std::cout<<"create success:"<<name<<std::endl;tidsptr->emplace_back(tid);}}
}void WaitThread(std::vector<pthread_t> &tids)
{for(auto &tid:tids){pthread_join(tid,nullptr);}
}int main()
{std::vector<pthread_t> tids;StartMaster(&tids);StartSlaver(&tids,5);WaitThread(tids);return 0;
}

3.生产消费模型

        生产消费模型是多线程编程中的一个经典场景,它描述了生产者线程和消费者线程之间如何共享数据缓冲区。在这个模型中,生产者线程负责创建数据并将其放入缓冲区,而消费者线程负责从缓冲区中取出数据并处理。

以下是生产消费模型的关键组成部分:

  1. 缓冲区:这是一个共享的资源,用于存储数据。它可以是一个固定大小的数组、链表或其他数据结构。

  2. 生产者线程:负责创建数据并将其放入缓冲区。生产者线程需要确保在缓冲区满时不会写入数据,以避免数据丢失。

  3. 消费者线程:负责从缓冲区中取出数据并处理。消费者线程需要确保在缓冲区空时不会读取数据,以避免空指针异常。

  4. 同步机制:为了确保生产者线程和消费者线程之间的同步,可以使用互斥锁、条件变量等同步机制。

在生产消费模型中,生产者线程和消费者线程之间通常存在以下关系:

  • 生产者线程在缓冲区未满时写入数据。
  • 消费者线程在缓冲区非空时读取数据。
  • 生产者线程和消费者线程之间通过互斥锁和条件变量进行同步。

4.阻塞队列

        阻塞队列(Blocking Queue)是一种特殊的队列,它提供了一种机制,使得生产者和消费者线程可以以不同的速度工作,而不会造成资源浪费或死锁。当队列满时,生产者线程会被阻塞,直到队列中有空闲空间;当队列空时,消费者线程会被阻塞,直到队列中有数据可消费。

以下是阻塞队列的一些关键特性:

  1. 生产者-消费者模型:阻塞队列支持生产者线程和消费者线程之间的数据传递。生产者线程将数据放入队列,而消费者线程从队列中取出数据。

  2. 线程安全:阻塞队列通常实现线程安全,可以被多个生产者和消费者线程同时访问。

  3. 阻塞机制:当队列满时,生产者线程会被阻塞,直到队列中有空闲空间;当队列空时,消费者线程会被阻塞,直到队列中有数据可消费。

  4. 无界队列和有界队列:阻塞队列可以是无界队列或有限大小的队列。无界队列没有容量限制,而有限大小的队列有最大容量限制。

#ifndef __BLOCK_QUEUE_HPP__
#define __BLOCK_QUEUE_HPP__#include <iostream>
#include <string>
#include <queue>
#include <pthread.h>template <class T>
class BlockQueue
{
private:bool IsFull(){return _block_queue.size() == _cap;}bool IsEmpty(){return _block_queue.empty();}
public:BlockQueue(int cap) : _cap(cap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_product_cond, nullptr);pthread_cond_init(&_consum_cond, nullptr);}void Enqueue(T &in) // 生产者用的接口{pthread_mutex_lock(&_mutex);if(IsFull()) //BUG??{// 生产线程去等待,是在临界区中休眠的!你现在还持有锁呢!!!// 1. pthread_cond_wait调用是: a. 让调用进程等待 b. 自动释放曾经持有的_mutex锁pthread_cond_wait(&_product_cond, &_mutex); }// 进行生产// _block_queue.push(std::move(in));std::cout << in << std::endl;_block_queue.push(in);// 通知消费者来消费pthread_cond_signal(&_consum_cond);pthread_mutex_unlock(&_mutex);}void Pop(T *out) // 消费者用的接口{pthread_mutex_lock(&_mutex);if(IsEmpty()){// 消费线程去等待,是在临界区中休眠的!你现在还持有锁呢!!!// 1. pthread_cond_wait调用是: a. 让调用进程等待 b. 自动释放曾经持有的_mutex锁pthread_cond_wait(&_consum_cond, &_mutex); }// 进行消费*out = _block_queue.front();_block_queue.pop();// 通知生产者来生产pthread_cond_signal(&_product_cond);pthread_mutex_unlock(&_mutex);}~BlockQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_product_cond);pthread_cond_destroy(&_consum_cond);}private:std::queue<T> _block_queue;   // 阻塞队列int _cap;                     // 总上限pthread_mutex_t _mutex;       // 保护_block_queue的锁pthread_cond_t _product_cond; // 专门给生产者提供的条件变量pthread_cond_t _consum_cond;  // 专门给消费者提供的条件变量
};#endif

5.信号量

        在多线程编程中,信号量(Semaphore)是一种同步机制,用于控制对共享资源的访问。信号量可以用来实现互斥锁,也可以用来实现线程间的同步。

  • 信号量的值可以增加(通常使用pthread_sem_post)或减少(通常使用 pthread_sem_wait)。
  • 当信号量的值大于0时,线程可以访问共享资源;当信号量的值小于或等于0时,线程必须等待。
  • 信号量的值必须始终为非负整数。

        pthread_sem_init是 POSIX 线程(pthread)库中的一个函数,用于初始化一个信号量。信号量是一种同步机制,用于控制对共享资源的访问。它可以用来实现互斥锁,也可以用来实现线程间的同步。

int pthread_sem_init(pthread_sem_t *sem, const pthread_semattr_t *sem_attr, unsigned int value);
  • em:这是指向pthread_sem_t类型的指针,用于指定要初始化的信号量。
  • sem_attr:这是一个可选参数,指向pthread_semattr_t类型的指针,用于指定信号量的属性。如果传递NULL,则使用默认属性。
  • value:这是信号量的初始值。如果传递0,则信号量会被初始化为一个空信号量。

        pthread_sem_post是 POSIX 线程(pthread)库中的一个函数,用于增加信号量的值。

int pthread_sem_post(pthread_sem_t *sem);
  • sem:这是指向pthread_sem_t类型的指针,用于指定要增加值的信号量。

        pthread_sem_wait是 POSIX 线程(pthread)库中的一个函数,用于减少信号量的值。

int pthread_sem_wait(pthread_sem_t *sem);
  • sem:这是指向pthread_sem_t类型的指针,用于指定要减少值的信号量。

6.唤醒队列

        在多线程编程中,当一个线程因为某种原因(如等待条件变量、同步机制等)而被阻塞时,它会进入一个特定的等待队列中。这个等待队列被称为唤醒队列(Wakeup Queue),它包含了所有等待某些事件的线程。当这些事件发生时,这些线程会被唤醒,并有机会继续执行。

        唤醒队列通常与条件变量、信号量等同步机制一起使用,以确保线程在等待事件时不会竞争共享资源。当事件发生时,其他线程可以通知等待的线程,然后等待的线程会从唤醒队列中移除,并有机会继续执行。

以下是一些常见的唤醒队列实现:

  1. 条件变量唤醒队列:当条件变量被信号或信号量通知时,所有等待该条件变量的线程都会被唤醒,并重新尝试获取互斥锁。

  2. 信号量唤醒队列:当信号量的值被增加时,所有等待该信号量的线程都会被唤醒,并有机会继续执行。

  3. 等待队列:操作系统内核中通常有一个全局的等待队列,用于管理所有等待事件的线程。当事件发生时,内核会遍历这个等待队列,并唤醒所有符合条件的线程。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Flutter iOS混淆打包
  • python中ocr图片文字识别样例(一)
  • 低级编程语言和高级编程语言
  • 【ArcGIS微课1000例】0121:面状数据共享边的修改方法
  • 如何优化前端页面的 AJAX 请求性能并避免冲突
  • 【算法题】53. 最大子数组和-力扣(LeetCode)
  • 从HarmonyOS升级到HarmonyOS NEXT-环信SDK数据迁移
  • 如何基于Flink CDC与OceanBase构建实时数仓,实现简化链路,高效排查
  • C#-日志系统
  • 多边形抠图 python
  • 怎么解除BitLocker对磁盘的加密?
  • 一个简单的基于C语言的HTTP代理服务器的案例
  • 人工智能——猴子摘香蕉问题
  • Altium Designer(AD)百度云下载与安装(附安装步骤)
  • 2024年华为杯中国研究生数学建模竞赛E题(高速公路应急车道紧急启用模型)思路
  • [deviceone开发]-do_Webview的基本示例
  • HTTP中的ETag在移动客户端的应用
  • java架构面试锦集:开源框架+并发+数据结构+大企必备面试题
  • linux安装openssl、swoole等扩展的具体步骤
  • MyEclipse 8.0 GA 搭建 Struts2 + Spring2 + Hibernate3 (测试)
  • pdf文件如何在线转换为jpg图片
  • Python进阶细节
  • vue 配置sass、scss全局变量
  • Vue全家桶实现一个Web App
  • Vue源码解析(二)Vue的双向绑定讲解及实现
  • 微信小程序上拉加载:onReachBottom详解+设置触发距离
  • 我建了一个叫Hello World的项目
  • “十年磨一剑”--有赞的HBase平台实践和应用之路 ...
  • linux 淘宝开源监控工具tsar
  • postgresql行列转换函数
  • 宾利慕尚创始人典藏版国内首秀,2025年前实现全系车型电动化 | 2019上海车展 ...
  • 格斗健身潮牌24KiCK获近千万Pre-A轮融资,用户留存高达9个月 ...
  • ​ 无限可能性的探索:Amazon Lightsail轻量应用服务器引领数字化时代创新发展
  • #Linux(权限管理)
  • #预处理和函数的对比以及条件编译
  • (145)光线追踪距离场柔和阴影
  • (6)【Python/机器学习/深度学习】Machine-Learning模型与算法应用—使用Adaboost建模及工作环境下的数据分析整理
  • (solr系列:一)使用tomcat部署solr服务
  • (附源码)php新闻发布平台 毕业设计 141646
  • (附源码)springboot猪场管理系统 毕业设计 160901
  • (简单有案例)前端实现主题切换、动态换肤的两种简单方式
  • (七)c52学习之旅-中断
  • **Java有哪些悲观锁的实现_乐观锁、悲观锁、Redis分布式锁和Zookeeper分布式锁的实现以及流程原理...
  • .net 获取某一天 在当月是 第几周 函数
  • .NET 通过系统影子账户实现权限维持
  • .Net6支持的操作系统版本(.net8已来,你还在用.netframework4.5吗)
  • @RequestBody的使用
  • @value 静态变量_Python彻底搞懂:变量、对象、赋值、引用、拷贝
  • [ Algorithm ] N次方算法 N Square 动态规划解决
  • [ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双端队列)原理了解
  • [ vulhub漏洞复现篇 ] Celery <4.0 Redis未授权访问+Pickle反序列化利用
  • [AIGC] MySQL存储引擎详解
  • [Assignment] C++1
  • [CakePHP] 在Controller中使用Helper
  • [Django开源学习 1]django-vue-admin