简单了解C++线程库

thread类简单介绍

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接
口,这使得代码的可移植性比较差C++11中最重要的特性就是对线程进行支持了,使得C++在
并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的
线程,必须包含< thread >头文件。

get_id()

#include <iostream>
#include <thread>

using namespace std;

int main()
{
	thread t1;
	cout << t1.get_id() << endl;
	return 0;
}

另外,由于我这里没有传参,所以这是一个空线程,它不会启动的。除非进行移动赋值。 

get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中
包含了一个结构体.
// vs下查看
typedef struct
{ /* thread identifier for Win32 */
 void *_Hnd; /* Win32 HANDLE */
 unsigned int _Id;
} _Thrd_imp_t;

在C++11,线程可以执行的对象有四种:

1.函数指针。

2.仿函数。

3.lambda表达式

4.包装器。

简单拿lambda表达式为例

#include <iostream>
#include <thread>

using namespace std;

int main()
{
	thread t1([] {cout << "hello world" << endl;});
	cout << t1.get_id() << endl;
	t1.join();
	return 0;
}

不过记得要进行join,避免内存泄露。

另外我们还可以给将要执行的函数传参数

C++11这里,thread的构造函数还有一个参数包,那么就代表我们可以传任意数量的参数。

不过这里有一个小坑,比如当我们用函数指针传参如果是引用的话,要在对应的位置加上ref(要传的参数),不然会导致编译报错。因为虽然我们以为我们传的是引用,但是图中要经过thread的类的构造函数,然后将这个引用的属性给去了,取而代之的是一个临时变量传过去,然后就因为类型不匹配而报错,如下

那么我们只要给出问题的参数加上ref,表示要保留其引用属性,就不会出问题了。

 另外,thread类是防拷贝的,所以不允许拷贝构造和赋值重载,但是可以移动构造和移动赋值重载。

可以通过joinable()来判断线程是否有效,比如以下情况线程无效:

1.采用无参的构造函数创建的线程对象。

2.线程对象的状态已经转移给其他线程对象了(通过移动构造或移动赋值)。

3.线程已经调用join或者detach了。

线程的原子性操作

  使用多线程能提高程序效率,但是也会带来线程安全问题,C++11的多线程库中,除了刚刚介绍的thread类,还有很多,比如mutex,还有condition_variable

使用mutex也要包含<mutex>这个头文件。

有一个变量  int sum。我们进行sum++,虽然对于C++来说这只是一条语句,但是它的操作并不是原子的,如果以汇编的角度来看,它这里是三条语句。

我们可以通过加锁的方式,来保证修改共享数据时的安全性,因为其他线程是阻塞等待的,那么就会影响程序整体的效率, 并且还有可能造成死锁。

锁的简单使用

#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

mutex mut;
unsigned long sum = 0;

void func(int& n)
{
	for (int i = 0; i < n; ++i)
	{
		mut.lock();
		++sum;
		mut.unlock();
	}
}

int main()
{
	//thread t1([] {cout << "hello world" << endl;});
	int n;
	cin >> n;
	thread t1(func, ref(n));
	thread t2(func, ref(n));
	//cout << t1.get_id() << endl;
	t1.join();
	t2.join();
	cout << "sum: " << sum << endl;
	return 0;
}

 但就如之前所说,效率低,因此C++11引入了原子操作,atomic

简单使用

#include <atomic>

using namespace std;

mutex mut;
atomic_long sum{ 0 };

void func(int& n)
{
	for (int i = 0; i < n; ++i)
	{
		++sum;
	}
}

 这样就能保证sum++是原子性的。

注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11
中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及
operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算
符重载默认删除掉了。
#include <atomic>
int main()
{
 atomic<int> a1(0);
 //atomic<int> a2(a1);   // 编译失败
 atomic<int> a2(0);
 //a2 = a1;               // 编译失败
 return 0;
}

lock_guard和unique_lock,mutex库

  刚刚展示了使用atomic实现了对某一个变量的加减实现原子性,但是很多情况下,我们不仅仅是想保护一个变量,而是一段代码,那这个时候还是要用到锁的,但是是RAII风格的锁,也就是智能指针风格的锁(毕竟都是C++11了嘛)。

  不过在这里可以提一下为什么要用RAII风格的锁,因为原来的锁(C++98),会有不能避免的会触发死锁的场景。

  因为C++11引入了异常,虽然异常可以被捕获,但是会直接跳出出去,导致还没来及释放锁,那么就会造成死锁问题。也就是在锁内的范围抛了异常。

  不过先来了解一下mutex库吧

