【C++中线程学习】

news2024/11/15 4:29:32

1、多线程

C++11之前没有引入线程的概念,如果想要实现多线程,需要借助操作系统平台提供的API,比如Linux的<pthead.h>,或者windows下的<windows.h>。
C++11提供了语言层面上的多线程,包含在头文件<thread.h>中,解决了跨平台的问题,提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。C++11新标准中引入了5个头文件来支持多线程编程。
在这里插入图片描述

1.1、多进程与多线程

- 多进程并发
使用多进程并发是将一个应用程序划分为多个独立的进程(其中每个进程只有一个线程),这些独立的进程间可以相互通信,共同完成任务。由于操作系统对进程提供了大量的保护机制,以避免一个进程修改了另一个进程的数据,使用多进程比多线程更容易写出安全的代码,但这也就造成了多进程并发的两个缺点:

  1. 在进程间的通信,无论是用信号、套接字、还是文件、管道等方式,其使用要么是比较复杂,要么就是速度较慢,或者干脆就是又复杂又慢;
  2. 运行多个进程的开销很大,操作系统要分配很多的资源来对这些进程进行管理。

由于多个进程并发完成同一任务时,不可避免的要操作同一数据和进程间的相互通信,上述的两个缺点也就决定了多进程不是一个好的选择。

- 多线程并发
多线程并发指的是在同一个进程中执行多个进程。

☆优点:
线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在。也就是说,同一进程的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。这样,统一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。

☆缺点:
由于缺少操作系统提供的保护机制,在多线程共享数据以及通信时,程序员就需要做出措施来保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避开死锁(deadlock)。

1.2、并发(concurrency)和并行(parallel)

- 并发
一个时间片运行一个线程的代码,宏观上是同时,但其实不是
在这里插入图片描述
- 并行
宏观与微观上都是同时运行
在这里插入图片描述

1.3、创建线程

创建线程:将函数添加进线程当中即可。

- 形式1:thread 线程名(函数名);

#include<iostream>
#include<thread>
using namespace std;
void thread_fun1(){
    cout<<"子线程Mythread1正在运行"<<endl;
}
int main(){
    //创建线程Mythread1
    thread Mythread1 (thread_fun1);
    //加入线程
    Mythread1.join();
    cout<<"主线程正在运行"<<endl;
}

- 形式2:thread 线程名(函数名(参数));

void thread_fun2(int x){
    cout<<x<<endl;
}

//...
    thread Mythread2 (thread_fun2(100));
    Mythread2.join();
//...

- 形式3:thread (函数,参数).join();

void thrad_fun3(int x){
    cout<<x<<endl;
}
//...
    thread (thread_fun3,1).join();
//...

- 形式4:利用类的仿函数作为线程处理函数

class A{
public:
    void operator()(){
        cout<<"子线程"<<endl;
    }
}
int main(){
    //类的实例化对象充当线程函数
    A a;
    thread Mythread4(a);
    Mythread4.join();
    //或者这样写
    //thread Mythread((A()));
    //Mythrread.join();
    cout<<"主线程"<<endl;
}

- 形式5:通过Lambda表达式创建线程
简单来讲,就是把函数得定义和调用放在一处实现。

//...
    thread Mythread5([]{cout<<"子线程调用"<<endl;});
    Mythread5.join();

- 形式6:通过智能指针的方式创建线程
即以智能指针为参数的函数作为线程的处理函数

void thread_fun3(unique_ptr<int>ptr){
    cout<<"子线程:"<<ptr.get()<<endl;
    cout<<"子线程id:"<<this_thread::get_id()<<endl;
}
int main(){
    //智能指针作为参数的线程处理函数
    int *p = new int(12);
    cout<<*p<<endl;
    unique_ptr<int> ptr(new int(1000));
    cout<<"主线程"<<ptr.get()<<endl;//ptr.get()用于获取智能指针的地址
    thread Mythread6(thread_fun3,move(ptr));
    Mythread6.join();
    cout<<"主线程id"<<this_thread::get_id()<<endl;
    cout<<"主线程:"<<ptr.get()<<endl;
    return 0;
}

