线程

线程的创建

在 C++ 中,线程的创建核心是通过std::thread类实现的,其构造函数需要传入一个可调用对象(Callable Object)作为线程入口。可调用对象包括普通函数、lambda 表达式、函数对象(functor)、类的成员函数等。下面详细介绍几种常见的线程创建方式:

一、使用普通函数创建线程

最基础的方式是将普通函数作为线程入口,可同时传递参数给函数。

#include <iostream>
#include <thread>// 普通函数:线程入口
void print_info(int thread_id, const std::string& message) {std::cout << "线程 " << thread_id << ": " << message << std::endl;
}int main() {// 创建线程:传入函数名和参数(参数按顺序传递)std::thread t1(print_info, 1, "Hello from thread 1");std::thread t2(print_info, 2, "Hello from thread 2");// 等待线程完成t1.join();t2.join();return 0;
}

说明

  • std::thread构造时,第一个参数是函数名,后续参数会被传递给该函数。
  • 若函数需要多个参数,直接在构造函数中按顺序添加即可。

二、使用 lambda 表达式创建线程

lambda 表达式适合编写简短的线程逻辑,尤其当需要捕获外部变量时非常方便。

#include <iostream>
#include <thread>int main() {int base = 100;  // 外部变量// 用lambda表达式创建线程(捕获外部变量base)std::thread t([&base](int offset) {// 线程逻辑:使用捕获的base和传入的offsetstd::cout << "线程内计算:" << base + offset << std::endl;}, 50);  // 传递给lambda的参数(offset=50)t.join();  // 等待线程完成return 0;
}

说明

  • lambda 的捕获列表([&base])用于访问外部变量,&表示按引用捕获(可修改外部变量),=表示按值捕获(只读)。
  • lambda 后的参数(如50)会作为 lambda 的输入参数。

三、使用函数对象(Functor)创建线程

函数对象是重载了operator()的类 / 结构体,适合需要携带状态(成员变量)的线程逻辑。

#include <iostream>
#include <thread>// 函数对象:重载operator()
struct Counter {int count;  // 携带的状态// 构造函数初始化状态Counter(int init) : count(init) {}// 线程入口:operator()void operator()(int step) {for (int i = 0; i < 5; ++i) {count += step;std::cout << "当前计数:" << count << std::endl;}}
};int main() {// 创建函数对象(初始状态count=0)Counter counter(0);// 用函数对象创建线程,传递参数step=2std::thread t(std::ref(counter), 2);  // 注意用std::ref传递引用t.join();// 线程执行后,counter的状态已被修改std::cout << "最终计数:" << counter.count << std::endl;return 0;
}

说明

  • 函数对象的成员变量(如count)可用于存储线程的状态,避免使用全局变量。
  • 若需在线程中修改原对象(而非副本),需用std::ref传递引用(否则std::thread会复制对象)。

四、使用类的成员函数创建线程

当线程逻辑需要访问类的成员变量时,可将类的成员函数作为线程入口,需同时指定对象指针。

#include <iostream>
#include <thread>
#include <string>class Worker {
private:std::string name;  // 成员变量public:Worker(const std::string& n) : name(n) {}// 成员函数:线程入口void work(int task_id) {std::cout << "工人 " << name << " 正在执行任务 " << task_id << std::endl;}
};int main() {Worker worker("Alice");  // 创建对象// 用成员函数创建线程:参数为(对象指针,成员函数地址,函数参数)std::thread t(&Worker::work, &worker, 1001);  // &worker是对象指针t.join();return 0;
}

说明

  • std::thread构造时,第一个参数是成员函数地址(&Worker::work),第二个参数是对象指针(&worker),后续参数是成员函数的参数。
  • 若对象是动态分配的(new Worker(...)),则传递堆对象的指针即可。