1.mutex

std::mutex,最常见的锁。

最常见的三个函数

lock() : 上锁,锁住互斥量,如果已经被申请,那么会阻塞等待。

unlock(): 解锁,释放互斥量

try_lock(): 申请锁,但是如果该锁已经被申请了,那么也不会阻塞。

2.recursive_mutex

std::recursive_mutex,主要是应用在递归需要锁的场景中。

允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,
释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同。

  3.timed_mutex

std::timed_mutex,

它的特点是try_lock_for()和try_lock_until()

try_lock_for() : 跟try_lock()有点像,try_lock()是如果没有申请到锁,那么直接返回false;try_lock_for()会给定一个时间范围,如果在该时间范围内没有申请到锁,那么先是会阻塞,超过了时间范围内还没有申请到锁的话就会返回false。

try_lock_until()  :接受一个时间点为参数,如果在时间点之前没有申请到锁则阻塞,超过了还没有就返回false。

4.recursive_timed_mutex

这个很好理解,就是递归 + timed_mutex 合体。

接下来正式介绍两把锁

5.lock_guard 

  很简单的锁,它的生命周期随对象,出了作用域就会自动调用析构函数,自动销毁,因此可以解决上面因为在锁的范围内抛异常而释放不了锁的情况。

  大致原理

template<class _Mutex>
class lock_guard
{
public:
// 在构造lock_gard时,_Mtx还没有被上锁
 explicit lock_guard(_Mutex& _Mtx)
 : _MyMutex(_Mtx)
 {
 _MyMutex.lock();
 }
// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
lock_guard(_Mutex& _Mtx, adopt_lock_t)
 : _MyMutex(_Mtx)
 {}
 ~lock_guard() _NOEXCEPT
 {
 _MyMutex.unlock();
 }
 lock_guard(const lock_guard&) = delete;
 lock_guard& operator=(const lock_guard&) = delete;
private:
 _Mutex& _MyMutex;
};

 另外,explicit 是 C++ 中的一个关键字。它主要用于类构造函数,以阻止不应该发生的隐式类型转换。

示例:

class Foo {  
public:  
    explicit Foo(int x) { /* ... */ }  
};  
  
void someFunction(Foo f) { /* ... */ }  
  
int main() {  
    Foo f(42); // 正确:显式调用构造函数  
    someFunction(Foo(42)); // 正确:显式调用构造函数  
    someFunction(42); // 错误:因为构造函数是explicit的,所以不能进行隐式转换  
    return 0;  
}

在上面的例子中,尝试隐式地将整数 42 转换为 Foo 对象会导致编译错误,因为 Foo 的构造函数被声明为 explicit。如果你试图进行这样的隐式转换,编译器会给出错误消息。

lock_guard简单使用

#include <mutex>
#include <atomic>

using namespace std;

mutex mut;
atomic_long sum{ 0 };

void func(int& n)
{
	for (int i = 0; i < n; ++i)
	{
		lock_guard<mutex> mu(mut);
		++sum;
	}
}

但是因为lock_guard过于简单而太单一,用户没有办法对锁进行控制,因此就有unique_lock。

6.unique_lock

   unique_lock也是采用了RAII的方式对锁进行了封装。unique_lock的使用很灵活,跟mutex很像,但是又保证了其安全性。

  

并且它支持移动赋值,和交换,owns_lock可以返回当前对象是否上了锁。 

简单示例

std::mutex mtx; // 全局互斥量

void print_block(int n, char c) {  
    std::unique_lock<std::mutex> lock(mtx); // 构造时锁定互斥量  
    lock.unlock(); // 手动解锁  
    // ... 在这里可以执行一些不需要互斥量保护的代码 ...  
    std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟耗时操作  
} 

面试题:支持两个线程交替打印,一个打印奇数,一个打印偶数

  因为是多线程的,就算我们上锁了,也不能保证是打印奇数的线程先启动,还是打印偶数的线程先启动。假设我们要求先打印奇数(假设是t1线程),但是即便是把调用t1线程的代码写在前面,t2线程写在后面,t2线程也依旧先启动该如何呢?

  首先要加锁是肯定的,因为我们要对变量进行++(假设是int x), 但是如果要保证交替打印,还需要线程同步了,在Linux的线程部分我们学过,这次再使用一下C++封装的一个类