在这里插入图片描述

- 形式7:类的成员函数做线程处理函数

class A {
public:
    void func(int x) {
        cout << "子线程id:" <<this_thread::get_id()<< endl;
    }
};
int main(){
    A a;
    thread Mythread7(&A::func,a,1);//注意写法
    Mythread7.join();
    cout<<"主线程id:"<<this_thread::get_id()<<endl;
    return 0;
}

在这里插入图片描述

1.4、join()与detach()方式

当线程启动后,一定要在和线程相关联的thread销毁前,确定以何种方式等待线程执行结束。

  1. join():等待启动的线程完成,再会继续往下执行;
  2. detach():启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。

注意:thread对象只能被join或detach一次。
可以用joinable()来判断对象是否被join过,已经join过的线程用joinable会返回0。

1.5、this_thread

this_thread是一个类,它有4个功能函数,具体如下:

函数使用说明
get_idthis_thread::get_id()获得线程id
yieldthis_thread::yield()放弃线程执行,回到就绪状态
sleep_forthis_thread::sleep_for(x)暂停x秒
sleep_until具体用法如下直到…时间才开始运行
#include<iostream>
#include<thread>
//包含标准时间库
#include<chrono>
//包含时间和日期函数
#include<ctime>
#include<iomanip>
//禁用编译器对localtime的警告4996
#pragma warning(disable:4996)
using namespace std;

int main(){
    using chrono::system_clock;
    time_t tt = system_clock::to_time_t(system_clock::now());//输出当前时间并转换为time_t类型
    struct tm *ptm = localtime(&tt);//将time_t类型的时间转换为struct tm类型
    cout<<"Current time:"<<put_time(ptm,"%X")<<endl;//必须大写X,若小写,输出的为日期
    cout<<"Waiting for the next minute to begin..."<<endl;
    ++ptm->tm_min;//增加当前分钟数
    ptm->tm_sec = 0;//将秒数设为0
    this_thread::sleep_until(system_clock::from_time_t(mktime(ptm)));//使当前线程休眠直到指定时间
    cout<<put_time(ptm,"%X")<<"reached"<<endl;
    getchar();
    return 0;
}

在这里插入图片描述

2、mutex

2.1、mutex

mutex头文件主要声明了与互斥量(mutex)相关的类。
互斥量mutex:是线程间通信的一种方式,只有用户互斥对象的线程才能访问公共资源,因为互斥对象只有一个,从而避免了多个线程同时访问公共资源
mutex提供了4种互斥类型,如下所示:

类型说明
mutex最基本的Mutex类
recursive_mutex递归Mutex类
time_mutex定时Mutex类
recursive_timed_mutex定时递归Mutex类

示例,不加锁的情况:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>

int counter = 0;
void increase(int time) {
    for (int i = 0; i < time; i++) {
        // 当前线程休眠1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        counter++;
    }
}

int main(int argc, char** argv) {
    std::thread t1(increase, 100);
    std::thread t2(increase, 100);
    t1.join();
    t2.join();
    std::cout << "counter:" << counter << std::endl;
    return 0;
}

第一次运行的结果为:
在这里插入图片描述
第二次运行的结果为:
在这里插入图片描述
加上锁:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>

int counter = 0;
std::mutex mtx;

void increase(int time) {
    for (int i = 0; i < time; i++) {
        //上锁
        mtx.lock();
        // 当前线程休眠1毫秒
        std::this_thread::sleep_for(std::chrono::milliseconds(1));
        counter++;
        //解锁
        mtx.unlock();
    }
}

int main(int argc, char** argv) {
    std::thread t1(increase, 100);
    std::thread t2(increase, 100);
    t1.join();
    t2.join();
    std::cout << "counter:" << counter << std::endl;
    return 0;
}