关键注意事项

  1. 线程必须被 join 或 detach
    std::thread对象销毁前,必须调用join()(等待线程结束)或detach()(分离线程,使其独立运行),否则会触发std::terminate()终止程序。

  2. 参数传递的拷贝问题
    线程构造时传递的参数会被拷贝到线程内部,若需传递引用,需用std::refstd::cref(常量引用),但需确保引用的对象生命周期长于线程。

  3. 线程入口的生命周期
    若线程入口是临时对象(如 lambda 或函数对象),需确保其生命周期覆盖线程执行期,避免悬空引用。

总结

C++ 线程创建的核心是通过std::thread绑定可调用对象,不同方式的适用场景:

  • 普通函数:适合简单、无状态的线程逻辑。
  • lambda 表达式:适合简短逻辑或需要捕获外部变量的场景。
  • 函数对象:适合需要携带状态的复杂逻辑。
  • 成员函数:适合面向对象编程中,线程逻辑需访问类成员的场景。

线程的销毁

我们使用std::thread创建的线程对象是进程中的子线程,一般进程中还有主线程,在程序中就是main线程,那么当我们创建线程后至少是有两个线程的,那么两个线程谁先执行完毕谁后执行完毕,这是随机的,但是当进程执行结束之后,主线程与子线程都会执行完毕,进程会回收线程拥有的资源。并且,主线程main执行完毕,其实整个进程也就执行完毕了。一般我们有两种方式让子线程结束,一种是主线程等待子线程执行完毕,我们使用join函数,让主线程回收子线程的资源;另外一种是子线程与主线程分离,我们使用detach函数,此时子线程驻留在后台运行,这个子线程就相当于被C++运行时库接管,子线程执行完毕后,由运行时库负责清理该线程相关的资源。使用detach之后,表明就失去了对子线程的控制。

void func()
{cout << "void func()" << endl;cout << "I'm child thread" << endl;
}void test()
{cout << "I'm main thread" << endl;thread th1(func);th1.join();//主线程等待子线程
}

线程的状态

线程类中有一成员函数joinable,可以用来检查线程的状态。如果该函数为true,表示可以使用join()或者detach()函数来管理线程生命周期。

void test()
{thread t([]{cout << "Hello, world!" << endl;});if (t.joinable()) {t.detach();}
}void test2()
{thread th1([]{cout << "Hello, world!" << endl;});if (t.joinable()) {t.join();}
}

线程id

为了唯一标识每个线程,可以给每个线程一个id,类型为std::thread::id,可以使用成员函数get_id()进行获取。

void test()
{thread th1([](){cout << "子线程ID:" << std::this_thread::get_id() << endl;});th1.join();
}

互斥锁mutex

互斥锁是一种同步原语,用于协调多个线程对共享资源的访问。互斥锁的作用是保证同一时刻只有一个线程可以访问共享资源,其他线程需要等待互斥锁释放后才能访问。在多线程编程中,多个线程可能同时访问同一个共享资源,如果没有互斥锁的保护,就可能出现数据竞争等问题。

然而,互斥锁的概念并不陌生,在Linux下,POSIX标准中也有互斥锁的概念,这里我们说的互斥锁是C++11语法层面提出来的概念,是C++语言自身的互斥锁std::mutex,互斥锁只有两种状态:上锁与解锁。

2、头文件

#include <mutex>
class mutex;

3、常用函数接口