std::condition_variable 是 C++ 标准库中的一个类,用于在多个线程之间同步。它通常与 std::mutex 和 std::unique_lock 一起使用,以实现线程之间的条件等待和通知。

 比如让一个线程等待

当然还需要控制一个线程唤醒其他线程,

第一个是唤醒在等待队列队尾的一个线程,第二个是唤醒等待队列中的所有线程。

注意:如果一个线程进入等待了,那么它会立即释放它申请到的锁;如果一个线程被唤醒了,那么它会立刻申请锁,这个操作是原子的。

所以我们可以通过加锁和控制线程同步来完成交替打印,代码:

#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <random>

using namespace std;

int main()
{
	mutex mut;
	int x = 1;
	int n = 500;
	condition_variable cv;
	srand(time(nullptr));


	thread t1([&]() {
		for (int i = 0; i < n; ++i)
		{
			unique_lock<mutex> lock(mut);
			while (x % 2 == 0) // 使用while是万分保险的,打印奇数
				cv.wait(lock);
			cout << "thread1 : " << x << endl;
			++x;
			cv.notify_one();
		}
		});

	thread t2([&]() {
		for (int i = 0; i < n; ++i)
		{
			unique_lock<mutex> lock(mut);
			while (x % 2 == 1) // 使用while是万分保险的,打印偶数
				cv.wait(lock);
			cout << "thread2 : " << x << endl;
			++x;
			cv.notify_one();
		}
		});

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

	return 0;
}

自旋锁

   与阻塞锁不同,自旋锁在申请锁的时候,如果该锁如果已经被申请了,那么该线程不会阻塞,而是采用循环的方式不断申请锁,直到成功为止。

  自旋锁适用于并发度不是特别高,且临界区较为短小的场景。

在这种情况下,利用自旋锁可以避免线程切换的开销,从而提高效率。然而,如果临界区较大,或者持有锁的线程长时间不释放锁,那么等待获取锁的线程会不断自旋,导致CPU资源的浪费。

 阻塞锁在获取锁时,如果锁已经被占用,线程会进入阻塞状态,直到锁被释放并唤醒。而自旋锁则通过循环等待的方式,不断尝试获取锁。

其实mutex里面有一个try_lock(),它也是不阻塞的,如果成功了返回true,失败了返回false,可以这样模拟自旋锁

int main()
{
	int x;
	mutex mtx;

	thread t1([&]() {
		while (!mtx.try_lock())
            ;
		++x;
		mtx.lock();
		});

	t1.join();
	return 0;
}

这种方式很占用CPU。,因此可以进行改良

while (!mtx.try_lock())
			this_thread::yield();

这个this_thread::yield();也就是不要让它那么频繁的循环申请,可以适当的把时间片让一让。

总之,自旋锁与阻塞锁相比,自旋锁不会使线程进入阻塞状态等待锁释放。阻塞锁在获取锁时,如果锁已经被占用,线程会进入阻塞状态,直到锁被释放并唤醒。而自旋锁则通过循环等待的方式,不断尝试获取锁。

atomic

  C++11加入了atomic这个类,它可以让++这样的操作变成原子性的

 一般建议对内置类型用,如果是自定义类型,也不是不可以,但是如果这个类比较大(临界区大了),还不如加锁。因为它的原理类似自旋锁。

atomic类中还有很多的方法

除了常见的加减操作,还有一个load方法,用法如下

int main()
{
	atomic<int> x;
	mutex mtx;

	thread t1([&]() {
		++x;
		});

	t1.join();
	printf("%d\n", x.load());
	return 0;
}

 当我们要打印的时候,要加上load,不然编译会报错。

这种东西的底层原理其实是 CAS---无锁编程。

简单说下无锁编程 CAS

深入了解可以看陈浩大佬的博客

无锁队列的实现 | 酷 壳 - CoolShellicon-default.png?t=N7T8https://coolshell.cn/articles/8239.html 

引用 伪代码

