罗剑锋的C++实战笔记学习(二):容器、算法库、多线程

news2024/11/18 0:42:46

4、容器

1)、容器的通用特性

所有容器都具有的一个基本特性:它保存元素采用的是值(value)语义,也就是说,容器里存储的是元素的拷贝、副本,而不是引用

容器操作元素的很大一块成本就是值的拷贝。所以,如果元素比较大,或者非常多,那么操作时的拷贝开销就会很高,性能也就不会太好

一个解决办法是,尽量为元素实现转移构造和转移赋值函数,在加入容器的时候使用std::move()来转移,减少元素复制的成本:

#include <iostream>
#include <vector>

class Point {
public:
    Point(int x, int y) : x_(x), y_(y), data("Expensive resource") {
        std::cout << "Point constructor called." << std::endl;
    }

    Point(const Point &other) : x_(other.x_), y_(other.y_), data(other.data) {
        std::cout << "Copy constructor called." << std::endl;
    }

    Point(Point &&other) noexcept: x_(other.x_), y_(other.y_), data(std::move(other.data)) {
        std::cout << "Move constructor called." << std::endl;
    }

private:
    int x_, y_;
    std::string data; // 假设这是一个昂贵的资源
};

int main() {
    // 不使用移动语义,直接拷贝
    {
        Point p(1, 2);
        std::vector<Point> v;
        v.push_back(p); // 这里会调用拷贝构造函数
        std::cout << "---- After normal push_back ----" << std::endl;
    }

    // 使用移动语义
    {
        Point p(3, 4);
        std::vector<Point> v;
        v.push_back(std::move(p)); // 这里会调用移动构造函数
        std::cout << "---- After move semantics push_back ----" << std::endl;
    }

    return 0;
}

输出:

Point constructor called.
Copy constructor called.
---- After normal push_back ----
Point constructor called.
Move constructor called.
---- After move semantics push_back ----

也可以使用C++11为容器新增加的emplace操作函数,它可以就地构造元素,免去了构造后再拷贝、转移的成本,不但高效,而且用起来也很方便:

#include <iostream>
#include <vector>
#include <string>

class Point {
public:
    Point(int x, int y) : x_(x), y_(y), data("Expensive resource") {
        std::cout << "Point constructor called." << std::endl;
    }

    Point(const Point &other) : x_(other.x_), y_(other.y_), data(other.data) {
        std::cout << "Copy constructor called." << std::endl;
    }

    Point(Point &&other) noexcept: x_(other.x_), y_(other.y_), data(std::move(other.data)) {
        std::cout << "Move constructor called." << std::endl;
    }

private:
    int x_, y_;
    std::string data; // 假设这是一个昂贵的资源
};

int main() {
    std::vector<Point> v;
    v.reserve(2);  // 预分配足够的空间,避免内部扩展

    // 使用emplace_back直接在容器内构造对象
    v.emplace_back(1, 2); // 这里不会调用拷贝或移动构造函数
    v.emplace_back(3, 4);

    // 当std::vector需要扩容时,会触发之前对象的移动构造函数
    v.emplace_back(5, 6);
    return 0;
}

输出:

Point constructor called.
Point constructor called.
Point constructor called.
Move constructor called.
Move constructor called.

还可以在容器里存放元素的指针,来间接保存元素。这里建议使用智能指针unique_ptr/shared_ptr,让它们帮你自动管理元素,一般情况下,shared_ptr是一个更好的选择,它的共享语义与容器的值语义基本一致

2)、顺序容器

顺序容器就是数据结构里的线性表,一共有5种:array、vector、deque、list、forward_list

按照存储结构,这5种容器又可以再细分成两组

  • 连续存储的数组:array、vector和deque
  • 指针结构的链表:list和forward_list

数组:

array和vector直接对应C的内置数组,内存布局与C完全兼容,所以是开销最低、速度最快的容器它们两个的区别在于容量能否动态增长。array是静态数组,大小在初始化的时候就固定了,不能再容纳更多的元素。而vector是动态数组,虽然初始化的时候设定了大小,但可以在后面随需增长,容纳任意数量的元素

#include <array>
#include <vector>

int main() {
    std::array<int, 2> arr;
    assert(arr.size() == 2);

    std::vector<int> v(2);
    for (int i = 0; i < 10; i++) {
        v.emplace_back(i);
    }
    assert(v.size() == 12);
    return 0;
}