3.1、构造函数
constexpr mutex() noexcept;
mutex( const mutex& ) = delete;
3.2、上锁
void lock();
3.3、尝试上锁
bool try_lock();
3.4、解锁
void unlock();
3.5、使用示例
int gCount = 0;
mutex mtx;//初始化互斥锁
​
void threadFunc()
{for(int idx = 0; idx < 1000000; ++idx){mtx.lock();//上锁++gCount;mtx.unlock();//解锁}
}
​
int main(int argc, char *argv[])
{thread th1(threadFunc);thread th2(threadFunc);
​th1.join();th2.join();cout << "gCount = " << gCount << endl;return 0;
}

三、lock_guard与unique_lock

在 C++ 多线程编程中,std::lock_guard 和 std::unique_lock 都是用于管理互斥锁(std::mutex)的RAII 风格工具类,核心作用是自动加锁和解锁,避免手动操作锁导致的死锁(如忘记解锁、异常时未释放锁等问题)。但它们的灵活性和适用场景有显著区别。

一、核心共同点

  • 都遵循RAII 原则:构造时获取锁,析构时自动释放锁(无论正常退出还是异常退出)。
  • 都用于保护临界区,防止多线程并发访问共享资源导致的数据竞争。

二、关键区别与适用场景

特性std::lock_guardstd::unique_lock
灵活性简单,功能有限灵活,支持更多操作
手动解锁不支持(只能通过析构函数自动解锁)支持(通过 unlock() 手动解锁)
延迟锁定不支持(构造时必须锁定)支持(通过 std::defer_lock 延迟锁定)
尝试锁定不支持支持(通过 std::try_to_lock 尝试锁定)
所有权转移不支持(不可复制、不可移动)支持(可移动,不可复制)
性能开销更低(轻量级)略高(因灵活性带来的额外状态管理)
适用场景简单临界区(全程需要锁定)复杂场景(如条件变量、中途解锁、延迟锁定等)

三、详细说明与示例

1. std::lock_guard:简单场景的首选

lock_guard 是轻量级锁管理工具,设计用于最简单的场景:进入临界区时加锁,离开时解锁,全程不需要手动干预。

特点

  • 构造函数必须锁定互斥量(要么直接锁定,要么接受一个已锁定的互斥量,通过 std::adopt_lock 标记)。
  • 没有 unlock() 方法,只能在析构时自动解锁(通常是离开作用域时)。
  • 不可复制、不可移动,所有权无法转移。

示例

#include <mutex>
#include <iostream>std::mutex mtx;
int shared_data = 0;void increment() {// 构造时自动锁定mtx,离开作用域(函数结束)时析构,自动解锁std::lock_guard<std::mutex> lock(mtx);// 临界区:安全访问共享资源shared_data++;std::cout << "当前值: " << shared_data << std::endl;// 无需手动解锁,lock析构时自动处理
}

适用场景

  • 临界区逻辑简单,从进入到退出全程需要锁定。
  • 不需要中途解锁、延迟锁定等复杂操作。
  • 追求最小性能开销。
2. std::unique_lock:复杂场景的灵活选择

unique_lock 是功能更全面的锁管理工具,支持手动解锁、延迟锁定、尝试锁定等操作,适合需要灵活控制锁状态的场景。

特点

  • 支持延迟锁定:通过 std::defer_lock 标记,构造时不锁定互斥量,后续可通过 lock() 手动锁定。
  • 支持手动解锁:通过 unlock() 方法中途释放锁,之后可再次通过 lock() 重新锁定。
  • 支持尝试锁定:通过 std::try_to_lock 标记,尝试锁定互斥量(成功返回 true,失败不阻塞)。
  • 支持所有权转移:可通过移动语义(std::move)转移锁的所有权(不可复制)。
  • 是条件变量(std::condition_variable)的必需参数:条件变量的 wait() 方法需要 unique_lock 作为参数,因为 wait() 会在等待时释放锁,被唤醒时重新获取锁(这要求锁可以手动解锁和锁定)。

示例 1:延迟锁定与手动解锁

#include <mutex>
#include <iostream>std::mutex mtx;void complex_operation() {// 延迟锁定:构造时不锁定,仅关联互斥量std::unique_lock<std::mutex> lock(mtx, std::defer_lock);// 做一些不需要锁定的操作std::cout << "准备锁定..." << std::endl;// 手动锁定lock.lock();std::cout << "已锁定,执行临界区操作..." << std::endl;// 中途手动解锁(释放锁,允许其他线程访问)lock.unlock();std::cout << "临时解锁,执行其他操作..." << std::endl;// 再次锁定lock.lock();std::cout << "再次锁定,完成剩余操作..." << std::endl;// 析构时自动解锁(若当前处于锁定状态)
}