int compare_and_swap (int* reg, int oldval, int newval)
{
  int old_reg_val = *reg;
  if (old_reg_val == oldval) {
     *reg = newval;
  }
  return old_reg_val;
}

 其中这个reg是一个指针,oldval代表的是当前我们真准备修改的值,newval是目标修改的值。那么无锁编程的原理就是,如果我们要对某个值进行++,如果加到一半该线程被切走了,然后别的线程加完了后切回来了,通过解引用指针得到的值与oldval进行对比,如果发现不相等,就说明其他线程已经修改了这个值,那么就不进行++了,而是修改了oldval了值,也就是oldval = *reg,然后再往后执行++。

总之CAS就是先比较,先比较跟旧值是否一样,如果一样再做加减的修改,如果不一样,先让旧值等于当前的值之后,再进行加减。

在C++11之后,给了接口

虽然我们可以直接用现成的atomic,但是我们也需要了解其原理

简单看看一个无锁队列

比如要往队列里面插入一个结点,多线程的方式

伪代码 

EnQueue(Q, data) //进队列
{
    //准备新加入的结点数据
    n = new node();
    n->value = data;
    n->next = NULL;

    do {
        p = Q->tail; //取链表尾指针的快照
    } while( CAS(p->next, NULL, n) != TRUE); 
    //while条件注释:如果没有把结点链在尾指针上,再试

    CAS(Q->tail, p, n); //置尾结点 tail = n;
}

 核心就是这里的do while循环,我们尾插结点,一般tail指针都指向NULL,p是尾结点,但是如果此时其他线程插入了一个结点,那么这里面的tail虽然还是NULL,但是 p->next已经不是NULL了,那么改变p的指向,使其重新指向链表的尾结点。

模拟atomic的++操作的原理(CAS)

int main()
{
		int n =20000;
		atomic<size_t> x = 0;
	
		thread t1([&]() {
			for (int i = 0; i < n; ++i)
			{
				size_t oldval, newval;
				do
				{
					oldval = x;
					newval = oldval + 1;
				} while (!atomic_compare_exchange_weak(&x, &oldval, newval));
			}
			});
	
		thread t2([&]() {
			for (int i = 0; i < n; ++i)
			{
				size_t oldval, newval;
				do
				{
					oldval = x;
					newval = oldval + 1;
				} while (!atomic_compare_exchange_weak(&x, &oldval, newval));
			}
			});
	
		t1.join();
		t2.join();
		cout << x << endl;
	return 0;
}

一些周边问题

关于shard_ptr 

  关于shard_ptr(只能指针),它本身是线程安全的,但是使用它的代码可能不是线程安全的。比如我们都知道shard_ptr里面有一个引用计数,每当shard_ptr进行了copy或者是指向了同一个资源的时候,这个引用计数就要++,但是这个++的操作不是原子的,我们需要额外的同步机制来保证线程安全,如果因为线程安全的问题导致引用计数不对,那么实际在析构的时候就会导致程序直接崩溃。

  另外操作shard_ptr所指向的资源的行为也不是线程安全的,也还是要通过加锁来保证线程安全。

关于懒汉模式

  作为单例模式的具体实现方式,饿汉模式和懒汉模式我们已经了解了。

其中饿汉模式是不会存在线程安全的问题的,因为程序已启动它就创建了。但是懒汉模式是存在线程安全的,当多个线程同时调用申请创建对象的时候就有可能创建多个对象。

一般问题都会出在这里

 当然这里加个锁就能轻松解决,并且可以通过双检查的方式既保证安全还保证效率

但是自从C++11之后(主要是编译器支持),线程安全的单例懒汉模式非常非常的简单如下

class A
{
public:
	static A& GetInstance()
	{
		static A a;
		return a;
	}
	int b = 1;
private:
	A()
	{
		cout << "  A  " << endl;
		b++;
	}
	// 防拷贝
	A(const A&) = delete;
	A& operator=(const A&) = delete;
};

int main()
{
	cout << A::GetInstance().b << endl;
	cout << A::GetInstance().b << endl;
	return 0;
}

乍一看可能觉得这是饿汉模式,但是它确实是懒汉模式。

注意!!局部的静态对象,是在第一次调用的时候初始化!

那么它是线程安全的吗?我们在构造函数里面对b进行了++。

在C++11之前,它不是线程安全的。但是在C++11之后,它是线程安全的。

