简单了解C++线程库

news2024/10/1 17:16:06

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.coloradmin.cn/o/1549812.html

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

相关文章

Llama模型下载

最近llama模型下载的方式又又变了&#xff0c;所以今天简单更新一篇文章&#xff0c;关于下载的&#xff0c;首先上官网&#xff0c;不管在哪里下载你都要去官网登记一下信息&#xff1a;https://llama.meta.com/llama2 然后会出现下面的信息登记网页&#xff1a; 我这里因为待…

9.windows ubuntu 子系统,centrifuge:微生物物种分类。

上次我们用了karken2和bracken进行了物种分类&#xff0c;这次我们使用centrifuge. Centrifuge 是一种用于快速和准确进行微生物分类和物种鉴定的软件。其主要功能包括&#xff1a; 快速分类和物种鉴定: Centrifuge 可以对高通量测序数据&#xff08;如 metagenomic 或 RNA-Se…

Redis 教程系列之Redis 发布订阅(十五)

Redis 发布订阅 Redis 发布订阅(pub/sub)是一种消息通信模式&#xff1a;发送者(pub)发送消息&#xff0c;订阅者(sub)接收消息。 Redis 客户端可以订阅任意数量的频道。 下图展示了频道 channel1 &#xff0c; 以及订阅这个频道的三个客户端 —— client2 、 client5 和 cl…

Mac安装minio

Mac安装minio 本文介绍使用 mac 安装 MinIO。 所有软件安装优先参考官网&#xff1a;MinIO Object Storage for MacOS — MinIO Object Storage for MacOS #使用 brew 安装 minio brew install minio/stable/minio#找到 minio tong ~ $ brew list minio /opt/homebrew/Cella…

【深度学习|基础算法】2.AlexNet学习记录

AlexNet示例代码与解析 1、前言2、模型tips3、模型架构4、模型代码backbonetrainpredict 5、模型训练6、导出onnx模型 1、前言 AlexNet由Hinton和他的学生Alex Krizhevsky设计&#xff0c;模型名字来源于论文第一作者的姓名Alex。该模型以很大的优势获得了2012年ISLVRC竞赛的冠…

循环神经网络之序列模型

自回归模型 自回归模型&#xff1a; 只与x 有关 &#xff0c;对自己执行回归隐变量自回归&#xff1a;与X 和过去观测总结h 都有关 案例 %matplotlib inline import torch from torch import nn from d2l import torch as d2lT 1000 # 总共产生1000个点 time torch.aran…

【深度学习基础(4)】pytorch 里的log_softmax, nll_loss, cross_entropy的关系

一、常用的函数有&#xff1a; log_softmax,nll_loss, cross_entropy 1.log_softmax log_softmax就是log和softmax合并在一起执行&#xff0c;log_softmaxlogsoftmax 2. nll_loss nll_loss函数全称是negative log likelihood loss, 函数表达式为&#xff1a;f(x,class)−x[…

备考ICA----Istio实验11---为多个主机配置TLS Istio Ingress Gateway实验

备考ICA----Istio实验11—为多个主机配置TLS Istio Ingress Gateway实验 1. 部署应用 kubectl apply -f istio/samples/helloworld/helloworld.yaml -l servicehelloworld kubectl apply -f istio/samples/helloworld/helloworld.yaml -l versionv12. 证书准备 接上一个实验…

Day23:事务管理、显示评论、添加评论

事务管理 事务的定义 什么是事务 事务是由N步数据库操作序列组成的逻辑执行单元&#xff0c;这系列操作要么全执行&#xff0c;要么全放弃执行。 事务的特性(ACID) 原子性(Atomicity):事务是应用中不可再分的最小执行体&#xff08;事务中部分执行失败就会回滚 。一致性(C…

开源大数据集群部署(十八)Hive 安装部署

作者&#xff1a;櫰木 1 创建hive Kerberos主体 bash /root/bigdata/getkeytabs.sh /etc/security/keytab/hive.keytab hive2 安装 在hd1.dtstack.com主机root权限下操作&#xff1a; 解压包 [roothd3.dtstack.com software]# tar -zxvf apache-hive-3.1.2-bin.tar.gz -C …

树与二叉树的应用试题解析

01&#xff0e;在有n个叶结点的哈夫曼树中&#xff0c;非叶结点的总数是( A ). A. n-1 B. n C. 2n-1 D.2n 02.给定整数集合{3,5,6,9,12}&#xff0c;与之对应的哈夫曼树是( D…

设计模式——行为型——策略模式Strategy

Q&#xff1a;策略模式的特点 A&#xff1a; 具体算法从具体的业务方法中独立出来策略模式是同行为的不同实现 Q&#xff1a;什么时候使用策略模式 A&#xff1a;多个if-else使用策略模式 收费对象类 public class CashContext {private CashStrategy cashStrategy;public…

iphoneX系统的参数

1. 2. 3. 4. 5.相关的网址信息 Apple iPhone X 規格、价格和评论 | Kalvo Apple iPhone X 規格、价格和评论 | Kalvo

Apache Hive的基本使用语法

一、数据库操作 创建数据库 create database if not exists myhive;查看数据库 use myhive; desc database myhive;创建数据库并指定hdfs存储 create database myhive2 location /myhive2;删除空数据库&#xff08;如果有表会报错&#xff09; drop database myhive;…

【二叉树】Leetcode 102. 二叉树的层序遍历【中等】

二叉树的层序遍历 给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xff09; 示例1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;[[3],[9,20],[15,7]] 解题思路…

LabVIEW无人机大气数据智能测试系统

LabVIEW无人机大气数据智能测试系统 随着无人机技术的迅速发展&#xff0c;大气数据计算机作为重要的机载设备&#xff0c;在确保飞行安全性方面发挥着重要作用。设计了一套基于LabVIEW的无人机大气数据智能测试系统&#xff0c;通过高效、稳定的性能测试&#xff0c;及时发现…

Chakra UI:重塑React组件开发的未来

随着前端开发技术的不断演进&#xff0c;React已经成为了一个不可或缺的开源JavaScript库&#xff0c;用于构建用户界面。然而&#xff0c;虽然React提供了构建用户界面的强大工具&#xff0c;但在组件的可访问性、可重复使用性和可组合性方面仍存在挑战。Chakra UI正是一个为解…

数据处理库Pandas的数据结构Series

Series是一种一维数据结构&#xff0c;每个元素都带有一个索引&#xff0c;与一维数组的含义相似&#xff0c;其中索引可以为数字或字符串&#xff0c;如图3-1所示。 Series 对象包含两个主要的属性&#xff1a;index 和 values&#xff0c;分别为上例中的左右两列。因为传给构…

春秋云境CVE-2022-23880

简介 taoCMS v3.0.2 文件管理处存在任意文件上传漏洞&#xff0c;攻击者可执行任意代码 正文 首先进入靶场&#xff0c;我们发现在首页底部有个管理界面&#xff0c;那么就直接点进去进入后台。 找到后台 找到后台&#xff0c;尝试弱口令&#xff0c;发现不成功&#xff0c…

Zookeeper的系统架构

先看一张图&#xff1a; ZooKeeper 的架构图中我们需要了解和掌握的主要有&#xff1a; 1&#xff1a; ZooKeeper分为服务器端&#xff08;Server&#xff09; 和客户端&#xff08;Client&#xff09;&#xff0c;客户端可以连接到整个ZooKeeper服务的任意服务器上&#xff…