示例 2:与条件变量配合

#include <mutex>
#include <condition_variable>
#include <thread>
#include <iostream>std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;void consumer() {std::unique_lock<std::mutex> lock(mtx);// 等待条件满足:会释放锁并阻塞,被唤醒时重新获取锁cv.wait(lock, []{ return data_ready; });// 条件满足,执行消费操作std::cout << "数据已准备好,开始处理..." << std::endl;
}void producer() {{std::lock_guard<std::mutex> lock(mtx);data_ready = true; // 生产数据} // 离开作用域,自动解锁cv.notify_one(); // 通知消费者
}int main() {std::thread t1(consumer);std::thread t2(producer);t1.join();t2.join();return 0;
}

适用场景

  • 需要中途解锁(如临界区中间有耗时操作但无需锁定)。
  • 需要延迟锁定(如先做准备工作,再根据条件决定是否锁定)。
  • 需要与条件变量配合(wait() 必须使用 unique_lock)。
  • 需要转移锁的所有权(如将锁传递给其他函数)。

四、总结

  • 优先使用 std::lock_guard:当场景简单,临界区全程需要锁定时,它更轻量、更高效。
  • 使用 std::unique_lock:当需要灵活性(手动解锁、延迟锁定、配合条件变量等)时,牺牲少量性能换取功能。

两者的核心目标都是安全管理锁的生命周期,避免手动操作锁导致的错误,选择时主要依据场景的复杂度和灵活性需求。

条件变量condition_variable

在 C++ 多线程编程中,std::condition_variable(条件变量)是用于线程间同步的核心机制,它允许线程在满足特定条件前阻塞等待,当条件满足时被其他线程唤醒,从而实现高效的协作(避免无效轮询)。

一、核心作用

条件变量解决的核心问题:让线程在 “条件不满足” 时进入休眠状态,在 “条件满足” 时被唤醒继续执行,避免线程通过 “轮询”(反复检查条件)浪费 CPU 资源。

例如:

  • 消费者线程等待生产者线程生成数据(“数据就绪” 是条件)。
  • 主线程等待子线程完成初始化(“初始化完成” 是条件)。

二、核心 API 与工作机制

std::condition_variable 定义在 <condition_variable> 头文件中,核心方法如下:

方法作用
wait(lock, pred)阻塞当前线程,释放锁并等待被唤醒;被唤醒后重新获取锁,检查pred是否为true,若为true则继续执行,否则重新阻塞。
notify_one()唤醒一个正在等待该条件变量的线程(若有)。
notify_all()唤醒所有正在等待该条件变量的线程。
关键细节:
  1. 必须配合互斥锁:条件变量的操作必须与互斥锁(std::mutex)结合,且必须使用 std::unique_lock(而非 std::lock_guard),因为 wait() 过程需要先释放锁、被唤醒后重新获取锁unique_lock 支持手动解锁 / 加锁,lock_guard 不支持)。

  2. 处理 “虚假唤醒”:操作系统可能在无明确通知时唤醒线程(虚假唤醒),因此 wait() 必须配合条件谓词(pred) 使用,确保只有当条件真正满足时才继续执行。

三、工作流程(以生产者 - 消费者为例)

  1. 消费者线程

    • 锁定互斥锁,检查条件(如 “数据是否就绪”)。
    • 若条件不满足,调用 wait():释放锁并阻塞等待。
    • 被唤醒后,重新获取锁,再次检查条件(避免虚假唤醒)。
    • 条件满足时,执行操作(如消费数据)。
  2. 生产者线程

    • 锁定互斥锁,修改共享资源(如生成数据)。
    • 调用 notify_one() 或 notify_all() 唤醒等待的消费者。
    • 释放锁(由 unique_lock 或 lock_guard 自动完成)。