第一次运行结果为:
在这里插入图片描述
第二次运行结果为:
在这里插入图片描述
注意:

  1. 任意时刻只允许一个线程对其上锁;
  2. mtx.lock():调用该函数得线程尝试加锁,如果上锁不成功,即其他线程已经上锁且未释放,则当前线程block。如果上锁成功,则执行后面的操作,操作完成后要调用mtx.unlock()释放锁,否则会导致死锁的产生;
  3. mutex还有一个操作为:mtx.try_lock(),字面意思就是“尝试上锁”,与mtx.lock()不同的是:如果上锁不成功,当前线程不会阻塞。

2.2、lock_guard

创建lock_guard时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开lock_guard对象的作用域时,lock_guard析构并释放互斥量。

特点:

  1. 创建即加锁,作用域结束后自动析构并解锁,不需要手动解锁;
  2. 不能中途解锁,必须等作用域结束才能解锁;
  3. 不能复制。

示例:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono>
#include <stdexcept>
using namespace std;

int counter = 0;
std::mutex mtx;

void increase(int time) {
    for (int i = 0; i < time; i++) {
        //上锁
        const lock_guard<std::mutex>lock(mtx);
        ++counter;
    }
}

int main(int argc, char** argv) {
    std::thread t1(increase, 100);
    std::thread t2(increase, 100);
    t1.join();
    t2.join();
    std::cout << "counter:" << counter << std::endl;
    return 0;
}

每次运行后结果都为200。

2.3、unique_lock

unique_lock是lock_guard的优化版,具有lock_guard的所有功能,还具有很多其他方法,使用起来更加灵活方便,能够应对更复杂的锁需要。

特点:

  1. 创建时可以不锁定(通过指定第二个参数defer_lock),而在需要时再锁;
  2. 可以随时加锁解锁;
  3. 作用域结束后自动析构并解锁;
  4. 不可复制,可移动;
  5. 条件变量需要改类型的锁作为参数(此时必须使用unique_lock)

ref():用于包装引用传递的值;
cref():用于包装按const引用传递的值。

3、condition_variable

condition_variable的头文件有两个variable类,一个是condition_variable,另一个是condition_variable_any。condition_variable必须结合unique_lock使用,而condition_variable_any可以使用任意的锁。

condition_variable条件变量可以阻塞(wait 、wait_for、wait_until)调用的线程直到使用(notify_one、notify_all)通知恢复为止。condition_variable是一个类,既有构造函数,也有析构函数,使用时需要构造对应的condition_variable对象,调用对象相应的函数来实现上面的功能。

类型说明
condition_variable构建对象
析构删除,释放资源
waitwait until notified
wait_forwait for timeout or until notified
wait_untilwait until notified or time point
notify_one解锁一个线程,若有多个,则未知哪个线程执行
botify_all解锁所有线程
cv_status这是一个类,表示variable的状态
enum class cv_status{no time_out, timeout};

3.1、wait

condition_variable提供了两种wait()函数分别是:

//只有一个参数为unique_lock对象,当前线程的执行会被阻塞,直到收到notify为止
void wait(unique_lock<mutex>&lck);
//有两个参数分别为unique_lock对象和一个可调用对象(函数或者Lambda表达式等),当前线程仅在pred=false时阻塞
template <class Predicate>
void wait(unique_lock<mutex>&lck, Predicate pred);

调用wait时,该函数会自动调用lck.unlock()释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行,然后阻塞当前线程,另外,一旦当前线程获得通知(notified,通常是另外某个线程调用notify_*唤醒了当前线程),wait()函数再次调用lck.lock()重新上锁然后wait返回退出,可以理解为lck的状态变换和wait函数被调用(退出)是同时进行的。

示例:

#include <iostream>           // std::cout
#include <thread>             // std::thread, std::this_thread::yield
#include <mutex>              // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable

std::mutex mtx;
std::condition_variable cv;

int cargo = 0;
//当cargo为0时,返回bool值0,否则返回1
bool shipment_available() {return cargo!=0;}