C++11之后,它是可以保证局部静态对象的初始化是线程安全的,只初始化一次!

所以C++11之后,写懒汉模式就可以这样写,是最简单的写法,但是C++11之前就不可以。

可以理解为static A a;变成了原子操作。

结果

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

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

相关文章

golang 在多线程中避免 CPU 指令重排

发布日期&#xff1a;2024-03-26 16:29:39 起因 golang 的发明初衷便是多线程&#xff0c;是一门专门用于多线程高并发的编程语言。其独创的 GMP 模型在多线程的开发上提供了很大的便利。 现代计算机基本上都是多核 CPU 的结构。CPU 在进行指令运行的时候&#xff0c;为了提高…

开源AI引擎:文本自动分类在公安及消防执法办案自动化中的应用

一、实际案例介绍 通过文本分类算法自动化处理文本数据&#xff0c;快速识别案件性质和关键特征&#xff0c;极大地提高了案件管理和分派的效率。本文将探讨这两种技术如何帮助执法机构优化资源分配&#xff0c;确保案件得到及时而恰当的处理&#xff0c;并增强公共安全管理的…

“约瑟夫环”问题的四种方法及详解注释(c++或c语言实现)

Ⅰ.故事背景 据说著名犹太历史学家Josephus有过以下的故事&#xff1a;在罗马人占领乔塔帕特后&#xff0c;39 个犹太人与Josephus及他的朋友躲到一个洞中&#xff0c;39个犹太人决定宁愿死也不要被敌人抓到&#xff0c;于是决定了一个自杀方式&#xff0c;41个人排成一个圆圈&…

笔记本作为其他主机显示屏(HDMI采集器)

前言&#xff1a; 我打算打笔记本作为显示屏来用&#xff0c;连上工控机&#xff0c;这不是贼方便吗 操作&#xff1a; 一、必需品 HDMI采集器一个 可以去绿联买一个&#xff0c;便宜的就行&#xff0c;我的大概就长这样 win10下载 PotPlayer 软件 下载链接&#xff1a;h…

VTK 示例 基本的流程-事件交互、球体、

流程可以总结如下&#xff1a; 导入所需的头文件&#xff1a; 首先&#xff0c;导入了一系列 VTK 头文件&#xff0c;这些文件包含了所需的类和函数声明。 创建对象&#xff1a; 创建了两个球体&#xff08;一个较大&#xff0c;一个较小&#xff09;&#xff0c;一个平面&…

JS-16-标签函数

一、模版字符串 模版字符串&#xff0c;可以非常方便地引用变量&#xff0c;并合并出最终的字符串。 它允许你嵌入表达式&#xff0c;并通过${expression}语法来执行这些表达式。模板字符串使用反引号&#xff08;&#xff09;而不是普通的单引号或双引号。 模板字符串有几个…

【git】git使用手册

目录 一 初始化 1.1 账号配置 1.2 ssh生成 1.2.1 配置ssh 1.2.2 测试SSH 1.3 初始化本地仓库并关联远程仓库 二 使用 2.1 上传 2.2 拉取 三 问题 3.1 关联失败 一 初始化 git的安装很简单,下载后大部分进行下一步完成即可----->地址: git工具下载 1.1 账号配置…

HCIP —— 链路聚合

链路聚合 背景 随着网络规模不断扩大&#xff0c;用户对骨干链路的带宽和可常性提出越来越高的要求&#xff0c;在传统技术中&#xff0c;常用更换高速率的设备的方式来增加带宽&#xff0c;但这种方案需要付出高额的费用&#xff0c;而且不够灵活。 而采用链路聚合技术可以在…

记录关于智能家居的路程的一个bug___Segmentation fault(段错误)

前言 其实发生段错误的情况有很多&#xff1a; 其实在项目的开发中最有可能的错误就是①和②&#xff0c;考虑到本项目数组用的比较少&#xff0c;所以主要是考虑错误①指针的误用。 有时候错误就是那么离谱&#xff0c;声音也算是一种设备&#xff1f;&#xff1f;&#xff…

Dockerfile:自定义镜像

Dockerfile 是一个文本文件&#xff0c;其中包含了一系列用于自动化构建Docker镜像的指令。通过编写Dockerfile&#xff0c;开发者能够明确地定义一个软件应用及其运行环境应该如何被封装进一个可移植、可重复构建的Docker镜像中。 第一步&#xff1a;在/tmp文件下新建docker…

