C++11线程以及线程同步

C++11中提供的线程类std::thread,基于此类创建一个新的线程相对简单,只需要提供线程函数和线程对象即可

一.命名空间 this_thread

C++11 添加一个关于线程的命名空间std::this_pthread ,此命名空间中提供四个公共的成员函数;

1.1 get_id()

调用命名空间std::this_thread 中的 get_id() 方法可以得到当前线程ID:

示例如下:

#include <iostream>
#include <thread>
#include <mutex>void func() {std::cout << "子线程ID:" << std::this_thread::get_id() << std::endl;
}int main() {std::cout << "主线程ID:" << std::this_thread::get_id() << std::endl;std::thread t1(func);t1.join();return 0;
}

1.2 sleep_for()

线程被创建出来之后有5中状态 创建态、就绪态、阻塞态、运行态、推出态 ;
线程和进程在使用时非常相识,在计算机中启动的多个线程都需要占用 CPU 资源,但是 CPU 的个数是有限的并且每个 CPU 在同一时间点不能同时处理多个任务。为了能够实现并发处理,多个线程都是分时复用CPU时间片,快速的交替处理各个线程中的任务。因此多个线程之间需要争抢CPU时间片,抢到了就执行,抢不到则无法执行(因为默认所有的线程优先级都相同,内核也会从中调度,不会出现某个线程永远抢不到 CPU 时间片的情况)。
命名空间 this_thread 中提供了一个休眠函数 sleep_for(),调用这个函数的线程会马上从运行态变成阻塞态并在这种状态下休眠一定的时长 ,因为阻塞态的线程已经让出了 CPU 资源,代码也不会被执行,所以线程休眠过程中对 CPU 来说没有任何负担;

示例:

#include <iostream>
#include <thread>
#include <mutex>void func() {for (size_t i = 0; i < 5; ++i){std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "子线程ID:" << std::this_thread::get_id() << std::endl;}
}int main() {std::cout << "主线程ID:" << std::this_thread::get_id() << std::endl;std::thread t1(func);t1.join();return 0;
}

在for循环中使用this_thread::sleep_for(chrono::seconds(2)); 后每次循环一次都会阻塞1s ,即每隔1s输出一次;注意: 程序休眠之后会从阻塞态变为就绪态,就绪态的线程需要再次抢夺CPU时间片,抢到之后会变成运行态,程序才能继续运行下去;

1.3 sleep_until

this_thread命名空间还提供另一个休眠函数 sleep_until ,和 sleep_for 有不同的参数类型;
sleep_until(): 指定线程阻塞到某一个时间点 time_point类型 ,之后解除;
sleep_for(): 指定线程阻塞一定的时间长度 duration类型 ,之后解除阻塞;

示例:

#include <iostream>
#include <thread>
#include <mutex>void func() {for (size_t i = 0; i < 5; ++i){/* 获取当前的时间 */auto current_time = std::chrono::system_clock::now();/* 时间间隔 */std::chrono::seconds sec(5);/* 当前时间点后休眠5秒 */std::this_thread::sleep_until(current_time + sec);std::cout << "子线程ID:" << std::this_thread::get_id() << std::endl;}
}int main() {std::cout << "主线程ID:" << std::this_thread::get_id() << std::endl;std::thread t1(func);t1.join();return 0;
}

sleep_until 和 sleep_for 函数功能一样 ,前者基于时间点阻塞 ,后者基于时间段阻塞。

1.4 yield()

this_thread命名空间提供能主动由运行态退让出已经抢到时间片 的线程函数 yield() ,最终变为就绪态,这样其他线程就能抢到CPU时间片;
线程调用了 yield () 之后会主动放弃 CPU 资源,但是这个变为就绪态的线程会马上参与到下一轮 CPU 的抢夺战中,不排除它能继续抢到 CPU 时间片的情况。

示例:

