C++并发编程指南07

文章目录

    • @[TOC]
      • 5.1 内存模型
        • 5.1.1 对象和内存位置
          • 图5.1 分解一个 `struct`,展示不同对象的内存位置
        • 5.1.2 对象、内存位置和并发
        • 5.1.3 修改顺序
          • 示例代码
      • 5.2 原子操作和原子类型
        • 5.2.1 标准原子类型
          • 标准库中的原子类型
          • 特殊的原子类型
          • 备选名称
          • 内存顺序参数
        • 5.2.2 `std::atomic_flag`
          • 初始化
          • 操作
          • 自旋锁实现
      • 5.2 原子操作和原子类型
        • 5.2.2 `std::atomic_flag`
          • 初始化
          • 操作
          • 自旋锁实现
        • 5.2.3 `std::atomic<bool>`
          • 初始化与赋值
          • 操作
          • “比较/交换”操作
          • 其他特性
        • 5.2.4 `std::atomic<T*>`
          • 初始化与赋值
          • 操作
          • 其他整型原子类型
      • 5.2 原子操作和原子类型
        • 5.2.5 标准原子整型的相关操作
          • 示例代码
        • 5.2.6 `std::atomic<>` 类模板
          • 示例代码
        • 5.2.7 原子操作的非成员函数
          • 常见的非成员函数
          • 内存顺序参数
          • 对 `std::shared_ptr<>` 的支持
          • 并行技术规范扩展

5.1 内存模型

内存模型在C++中分为两大部分:内存布局和并发。并发的基本结构非常重要,特别是低层原子操作。由于C++中的所有对象都与内存位置有关,我们将从基本结构开始讲解。


5.1.1 对象和内存位置

在C++程序中,数据都是由对象构成的。例如,可以创建一个 int 的衍生类,或者使用具有成员函数的基本类型,甚至像Smalltalk和Ruby那样——“一切都是对象”。对象是对C++数据构建块的声明,C++标准定义类对象为“存储区域”,但对象也可以将自己的特性赋予其他对象。

  • 基本类型:如 intfloat
  • 用户定义类的实例:如自定义的类。
  • 复杂对象:如数组、派生类的实例、具有非静态数据成员的类实例等。

无论是哪种类型,都会存储在一个或多个内存位置上。每个内存位置要么是一个标量类型的对象,要么是标量类型的子对象,例如 unsigned shortmy_class* 或序列中的相邻位域。当使用位域时需要注意:虽然相邻位域是不同的对象,但仍被视为相同的内存位置。

图5.1 分解一个 struct,展示不同对象的内存位置

在这里插入图片描述

struct MyStruct {
    int bf1 : 3;  // 位域bf1
    int bf2 : 5;  // 位域bf2
    int bf3 : 0;  // 位域bf3,宽度为0
    int bf4 : 7;  // 位域bf4
    std::string s;
};
  • 完整的 struct 是由多个子对象(每一个成员变量)组成的对象。
  • 位域 bf1bf2 共享同一个内存位置(假设 int 是4字节、32位类型)。
  • std::string 类型的对象 s 由内部多个内存位置组成。
  • 其他成员各自拥有自己的内存位置。
  • 宽度为0的位域 bf3 如何与 bf4 分离,并拥有各自的内存位置。

四个需要牢记的原则

  1. 每个变量都是对象,包括其成员变量的对象。
  2. 每个对象至少占有一个内存位置。
  3. 基本类型都有确定的内存位置(无论类型大小如何,即使它们是相邻的,或是数组的一部分)。
  4. 相邻位域是相同内存中的一部分。

你会奇怪,这些在并发中有什么作用?


5.1.2 对象、内存位置和并发

这部分对于C++的多线程编程至关重要。当两个线程访问不同的内存位置时,不会存在任何问题;当两个线程访问同一个内存位置时,就需要小心处理。

  • 只读访问:如果线程不更新数据,只读数据不需要保护或同步。
  • 写入访问:当线程对内存位置上的数据进行修改,就可能会产生条件竞争。