GEE:将分类特征和标签提取到样本点,并以(csv/shp格式)下载到本地

作者:CSDN @ _养乐多_ 本文将介绍在Google Earth Engine(GEE)平台上,下载用于机器学习分类或者回归的样本点数据,样本点数据携带了分类特征和标签信息,可以以csv格式或者SHP格式。 结果如下图所示, 文章目录 一、核心函数1.1 采样1.2 下载函数二、代码链接三、完整代码…

Machine Learning机器学习之K近邻算法(K-Nearest Neighbors,KNN)

目录 前言 背景介绍&#xff1a; 思想&#xff1a; 原理&#xff1a; KNN算法关键问题 一、构建KNN算法 总结&#xff1a; 博主介绍&#xff1a;✌专注于前后端、机器学习、人工智能应用领域开发的优质创作者、秉着互联网精神开源贡献精神&#xff0c;答疑解惑、坚持优质作品共…

Python入门练习 - 学生管理系统

Python 实现读书管理系统 """ 实现一个命令行版的读书管理系统 """ import os.path import sys# 使用这个全局变量&#xff0c;来管理所有的学生信息 # 这个列表的每个元素都是一个‘字典’&#xff0c;每 个 字典就分别表示了一个同学students …

电脑访问网页获取路由器WAN口内网IP

因为运维过程中容易出现路由器配置了固定IP但是没人知道后台密码&#xff0c;不确定这个办公室的IP地址&#xff0c;且使用tracert路由追踪也只会出现路由器的LAN口网关并不会出现WAN口IP。 今日正好遇到了个好方法&#xff0c;经过测试可以正常使用。 方法如下&#xff1a; 内…

机器视觉矿山安全生产风险预警系统

一、简介 十四五规划和2035年远景目标纲要针对企业安全生产提出了多项要求。其中&#xff0c;提高安全生产水平要求完善和贯彻执行安全生产责任制&#xff0c;建立公共安全隐患排查和安全预防控制体系&#xff0c;要求将安全生产提升至预防和控制阶段。 目前&#xff0c;矿山…

0DAY漏洞是什么,如何进行有效的防护

零日漏洞&#xff0c;指的是软件或系统中未被公开的、未被厂商知晓的安全漏洞。这些漏洞未被修复&#xff0c;因此黑客可以利用它们进行攻击&#xff0c;而受害者往往无法防范。由于这些漏洞的存在时间很短&#xff0c;因此称之为“零日漏洞”&#xff0c;也称为“0day漏洞”。…

LeetCode:1319. 连通网络的操作次数(并查集 Java)

目录 1319. 连通网络的操作次数 题目描述&#xff1a; 实现代码与解析&#xff1a; 并查集 原理思路&#xff1a; 1319. 连通网络的操作次数 题目描述&#xff1a; 用以太网线缆将 n 台计算机连接成一个网络&#xff0c;计算机的编号从 0 到 n-1。线缆用 connections 表示…

【Bug-ModuleNotFoundError: No module named ‘models‘】

&#x1f680; 作者 &#xff1a;“码上有前” &#x1f680; 文章简介 &#xff1a;Python &#x1f680; 欢迎小伙伴们 点赞&#x1f44d;、收藏⭐、留言&#x1f4ac; 出现这个错误&#xff1a; 出现了ModuleNotFoundError: No module named models’的问题。 文件在Model…

春秋云境CVE-2023-27179

简介 GDidees CMS v3.9.1及更低版本被发现存在本地文件泄露漏洞&#xff0c;漏洞通过位于 /_admin/imgdownload.php 的 filename 参数进行利用。 正文 进入靶场发现没有什么可以利用的地方&#xff0c;那么就按照靶场提示来&#xff0c;直接访问/_admin/imgdownload.php 打开…

SQLite数据库浏览器sqlite-web

什么是 sqlite-web &#xff1f; sqlite-web是一个用 Python 编写的基于 Web 的 SQLite 数据库浏览器。 软件特点&#xff1a; 可与您现有的 SQLite 数据库配合使用&#xff0c;也可用于创建新数据库。添加或删除&#xff1a; 表格列&#xff08;支持旧版本的 SQLite&#xff…
最新文章