deque也是一种可以动态增长的数组,它和vector的区别是,它可以在两端高效地插入删除元素,而vector则只能用push_back在末端追加元素

#include <deque>

int main() {
    std::deque<int> d;
    d.emplace_back(9); // 末端添加一个元素
    d.emplace_front(1); // 前端添加一个元素
    assert(d.size() == 2);
    return 0;
}

链表:

vector和deque里的元素因为是连续存储的,所以在中间的插入删除效率就很低,而list和forward_list是链表结构,插入删除操作只需要调整指针,所以在任意位置的操作都很高效

链表的缺点是查找效率低,只能沿着指针顺序访问,这方面不如vector随机访问的效率高。list是双向链表,可以向前或者向后遍历,而forward_list是单向链表,只能向前遍历,查找效率就更低了

链表结构比起数组结构还有一个缺点,就是存储成本略高,因为必须要为每个元素附加一个或者两个的指针,指向链表的前后节点

扩容机制:

vector/deque和list/forward_list都可以动态增长来容纳更多的元素,但它们的内部扩容机制却是不一样的

当vector的容量到达上限的时候(capacity),它会再分配一块两倍大小的新内存,然后把旧元素拷贝或者移动过去。这个操作的成本是非常大的,所以,在使用vector的时候最好能够预估容量,使用reserve提前分配足够的空间,减少动态扩容的拷贝代价

deque、list会按照固定的步长(例如N个字节、一个节点)去增加容量。但在短时间内插入大量数据的时候就会频繁分配内存,效果反而不如vector一次分配来得好

如何选择:

如果没有什么特殊需求,首选的容器就是array和vector,它们的速度最快、开销最低,数组的形式也令它们最容易使用,搭配算法也可以实现快速的排序和查找

剩下的deque、list和forward_list则适合对插入删除性能比较敏感的场合,如果还很在意空间开销,那就只能选择非链表的deque了

3)、有序容器

顺序容器的特点是,元素的次序是由它插入的次序而决定的,访问元素也就按照最初插入的顺序。而有序容器则不同,它的元素在插入容器后就被按照某种规则自动排序,所以是有序的

标准库里一共有四种有序容器:set/multiset和map/multimap(底层是通过红黑树实现)。有multi前缀的容器表示可以容纳重复的key

在定义有序容器的时候必须要指定key的比较函数。只不过这个函数通常是默认的less,表示小于关系,不用特意写出来

C++里的int、string等基本类型都支持比较排序,但很多自定义类型没有默认的比较函数,需要重载<或者自定义模板参数

比如说有一个Point类,它是没有大小概念的,但只要给它重载<操作符,就可以放进有序容器里了:

#include <iostream>
#include <set>

class Point {
public:
    Point(int x, int y) : x_(x), y_(y) {}

    bool operator<(const Point &other) const {
        if (x_ != other.x_) {
            return x_ < other.x_;
        } else {
            return y_ < other.y_;
        }
    }

    friend std::ostream &operator<<(std::ostream &os, const Point &p) {
        os << "(" << p.x_ << ", " << p.y_ << ")";
        return os;
    }

private:
    int x_;
    int y_;
};

int main() {
    std::set<Point> points;
    points.emplace(7, 2);
    points.emplace(3, 5);
    for (const auto &point: points) {
        std::cout << point << std::endl;
    }
    return 0;
}

另一种方式是编写专门的函数对象或者lambda表达式,然后在容器的模板参数里指定。这种方式更灵活,而且可以实现任意的排序准则:

#include <iostream>
#include <set>

template<typename Iter>
void printRangeWithCommas(Iter begin, Iter end) {
    if (begin == end) return;
    for (Iter it = begin; it != end; ++it) {
        std::cout << *it;
        if (std::next(it) != end) {
            std::cout << ",";
        }
    }
    std::cout << "\n";
}

int main() {
    std::set<int> s = {7, 3, 9};
    printRangeWithCommas(s.begin(), s.end()); // 调用函数打印集合,输出: 3,7,9

    auto comp = [](auto a, auto b) {
        return a > b;
    };
    std::set<int, decltype(comp)> gs(comp);
    std::copy(s.begin(), s.end(), std::inserter(gs, gs.end()));

    printRangeWithCommas(gs.begin(), gs.end()); // 再次调用函数打印另一个集合,输出: 9,7,3

    return 0;
}
4)、无序容器