#include <iostream>
#include <thread>
#include <mutex>void func() {for (size_t i = 0; i < 10000000; ++i){std::this_thread::yield();std::cout << "子线程ID:" << std::this_thread::get_id() << ",i = " << i << std::endl;}
}int main() {std::cout << "主线程ID:" << std::this_thread::get_id() << std::endl;std::thread t1(func);std::thread t2(func);t1.join();t1.join();return 0;
}

func() 中的 for 循环会占用大量的时间 ,在极端情况下,如果当前线程占用 CPU 资源不释放就会导致其他线程中的任务无法被处理,或者该线程每次都能抢到 CPU 时间片,导致其他线程中的任务没有机会被执行。解决方案就是每执行一次循环,让该线程主动放弃 CPU 资源,重新和其他线程再次抢夺 CPU 时间片,如果其他线程抢到了 CPU 时间片就可以执行相应的任务了。

注意:
yield() 的目的是避免一个线程长时间占用CPU资源,从而多线程处理能力下降;
yield() 是让当前线程主动放弃自己抢到的CPU资源,但是在下一轮还会继续抢;

二. C++ 线程类

2.2.1 join() 函数

在子线程对象中调用 join()函数,调用此函数的线程会被阻塞 ,但是子线程对象中的任务函数会继续执行 ,当任务执行完毕之后 join()函数会清理当前子线程中的相关资源后返回,同时该线程函数会解除阻塞继续执行下去。==函数在那个线程中被执行,函数就阻塞那个函数。

如果要阻塞主线程的执行,只需要在主线程中通过子线程对象调用这个方法即可,当调用这个方法的子线程对象中的任务函数执行完毕之后,主线程的阻塞也就随之解除了。

int main() {std::cout << "主线程ID:" << std::this_thread::get_id() << std::endl;std::thread t1(func);std::thread t2(func);t1.join();t1.join();return 0;
}

当主线程运行到t1.join() ; 根据子线程对象 t1 的任务函数 func() 的执行情况,主线程会:
任务函数 func() 还没执行完毕,主线程阻塞直到任务执行完毕,主线程解除阻塞,继续向下执行
任务函数 func() 执行完毕,主线程不会阻塞 ,继续向下运行。

2.2.2 detach() 函数

detach() 函数的是进行线程分离 ,分离主线程和子线程。在线程分离之后,主线程退出也会销毁创建的所有子线程,在主线程推出之前,子线程可以脱离主线程继续独立运行,任务结束完毕之后,这个子线程会自动释放自己占用的系统资源。

#include <iostream>
#include <thread>
#include <mutex>void func() {for (size_t i = 0; i < 5; ++i){auto current_time = std::chrono::system_clock::now();std::chrono::seconds sec(1);std::this_thread::sleep_until(current_time + sec);std::cout << "i = " << i << std::endl;}
}void func1(int num) {for (size_t i = 0; i < 5; ++i) {std::cout << "num: = " << num << std::endl;}
}int main() {std::cout << "主线程ID:" << std::this_thread::get_id() << std::endl;std::thread t1(func);std::thread t2(func1, 111);std::cout << "线程t1的线程ID:" << t1.get_id() << std::endl;std::cout << "线程t2的线程ID:" << t2.get_id() << std::endl;/* 线程分离 */t1.detach();t2.detach();/* 主线程等待子线程执行完毕 */std::this_thread::sleep_for(std::chrono::seconds(5));return 0;
}

注意:线程分离函数 detach () 不会阻塞线程,子线程和主线程分离之后,在主线程中就不能再对这个子线程做任何控制了,比如:通过 join () 阻塞主线程等待子线程中的任务执行完毕。

2.2.3 joinable() 函数

joinable() 函数用于判断主线程和子线程是否处于关联(连接)状态,通常情况下两者处于关联状态,该函数返回一个布尔类型:
返回 true : 主线程和子线程有关联;
返回 false 主线程和子线程没有关联;