为了避免条件竞争,线程需要以一定的顺序执行。有两种主要方式:

  1. 使用互斥量:通过同一互斥量在两个线程同时访问前锁住,确保在同一时间内只有一个线程能够访问对应的内存位置。
  2. 使用原子操作:决定两个线程的访问顺序,当多个线程访问同一个内存地址时,对每个访问者都需要设定顺序。

如果不规定对同一内存地址访问的顺序,那么访问就不是原子的。当两个线程都是“写入者”时,就会产生数据竞争和未定义行为。

未定义的行为:是C++中的黑洞。一旦应用中有任何未定义的行为,就很难预料会发生什么事情。数据竞争绝对是一个严重的错误,要不惜一切代价避免它。

另一个重点是:当程序对同一内存地址中的数据访问存在竞争时,可以使用原子操作来避免未定义行为。当然,这不会影响竞争的产生——原子操作并没有指定访问顺序——而原子操作会把程序拉回到定义行为的区域内。


5.1.3 修改顺序

C++程序中的对象都有一个由程序中的所有线程对象在初始化开始阶段确定好的修改顺序。大多数情况下,这个顺序不同于执行中的顺序,但在给定的程序中,所有线程都需要遵守这个顺序。

  • 非原子类型:必须确保有足够的同步操作,以确保线程都遵守了修改顺序。当不同线程在不同序列中访问同一个值时,可能会遇到数据竞争或未定义行为。
  • 原子类型:编译器有责任去做同步。

因为当线程按修改顺序访问一个特殊的输入时,所以投机执行是不允许的。之后的读操作必须由线程返回新值,并且之后的写操作必须发生在修改顺序之后。虽然所有线程都需要遵守程序中每个独立对象的修改顺序,但没有必要遵守在独立对象上的操作顺序。

示例代码
#include <atomic>
#include <thread>

std::atomic<int> counter(0);