无序容器也有四种,名字里也有set和map,只是加上了unordered(无序)前缀,分别是unordered_set/unordered_multiset、unordered_map/unordered_multimap

无序容器用法上与有序容器几乎是一样的,区别在于内部数据结构:它不是红黑树,而是散列表(也叫哈希表,hash table)

因为它采用散列表存储数据,元素的位置取决于计算的散列值,没有规律可言,所以就是无序的

#include <iostream>
#include <unordered_map>

int main() {
    using map_type =
            std::unordered_map<int, std::string>;

    map_type dict;

    dict[1] = "one";
    dict.emplace(2, "two");
    dict[10] = "ten";

    for (auto &x: dict) { // 遍历顺序不确定,既不是插入顺序,也不是大小序
        std::cout << x.first << "=>"
                  << x.second << ",";
    }
    return 0;
}

无序容器要求key具备两个条件,一是可以计算hash值,二是能够执行相等比较操作。第一个是因为散列表的要求,只有计算hash值才能放入散列表,第二个则是因为hash值可能会冲突,所以当hash值相同时,就要比较真正的key值

#include <iostream>
#include <unordered_set>

class Point {
public:
    Point(int x, int y) : x_(x), y_(y) {}

    int getX() const { return x_; }

    int getY() const { return y_; }

    // 重载operator==用于比较
    bool operator==(const Point &other) const {
        return x_ == other.x_ && y_ == other.y_;
    }

    // 为了使用Point作为std::unordered_set或std::unordered_map的键,需要定义哈希函数
    struct Hash {
        std::size_t operator()(const Point &p) const noexcept {
            std::size_t h1 = std::hash<int>{}(p.getX());
            std::size_t h2 = std::hash<int>{}(p.getY());
            return h1 ^ (h2 << 1);
        }
    };

    // 重载operator<<以便可以直接输出Point对象
    friend std::ostream &operator<<(std::ostream &os, const Point &p) {
        return os << "(" << p.x_ << ", " << p.y_ << ")";
    }

private:
    int x_, y_;
};

int main() {
    std::unordered_set<Point, Point::Hash> pointSet;

    pointSet.insert(Point(1, 2));
    pointSet.insert(Point(3, 4));
    // 尝试插入重复的点,不会成功
    pointSet.insert(Point(1, 2));

    for (const auto &point: pointSet) {
        std::cout << point << std::endl;
    }

    return 0;
}
5)、小结
  1. 标准容器可以分为三大类,即顺序容器、有序容器和无序容器
  2. 所有容器中最优先选择的应该是array和vector,它们的速度最快,开销最低
  3. list是链表结构,插入删除的效率高,但查找效率低
  4. 有序容器是红黑树结构,对key自动排序,查找效率高,但有插入成本
  5. 无序容器是散列表结构,由hash值计算存储位置,查找和插入的成本都很低
  6. 有序容器和无序容器都属于关联容器,元素有key的概念,操作元素实际上是在操作key,所以要定义对key的比较函数或者散列函数

5、算法库

1)、迭代器

迭代器常用的函数如下:

  • begin()end():得到表示两个端点的迭代器
  • distance():计算两个迭代器之间的距离
  • advance():前进或者后退 N 步
  • next()prev():计算迭代器前后的某个位置
#include <array>

int main() {
    std::array<int, 5> arr = {0, 1, 2, 3, 4}; // array静态数组容器

    auto b = begin(arr); // 全局函数获取迭代器,首端
    auto e = end(arr); // 全局函数获取迭代器,末端

    assert(std::distance(b, e) == 5); // 迭代器的距离

    auto p = std::next(b); // 获取下一个位置
    assert(std::distance(b, p) == 1); // 迭代器的距离
    assert(std::distance(p, b) == -1); // 反向计算迭代器的距离

    std::advance(p, 2); // 迭代器前进两个位置,指向元素3
    assert(*p == 3);
    assert(p == std::prev(e, 2)); // 是末端迭代器的前两个位置

    return 0;
}
2)、for_each
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {3, 5, 1, 7, 10};

    for (const auto &x: v) { // range for循环
        std::cout << x << ",";
    }
    std::cout << "\n";

    auto print = [](const auto &x) // 定义一个lambda表达式
    {
        std::cout << x << ",";
    };

    for_each(cbegin(v), cend(v), print); // for_each算法
    std::cout << "\n";

    for_each( // for_each算法,内部定义lambda表达式
            cbegin(v), cend(v), // 获取常量迭代器
            [](const auto &x) // 匿名lambda表达式
            {
                std::cout << x << ",";
            }
    );
    std::cout << "\n";

    return 0;
}
3)、排序算法