#include <iostream>
#include <thread>
#include <mutex>void func() {std::this_thread::sleep_for(std::chrono::seconds(2));
}int main() {std::thread t1;std::cout << "before starting, joinable: " << t1.joinable() << std::endl;t1 = std::thread(func);std::cout << "after starting, joinable: " << t1.joinable() << std::endl;t1.join();std::cout << "after joining, joinable: " << t1.joinable() << std::endl;std::thread t2(func);std::cout << "after starting, joinable: " << t2.joinable() << std::endl;t2.detach();std::cout << "after detaching, joinable: " << t2.joinable() << std::endl;return 0;
}

打印结果:

结论:
1.创建子线程对象时,如果没有指定任务函数,那么子线程不会启动,主线程和子线程也不会进行连接;
2.创建子线程对象时,如果指定任务函数,子线程启动并执行任务,主线程和子线程自动连接成功;
3.子线程调用detach()函数后,父子线程分离,两者的连接断开,调用joinable()返回 fasle;
4.子线程调用 join()函数后,子线程中的任务函数继续执行,知道任务处理完毕,此时join()清理回收当前线程的相关资源,此时子线程和主线程连接断开了,此时调用join()函数之后再调用joinable()返回false。

三.C++ 线程同步

进行多线程编程,如果多个线程需要对同一块内存进行操作,如:同时读、同时写、同时读写 对后两种情况而言如果不做任何的人为干涉会出现各种错误数据。这是因为线程在运行的时候需要先得到 CPU 时间片,时间片用完之后需要放弃已获得的 CPU 资源,就这样线程频繁地在就绪态和运行态之间切换,更复杂一点还可以在就绪态、运行态、挂起态之间切换,这样就会导致线程的执行顺序并不是有序的,而是随机的混乱的。

3.1互斥锁

解决多线程数据混乱的方案就是进行线程同步,最常用的是互斥锁 ,在C++11 中提供了四种互斥锁:
1.std::mutex : 独占的互斥锁,不能递归使用;
2.std::timed_mutex: 带超时的独占互斥锁,不能递归使用;
3.std::recursive_mutex: 递归互斥锁,不带超时功能;
4.std::recursive_timed_mutex : 带超时的递归互斥锁;

3.1.1 std::mutex

独占互斥锁对象有两种状态:锁定和未锁定
如果互斥锁是打开的,调用 lock() 函数的线程会得到互斥锁的所有权,并将其上锁。其他线程再调用该函数时由于得不到互斥锁的所有权,就会被 lock()函数阻塞。
当拥有互斥锁所有权的线程将互斥锁解锁 ,此时被 lock() 阻塞的线程解除阻塞 ,抢到互斥锁所有权的线程加锁成功并继续开锁,没抢到互斥锁所有权的线程继续阻塞;

还可以使用 try_lock() 获取互斥锁的所有权并对互斥锁加锁:

和 lock()的区别在于 ,try_lock()不会阻塞线程,lock()会阻塞线程:

如果互斥锁是未锁定状态,得到了互斥锁所有权并加锁成功,函数返回 true;
如果互斥锁是锁定状态,无法得到互斥锁所有权加锁失败,函数返回 false;

互斥锁被锁定之后可以通过 unlock()进行解锁,但是需要注意:只有拥有互斥锁所有权的线程(对互斥锁上锁的线程)才能将其解锁,其他线程没有权限做这件事;

使用互斥锁进行线程同步的流程:
1.找到多个线程操作的共享资源(全局变量、堆内存、类成员变量),成为临界资源 ;
2.找到共享资源相关的上下文代码,即临界区
3.再临界区的上边调用互斥锁类的 lock() 方法;
4.再临界区的下边调用互斥锁类的 unlock() 方法;

线程同步的目的:使多线程按照顺序依次进行执行临界区代码,对共享资源的访问从并行访问变成线性访问,访问效率降低了但是保证了数据的正确性;

当线程对互斥锁对象加锁,并且执行完临界区代码之后,一定要使用这个线程对互斥锁解锁,否则最终会造成线程的死锁。死锁之后当前应用程序中的所有线程都会被阻塞,并且阻塞无法解除,应用程序也无法继续运行。