void consume (int n) {
    for (int i=0; i<n; ++i) {
        std::unique_lock<std::mutex> lck(mtx);//自动上锁
        //第二个参数为false才阻塞(wait),阻塞完即unlock,给其它线程资源
        cv.wait(lck,shipment_available);
        // consume:
        std::cout << cargo << '\n';
        cargo=0;
    }
}

int main ()
{
    std::thread consumer_thread (consume,10);

    for (int i=0; i<10; ++i) {
        //每次cargo每次为0时,shipment_avariable会返回false,就不会进入while下的语句(即不会放弃当前线程)
        while (shipment_available())    std::this_thread::yield();
        std::unique_lock<std::mutex> lck(mtx);
        cargo = i+1;
        cv.notify_one();
    }

    consumer_thread.join();,
    return 0;
}

说明:

  1. 主线程中的while,要在cargo为0时才会执行;
  2. 每次cargo置0后,子线程consumer_thread会解锁,主线程得以执行;
  3. 且每次cargo被置0后,wait就会启动等待。

3.2、wait_for

与wait()类似,不过wait_for()可以指定等待一个时间段,在当前线程收到notify或者rel_time超时之前,该线程都会处于阻塞状态,而一旦超时或收到通知,wait_for返回,剩下的处理步骤与wait类似。

template <class Rep, class Period>
cv_status wait_for(unique_lock<mutex>&lck, const chrono::duration<Rep,Period>&rel_time);

另外,wait_for()的重载版本的最后一个参数pred表示wait_for的预测条件,只有当pred为false时,调用wait_for()才会阻塞当前线程,并且在收到其他线程的通知后,只有当pred为true时才会解除阻塞。

template<class Rep, class Period>
cv_status wait_for(unique_lock<mutex>&lck, const chrono::duration<Rep, Period>&rel_time, Predicate pred);

示例:

#include<iostream>
#include<thread>
#include<chrono>
#include<mutex>
#include<condition_variable>

using namesapce std;

condition_variable cv;

int value;
void readvalue(){
    cin>>value;
    cv.notify_one();
}

int main(){
    cout<<"请输入一个整数:"<<endl;
    thread th(readvalue);
    
    mutex mtx;
    unique_lock<mutex>lck(mtx);
    while(cv.wait_for(lck,chrono::seconds(1)) == cv_status::timeout){
        cout<<"."<<endl;
    }
    cout<<"输入的整数为:"<<value<<endl;

    th.join();
    return 0;
}

说明:

通知或超时都会解锁,所以主线程会一直输出。

4、线程池

4.1、线程池的概念

在一个程序中,如果我们需要多次使用线程,这就意味着,需要多次的创建和销毁,而创建线程的过程必定会消耗内存,线程过多会带来调用的开销,进而影响缓存局部性能和整体性能。
所存在的问题如下:

  1. 创建太多线程,会浪费一定的资源,有些线程没有得到充分利用;
  2. 销毁太多线程,会导致之后再浪费时间重新进行创建;
  3. 创建线程太慢,会导致长时间的等待,弱化性能;
  4. 销毁线程太慢,会导致其他线程饥饿。
    而线程池的作用就体现出来了,它维护着多个线程,避免了在处理短时间任务时,创建与销毁线程的代价。

4.2、线程池的实现

在程序开始运行前就创建多个线程,这样,在程序运行时,只需要从线程池中拿来用就可以了,大大提高了程序运行效率。

一般线程池都由以下几个部分构成:

  1. 线程池管理(ThreadPoolManager):用于创建并管理线程池,也就是线程池类;
  2. 工作线程(WorkThread):线程池中线程;
  3. 任务队列task:用于存放没有处理的任务,提供一种缓冲机制;
  4. append:用于添加任务的接口。
    线程池实现代码:
#ifndef _THREADPOOL_H
#define _THREADPOOL_H
#include <vector>
#include <queue>
#include <thread>
#include <iostream>
#include <stdexcept>
#include <condition_variable>
#include <memory> //unique_ptr
#include<assert.h>

const int MAX_THREADS = 1000; //最大线程数目