sort()是经典的快排算法,示例如下:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v = {3, 5, 1, 7, 10};

    auto print = [](const auto &x) // lambda表达式输出元素
    {
        std::cout << x << ",";
    };

    std::sort(begin(v), end(v)); // 快速排序
    for_each(cbegin(v), cend(v), print);
    return 0;
}

一些常见问题对应的算法:

  • 要求排序后仍然保持元素的相对顺序,应该用stable_sort,它是稳定的
  • 选出前几名(TopN),应该用partial_sort
  • 选出前几名,但不要求再排出名次(BestN),应该用nth_element
  • 中位数(Median)、百分位数(Percentile),还是用nth_element
  • 按照某种规则把元素划分成两组,用partition
  • 第一名和最后一名,用minmax_element
#include <iostream>
#include <vector>

template<typename Iter>
void printRangeWithCommas(const std::string &prefix, Iter begin, Iter end) {
    if (begin == end) return;
    std::cout << prefix;
    for (auto it = begin; it != end; ++it) {
        std::cout << (it == begin ? "" : ",") << *it;
    }
    std::cout << '\n';
}

int main() {
    std::vector<int> v = {3, 5, 1, 7, 10};

    // top3
    std::partial_sort(begin(v), next(begin(v), 3), end(v)); // 取前3名
    printRangeWithCommas("top3: ", v.begin(), next(begin(v), 3));

    // best3
    std::nth_element(begin(v), next(begin(v), 3), end(v)); // 最好的3个
    printRangeWithCommas("best3: ", v.begin(), next(begin(v), 3));

    // median
    auto mid_iter = // 中位数的位置
            next(begin(v), v.size() / 2);
    std::nth_element(begin(v), mid_iter, end(v)); // 排序得到中位数
    std::cout << "median: " << *mid_iter << std::endl;

    // partition
    auto pos = std::partition( // 找出所有大于9的数
            begin(v), end(v), [](const auto &x) // 定义一个lambda表达式
            {
                return x > 9;
            });
    printRangeWithCommas("values > 9: ", v.begin(), pos);

    // min/max
    auto [minIt, maxIt] = std::minmax_element( // 找出第一名和倒数第一
            cbegin(v), cend(v));
    std::cout << "min value: " << *minIt << ", max value: " << *maxIt << std::endl;
    return 0;
}

在使用这些排序算法时,对迭代器要求比较高,通常都是随机访问迭代器(minmax_element除外),所以最好在顺序容器array/vector上调用

4)、查找算法

binary_search:在已经排好序的区间里执行二分查找,但它只返回一个bool值,告知元素是否存在

#include <vector>

int main() {
    std::vector<int> v = {3, 5, 1, 7, 10, 99, 42};
    std::sort(begin(v), end(v)); // 快速排序

    // 二分查找,只能确定元素在不在
    bool found = binary_search(cbegin(v), cend(v), 7);
    assert(found);
    return 0;
}

想要在已序容器上执行二分查找,要用算法lower_bound,它返回第一个大于或等于值的位置

#include <vector>

int main() {
    std::vector<int> v = {3, 5, 1, 7, 10, 99, 42};
    std::sort(begin(v), end(v));

    // 找到第一个>=7的位置
    auto pos = std::lower_bound(cbegin(v), cend(v), 7);
    bool found = (pos != cend(v)) && (*pos == 7); // 可能找不到,所以必须要判断
    assert(found); // 7在容器里

    // 找到第一个>=9的位置
    pos = std::lower_bound(cbegin(v), cend(v), 9);
    found = (pos != cend(v)) && (*pos == 9); // 可能找不到,所以必须要判断
    assert(!found); // 9不在容器里
    return 0;
}

