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;  // 位域bf1int bf2 : 5;  // 位域bf2int bf3 : 0;  // 位域bf3,宽度为0int bf4 : 7;  // 位域bf4std::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<> 类模板以及原子操作的非成员函数来管理多线程程序中的同步问题。希望这些示例和解释能帮助你更好地理解和应用这些同步机制。

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

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

相关文章

智慧园区如何融合五大技术实现全方位智能管理与服务创新

内容概要 在现代社会&#xff0c;智慧园区正逐渐成为管理与服务创新的风向标。以快鲸智慧园区管理系统为例&#xff0c;它为园区的数字化管理提供了一种全新的模式。该系统的核心在于如何充分应用物联网技术&#xff0c;自动化与信息化的结合&#xff0c;使得园区能够实现实时…

JxBrowser 7.41.7 版本发布啦!

JxBrowser 7.41.7 版本发布啦&#xff01; • 已更新 #Chromium 至更新版本 • 实施了多项质量改进 &#x1f517; 点击此处了解更多详情。 &#x1f193; 获取 30 天免费试用。

DeepSeek R1-Zero vs. R1:强化学习推理的技术突破与应用前景

&#x1f4cc; 引言&#xff1a;AI 推理的新时代 近年来&#xff0c;大语言模型&#xff08;LLM&#xff09; 的规模化扩展成为 AI 研究的主流方向。然而&#xff0c;LLM 的扩展是否真的能推动 通用人工智能&#xff08;AGI&#xff09; 的实现&#xff1f;DeepSeek 推出的 R1…

python学opencv|读取图像(四十七)使用cv2.bitwise_not()函数实现图像按位取反运算

【0】基础定义 按位与运算&#xff1a;两个等长度二进制数上下对齐&#xff0c;全1取1&#xff0c;其余取0。按位或运算&#xff1a;两个等长度二进制数上下对齐&#xff0c;有1取1&#xff0c;其余取0。 按位取反运算&#xff1a;一个二进制数&#xff0c;0变1,1变0。 【1】…

第十四讲 JDBC数据库

1. 什么是JDBC JDBC&#xff08;Java Database Connectivity&#xff0c;Java数据库连接&#xff09;&#xff0c;它是一套用于执行SQL语句的Java API。应用程序可通过这套API连接到关系型数据库&#xff0c;并使用SQL语句来完成对数据库中数据的查询、新增、更新和删除等操作…

低代码系统-产品架构案例介绍、轻流(九)

轻流低代码产品定位为零代码产品&#xff0c;试图通过搭建来降低企业成本&#xff0c;提升业务上线效率。 依旧是从下至上&#xff0c;从左至右的顺序 名词概述运维层底层系统运维层&#xff0c;例如上线、部署等基础服务体系内置的系统能力&#xff0c;发消息、组织和权限是必…

第一届“启航杯”网络安全挑战赛WP

misc PvzHE 去这个文件夹 有一张图片 QHCTF{300cef31-68d9-4b72-b49d-a7802da481a5} QHCTF For Year 2025 攻防世界有一样的 080714212829302316092230 对应Q 以此类推 QHCTF{FUN} 请找出拍摄地所在位置 柳城 顺丰 forensics win01 这个软件 云沙盒分析一下 md5 ad4…

基于Python的人工智能患者风险评估预测模型构建与应用研究(上)

一、引言 1.1 研究目标与内容 本研究旨在运用 Python 语言,整合多种人工智能技术,构建高精度、高可靠性且具有良好可解释性的患者风险评估预测模型,为医疗领域的临床决策提供强有力的支持。具体研究内容涵盖以下几个方面: 人工智能技术在风险评估中的应用研究:深入剖析机…

unity学习24:场景scene相关生成,加载,卸载,加载进度,异步加载场景等

目录 1 场景数量 SceneManager.sceneCount 2 直接代码生成新场景 SceneManager.CreateScene 3 场景的加载 3.1 用代码加载场景&#xff0c;仍然build setting里先加入配置 3.2 卸载场景 SceneManager.UnloadSceneAsync(); 3.3 同步加载场景 SceneManager.LoadScene 3.3.…

每日一题——序列化二叉树

序列化二叉树 BM39 序列化二叉树题目描述序列化反序列化 示例示例1示例2 解题思路序列化过程反序列化过程 代码实现代码说明复杂度分析总结 BM39 序列化二叉树 题目描述 请实现两个函数&#xff0c;分别用来序列化和反序列化二叉树。二叉树的序列化是将二叉树按照某种遍历方式…