template <typename T>
class threadPool
{
public:
    threadPool(int number = 1);//默认开一个线程
    ~threadPool();
    std::queue<T *> tasks_queue;		   //任务队列

    bool append(T *request);//往请求队列<task_queue>中添加任务<T *>

private:
    //工作线程需要运行的函数,不断的从任务队列中取出并执行
    static void *worker(void *arg);
    void run();

private:
    std::vector<std::thread> work_threads; //工作线程

    std::mutex queue_mutex;
    std::condition_variable condition;  //必须与unique_lock配合使用
    bool stop;
};//end class

//构造函数,创建线程
template <typename T>
threadPool<T>::threadPool(int number) : stop(false)
{
    if (number <= 0 || number > MAX_THREADS)
        throw std::exception();
    for (int i = 0; i < number; i++)
    {
        std::cout << "created Thread num is : " << i <<std::endl;
        work_threads.emplace_back(worker, this);//添加线程
        //直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
    }
}
template <typename T>
inline threadPool<T>::~threadPool()
{

    std::unique_lock<std::mutex> lock(queue_mutex);
    stop = true;

    condition.notify_all();
    for (auto &ww : work_threads)
        ww.join();//可以在析构函数中join
}
//添加任务
template <typename T>
bool threadPool<T>::append(T *request)
{
    /*操作工作队列时一定要加锁,因为他被所有线程共享*/
    queue_mutex.lock();//同一个类的锁
    tasks_queue.push(request);
    queue_mutex.unlock();
    condition.notify_one(); //线程池添加进去了任务,自然要通知等待的线程
    return true;
}
//单个线程
template <typename T>
void *threadPool<T>::worker(void *arg)
{
    threadPool *pool = (threadPool *)arg;
    pool->run();//线程运行
    return pool;
}
template <typename T>
void threadPool<T>::run()
{
    while (!stop)
    {
        std::unique_lock<std::mutex> lk(this->queue_mutex);
        /* unique_lock() 出作用域会自动解锁 */
        this->condition.wait(lk, [this] { return !this->tasks_queue.empty(); });
        //如果任务为空,则wait,就停下来等待唤醒
        //需要有任务,才启动该线程,不然就休眠
        if (this->tasks_queue.empty())//任务为空,双重保障
        {
            assert(0&&"断了");//实际上不会运行到这一步,因为任务为空,wait就休眠了。
            continue;
        }
        else
        {
            T *request = tasks_queue.front();
            tasks_queue.pop();
            if (request)//来任务了,开始执行
                request->process();
        }
    }
}
#endif

说明:

  1. 构造函数创建所需要的线程数;
  2. 一个线程对应一个任务,任务可能随时完成,线程则可能休眠,所以用任务队列queue实现(线程数量有限),线程采用wait机制;
  3. 任务在不断地添加,有可能大于线程数,处于队首的任务先执行;
  4. 只有添加任务(append)后,才开启condition.notify_one();
  5. wait表示任务为空时,线程休眠,等待新任务的加入;
  6. 添加新任务时需要添加锁,因为共享资源。
    测试代码:
#include "mythread.h"
#include<string>
#include<math.h>
using namespace std;
class Task
{
    public:
    void process()
    {
        //cout << "run........." << endl;
        //测试任务数量
        long i=1000000;
        while(i!=0)
        {
            int j = sqrt(i);
            i--;
        }
    }
};
int main(void)
{
    threadPool<Task> pool(6);//6个线程,vector
    std::string str;
    while (1)
    {
            Task *tt = new Task();
            //使用智能指针
            pool.append(tt);//不停的添加任务,任务是队列queue,因为只有固定的线程数
            cout<<"添加的任务数量: "<<pool.tasks_queue.size()<<endl;;
            delete tt;
    }
}

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

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

相关文章

[微信小程序] css 解决纯数字或字母不自动换行的问题、控制文字行数

效果 css 代码 word-break: break-all; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;解释 word-break: break-all; 作用&#xff1a;这个属性允许在单词内部进行换行&#xff0c;即使单词很长也…