#include <iostream>
#include <thread>
#include <mutex>int g_number = 0;
std::mutex g_mtx;void func(int id) {for (size_t i = 0; i < 5; ++i) {g_mtx.lock();g_number++;std::cout << "id:" << id << " number =" << g_number << std::endl;g_mtx.unlock();std::this_thread::sleep_for(std::chrono::seconds(2));}
}int main() {std::thread t1(func, 0);std::thread t2(func, 1);t1.join();t2.join();return 0;
}

注意:
1.在所有线程的任务函数执行完毕之前,互斥锁对象是不能被析构的,一定要在程序中保证对象的可用性;
2.互斥锁的个数和共享资源的个数相等,每一个共享资源对应一个互斥锁对象,与线程数无关;

3.1.2 std::lock_guard

lock_guard 是C++11新增的一个模板类,可以简化互斥锁 lock()和unlock()的写法,同时也更安全。

void func(int id) {for (size_t i = 0; i < 5; ++i) {std::lock_guard<std::mutex> lock_mtx(g_mtx);g_number++;std::cout << "id:" << id << " number =" << g_number << std::endl;std::this_thread::sleep_for(std::chrono::seconds(2));}
}

lock_guard 在使用构造函数构造对象时,会自动锁定互斥量,且在退出作用域后进行析构就会自动解锁,以保证互斥量的正确性,避免忘记 unlock() 而导致的线程死锁。

3.1.3 std::recursive_mutex

递归互斥锁: std::recursive_mutex 允许同一线程多次获得互斥锁,可以用来解决同一线程需要多次获取互斥量时的死锁问题 ,以下案例使用独占非递归互斥量会发生死锁:

#include <iostream>
#include <thread>
#include <mutex>class Calculate
{
public:Calculate() :m_num(2) {}void mul(int x) {std::lock_guard<std::mutex>lock(m_mutex);m_num *= x;}void div(int x) {std::lock_guard<std::mutex>lock(m_mutex);m_num = m_num / x;}void both(int x, int y) {std::lock_guard<std::mutex>lock(m_mutex);mul(x);div(x);}
private:int m_num;std::mutex m_mutex;
};int main() {Calculate cal;cal.both(3, 4);return 0;
}

在执行到 cal.both(3,4); 调用之后程序会发生死锁,在 both() 中已经对互斥锁加锁了,继续调用 mult() 函数,已经得到互斥锁所有权的线程再次获取这个互斥锁的所有权便会造成死锁 (C++异常退出);使用递归互斥锁 std::recursive_mutex ,其允许一个线程多次获取互斥锁的所有权。

#include <iostream>
#include <thread>
#include <mutex>class Calculate
{
public:Calculate() :m_num(2) {}void mul(int x) {std::lock_guard<std::recursive_mutex>lock(m_mutex);m_num *= x;}void div(int x) {std::lock_guard<std::recursive_mutex>lock(m_mutex);m_num = m_num / x;}void both(int x, int y) {std::lock_guard<std::recursive_mutex>lock(m_mutex);mul(x);div(x);}private:int m_num;std::recursive_mutex m_mutex;
};int main() {Calculate cal;cal.both(3, 4);return 0;
}

总结:
递归互斥锁可以解决同一个互斥锁频繁获取互斥锁资源的问题,但是建议少用:
1.使用递归锁的场景往往可以都是简化的,使用递归锁可能会导致复杂逻辑产生,可能会导致bug产生;
2.互斥递归锁比非互斥递归锁效率低一些;
3.递归互斥锁虽运行同一个线程多次获取同一个互斥锁的所有权,但是最大使用次数未知,使用次数过多可能会抛出异常 std::system 错误;

3.1.4 std::timed_mutex

std::timed_mutex 是独占超时互斥锁 ,在获取互斥锁资源是增加一个超时等待功能 ,因为不知道获取锁资源需要等待多长时间,为了保证不一直等待下去,设置一个超时时长,超时后线程会解除阻塞做其他事情。

std::timed_mutex 比 std::mutex 多了两个成员函数:try_lock_for() 和 try_lock_until() :

1.try_lock_for 函数是当线程获取不到互斥锁资源之后,让线程阻塞一定的时间长度;
2.try_lock_until 函数是当线程获取不到互斥锁资源时,让线程阻塞到某一个时间点;