lower_bound的返回值是一个迭代器,需要做判断才能知道是否真的找到了。判断的条件有两个,一个是迭代器是否有效,另一个是迭代器的值是不是要找的值

upper_bound算法:返回第一个大于值的元素(也是在已序容器上执行二分查找)

对于有序容器set/map,就不需要调用这三个算法了,它们有等价的成员函数find/lower_bound/upper_bound,效果是一样的

#include <iostream>
#include <set>

int main() {
    std::multiset<int> s = {3, 5, 1, 7, 7, 7, 10, 99, 42};

    auto pos = s.find(7); // 二分查找,返回迭代器
    assert(pos != s.end());

    auto lower_pos = s.lower_bound(7); // 获取区间的左端点
    auto upper_pos = s.upper_bound(7); // 获取区间的右端点

    auto print = [](const auto &x) {
        std::cout << x << ",";
    };
    // 输出7,7,7
    std::for_each(lower_pos, upper_pos, print);
    return 0;
}

标准库里还有一些查找算法可以用于未排序的容器,这些算法以find和search命名,其中用于查找区间的find_first_of/find_end

#include <vector>
#include <array>

int main() {
    std::vector<int> v = {1, 9, 11, 3, 5, 7};

    // 查找算法,找到第一个出现的位置
    auto pos = std::find(begin(v), end(v), 3);
    assert(pos != end(v));

    // 查找算法,用lambda判断条件
    pos = std::find_if(
            begin(v), end(v),
            [](auto x) {
                return x % 2 == 0;
            }
    );
    assert(pos == end(v));

    std::array<int, 2> arr = {3, 5};
    // 查找一个子区间
    pos = std::find_first_of(
            begin(v), end(v),
            begin(arr), end(arr)
    );
    assert(pos != end(v));
    return 0;
}
5)、小结
  1. 算法是专门操作容器的函数,是一种智能for循环,它的最佳搭档是lambda表达式
  2. 算法通过迭代器来间接操作容器,使用两个端点指定操作范围,迭代器决定了算法的能力
  3. for_each算法是for的替代品,以函数式编程替代了面向过程编程
  4. 有多种排序算法,最基本的是sort,但应该根据实际情况选择其他更合适的算法,避免浪费
  5. 在已序容器上可以执行二分查找,应该使用的算法是lower_bound
  6. list/set/map提供了等价的排序、查找函数,更适应自己的数据结构
  7. find/search是通用的查找算法,效率不高,但不必排序也能使用

6、多线程

1)、仅调用一次

要先声明一个once_flag类型的变量,最好是静态、全局的(线程可见),作为初始化的标志。然后调用专门的call_once()函数,以函数式编程的方式,传递这个标志和初始化函数。这样C++就会保证,即使多个线程重入call_once(),也只能有一个线程会成功运行初始化

#include <iostream>
#include <thread>

int main() {
    static std::once_flag flag; // 全局的初始化标志

    auto f = []() {
        std::call_once(flag, // 仅一次调用,注意要传flag
                       []() { // 匿名lambda,初始化函数,只会执行一次
                           std::cout << "only once" << std::endl;
                       }
        );
    };

    // 使用vector管理线程,确保所有线程执行完毕后再退出main
    std::vector<std::thread> threads;

    for (int i = 0; i < 2; ++i) {
        threads.emplace_back(f);
    }

    // 等待所有线程完成
    for (std::thread &t: threads) {
        t.join();
    }
    return 0;
}
2)、线程局部存储

有thread_local标记的变量在每个线程里都会有一个独立的副本,是线程独占的

#include <iostream>
#include <thread>

int main() {
    thread_local int n = 0; // 线程局部存储变量

    auto f = [&](int x) {
        n += x; // 使用线程局部变量,互不影响
        std::cout << n << std::endl;
    };

    // 使用vector管理线程,确保所有线程执行完毕后再退出main
    std::vector<std::thread> threads;

    threads.emplace_back(f, 10);
    threads.emplace_back(f, 20);

    // 等待所有线程完成
    for (std::thread &t: threads) {
        t.join();
    }
    return 0;
}

在程序执行后,可以看到两个线程分别输出了10和20,互不干扰

3)、原子变量

目前,C++只能让一些最基本的类型原子化,比如atomic_int、atomic_long等。这些原子变量都是模板类atomic的特化形式,包装了原始的类型,具有相同的接口,用起来和bool、int几乎一模一样,但却是原子化的,多线程读写不会出错