void increment_counter() {
    for (int i = 0; i < 1000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::thread t1(increment_counter);
    std::thread t2(increment_counter);

    t1.join();
    t2.join();

    std::cout << "Counter: " << counter.load() << std::endl;
}

在这个示例中,std::atomic<int> 确保了对 counter 的访问是线程安全的,并且通过 fetch_addload 方法保证了修改顺序。

注意:虽然 memory_order_relaxed 不提供顺序保证,但它确保了操作的原子性,从而避免了数据竞争。


了解了对象和内存地址的概念后,接下来我们来看什么是原子操作以及如何规定顺序。


以上内容展示了如何使用对象、内存位置和并发来管理多线程程序中的同步问题。希望这些示例和解释能帮助你更好地理解和应用这些同步机制。

以下是经过优化排版后的5.2节内容,详细解释了C++中的原子操作和原子类型。每个部分都有详细的注释和结构化展示。


5.2 原子操作和原子类型

5.2.1 标准原子类型

原子操作是指不可分割的操作,系统的所有线程中不可能观察到原子操作完成了一半。如果读取对象的加载操作是原子的,那么这个对象的所有修改操作也是原子的,因此加载操作得到的值要么是对象的初始值,要么是某次修改操作存入的值。

另一方面,非原子操作可能会被另一个线程观察到只完成一半。如果这个操作是一个存储操作,那么其他线程看到的值可能既不是存储前的值,也不是存储的值。如果非原子操作是一个读取操作,可能先取到对象的一部分,然后值被另一个线程修改,然后再取到剩余的部分,所以它取到的既不是第一个值,也不是第二个值。这就构成了数据竞争(见5.1节),出现未定义行为。

标准库中的原子类型

标准原子类型定义在头文件 <atomic> 中。这些类型的操作都是原子的,语言定义中只有这些类型的操作是原子的,也可以用互斥锁来模拟原子操作。

  • is_lock_free() 成员函数:几乎所有的原子类型都有一个 is_lock_free() 成员函数,可以让用户查询某个原子类型的操作是否直接使用了原子指令(x.is_lock_free() 返回 true),还是内部使用了一个锁结构(x.is_lock_free() 返回 false)。

  • 无锁状态宏:C++17 中,所有原子类型有一个静态常量成员变量 is_always_lock_free,如果相应硬件上的原子类型是无锁类型,则返回 true。例如:

    std::atomic<int> counter;
    if (counter.is_always_lock_free) {
        // 该平台上的 std::atomic<int> 是无锁的
    }
    
  • 宏定义:编译时对各种整型原子操作是否无锁进行判别,如 ATOMIC_BOOL_LOCK_FREE, ATOMIC_CHAR_LOCK_FREE 等。如果原子类型是无锁结构,值为 2;如果是基于锁的实现,值为 0;如果无锁状态在运行时才能确定,值为 1。

特殊的原子类型
  • std::atomic_flag:这是一个简单的布尔标志,并且在这种类型上的操作都是无锁的。初始化后,可以使用 test_and_set()clear() 成员函数进行查询和设置。

    std::atomic_flag f = ATOMIC_FLAG_INIT;
    f.clear(std::memory_order_release);  // 清除标志
    bool x = f.test_and_set();           // 设置标志并获取旧值
    
  • 其他原子类型:可以通过特化 std::atomic<> 得到更多功能,但不一定都是无锁的。主流平台上,原子变量是无锁的内置类型(如 std::atomic<int>std::atomic<void*>)。

备选名称

为了历史兼容性,标准库提供了备选名称,如 atomic_bool 对应 std::atomic<bool>,具体见表5.1。

原子类型相关特化类
atomic_boolstd::atomic
atomic_charstd::atomic
atomic_scharstd::atomic
atomic_ucharstd::atomic
atomic_intstd::atomic
atomic_uintstd::atomic
内存顺序参数

每种原子类型的操作都有一个内存序参数,用于指定存储的顺序。常见的内存顺序选项包括:

  • Store 操作memory_order_relaxed, memory_order_release, memory_order_seq_cst
  • Load 操作memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_seq_cst
  • Read-modify-write 操作memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_seq_cst

默认的内存序是 memory_order_seq_cst


5.2.2 std::atomic_flag

std::atomic_flag 是最简单的原子类型,只能在两个状态间切换:设置和清除。它作为构建块存在,通常用于实现自旋锁等简单同步机制。

初始化

std::atomic_flag 类型的对象必须使用 ATOMIC_FLAG_INIT 进行初始化:

std::atomic_flag f = ATOMIC_FLAG_INIT;
操作
  • clear() 成员函数:清除标志,使用释放语义。
  • test_and_set() 成员函数:设置标志并检索旧值,可以指定内存顺序。

示例代码:

f.clear(std::memory_order_release);  // 使用释放语义清除标志
bool x = f.test_and_set();           // 设置标志并获取旧值,默认内存序为 memory_order_seq_cst
自旋锁实现

std::atomic_flag 非常适合实现自旋锁。以下是一个简单的自旋锁实现:

class spinlock_mutex {
    std::atomic_flag flag;
public:
    spinlock_mutex():
        flag(ATOMIC_FLAG_INIT)
    {}

    void lock() {
        while(flag.test_and_set(std::memory_order_acquire));  // 等待直到获取锁
    }

    void unlock() {
        flag.clear(std::memory_order_release);  // 释放锁
    }
};

在这个例子中,spinlock_mutex 类使用 std::atomic_flag 来实现一个简单的自旋锁。lock() 方法会不断循环调用 test_and_set(),直到成功获取锁为止。unlock() 方法则通过调用 clear() 来释放锁。

由于 std::atomic_flag 的局限性,实际操作中最好使用 std::atomic<bool>,它提供了更多的功能和灵活性。


以上内容展示了如何使用原子操作和原子类型来管理多线程程序中的同步问题。希望这些示例和解释能帮助你更好地理解和应用这些同步机制。

以下是经过优化排版后的5.2.2至5.2.4节内容,详细解释了C++中的 std::atomic_flagstd::atomic<bool>std::atomic<T*> 的使用方法和特性。每个部分都有详细的注释和结构化展示。


5.2 原子操作和原子类型

5.2.2 std::atomic_flag

std::atomic_flag 是最简单的原子类型,用于在两个状态间切换:设置和清除。它通常作为构建块存在,适用于一些特别的情况。

初始化

std::atomic_flag 类型的对象必须通过 ATOMIC_FLAG_INIT 进行初始化:

std::atomic_flag f = ATOMIC_FLAG_INIT;
  • 初始化:标志总是初始化为“清除”状态。
  • 静态存储:如果 std::atomic_flag 是静态存储的,则保证其是静态初始化的,避免初始化顺序问题。
操作

初始化后,可以进行以下操作:

  • 销毁
  • 清除:使用 clear() 成员函数,并指定内存顺序。
  • 设置(查询之前的值):使用 test_and_set() 成员函数,并指定内存顺序。

示例代码:

f.clear(std::memory_order_release);  // 使用释放语义清除标志
bool x = f.test_and_set();           // 设置标志并获取旧值,默认内存序为 memory_order_seq_cst
  • clear():是一个存储操作,不能有 memory_order_acquirememory_order_acq_rel 语义。
  • test_and_set():是一个“读-改-写”操作,可以应用于任何内存顺序。
自旋锁实现

std::atomic_flag 非常适合实现自旋锁。以下是一个简单的自旋锁实现:

class spinlock_mutex {
    std::atomic_flag flag;
public:
    spinlock_mutex():
        flag(ATOMIC_FLAG_INIT)
    {}

    void lock() {
        while(flag.test_and_set(std::memory_order_acquire));  // 等待直到获取锁
    }

    void unlock() {
        flag.clear(std::memory_order_release);  // 释放锁
    }
};

在这个例子中,spinlock_mutex 类使用 std::atomic_flag 来实现一个简单的自旋锁。lock() 方法会不断循环调用 test_and_set(),直到成功获取锁为止。unlock() 方法则通过调用 clear() 来释放锁。

由于 std::atomic_flag 的局限性,实际操作中最好使用 std::atomic<bool>,它提供了更多的功能和灵活性。


5.2.3 std::atomic<bool>

std::atomic<bool> 是最基本的原子布尔类型,具有比 std::atomic_flag 更多的功能。

初始化与赋值

虽然不能拷贝构造和拷贝赋值,但可以通过非原子的 bool 类型进行构造和赋值:

std::atomic<bool> b(true);
b = false;
操作
  • store():写入 truefalse,类似于 std::atomic_flag 中的 clear()
  • test_and_set():可以替换为更通用的 exchange(),允许使用新值替换已存储的值,并检索原始值。
  • load():加载当前值。
  • compare_exchange_weak()compare_exchange_strong():比较当前值与期望值,当两值相等时存储新值;否则更新期望值为当前值。

示例代码:

std::atomic<bool> b;
bool x = b.load(std::memory_order_acquire);  // 加载当前值
b.store(true);                               // 存储 true
x = b.exchange(false, std::memory_order_acq_rel);  // 交换值并返回原始值
“比较/交换”操作
  • compare_exchange_weak():可能会伪失败,尤其是在缺少单条 CAS 操作的机器上。
  • compare_exchange_strong():保证不会伪失败,但在某些情况下可能需要额外的开销。

示例代码:

bool expected = false;
extern std::atomic<bool> b;  // 假设已经初始化
while (!b.compare_exchange_weak(expected, true) && !expected);
  • 内存顺序参数:可以在成功和失败的情况下分别指定不同的内存顺序。默认情况下,所有操作都使用 memory_order_seq_cst
其他特性
  • is_lock_free():检查操作是否无锁。这是除了 std::atomic_flag 外所有原子类型共有的特征。

5.2.4 std::atomic<T*>

std::atomic<T*> 是特化的原子指针类型,支持对指针的操作。

初始化与赋值

虽然不能拷贝构造和拷贝赋值,但可以通过合适的类型指针进行构造和赋值:

std::atomic<Foo*> p(some_array);  // some_array 是 Foo 类型的数组
操作
  • load():加载当前值。
  • store():存储新值。
  • exchange():交换值。
  • compare_exchange_weak()compare_exchange_strong():比较当前值与期望值,当两值相等时存储新值;否则更新期望值为当前值。
  • fetch_add()fetch_sub():在存储地址上做原子加法和减法,提供简易的封装。

示例代码:

class Foo {};
Foo some_array[5];
std::atomic<Foo*> p(some_array);

Foo* x = p.fetch_add(2);  // p 加 2,并返回原始值
assert(x == some_array);
assert(p.load() == &some_array[2]);

x = (p -= 1);  // p 减 1,并返回原始值
assert(x == &some_array[1]);
assert(p.load() == &some_array[1]);

p.fetch_add(3, std::memory_order_release);  // 使用释放语义增加指针
  • 内存顺序参数fetch_add()fetch_sub() 是“读-改-写”操作,可以使用任意的内存顺序。
其他整型原子类型

剩下的原子类型基本上都是整型原子类型,并且拥有类似的接口(除了内置类型不同)。例如:

  • std::atomic<int>
  • std::atomic<unsigned int>
  • std::atomic<long>

这些类型的接口和操作方式与 std::atomic<bool>std::atomic<T*> 类似。


以上内容展示了如何使用 std::atomic_flagstd::atomic<bool>std::atomic<T*> 来管理多线程程序中的同步问题。希望这些示例和解释能帮助你更好地理解和应用这些同步机制。

以下是经过优化排版后的5.2.5至5.2.7节内容,详细解释了C++中的标准原子整型操作、std::atomic<> 类模板以及原子操作的非成员函数。每个部分都有详细的注释和结构化展示。


5.2 原子操作和原子类型

5.2.5 标准原子整型的相关操作

除了基本的操作集合(如 load()store()exchange()compare_exchange_weak()compare_exchange_strong()),标准原子整型(如 std::atomic<int>std::atomic<unsigned long long>)还提供了一套完整的操作:

  • fetch_add():原子加法操作,并返回旧值。
  • fetch_sub():原子减法操作,并返回旧值。
  • fetch_and():按位与操作,并返回旧值。
  • fetch_or():按位或操作,并返回旧值。
  • fetch_xor():按位异或操作,并返回旧值。

此外,还支持复合赋值方式(如 +=, -=, &=, |=, ^=)以及前缀和后缀的自增和自减操作(如 ++x, x++, --x, x--)。

示例代码
std::atomic<int> counter(0);

counter.fetch_add(1);          // 原子加法操作
counter.fetch_sub(1);          // 原子减法操作
counter.fetch_and(0xFF);       // 按位与操作
counter.fetch_or(0x100);       // 按位或操作
counter.fetch_xor(0x80);       // 按位异或操作

counter += 5;                  // 复合赋值操作
++counter;                     // 前缀自增
counter++;                     // 后缀自增

这些操作通常用于计数器或掩码等场景。如果需要更复杂的操作(如除法、乘法或移位操作),可以使用 compare_exchange_weak() 或其他同步机制来实现。


5.2.6 std::atomic<> 类模板

std::atomic<> 类模板允许用户定义自己的原子类型,但有一些限制条件:

  • 拷贝赋值运算符:类型必须有编译器生成的拷贝赋值运算符,不能有任何虚函数或虚基类。
  • 所有基类和非静态数据成员:也必须支持拷贝赋值操作。
  • 比较操作:比较-交换操作类似于 memcmp,而不是为用户定义类型(UDT)定义的比较操作符。
示例代码
struct MyType {
    int x;
    double y;
};

std::atomic<MyType> atomic_var(MyType{1, 2.0});

// 使用 load() 和 store()
MyType old_val = atomic_var.load();
MyType new_val{3, 4.0};
atomic_var.store(new_val);

// 使用 exchange()
MyType exchanged_val = atomic_var.exchange(MyType{5, 6.0});

// 使用 compare_exchange_weak()
MyType expected = atomic_var.load();
MyType desired{7, 8.0};
bool success = atomic_var.compare_exchange_weak(expected, desired);

对于大多数平台,当 UDT 的大小等于或小于一个 intvoid* 类型时,std::atomic<UDT> 会使用原子指令。某些平台可能支持双字节比较和交换(DWCAS)指令,适用于两倍于 intvoid* 大小的类型。

需要注意的是,复杂的数据结构(如 std::vector<int>)不适合用作原子类型,因为它们包含多个操作,而不仅仅是赋值和比较。在这种情况下,最好使用 std::mutex 来保护数据。


在这里插入图片描述

5.2.7 原子操作的非成员函数

除了成员函数外,C++ 标准库还提供了非成员函数来操作原子类型。这些非成员函数通常以 atomic_ 作为前缀,并且可以重载不同的原子类型。

常见的非成员函数
  • std::atomic_load:加载原子变量的值。
  • std::atomic_store:存储新值到原子变量。
  • std::atomic_exchange:交换原子变量的值,并返回旧值。
  • std::atomic_compare_exchange_weakstd::atomic_compare_exchange_strong:比较并交换值。

示例代码:

std::atomic<int> a(10);

// 成员函数形式
int val1 = a.load();                // 加载值
a.store(20);                        // 存储新值
int val2 = a.exchange(30);          // 交换值并返回旧值
bool success = a.compare_exchange_weak(val1, 40);  // 比较并交换值

// 非成员函数形式
int val3 = std::atomic_load(&a);    // 加载值
std::atomic_store(&a, 50);          // 存储新值
int val4 = std::atomic_exchange(&a, 60);  // 交换值并返回旧值
success = std::atomic_compare_exchange_weak(&a, &val3, 70);  // 比较并交换值
内存顺序参数

非成员函数可以通过 _explicit 后缀指定内存顺序参数。例如:

std::atomic_store_explicit(&a, 80, std::memory_order_release);
bool success_explicit = std::atomic_compare_exchange_weak_explicit(
    &a, &val3, 90, std::memory_order_acquire, std::memory_order_relaxed);
std::shared_ptr<> 的支持

C++ 标准库还为 std::shared_ptr<> 提供了原子操作的非成员函数:

std::shared_ptr<my_data> p;
void process_global_data() {
    std::shared_ptr<my_data> local = std::atomic_load(&p);
    process_data(local);
}

void update_global_data() {
    std::shared_ptr<my_data> local(new my_data);
    std::atomic_store(&p, local);
}

这些函数打破了“只有原子类型才能提供原子操作”的原则,使得 std::shared_ptr<> 也能进行原子操作。

并行技术规范扩展

并行技术规范扩展提供了一种原子类型 std::experimental::atomic_shared_ptr<T>,声明在 <experimental/atomic> 头文件中。它支持无锁实现,并提供了 loadstoreexchangecompare-exchange 等操作。

示例代码:

#include <experimental/atomic>

std::experimental::atomic_shared_ptr<my_data> atomic_p;

void process_global_data() {
    std::shared_ptr<my_data> local = atomic_p.load();
    process_data(local);
}

void update_global_data() {
    std::shared_ptr<my_data> local(new my_data);
    atomic_p.store(local);
}

通过 is_lock_free() 函数可以确定在对应的硬件平台上是否无锁。


以上内容展示了如何使用标准原子整型操作、std::atomic<> 类模板以及原子操作的非成员函数来管理多线程程序中的同步问题。希望这些示例和解释能帮助你更好地理解和应用这些同步机制。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

日志收集Day007

1.配置ES集群TLS认证: (1)elk101节点生成证书文件 cd /usr/share/elasticsearch ./bin/elasticsearch-certutil cert -out config/elastic-certificates.p12 -pass "" --days 3650 (2)elk101节点为证书文件修改属主和属组 chown elasticsearch:elasticsearch con…

AJAX综合案例——图书管理

黑马程序员视频地址&#xff1a; AJAX-Day02-10.案例_图书管理AJAX-Day02-10.案例_图书管理_总结_V1.0是黑马程序员前端AJAX入门到实战全套教程&#xff0c;包含学前端框架必会的&#xff08;ajaxnode.jswebpackgit&#xff09;&#xff0c;一套全覆盖的第25集视频&#xff0c…

Linux_线程同步生产者消费者模型

同步的相关概念 同步&#xff1a;在保证数据安全的前提下&#xff0c;让线程能够按照某种特定的顺序访问临界资源&#xff0c;从而有效避免饥饿问题&#xff0c;叫做同步竞态条件&#xff1a;因为时序问题&#xff0c;而导致程序异常&#xff0c;我们称之为竞态条件。 同步的…

Qt u盘自动升级软件

Qt u盘自动升级软件 Chapter1 Qt u盘自动升级软件u盘自动升级软件思路&#xff1a;step1. 获取U盘 判断U盘名字是否正确&#xff0c; 升级文件是否存在。step2. 升级step3. 升级界面 Chapter2 Qt 嵌入式设备应用程序&#xff0c;通过U盘升级的一种思路Chapter3 在开发板上运行的…

拦截器快速入门及详解

拦截器Interceptor 快速入门 什么是拦截器&#xff1f; 是一种动态拦截方法调用的机制&#xff0c;类似于过滤器。 拦截器是Spring框架中提供的&#xff0c;用来动态拦截控制器方法的执行。 拦截器的作用&#xff1a;拦截请求&#xff0c;在指定方法调用前后&#xff0c;根…

信息安全专业优秀毕业设计选题汇总:热点选题

目录 前言 毕设选题 开题指导建议 更多精选选题 选题帮助 最后 前言 大家好,这里是海浪学长毕设专题! 大四是整个大学期间最忙碌的时光&#xff0c;一边要忙着准备考研、考公、考教资或者实习为毕业后面临的升学就业做准备,一边要为毕业设计耗费大量精力。学长给大家整理…

Linux中使用unzip

安装命令 yum install unzip unzip常用选项和参数 选项 说明 -q 隐藏解压过程中的消息输出 -d /path/to/directory 指定解压文件的目标目录 -P password 如果.zip文件被密码保护&#xff0c;使用此选项可以指定打开文件所需的密码 解压命令 unzip 要解压的压缩包unz…

ThreadLocal源码解析

文章目录 一、概述二、get()方法三、set()方法四、可能导致的内存泄漏问题五、remove六、思考&#xff1a;为什么要将ThreadLocalMap的value设置为强引用&#xff1f; 一、概述 ThreadLocal是线程私有的&#xff0c;独立初始化的变量副本。存放在和线程进行绑定的ThreadLocalMa…

批量解密,再也没有任何限制了

有的时候我们在网上下载了PDF文档。发现没有办法进行任何的操作&#xff0c;就连打印权限都没有。今天给大家介绍的这个软件可以一键帮你进行PDF解密&#xff0c;非常方便&#xff0c;完全免费。 PDF智能助手 批量解密PDF文件 这个软件不是很大&#xff0c;只有10MB&#xff…

《LLM大语言模型+RAG实战+Langchain+ChatGLM-4+Transformer》

文章目录 Langchain的定义Langchain的组成三个核心组件实现整个核心组成部分 为什么要使用LangchainLangchain的底层原理Langchain实战操作LangSmithLangChain调用LLM安装openAI库-国内镜像源代码运行结果小结 使用Langchain的提示模板部署Langchain程序安装langserve代码请求格…

车载软件 --- 大一新生入门汽车零部件嵌入式开发

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 简单&#xff0c;单纯&#xff0c;喜欢独处&#xff0c;独来独往&#xff0c;不易合同频过着接地气的生活…

有效运作神经网络

内容来自https://www.bilibili.com/video/BV1FT4y1E74V&#xff0c;仅为本人学习所用。 文章目录 训练集、验证集、测试集偏差、方差正则化正则化参数为什么正则化可以减少过拟合Dropout正则化Inverted Dropout其他的正则化方法数据增广Early stopping 归一化梯度消失与梯度爆…

【深度优先搜索篇】走迷宫的魔法:算法如何破解迷宫的神秘密码

当你在夜晚孤军奋战时&#xff0c;满天星光以为你而闪烁。 欢迎拜访&#xff1a;羑悻的小杀马特.-CSDN博客 本篇主题&#xff1a;轻轻松松拿捏洛谷走迷宫问题 制作日期&#xff1a;2024.12.31 隶属专栏&#xff1a;C/C题海汇总 首先我…

SQL进阶实战技巧:如何分析浏览到下单各步骤转化率及流失用户数?

目录 0 问题描述 1 数据准备 2 问题分析 3 问题拓展 3.1 跳出率计算 3.2 计算从浏览商品到支付订单的不同路径的用户数&#xff0c;并按照用户数降序排列。 往期精彩 0 问题描述 统计从浏览商品到最终下单的各个步骤的用户数和流失用户数,并计算转化率 用户表结构和…

Autosar-Os是怎么运行的?(内存保护)

写在前面&#xff1a; 入行一段时间了&#xff0c;基于个人理解整理一些东西&#xff0c;如有错误&#xff0c;欢迎各位大佬评论区指正&#xff01;&#xff01;&#xff01; 1.功能概述 以TC397芯片为例&#xff0c;英飞凌芯片集成了MPU模块&#xff0c; MPU模块采用了硬件机…

什么是Maxscript?为什么要学习Maxscript?

MAXScript是Autodesk 3ds Max的内置脚本语言,它是一种与3dsMax对话并使3dsMax执行某些操作的编程语言。它是一种脚本语言,这意味着您不需要编译代码即可运行。通过使用一系列基于文本的命令而不是使用UI操作,您可以完成许多使用UI操作无法完成的任务。 Maxscript是一种专有…

(一)QT的简介与环境配置WIN11

目录 一、QT的概述 二、QT的下载 三、简单编程 常用快捷键 一、QT的概述 简介 Qt&#xff08;发音&#xff1a;[kjuːt]&#xff0c;类似“cute”&#xff09;是一个跨平台的开发库&#xff0c;主要用于开发图形用户界面&#xff08;GUI&#xff09;应用程序&#xff0c;…

vim交换文件的作用

1.数据恢复&#xff1a;因为vim异常的退出&#xff0c;使用交换文件可以恢复之前的修改内容。 2.防止多人同时编辑&#xff1a;vim检测到交换文件的存在,会给出提示&#xff0c;以避免一个文件同时被多人编辑。 &#xff08;vim交换文件的工作原理&#xff1a;vim交换文件的工作…

SpringCloudGateWay和Sentinel结合做黑白名单来源控制

假设我们的分布式项目&#xff0c;admin是8087&#xff0c;gateway是8088&#xff0c;consumer是8086 我们一般的思路是我们的请求必须经过我们的网关8088然后网关转发到我们的分布式项目&#xff0c;那我要是没有处理我们绕过网关直接访问项目8087和8086不也是可以&#xff1…

将多目标贝叶斯优化与强化学习相结合用于TinyML

论文标题 Combining Multi-Objective Bayesian Optimization with Reinforcement Learning for TinyML 作者信息 Mark Deutel, Friedrich-Alexander-Universitt Erlangen-Nrnberg, Germany Georgios Kontes, Fraunhofer IIS, Fraunhofer Institute for Integrated Circuits …