当两个函数返回值:当得到互斥锁所有权后,函数会马上解除阻塞 ,返回true ,如果阻塞的时长用完或达到某时间点后,函数会解除阻塞 ,返回false。

#include <iostream>
#include <thread>
#include <mutex>std::timed_mutex g_mutex;void func()
{std::chrono::seconds timeout(2);while (true){/* 通过阻塞一定的时长来争取得到互斥锁所有权 */if (g_mutex.try_lock_for(timeout)) {std::cout << "当前线程ID: " << std::this_thread::get_id() << ", 得到互斥锁所有权..." << std::endl;/* 模拟处理任务用了一定的时长 */std::this_thread::sleep_for(std::chrono::seconds(10));/* 互斥锁解锁 */ g_mutex.unlock();break;} else {std::cout << "当前线程ID: " << std::this_thread::get_id() << ", 没有得到互斥锁所有权..." << std::endl;/* 模拟处理其他任务用了一定的时长 */ std::this_thread::sleep_for(std::chrono::milliseconds(50));}}
}int main() {std::thread t1(func);std::thread t2(func);t1.join();t2.join();return 0;
}

关于递归超时互斥锁 std::recursive_timed_mutex 的使用方式和 std::timed_mutex 是一样的,只不过它可以允许一个线程多次获得互斥锁所有权,而 std::timed_mutex 只允许线程获取一次互斥锁所有权。另外,递归超时互斥锁 std::recursive_timed_mutex 也拥有和 std::recursive_mutex 一样的弊端,不建议频繁使用。

3.2 条件变量

C++11 提供了另一种用于等待的同步机制,能阻塞一个或多个线程,直到收到另一个线程发出的通知或超时时,才能唤醒当前阻塞的线程。条件变量需要和互斥量配合使用。

C++11 提供了两种条件变量:
condition_variable: 配合 std::unique_lock<std::mutex> 进行 wait 操作,也就是阻塞线程的操作;
conditon_variable_any : 可以和任意带有 lock() 、unlock()语义的 mutex 搭配使用,即存在四种:
1.std::mutex : 独占的非递归互斥锁;
2.std::timed_mutex: 带超时的独占非递归锁;
3.std::recursive_mutex: 不带超时功能的递归互斥锁;
4.std::recursive_timed_mutex: 带超时的递归互斥锁;

void Worker() {std::unique_lock<std::mutex> lock(mutex);// 等待主线程发送数据。cv.wait(lock, [] { return ready; });// 等待后,继续拥有锁。std::cout << "工作线程正在处理数据..." << std::endl;// 睡眠一秒以模拟数据处理。std::this_thread::sleep_for(std::chrono::seconds(1));data += " 已处理";// 把数据发回主线程。processed = true;std::cout << "工作线程通知数据已经处理完毕。" << std::endl;// 通知前,手动解锁以防正在等待的线程被唤醒后又立即被阻塞。lock.unlock();cv.notify_one();
}
int main() {std::thread worker(Worker);// 把数据发送给工作线程。{std::lock_guard<std::mutex> lock(mutex);std::cout << "主线程正在准备数据..." << std::endl;// 睡眠一秒以模拟数据准备。std::this_thread::sleep_for(std::chrono::seconds(1));data = "样本数据";ready = true;std::cout << "主线程通知数据已经准备完毕。" << std::endl;}cv.notify_one();// 等待工作线程处理数据。{std::unique_lock<std::mutex> lock(mutex);cv.wait(lock, [] { return processed; });}std::cout << "回到主线程,数据 = " << data << std::endl;worker.join();return 0;
}
主线程正在准备数据...
主线程通知数据已经准备完毕。
工作线程正在处理数据...
工作线程通知数据已经处理完毕。
回到主线程,数据 = 样本数据 已处理

本篇博文非作者原创,部分内容属转载。

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

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

相关文章

电商营销场景的RocketMQ实战01-RocketMQ原理