原子变量和原始的类型一个重要的区别是:原子变量禁用了拷贝构造函数,所以在初始化的时候不能用=的赋值形式,只能用圆括号或者花括

#include <iostream>
#include <thread>

void incrementAtomicInt(std::atomic_int &counter, int numIterations) {
    for (int i = 0; i < numIterations; ++i) {
        ++counter;
    }
}

void incrementAtomicLong(std::atomic_long &counter, long numIterations) {
    for (long i = 0; i < numIterations; ++i) {
        counter += 10;
    }
}

int main() {
    const int numThreads = 4;
    const int iterationsPerThread = 100;

    std::atomic_int x{0};
    std::atomic_long y{1000L};

    std::vector<std::thread> threads;

    // 对x进行递增操作的线程
    for (int i = 0; i < numThreads; ++i) {
        threads.emplace_back(incrementAtomicInt, std::ref(x), iterationsPerThread);
    }

    // 对y进行递增操作的线程
    for (int i = 0; i < numThreads; ++i) {
        threads.emplace_back(incrementAtomicLong, std::ref(y), iterationsPerThread / 10); // 注意调整迭代次数以匹配期望的增量
    }

    // 等待所有线程完成
    for (std::thread &t: threads) {
        t.join();
    }

    std::cout << "final value of x: " << x << std::endl;
    std::cout << "final value of y: " << y << std::endl;

    assert(x == numThreads * iterationsPerThread);
    assert(y == 1000L + numThreads * (iterationsPerThread / 10) * 10);
    return 0;
}

除了模拟整数运算,原子变量还有一些特殊的原子操作,比如store、load、fetch_add、fetch_sub、exchange、compare_exchange_weak/compare_exchange_strong以及CAS(Compare And Swap)操作

4)、线程

C++标准库里有专门的线程类thread,使用它就可以简单地创建线程,在名字空间std::this_thread里,还有yield()get_id()sleep_for()sleep_until()等几个方便的管理函数

#include <iostream>
#include <chrono>
#include <thread>

int main() {
    static std::atomic_flag flag{false};
    static std::atomic_int n;

    auto f = [&]() {
        auto value = flag.test_and_set(); // TAS检查原子标志量

        if (value) {
            std::cout << "flag has been set." << std::endl;
        } else {
            std::cout << "set flag by " << std::this_thread::get_id() << std::endl; // 输出线程id
        }

        n += 100; // 原子变量加法运算

        std::this_thread::sleep_for(std::chrono::milliseconds(n.load() * 10));
        std::cout << n << std::endl;
    };

    std::vector<std::thread> threads;
    for (int i = 0; i < 2; ++i) {
        threads.emplace_back(f);
    }

    for (std::thread &t: threads) {
        t.join();
    }
    return 0;
}

函数async()含义是异步运行一个任务,隐含的动作是启动一个线程去执行,但不绝对保证立即启动(也可以在第一个参数传递 std::launch::async,要求立即启动线程)

#include <iostream>
#include <chrono>
#include <thread>
#include <future>

int main() {
    auto task = [](auto x) {
        std::this_thread::sleep_for(std::chrono::milliseconds(x));
        std::cout << "sleep for " << x << std::endl;
        return x;
    };

    auto f = std::async(task, 10);// 启动一个异步任务
    f.wait(); // 等待任务完成

    assert(f.valid());// 确实已经完成了任务
    std::cout << f.get() << std::endl; // 获取任务的执行结果
    return 0;
}

async()会返回一个future变量,如果任务有返回值,就可以用成员函数get()获取。不过要特别注意,get()只能调一次,再次获取结果会发生错误,抛出异常 std::future_error

这里还有一个很隐蔽的坑,如果不显式获取async()的返回值(即future对象),它就会同步阻塞直至任务完成(由于临时对象的析构函数)。所以,即使不关心返回值,也总要用auto来配合async(),避免同步阻塞

5)、小结
  1. 多线程是并发最常用的实现方式,好处是任务并行、避免阻塞,坏处是开发难度高,有数据竞争、死锁等很多坑
  2. call_once()实现了仅调用一次的功能,避免多线程初始化时的冲突
  3. thread_local实现了线程局部存储,让每个线程都独立访问数据,互不干扰
  4. atomic实现了原子化变量,可以用作线程安全的计数器,也可以实现无锁数据结构
  5. async()启动一个异步任务,相当于开了一个线程,但内部通常会有优化,比直接使用线程更好