正向解析、反向解析、DNS主从、DNS多区域、ntp时间同步

DNS配置回顾 编号主机名IP地址说明1web服务器192.168.1.17发布部署web服务2dns服务器192.168.1.20用于解析域名和IP地址3clien主机192.168.1.18用于模拟客户机 修改 client主机&#xff1a;修改了dns的访问主机&#xff1b;临时修改echo "nameserver IP地址"&…

【Git多人协作开发】不同的分支下的多人协作开发模式

目录 0.前言背景 1.开发者1☞完成准备工作&协作开发 1.1查看分支情况 1.2创建本地分支feature-1 1.3三板斧 1.4push推本地分支feature-1到远程仓库 2.开发者2☞完成准备工作&协作开发 2.1创建本地分支feature-2 2.2三板斧 2.2push推送本地feature-2到远程仓库…

redis构建集群时,一直Waiting for the cluster to join

redis构建集群时&#xff0c;一直Waiting for the cluster to join 前置条件参考 前置条件 这是我搭建的集群相关信息&#xff0c;三台虚拟机&#xff0c;分别是一主一从。在将所有虚拟机中redis服务器用到的tcp端口都打开之后&#xff0c;进行构建集群。但是出现上面的情况。 …

MyCms开源免费的自媒体商城博客CMS企业建站系统

MyCms是一款基于Laravel开发的开源免费的自媒体博客CMS系统&#xff0c;适用于个人网站及企业网站开发使用&#xff0c;MyCms基于Apache2.0开源协议发布&#xff0c;免费且不限制商业使用。 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/89575879 更…

DC系列靶场---DC 3靶场的渗透测试(一)

信息收集 Nmap扫描 nmap -sS -sV -T4 -p- -O 172.30.1.142//-sS TCP的SYN扫描 //-sV 服务版本检测 //-T4 野蛮的扫描&#xff08;常用&#xff09; //-O 识别操作系统 使用Nmap扫描只看到一个80端口&#xff0c;Apache的2.4.18版本。 http探测 使用Wappalyzer插件可以到…

防火墙限制docker了

今天有个安全方面的需求&#xff0c;演示环境禁止将3306等高危端口暴露到外网。 于是同事开启了防火墙&#xff0c;仅将应用端口暴露。结果导致演示环境无法使用。 由于公司的应用是基于docker部署的。结果他问我为什么同一台机器&#xff0c;应用无法访问mysql。 docker对于…

Apollo使用(3):分布式docker部署

Apollo 1.7.0版本开始会默认上传Docker镜像到Docker Hub&#xff0c;可以按照如下步骤获取 一、获取镜像 1、Apollo Config Service 获取镜像 docker pull apolloconfig/apollo-configservice:${version} 我事先下载过该镜像&#xff0c;所以跳过该步骤。 2、Apollo Admin S…

自动驾驶(八十七)---------虚拟机Hypervisor

在汽车电子电气架构发展中&#xff0c;SOA和中央计算是重要的趋势&#xff0c;boach的下图广为流传。因为仪表、座舱、智驾根据不同的需求选择不同的系统&#xff0c;这就导致虚拟机技术是实现中央计算的重要技术。 Hypervisor可以在一台硬件上运行多个虚拟机&#xff08;VM&am…

C# 知识点总结

入门 C#程序在.NET上运行&#xff0c;.NET framework包含两个部分&#xff1a; ①&#xff1a;.NET framework类库 ②&#xff1a;公共语言运行库CLR&#xff08;.NET虚拟机&#xff09; CLS&#xff08;公共语言规范&#xff09; CTS&#xff08;通用类型系统&#xff09; .N…

Leetcode3219. 切蛋糕的最小总开销 II

Every day a Leetcode 题目来源&#xff1a;3219. 切蛋糕的最小总开销 II 解法1&#xff1a;贪心 谁的开销更大&#xff0c;就先切谁&#xff0c;并且这个先后顺序与切的次数无关。 代码&#xff1a; /** lc appleetcode.cn id3219 langcpp** [3219] 切蛋糕的最小总开销 I…