Go学习:类型转换需注意的点 以及 类型别名

目录 1. 类型转换 2. 类型别名 1. 类型转换 在从前的学习中&#xff0c;知道布尔bool类型变量只有两种值true或false&#xff0c;C/C、Python、JAVA等编程语言中&#xff0c;如果将布尔类型bool变量转换为整型int变量&#xff0c;通常采用 “0为假&#xff0c;非0为真”的方…

CF 766A.Mahmoud and Longest Uncommon Subsequence(Java实现)

题目分析 (小何同学语文不太好&#xff0c;看这个题弯弯绕绕&#xff0c;看不懂一点&#xff0c;哈哈哈。)在尝试示例中分析之后&#xff0c;题目的意思大概就是&#xff0c;两个字符串相同就输出-1&#xff0c;不同就输出最长的那个字符串长度 思路分析 数据输入存值之后&…

大数据相关职位介绍之一(数据分析,数据开发,数据产品经理,数据运营)

大数据相关职位介绍之一 随着大数据、人工智能&#xff08;AI&#xff09;和机器学习的快速发展&#xff0c;数据分析与管理已经成为各行各业的重要组成部分。从互联网公司到传统行业的数字转型&#xff0c;数据相关职位在中国日益成为推动企业创新和提升竞争力的关键力量。以…

【力扣系列题目】最后一块石头的重量 分割回文串 验证回文串 等差数列划分{最大堆 背包 动态规划}

文章目录 七、最后一块石头的重量最后一块石头的重量【堆】[最后一块石头的重量 II](https://leetcode.cn/problems/last-stone-weight-ii/)【背包】 八、分割回文串分割回文串【分割子串方案数量】[分割回文串 II](https://leetcode.cn/problems/omKAoA/)【最少分割次数】[分割…

go gin配置air

一、依赖下载 安装最新&#xff0c;且在你工作区下进行安装&#xff0c;我的是D:/GO是我的工作区&#xff0c;所有项目都在目录下的src&#xff0c; go install github.com/air-verse/airlatest 如果出现类似报错&#xff1a; 将图中第三行 github.com/air-verse/air 替换最…

读书笔记--分布式服务架构对比及优势

本篇是在上一篇的基础上&#xff0c;主要对共享服务平台建设所依赖的分布式服务架构进行学习&#xff0c;主要记录和思考如下&#xff0c;供大家学习参考。随着企业各业务数字化转型工作的推进&#xff0c;之前在传统的单一系统&#xff08;或单体应用&#xff09;模式中&#…

openRv1126 AI算法部署实战之——ONNX模型部署实战

在RV1126开发板上部署ONNX算法&#xff0c;实时目标检测RTSP传输。视频演示地址 rv1126 yolov5 实时目标检测 rtsp传输_哔哩哔哩_bilibili 一、准备工作 1.从官网下载YOLOv5-v7.0工程&#xff08;YOLOv5的第7个版本&#xff09; 手动在线下载&#xff1a; Releases ultraly…

【C++题解】1055. 求满足条件的整数个数

欢迎关注本专栏《C从零基础到信奥赛入门级&#xff08;CSP-J&#xff09;》 问题&#xff1a;1055. 求满足条件的整数个数 类型&#xff1a;简单循环 题目描述&#xff1a; 在 1∼n 中&#xff0c;找出能同时满足用 3 除余 2 &#xff0c;用 5 除余 3 &#xff0c;用 7 除余…

亚博microros小车-原生ubuntu支持系列:17 gmapping

前置依赖 先看下亚博官网的介绍 Gmapping简介 gmapping只适用于单帧二维激光点数小于1440的点&#xff0c;如果单帧激光点数大于1440&#xff0c;那么就会出【[mapping-4] process has died】 这样的问题。 Gmapping是基于滤波SLAM框架的常用开源SLAM算法。 Gmapping基于RBp…

R语言统计分析——ggplot2绘图4——刻面

参考资料&#xff1a;R语言实战【第2版】 如果组在途中并排出现而不是重叠为单一的图形&#xff0c;关系就是清晰的。我们可以使用facet_wrap()函数和facet_grid()函数创建网格图形&#xff08;在ggplot2中也称刻面图&#xff09;&#xff0c;相关语法如下&#xff1a; 语法结…