四、完整示例:生产者 - 消费者模型

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>// 共享队列(缓冲区)
std::queue<int> buffer;
const int MAX_SIZE = 5;  // 缓冲区最大容量// 同步工具
std::mutex mtx;
std::condition_variable cv_producer;  // 生产者等待的条件变量(缓冲区不满)
std::condition_variable cv_consumer;  // 消费者等待的条件变量(缓冲区非空)// 生产者:向缓冲区添加数据
void producer(int id) {for (int i = 0; i < 10; ++i) {std::unique_lock<std::mutex> lock(mtx);// 等待缓冲区不满(若满则阻塞)cv_producer.wait(lock, []{ return buffer.size() < MAX_SIZE; });// 生产数据int data = id * 100 + i;buffer.push(data);std::cout << "生产者 " << id << " 生产: " << data << ",缓冲区大小: " << buffer.size() << std::endl;// 通知消费者:缓冲区非空cv_consumer.notify_one();// 模拟生产耗时std::this_thread::sleep_for(std::chrono::milliseconds(100));}
}// 消费者:从缓冲区取出数据
void consumer(int id) {for (int i = 0; i < 10; ++i) {std::unique_lock<std::mutex> lock(mtx);// 等待缓冲区非空(若空则阻塞)cv_consumer.wait(lock, []{ return !buffer.empty(); });// 消费数据int data = buffer.front();buffer.pop();std::cout << "消费者 " << id << " 消费: " << data << ",缓冲区大小: " << buffer.size() << std::endl;// 通知生产者:缓冲区不满cv_producer.notify_one();// 模拟消费耗时std::this_thread::sleep_for(std::chrono::milliseconds(150));}
}int main() {// 创建2个生产者和2个消费者std::thread p1(producer, 1);std::thread p2(producer, 2);std::thread c1(consumer, 1);std::thread c2(consumer, 2);// 等待所有线程完成p1.join();p2.join();c1.join();c2.join();return 0;
}

五、关键注意事项

  1. 必须使用 unique_lockwait() 方法的第一个参数必须是 std::unique_lock<std::mutex>,因为 wait() 内部会执行 “解锁→阻塞→被唤醒后重新加锁” 的操作,unique_lock 支持这种灵活的锁状态管理(lock_guard 不支持手动解锁,无法配合 wait())。

  2. 条件谓词不可省略:即使你认为 “不会有虚假唤醒”,也必须在 wait() 中传入条件谓词(第二个参数)。例如:

    // 错误:未处理虚假唤醒
    cv.wait(lock); // 正确:确保条件满足才继续
    cv.wait(lock, []{ return condition; }); 
    
  3. notify_one() 与 notify_all() 的选择

    • notify_one():唤醒一个等待线程,适用于 “只有一个线程能处理” 的场景(如缓冲区只有一个数据)。
    • notify_all():唤醒所有等待线程,适用于 “多个线程都能处理” 的场景(如广播一个全局事件)。过度使用 notify_all() 可能导致线程唤醒后竞争锁,浪费资源。
  4. 避免持有锁时长时间操作:唤醒线程后,应尽快释放锁(完成临界区操作),避免其他线程被唤醒后因无法获取锁而阻塞。

  5. 生命周期管理:确保条件变量在所有等待线程退出前保持有效,避免访问已销毁的条件变量。

六、总结

std::condition_variable 是多线程协作的高效工具,通过 “等待 - 通知” 机制替代轮询,减少 CPU 浪费。其核心是:线程在条件不满足时阻塞,条件满足时被唤醒,配合互斥锁和条件谓词确保同步安全。典型应用包括生产者 - 消费者模型、线程池任务调度、事件驱动同步等。

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

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

相关文章

MySQL基础全面解析

MySQL作为最流行的关系型数据库管理系统之一&#xff0c;是每一位开发者必备的核心技能。本文将系统性地解析MySQL的基础知识&#xff0c;结合关键概念与实战应用&#xff0c;帮助您构建扎实的数据库基础。1. SQL与NoSQL的本质区别SQL&#xff08;结构化查询语言&#xff09;数…

4、幽络源微服务项目实战:后端公共模块创建与引入多租户模块

前言 上节我们将电网巡检系统的前端vue2项目创建、配置&#xff0c;并构建了最基础的多租户界面&#xff0c;本节来继续构建后端的公共模块、多租户模块&#xff0c;并将公共模块引入到多租户模块中。 创建公共模块和多租户模块 在back父工程下创建两个Module&#xff0c;和…

STM32学习路线开启篇:芯片简介与课程简介

编写不易,请多多指教,觉得不错可以关注一下,相互学习 前言 一、课程配套资源 1、面包板 2、面包板专用的跳线 3、面包板的飞线 4、杜邦线 5、STM32F103C8T6最小系统板 6、0.96寸的OLED显示屏模块 7、电位器 8、按钮 9、LED灯 10、STLINK 11、USB转串口(TTL)模块 12、源蜂鸣器模…

图像直方图

图像直方图就是用来统计图像像素值分布的。灰度图分布读取灰度图phone cv2.imread(phone.png, cv2.IMREAD_GRAYSCALE) a phone.ravel() plt.hist(a, bins256) plt.show()如何可以获得当前像素值分布读取各通道的像素值分布img cv2.imread(phone.png) colors (b, g, r) for …

分类别柱状图(Vue3)

效果图&#xff1a;需求&#xff1a;男女年龄段占比<template><div class"go-ClassifyBar01"><v-chartref"vChartRef":option"option"style"width: 100%; height: 800px"></v-chart></div> </templa…

Apache Dubbo学习笔记-使用Dubbo发布、调用服务

Apache Dubbo经常作为一个RPC框架来使用&#xff0c;这篇文章主要介绍使用Dubbo配合注册中心来发布和调用服务。 Apache Dubbo和Spring Boot、JDK的版本对应关系。 Dubbo 分支最新版本JDKSpring Boot组件版本详细说明3.3.x (当前文档)3.3.08, 17, 212.x、3.x详情- 版本变更记录…

Python学习——字典和文件

前面python的学习中我们已经学习了python的函数和列表元组相关的内容&#xff0c;接下来我们来学习剩下的python语法&#xff1a;字典和文件 相关代码已经上传到作者的个人gitee&#xff1a;楼田莉子/Python 学习喜欢请点个赞谢谢 目录 字典 创建字典 查找key 新增/修改元素 …

swiper插件的使用

官方网址&#xff1a;https://www.swiper.com.cn/ 1、点击导航栏&#xff0c;获取Swiper里边的下载Swiper 2、选择要下载的版本【本次案例版本5.4.5】&#xff0c;然后解压缩文件夹&#xff0c;拿到swiper.min.js和swiper.min.css文件&#xff0c;放到项目对应的css文件和js文…

Vue3+JS 组合式 API 实战:从项目痛点到通用 Hook 封装

Vue3 组合式 API 的实战技巧 —— 组合式 API 帮我解决了不少 Options API 难以应对的问题&#xff0c;尤其是在代码复用和复杂组件维护上。一、为什么放弃 Options API&#xff1f;聊聊三年项目里的真实痛点​刚接触 Vue3 时&#xff0c;我曾因 “惯性” 继续用 Options API 写…

把 AI 塞进「电梯按钮」——基于 64 kB 零样本声纹的离线故障预测按钮

标签&#xff1a;零样本声纹、电梯按钮、离线 AI、TinyML、RISC-V、低功耗、GD32V303、故障预警 ---- 1. 背景&#xff1a;为什么按钮要「听声音」&#xff1f; 全国 700 万台电梯&#xff0c;按钮故障率 0.3 %/年&#xff0c;却常出现&#xff1a; • 机械卡滞、触点氧化&…

清华大学联合项目 论文解读 | MoTo赋能双臂机器人:实现零样本移动操作

研究背景 移动操作是机器人领域的核心挑战&#xff0c;它使机器人能够在各种任务和动态日常环境中为人类提供帮助。传统的移动操作方法由于缺乏大规模训练&#xff0c;往往难以在不同任务和环境中实现泛化。而现有操作基础模型虽在固定基座任务中表现出强泛化性&#xff0c;却无…

go webrtc - 2 webrtc重要概念

webrtc是一套音视频传输技术生态&#xff0c;不是一个协议或一个什么东西。3种模式本文基于 SFU 形式阐述&#xff01;重要概念&#xff1a;sfu 服务负责&#xff1a;信令 服务负责&#xff1a;peerConnection&#xff1a;track&#xff1a;房间&#xff1a;虚拟分组概念用户&a…

“下游任务”概念详解:从定义到应用场景

“下游任务”概念详解&#xff1a;从定义到应用场景 一、什么是“下游任务”&#xff1f; 在机器学习&#xff08;尤其是深度学习&#xff09;中&#xff0c;“下游任务”&#xff08;Downstream Task&#xff09;是相对“上游过程”而言的目标任务——可以理解为&#xff1a;我…

视频怎么做成 GIF?用 oCam 一键录制 GIF 动画超简单

GIF 动图因其生动直观、无需点击播放的特点&#xff0c;越来越受欢迎。你是否也曾看到一段有趣的视频&#xff0c;想把它做成 GIF 发给朋友或用在PPT里&#xff1f;其实&#xff0c;将视频片段转换为 GIF 并不需要复杂的视频剪辑技术&#xff0c;使用一款支持直接录制为 GIF 的…

Vue.config.js中的Webpack配置、优化及多页面应用开发

Vue.config.js中的Webpack配置、优化及多页面应用开发 在Vue CLI 3项目中&#xff0c;vue.config.js文件是工程化配置的核心入口&#xff0c;它通过集成Webpack配置、优化策略和多页面开发支持&#xff0c;为项目构建提供高度可定制化的解决方案。本文将从基础配置、性能优化、…

行业学习【电商】:直播电商的去头部化、矩阵号?

声明&#xff1a;以下部分内容含AI生成这两个词是当前直播电商和MCN领域的核心战略&#xff0c;理解了它们就理解了行业正在发生的深刻变化。一、如何理解“去头部化”&#xff1f;“去头部化” 指的是平台或MCN机构有意识地减少对超头部主播&#xff08;如曾经的李佳琦、薇娅&…

【MFC视图和窗口基础:文档/视图的“双胞胎”魔法 + 单文档程序】

大家好&#xff0c;我是你的MFC编程小伙伴&#xff01;学MFC就像探险古墓&#xff1a;到处是神秘的“房间”&#xff08;窗口&#xff09;和“宝藏”&#xff08;数据&#xff09;。今天咱们聊聊核心概念 – 视图、窗口和文档。这些是MFC的“骨架”&#xff0c;懂了它们&#x…

深度学习(六):代价函数的意义

在深度学习的浩瀚世界中&#xff0c;代价函数&#xff08;Cost Function&#xff09;&#xff0c;又称损失函数&#xff08;Loss Function&#xff09;或目标函数&#xff08;Objective Function&#xff09;&#xff0c;扮演着至关重要的角色&#xff0c;它就像一个导航员&…

Kable使用指南:Android BLE开发的现代化解决方案

概述 Kable&#xff08;com.juul.kable:core&#xff09;是一个专为Android蓝牙低功耗&#xff08;BLE&#xff09;开发设计的Kotlin协程友好库。它通过提供简洁的API和响应式编程模式&#xff0c;极大地简化了BLE设备交互的复杂性。本文将详细介绍Kable的使用方法&#xff0c;…

Android图案解锁绘制

使用到的库是Pattern Locker,根据示例进行了修改,把默认样式和自定义样式进行了合并调整。 设置密码 布局 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xm…