架构图 Broker主从架构与集群模式 RocketMQ原理深入剖析 Broker主从架构原理 HAConnection与HAClient Broker基于raft协议的主从架构 Consumer运行原理 基础知识 001_RocketMQ架构设计与运行流程分析 RocketMQ这一块&#xff0c;非常关键的一个重要的技术&#xff0c;面试的时候…

键盘打字盲打练习系列之指法练习——2

一.欢迎来到我的酒馆 盲打&#xff0c;指法练习&#xff01; 目录 一.欢迎来到我的酒馆二.开始练习 二.开始练习 前面一个章节简单地介绍了基准键位、字母键位和数字符号键位指法&#xff0c;在这个章节详细介绍指法。有了前面的章节的基础练习&#xff0c;相信大家对盲打也有了…

设计模式-结构型模式之适配器设计模式

文章目录 一、结构型设计模式二、适配器模式 一、结构型设计模式 这篇文章我们来讲解下结构型设计模式&#xff0c;结构型设计模式&#xff0c;主要处理类或对象的组合关系&#xff0c;为如何设计类以形成更大的结构提供指南。 结构型设计模式包括&#xff1a;适配器模式&…

Day04:每日一题:2661. 找出叠涂元素

2661. 找出叠涂元素 给你一个下标从 0 开始的整数数组 arr 和一个 m x n 的整数 矩阵 mat 。 arr 和 mat 都包含范围 [1&#xff0c;m * n] 内的 所有 整数。从下标 0 开始遍历 arr 中的每个下标 i &#xff0c;并将包含整数 arr[i] 的 mat 单元格涂色。请你找出 arr 中在 mat…

linux 磁盘管理、分区管理常用命令

文章目录 基础命令挂载新硬盘/分区添加内存交换分区swaplvm分区管理模式 基础命令 查看目录文件大小 du -sh /backup du -sh /backup/* du -sh *查看磁盘挂载信息 df -lhT查看某个目录挂载在哪个分区&#xff0c;以及分区的磁盘使用情况 df [目录] #例如&#xff1a;df /ho…

Scrapy爬虫异步框架之持久化存储(一篇文章齐全)

1、Scrapy框架初识&#xff08;点击前往查阅&#xff09; 2、Scrapy框架持久化存储 3、Scrapy框架内置管道&#xff08;点击前往查阅&#xff09; 4、Scrapy框架中间件&#xff08;点击前往查阅&#xff09; 5、Scrapy框架全站、分布式、增量式爬虫 Scrapy 是一个开源的、…

对于Web标准以及W3C的理解、对viewport的理解、xhtml和html有什么区别?

1、对于Web标准以及W3C的理解 Web标准 Web标准简单来说可以分为结构、表现、行为。 其中结构是由HTML各种标签组成&#xff0c;简单来说就是body里面写入标签是为了页面的结构。 表现指的是CSS层叠样式表&#xff0c;通过CSS可以让我们的页面结构标签更具美感。 行为指的是…

【Python表白系列】这个情人节送她一个漂浮的爱心吧(完整代码)

文章目录 漂浮的爱心环境需求完整代码详细分析系列文章 漂浮的爱心 环境需求 python3.11.4PyCharm Community Edition 2023.2.5pyinstaller6.2.0&#xff08;可选&#xff0c;这个库用于打包&#xff0c;使程序没有python环境也可以运行&#xff0c;如果想发给好朋友的话需要这…

c/c++概念辨析-指针常量常量指针、指针函数函数指针、指针数组数组指针

概念澄清&#xff1a; 统一规则&#xff1a; 不管是XX指针&#xff0c;还是指针XX&#xff0c;后者是本体&#xff0c;前者只是个定语&#xff0c;前者也可以替换为其他同类&#xff08;例如字符串&#xff09;&#xff0c;帮助理解。 XX指针&#xff1a; 可简单理解为&#…

基于helm的方式在k8s集群中部署gitlab - 部署(一)

文章目录 1. 背景说明2. 你可以学到什么&#xff1f;3. 前置条件4. 安装docker服务&#xff08;所有节点&#xff09;5. 部署k8s集群5.1 系统配置&#xff08;所有节点&#xff09;5.2 安装kubelet组件(所有节点)5.2.1 编写kubelet源5.2.2 安装kubelet5.2.3 启动kubelet 5.3 集…

从零开始,用Docker-compose打造SkyWalking、Elasticsearch和Spring Cloud的完美融合

&#x1f38f;&#xff1a;你只管努力&#xff0c;剩下的交给时间 &#x1f3e0; &#xff1a;小破站 "从零开始&#xff0c;用Docker-compose打造SkyWalking、Elasticsearch和Spring Cloud的完美融合 前言准备工作编写docker-compose.yml文件为什么使用本机ip为什么skywa…

python中的字符串

字符串 字符串是编程语言中的一种基本数据类型&#xff0c;用于表示一串字符序列。在Python中&#xff0c;字符串是不可变的&#xff0c;也就是说一旦字符串被创建&#xff0c;就无法修改其中的字符。 Python中的字符串可以用单引号或双引号括起来&#xff0c;例如&#xff1…

【LeetCode】 160. 相交链表

相交链表 题目题解 题目 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交&#xff1a; 题目数据 保证 整个链式结构中不存在环。 注意&am…

卡码网15 .链表的基本操作III

链表的基础操作III 时间限制&#xff1a;1.000S 空间限制&#xff1a;128MB 题目描述 请编写一个程序&#xff0c;实现以下链表操作&#xff1a;构建一个单向链表&#xff0c;链表中包含一组整数数据。 1. 实现在链表的第 n 个位置插入一个元素&#xff0c;输出整个链表的…

0-1背包问题详解

0-1背包问题 部分一&#xff1a;问题描述 0-1背包问题是一类经典的组合优化问题&#xff0c;它出现在很多实际生活和工业环境中。问题描述如下&#xff1a; 假设你是一个冒险家&#xff0c;带着一个可承重的背包&#xff0c;面对一堆宝物。每件宝物都有自己的价值&#xff0…

【设计模式-2.1】创建型——单例模式

说明&#xff1a;设计模式根据用途分为创建型、结构性和行为型。创建型模式主要用于描述如何创建对象&#xff0c;本文介绍创建型中的单例模式。 饿汉式单例 单例模式是比较常见的一种设计模式&#xff0c;旨在确保对象的唯一性&#xff0c;什么时候去使用这个对象都是同一个…

Rust UI开发(一):使用iced构建UI时,如何在界面显示中文字符

注&#xff1a;此文适合于对rust有一些了解的朋友 iced是一个跨平台的GUI库&#xff0c;用于为rust语言程序构建UI界面。 iced的基本逻辑是&#xff1a; UI交互产生消息message&#xff0c;message传递给后台的update&#xff0c;在这个函数中编写逻辑&#xff0c;然后通过…

Pytest做性能测试?

Pytest其实也是可以做性能测试或者基准测试的。是非常方便的。 可以考虑使用Pytest-benchmark类库进行。 安装pytest-benchmark 首先&#xff0c;确保已经安装了pytest和pytest-benchmark插件。可以使用以下命令安装插件&#xff1a; pip install pytest pytest-benchmark …

黑马一站制造数仓实战1

1. 项目目标 一站制造 企业中项目开发的落地&#xff1a;代码开发 代码开发&#xff1a;SQL【DSL SQL】 SparkCore SparkSQL 数仓的一些实际应用&#xff1a;分层体系、建模实现 2. 内容目标 项目业务介绍&#xff1a;背景、需求 项目技术架构&#xff1a;选型、架构 项目环境…

ubuntu系统下搭建本地物联网mqtt服务器的步骤

那么假如我们需要做一些终端设备&#xff0c;例如温湿度传感器、光照等物联网采集设备要接入呢&#xff1f;怎么样才能将数据报送到服务器呢&#xff1f; 以下内容基于我们ubuntu系统下的emqx成功启动的基础上。我们可以用浏览器键入控制板的地址&#xff0c;如果启动成功&…