参考:

12 | 三分天下的容器:恰当选择,事半功倍

13 | 五花八门的算法:不要再手写for循环了

14 | 十面埋伏的并发:多线程真的很难吗?

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

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

相关文章

ubuntu系统盘扩容

目录 1 介绍 2 步骤 2.1 关闭虚拟机 2.2 编辑虚拟机设置 2.3 设置扩展大小 2.4 打开虚拟机 2.5 找到磁盘管理 2.6 扩展 1 介绍 本部分主要记述怎么给ubuntu系统盘扩展存储容量&#xff0c;整个过程相对简单&#xff0c;扩容方式轻松、容易。 2 步骤 2.1 关闭虚拟机 2…

【音视频 | RTSP】RTSP协议详解 及 抓包例子解析(详细而不赘述)

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…

【设计模式】工厂模式(定义 | 特点 | Demo入门讲解)

文章目录 定义简单工厂模式案例 | 代码Phone顶层接口设计Meizu品牌类Xiaomi品牌类PhoneFactory工厂类Customer 消费者类 工厂方法模式案例 | 代码PhoneFactory工厂类 Java高级特性---工厂模式与反射的高阶玩法方案&#xff1a;反射工厂模式 总结 其实工厂模式就是用一个代理类帮…

Pytorch(笔记7损失函数类型)

前言 损失函数&#xff08;Loss Function&#xff09;&#xff1a;是定义在单个样本上的&#xff0c;是指一个样本的误差&#xff0c;度量模型一次预测的好坏。 代价函数&#xff08;Cost Function&#xff09;成本函数经验风险&#xff1a;是定义在整个训练集上的&#xff0c…

【vue动态组件】VUE使用component :is 实现在多个组件间来回切换

VUE使用component :is 实现在多个组件间来回切换 component :is 动态父子组件传值 相关代码实现&#xff1a; <component:is"vuecomponent"></component>import componentA from xxx; import componentB from xxx; import componentC from xxx;switch(…

每日一题——Python实现PAT乙级1096 大美数(举一反三+思想解读+逐步优化)3千字好文

一个认为一切根源都是“自己不够强”的INTJ 个人主页&#xff1a;用哲学编程-CSDN博客专栏&#xff1a;每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 目录 我的写法 时间复杂度分析 空间复杂度分析 总结 哲学和编程思想 1. 抽象与具体化 …

2024亚太杯中文赛B题洪水灾害的数据分析与预测原创论文分享

大家好&#xff0c;从昨天肝到现在&#xff0c;终于完成了2024年第十四届 APMCM 亚太地区大学生数学建模竞赛B题洪水灾害的数据分析与预测的完整论文啦。 实在精力有限&#xff0c;具体的讲解大家可以去讲解视频&#xff1a; 2024亚太杯中文赛B题洪水灾害预测原创论文保姆级教…

Ad-hoc命令和模块简介

华子目录 Ad-hoc命令和模块简介1.概念2.格式3.Ansible命令常用参数4.模块类型4.1 三种模块类型4.2Ansible核心模块和附加模块 示例1示例2 Ad-hoc命令和模块简介 1.概念 Ansible提供两种方式去完成任务&#xff0c;一是ad-hoc命令&#xff0c;一是写Ansible playbook(剧本)Ad-…

uni-app打包小程序的一些趣事~

前言 Huilderx版本&#xff1a;4.15 uni-app Web端版本&#xff1a;3.4.21 问题1 Web端/APP端样式好好的&#xff0c;打包微信小程序就乱了咋整&#xff1f; 使用::v-deep/::deep/deep(){}都是没用滴~~ 原因&#xff1f; 解决&#xff1a; <script lang"ts"…

2024/7/6 英语每日一段

More than half of late-teens are specifically calling for more youth work that offers “fun”, with older teenagers particularly hankering for more jollity, according to a study carried out by the National Youth Agency. One in 10 said they have zero option…

将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置