智能爬虫管理:定制化数据抓取方案为企业赋能

摘要&#xff1a; 在数据驱动的时代&#xff0c;精准而高效的数据抓取成为企业决策的命脉。本文将探讨如何通过定制化的智能爬虫管理方案&#xff0c;赋能企业实现数据洞察力的飞跃。我们将深入解析定制化数据抓取的核心优势&#xff0c;分享成功案例&#xff0c;并揭秘如何利…

【HTML — 构建网络】HTML 入门

在本文中,我们将介绍 HTML 的绝对基础知识。为了帮助您入门,本文定义了元素、属性以及您可能听说过的所有其他重要术语。它还解释了这些在 HTML 中的位置。您将学习 HTML 元素的结构、典型的 HTML 页面的结构以及其他重要的基本语言功能。在此过程中,也将有机会玩转 HTML! …

动手学深度学习6.5 汇聚层-笔记练习(PyTorch)

以下内容为结合李沐老师的课程和教材补充的学习笔记&#xff0c;以及对课后练习的一些思考&#xff0c;自留回顾&#xff0c;也供同学之人交流参考。 本节课程地址&#xff1a;池化层_哔哩哔哩_bilibili 本节教材地址&#xff1a;6.5. 汇聚层 — 动手学深度学习 2.0.0 docume…

数据结构--二叉树详解

一&#xff0c;概念 1&#xff0c;结点的度&#xff1a;一个结点含有子树的个数称为该结点的度 2&#xff0c; 树的度&#xff1a;一棵树中&#xff0c;所有结点度的最大值称为树的度&#xff1b; 3&#xff0c;叶子结点或终端结点&#xff1a;度为0的结点称为叶结点&#x…

C++内存管理(候捷)第四讲 笔记

上中下三个classes分析 Loki allocator的三个类&#xff0c;从低阶到高阶分别为&#xff1a;Chunk, FixedAllocator, SmallObjAllocator Chunk&#xff1a;pData指针&#xff0c;指向分配的一个chunk&#xff0c;firstAvailableBlock_索引&#xff0c;指向第一个可用区块是第几…

WHAT - 一个 Github 仓库的 License 如何解读

目录 一、背景二、解读许可证说明的作用常见的开源许可证类型使用他人代码仓库时需要注意的事项结论 实践作为开发者1. 选择许可证类型2. 在 README 文件中编写许可证信息 作为使用者1. 确定权限2. 了解和遵守条款 总结 一、背景 我们经常在一些 Github 仓库里看到 License 部…

GAMES104:05游戏引擎中的渲染系统2:渲染中的光照、材质和shader-学习笔记

文章目录 一、渲染方程及其挑战二、基础光照解决方案-简化版简化光源简化材质简化阴影 三、基于预计算的全局光照3.1挑战和计算思路傅里叶变换球谐函数&#xff08;Spherical Harmonics&#xff09; 3.2 SH Lightmap&#xff1a;预计算 GI3.3 探针 Probe&#xff1a;Light Prob…

uni-app:踩坑路---关于使用了transform导致fixed定位不生效的问题

前言&#xff1a; 继续记录&#xff0c;在上篇文章中&#xff0c;弹出框遮罩层在ios上没有正确的铺盖全屏&#xff0c;是因为机型的原因&#xff0c;也和我们的代码结构有相关的问题。今天再来展示另外一个奇葩的问题。 这次我使用了在本篇博客中的弹出框组件CustomDialog.vue…

Matlab进阶绘图第65期—带分组折线段的柱状图

带分组折线段的柱状图是在原始柱状图的基础上&#xff0c;在每组柱状图位置处分别添加折线段&#xff0c;以进行对比或添加额外信息。 由于Matlab中未收录带分组折线段的柱状图的绘制函数&#xff0c;因此需要大家自行设法解决。 本文使用自制的BarwithGroupedLine小工具进行…