我们倾向于将IConfiguration对象转换成一个具体的对象&#xff0c;以面向对象的方式来使用配置&#xff0c;我们将这个转换过程称为配置绑定。除了将配置树叶子节点配置节的绑定为某种标量对象外&#xff0c;我们还可以直接将一个配置节绑定为一个具有对应结构的符合对象。除此…

C语言课程回顾:八、C语言之函 数

C语言之函 数 8 函 数8.1 概述8.2 函数定义的一般形式8.3 函数的参数和函数的值8.3.1 形式参数和实际参数8.3.2 函数的返回值 8.4 函数的调用8.4.1 函数调用的一般形式8.4.2 函数调用的方式8.4.3 被调用函数的声明和函数原型 8.5 函数的嵌套调用8.6 函数的递归调用8.7 数组作为…

大厂面试官问我:MySQL宕机重启了,怎么知道哪些事务是需要回滚的哪些是需要提交的?【后端八股文九:Mysql事务八股文合集】

本文为【Mysql事务八股文合集】初版&#xff0c;后续还会进行优化更新&#xff0c;欢迎大家关注交流~ 大家第一眼看到这个标题&#xff0c;不知道心中是否有答案了&#xff1f;在面试当中&#xff0c;面试官经常对项目亮点进行深挖&#xff0c;来考察你对这个项目亮点的理解以及…

AIGC | 为机器学习工作站安装NVIDIA 4070 Ti Super显卡驱动

[ 知识是人生的灯塔&#xff0c;只有不断学习&#xff0c;才能照亮前行的道路 ] 0x00 前言简述 话接上篇《AIGC | Ubuntu24.04桌面版安装后必要配置》文章&#xff0c;作为作者进行机器学习的基础篇&#xff08;筑基期&#xff09;&#xff0c;后续将主要介绍机器学习环境之如何…

32位Arm嵌入式开发Ubuntu环境设置

32位Arm嵌入式开发Ubuntu环境设置 今天在调试一块32位ARM A7开发板时老是不成功&#xff0c;我装的是Ubuntu22.04版&#xff0c;在终端下运行工具链里的gdb程序居然报了一大堆错误&#xff0c;缺这个缺那个&#xff0c;按照提示装了一遍&#xff0c;再运行发现需要Python2.7环境…

NSK发布新版在线计算工具

July 01, 2024 NSK Ltd. Corporate Communications Department NSK Ltd. announced today that it has improved the engineering tools available on its website. The new engineering tools — NSK Online Catalog, Technical Calculations, and 2D/3D CAD Data — which …

STM32第十五课:LCD屏幕及应用

文章目录 需求一、LCD显示屏二、全屏图片三、数据显示1.显示欢迎词2.显示温湿度3.显示当前时间 四、需求实现代码 需求 1.在LCD屏上显示一张全屏图片。 2.在LCD屏上显示当前时间&#xff0c;温度&#xff0c;湿度。 一、LCD显示屏 液晶显示器&#xff0c;简称 LCD(Liquid Cry…

分析Profiler Timeline中的算子序列,通过寻找频繁项集的办法,得到TOPK可融合的算子序列

分析Profiler Timeline中的算子序列,通过寻找频繁项集的办法,得到TOPK可融合的算子序列 1.相关链接2.代码【仅分析带通信算子的Pattern】3.在实际工程中发现 [all_gather, matrix_mm_out]频率最高4.[Ascend MC2](https://gitee.com/ascend/MindSpeed/blob/master/docs/features…

路径规划之基于二次规划的路径平滑Matlab代码

参考&#xff1a; 自动驾驶决策规划算法第二章第二节(上) 参考线模块_哔哩哔哩_bilibili 自动驾驶决策规划算法第二章第二节(下) 参考线代码实践_哔哩哔哩_bilibili QP函数&#xff0c;二次规划的逻辑 function [smooth_path_x,smooth_path_y] QP(path_x, path_y, w_cost_s…

docker也能提权??内网学习第6天 rsync未授权访问覆盖 sudo(cve-2021-3156)漏洞提权 polkit漏洞利用

现在我们来说说liunx提权的操作&#xff1a;前面我们说了环境变量&#xff0c;定时任务来进行提权的操作 rsync未授权访问覆盖 我们先来说说什么是rsync rsync是数据备份工具&#xff0c;默认是开启的873端口 我们在进行远程连接的时候&#xff0c;如果它没有让我们输入账号…