【C++】C++11:线程库和包装器

news2024/11/25 3:24:01

C++11最后一篇文章

文章目录

  • 前言
  • 一、线程库
  • 二、包装器和绑定
  • 总结


前言

上一篇文章中我们详细讲解了lambda表达式的使用,我们今天所用的线程相关的知识会大量的用到lambda表达式,所以对lambda表达式还模糊不清的可以先将上一篇文章看明白。


一、线程库

C++11 之前,涉及到多线程问题,都是和平台相关的,比如 windows linux 下各有自己的接
口,这使得代码的可移植性比较差 C++11 中最重要的特性就是对线程进行支持了,使得 C++
并行编程时不需要依赖第三方库 ,而且在原子操作中还引入了原子类的概念。要使用标准库中的
线程,必须包含 < thread > 头文件。
thread():构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
thread(fn, args1, args2, ...):构造一个线程对象,并关联线程函数fnargs1args2...为线程函数的参数
get_id():获取线程id
joinable():线程是否还在执行,joinable代表的是一个正在执行中的线程。
join():该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
detach():在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关.
注意:
1. 线程是操作系统中的一个概念, 线程对象可以关联一个线程,用来控制线程以及获取线程的
状态
2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
下面我们实现用m个线程分别打印n个数:
int main()
{
	size_t m;
	cin >> m;
	vector<thread> vth(m);
	for (int i = 0; i < m; i++)
	{
		size_t n = 0;
		cin >> n;
		vth[i] = thread([n,i]()
			{
				for (int j = 0; j < n; j++)
				{
					cout << "我是第" << i << "个线程,打印:" << j + 1 << endl;
				}
				cout << endl;
			});
	}
	for (auto& t : vth)
	{
		t.join();
	}
	return 0;
}

上述代码最巧妙的一点就是将线程放到了vector中,每次用一个匿名的线程对象利用lambda函数将这个线程要执行的函数给向量中的线程,然后完成用m个线程打印不同的n个数,在结束前一定要将线程等待回收了。下面我们运行起来:

 可以看到成功完成了任务,在这个程序中我们用到了线程的构造函数和join函数,也体现了lambda函数的好用。

下面我们演示一下的get_id接口的使用:

 这样使用不知道大家会不会感到别扭呢,实际上在我们日常使用中是这样使用的:

 this_thread是一个命名空间,在这个命名空间中也有线程先关的函数,并且这里的函数不依靠线程对象来调用,哪个线程调用这个函数就打印哪个线程的ID。下面我们可以再用用this_thread这个命名空间中的其他接口:

 sleep_until是让这个线程休眠到某个时间,sleep_for是相对时间,这个接口是最常用的接口。下面我们演示一下sleep_for接口:

 在官方文档演示中我们可以看到有一个chrono的命名空间:

 这个命名空间中主要是一些时间函数的使用:

 可以看到确实打印完成后等待2秒再运行下一句指令,我们再试试微秒:

 这里就不再演示了,大家感兴趣的可以在自己的编译器上测试。yield接口是主动让出时间片,在无锁编程中会用到这个接口。

总结:

当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。
线程函数一般情况下可按照以下三种方式提供:
1.函数指针
2.lambda表达式
3.函数对象
4. thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个
线程对象关联线程的状态转移给其他线程对象,转移期间不意向线程的执行。
5. 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
采用无参构造函数构造的线程对象
线程对象的状态已经转移给其他线程对象
线程已经调用jion或者detach结束

下面我们讲解原子性操作:

int x = 0;

void func(int n)
{
	for (int i = 0; i < n; i++)
	{
		++x;
	}
}
int main()
{
	thread t1(func, 10000);
	thread t2(func, 20000);
	t1.join();
	t2.join();
	cout << x << endl;
	return 0;
}

首先上面的代码中有一个全局变量x,然后两个线程都调用func函数让x++,在我们看来x最后的结果应该是30000才对,那么结果是不是这样呢我们看一下:

 我们可以看到结果是不对的,这就是经典的线程安全问题。解决线程安全的方法有:CAS,加锁以及原子性操作,下面我们先用加锁演示一下:

#include <mutex>
int x = 0;
mutex mtx;
void func(int n)
{
	mtx.lock();
	for (int i = 0; i < n; i++)
	{
		++x;
	}
	mtx.unlock();
}
int main()
{
	thread t1(func, 10000);
	thread t2(func, 20000);
	t1.join();
	t2.join();
	cout << x << endl;
	return 0;
}

 首先我们的锁一定是一个全局的,因为要让两个线程看到同一把锁,如果我们将锁放在func函数内部,那么两个线程用的两把锁是没作用的。

 可以看到确实解决了刚刚的问题,当然我们也可以在进入循环的时候加锁:

void func(int n)
{
	for (int i = 0; i < n; i++)
	{
		mtx.lock();
		++x;
		mtx.unlock();
	}
}

 那么这两种加锁方式有什么区别呢?首先我们刚刚演示的第一种方法是串行的,也就是说当一个线程时间片到了后才会让下一个线程进入for循环,第二个方法是并行的,两个线程都可以进入func函数,进入for循环后谁先申请到锁谁就可以++x变量,下面我们可以看看哪一种方法的效率高:

#include <mutex>
int x = 0;
mutex mtx;
void func(int n)
{
	mtx.lock();
	for (int i = 0; i < n; i++)
	{
		++x;
	}
	mtx.unlock();
}
void func(int n)
{
	for (int i = 0; i < n; i++)
	{
		mtx.lock();
		++x;
		mtx.unlock();
	}
}
int main()
{
	int n = 100000;
	size_t begin = clock();
	thread t1(func, n);
	thread t2(func, n);
	t1.join();
	t2.join();
	size_t end = clock();
	cout << x << endl;
	cout << "--------------------------------" << endl;
	cout << end - begin << endl;
	return 0;
}
我们在每次运行前记录一下时间,结束后再记录一下时间,最后看哪种方式的时间少谁的效率就高:

串行的方式是2秒

并行的方式是30秒。为什么在这种情况下串行的效率更高呢?这是因为并行的方式每次解锁后都会频繁的切换线程的上下文,这会浪费很多的时间。

下面我们用lambda表达式改造一下刚刚的代码:

int main()
{
	int n = 100000;
	int x = 0;
	mutex mtx;
	size_t begin = clock();
	thread t1([&, n]()
		{
			mtx.lock();
			for (int i = 0; i < n; i++)
			{
				++x;
			}
			mtx.unlock();
		});
	thread t2([&, n]()
		{
			mtx.lock();
			for (int i = 0; i < n; i++)
			{
				++x;
			}
			mtx.unlock();
		});
	t1.join();
	t2.join();
	size_t end = clock();
	cout << end - begin << endl;
	return 0;
}

 我们从文档中可以看到,c++中的锁有多种:

 recursive_mutex是递归锁,下面我们演示一下:

int x = 0;
void func(int n)
{
	if (n == 0)
	{
		return;
	}
	++x;
	func(n - 1);
}
int main()
{
	thread t1(func, 1000);
	thread t2(func, 200);
	t1.join();
	t2.join();
	cout << x << endl;
	return 0;
}

 在这种递归的情况下官方更推荐使用递归锁,如下图:

int x = 0;
recursive_mutex mtx;
void func(int n)
{
	if (n == 0)
	{
		return;
	}
	mtx.lock();
	++x;
	mtx.unlock();
	func(n - 1);
}
int main()
{
	thread t1(func, 1000);
	thread t2(func, 200);
	t1.join();
	t2.join();
	cout << x << endl;
	return 0;
}

注意:我们的锁一定不可以加到if判断语句前面

 因为一旦在判断语句前面加锁那么一定会有线程无法进入递归结束条件从而造成死递归。同样我们也不能在递归后(func(n-1))后面解锁,这样就造成了加锁后还没解锁就进入递归下一层栈帧,下一次进入此函数又重新加锁,这样就造成了死锁问题。

下面我们再看看抛异常期间出现的经典死锁问题:

int x = 0;
mutex mtx;
void func(int n)
{
	for (int i = 0; i < n; i++)
	{
		try
		{
			mtx.lock();
			++x;
			//抛异常
			if (rand() % 3 == 0)
			{
				throw exception("抛异常");
			}
			mtx.unlock();
		}
		catch (const exception& e)
		{
			cout << e.what() << endl;
		}
	}
}
int main()
{
	thread t1(func, 10000);
	thread t2(func, 20000);
	t1.join();
	t2.join();
	return 0;
}

 当线程进入func函数后,我们手动设置了一个异常只要随机数%3等于0就直接抛异常,在这之前我们刚刚加了锁,抛异常进入catch后跳过了解锁步骤导致死锁问题,下面我们运行一下看是不是这样:

 可以看到确实是这样,那么如何解决这样的问题呢?

 在锁的文档中我们可以看到有这样两个接口,lock_guard接口就能解决我们的问题:

 lock_guard实际上就是一个创建了自动加锁,生命周期到了自动解锁的类对象,其实我们在讲linux线程的时候已经实现过了,感兴趣的可以看看我们linux多线程的文章。下面我们用一下这个借口:

template <class Lock>
class LockGuard
{
public:
	LockGuard(Lock& lk)
		:_lk(lk)
	{
		_lk.lock();
	}
	~LockGuard()
	{
		_lk.unlock();
	}
private:
	Lock _lk;
};

 

 我们可以看到这次就不会死循环的打印了,解锁后自动退出程序了。解锁的原理的也很简单,当我们这个lock对象出了try这个作用域后就自动销毁解锁了,所以就解决了我们上面那个抛异常造成死锁的问题。

下面我们再试试unique_lock接口,这个接口与lock_guard都是RAII类型,但是unique_lock接口的功能会更多,如下图:

 可以看到也可以解决刚刚抛异常出现的问题。

下面我们看看官网中atomic的接口:

多线程最主要的问题是共享数据带来的问题 ( 即线程安全 ) 。如果共享数据都是只读的,那么没问
题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数
据。但是, 当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦

 

atomic接口都是封装了CAS的接口,下面我们演示一下:

int main()
{
	atomic<int> x = 0;
	int n = 10000;
	thread t1([&,n]()
		{
			for (int i = 0; i < n; i++)
			{
				++x;
			}
		});
	thread t2([&, n]()
		{
			for (int i = 0; i < n; i++)
			{
				++x;
			}
		});
	t1.join();
	t2.join();
	return 0;
}

 那么原子性的值该如何打印呢?

 实际上对于原子的变量有一个load接口是专门用来打印的:

 对于其他原子的接口我们就不在一一演示了,下面我们讲一个经典面试题:两个线程,一个线程打印奇数一个线程打印偶数交替打印。

在解决这个问题的时候我们需要用到条件变量,条件变量看过linux线程那篇文章的应该很熟悉,与那里是一模一样的,下面我们直接展示c++中条件变量的接口:

条件变量有3个常用的接口,wait是让一个线程进入休眠,休眠期间会释放锁。one是唤醒一个线程,唤醒后会自动恢复之前休眠期间释放的锁,all是唤醒所有休眠的线程。

 通过文档我们可以看到条件变量是需要配合互斥锁使用的,因为条件变量本身并不是线程安全的,这里的条件变量的构造函数是用的unique_lock这个锁来初始化的,所以我们传参数的时候一定要看清楚。

 唤醒一个线程或者唤醒多个线程的接口是不需要参数的,直接使用接口就好,下面我们先写出交替打印奇偶数的框架:

int main()
{
	int n = 100;
	int x = 1;
	mutex mtx;
	thread t1([&, n]()
		{
			while (x < n)
			{
				cout << this_thread::get_id() << ": " << x << endl;
				++x;
			}
		});
	thread t2([&, n]()
		{
			while (x < n)
			{
				cout << this_thread::get_id() << ": " << x << endl;
				++x;
			}
		});
	t1.join();
	t2.join();
	return 0;
}

 通过结果我们可以看到是完不成我们给的交替打印的任务的,那么我们在加锁试一下:

 通过结果也可以看到是无法完成我们的任务的,下面我们就加上条件变量来试一下:

int main()
{
	int n = 100;
	int x = 1;
	mutex mtx;
	condition_variable cv;
	thread t1([&, n]()
		{
			while (x < n)
			{
				unique_lock<mutex> lock(mtx);
				if (x % 2 == 0)  //偶数就进去等待
				{
					cv.wait(lock);
				}
				cout << this_thread::get_id() << ": " << x << endl;
				++x;
				//唤醒等待的线程
				cv.notify_one();
			}
		});
	thread t2([&, n]()
		{
			while (x < n)
			{
				unique_lock<mutex> lock(mtx);
				if (x % 2 != 0)  //遇到奇数就等待
				{
					cv.wait(lock);
				}
				cout << this_thread::get_id() << ": " << x << endl;
				++x;
				//唤醒另一个休眠的线程
				cv.notify_one();
			}
		});
	t1.join();
	t2.join();
	return 0;
}

 通过结果我们可以看到已经完成了交替打印的任务,通过这个程序可以让我们学习到条件变量的使用,下面我们讲解一下这个程序:

首先我们通过unique_lock来加锁保证了t1先运行,t2阻塞。为什么能保证呢?1.t1先抢到锁,t2后抢到锁,t1先运行,t2阻塞在锁上面。2.t2先抢到锁,t1后抢到锁,t2先运行,t1阻塞在锁上面,但是t2会被下一步wait阻塞,并且wait的时候会解锁,保证了t1先运行。

我们用if判断可以防止一个线程不断运行,下面我们分析一下:

假设t1先获取到锁,t2后获取到锁,t2阻塞在锁上面。然后t1打印奇数,++x,x变成偶数,然后t1唤醒,但是没有线程wait,然后t1出了作用域解锁,然后t1的时间片到了切了出去。然后t2获取到锁打印,然后notify,但是没有线程等待lock出作用域解锁。假设t2的时间片充足再次获取到锁,如果没有条件控制,就会导致t2连续打印。

当然我们的程序还有一个小风险,当我们数++到100的时候很有可能进不去while判断条件,所以我们修改一下:

int main()
{
	int n = 100;
	int x = 1;
	mutex mtx;
	condition_variable cv;
	thread t1([&, n]()
		{
			while (1)
			{
				unique_lock<mutex> lock(mtx);
				if (x >= 100)
				{
					break;
				}
				if (x % 2 == 0)  //偶数就进去等待
				{
					cv.wait(lock);
				}
				cout << this_thread::get_id() << ": " << x << endl;
				++x;
				//唤醒等待的线程
				cv.notify_one();
			}
		});
	thread t2([&, n]()
		{
			while (1)
			{
				unique_lock<mutex> lock(mtx);
				if (x > 100)
				{
					break;
				}
				if (x % 2 != 0)  //遇到奇数就等待
				{
					cv.wait(lock);
				}
				cout << this_thread::get_id() << ": " << x << endl;
				++x;
				//唤醒另一个休眠的线程
				cv.notify_one();
			}
		});
	t1.join();
	t2.join();
	return 0;
}
以上就是线程相关的所有内容了。

二、包装器

function 包装器
function 包装器 也叫作适配器。 C++ 中的 function 本质是一个类模板,也是一个包装器。

 上图是包装器的声明,可以看到包装器的模板非常怪,下面我们就直接演示如何使用包装器了。

int f(int a, int b)
{
	return a + b;
}
struct Functor
{
public:
	int operator()(int a, int b)
	{
		return a + b;
	}
};
int main()
{
	int(*ptf)(int, int) = f;
	return 0;
}

首先像上面的代码我们用函数指针接收f函数会显得很麻烦,因为函数指针本身就很繁琐,下面我们看看包装器是如何接收的:

#include <functional>
int f(int a, int b)
{
	return a + b;
}
struct Functor
{
public:
	int operator()(int a, int b)
	{
		return a + b;
	}
};
int main()
{
	function<int(int, int)> f1 = f;
	function<int(int, int)> f2 = f;
	return 0;
}

 我们可以看到使用非常简单,甚至还可以使用lambda:

 包装器的使用非常简单,就像函数一样。而且我们还可以将包装器当成函数声明:

int main()
{
	function<int(int, int)> f1 = f;
	function<int(int, int)> f2 = Functor();
	function<int(int, int)> f3 = [](int a, int b) { return a + b; };
	cout << f1(10, 20) << endl;
	cout << f2(30, 40) << endl;
	cout << f3(50, 50) << endl;
	map<string, function<int(int, int)>> sop;
	sop["函数指针"] = f1;
	sop["仿函数"] = f2;
	sop["lambda"] = f3;
	return 0;
}

下面我们演示一下包装器包装成员函数:

class Plus
{
public:
	static int plusi(int a, int b)
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return a + b;
	}
};
int main()
{
	//静态成员函数
	function<int(int, int)> f1 = Plus::plusi;
	//普通成员函数
	function<double(Plus,double, double)> f2 = &Plus::plusd;
	return 0;
}

静态成员函数与刚刚的使用没有区别,主要是普通成员函数,因为普通成员函数参数多了一个this指针,并且语法是:域名前面必须加&

 当然这里不仅可以传类对象,也可以传指针。

那么包装器的最大作用是什么呢?那就是统一类型。

int f(int a, int b)
{
	return a + b;
}
class Plus
{
public:
	int plusi(int a, int b)
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return a + b;
	}
};
int main()
{
	Plus plus;
	function<int(int, int)> f2 = f;
	//正常对于成员函数需要多一个参数
	function<int(Plus,int, int)> f1 = &Plus::plusi;
	//和25行统一参数类型
	function<int(int, int)>f3 = [&](int a, int b) {return plus.plusi(a,b); };
	return 0;
}

可以看到本来成员函数需要多一个参数的,但是我们直接捕捉了plus对象然后调用这个对象中的函数就完成了类型统一(都是int(int,int))

template<class F, class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
double f(double i)
{
	return i / 2;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};
int main()
{
	// 函数名
	cout << useF(f, 11.11) << endl;
	// 函数对象
	cout << useF(Functor(), 11.11) << endl;
	// lamber表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
	return 0;
}

下面我们将上面的这段代码进行一下类型统一,让useF的类型都是相同的。

int main()
{
	cout << useF(function<double(double)>(f), 11.11) << endl;
	Functor func;
	cout << useF(function<double(double)>(func), 11.11) << endl;
	cout << useF(function<double(double)>([](double d) {return d / 4; }), 11.11);
	return 0;
}

可以看到我们useF使用起来参数都是相同这样在模板实例化的时候只会实例化一份而不是像上面那样一次实例化三个函数浪费多余的空间。

绑定

std::bind 函数定义在头文件中, 是一个函数模板,它就像一个函数包装器 ( 适配器 ) 接受一个可
调用对象( callable object ),生成一个新的可调用对象来 适应 原对象的参数列表 。一般而
言,我们用它可以把一个原本接收 N 个参数的函数 fn ,通过绑定一些参数,返回一个接收 M 个( M
可以大于 N ,但这么做没什么意义)参数的新函数。同时,使用 std::bind 函数还可以实现参数顺
序调整等操作。
下面我们看看绑定的原型:
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2) 
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
可以将 bind 函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对
象来 适应 原对象的参数列表。
调用 bind 的一般形式: auto newCallable = bind(callable,arg_list);
其中, newCallable 本身是一个可调用对象, arg_list 是一个逗号分隔的参数列表,对应给定的
callable 的参数。 当我们调用 newCallable 时, newCallable 会调用 callable, 并传给它 arg_list
的参数
arg_list 中的参数可能包含形如 _n 的名字,其中 n 是一个整数,这些参数是 占位符 ,表示
newCallable 的参数,它们占据了传递给 newCallable 的参数的 位置 。数值 n 表示生成的可调用对
象中参数的位置: _1 newCallable 的第一个参数, _2 为第二个参数,以此类推。
下面我们用代码演示一下:
int ssub(int a, int b)
{
	return a - b;
}
class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b;
	}
};

int main()
{
	function<int(int, int)> f = bind(ssub, placeholders::_1, placeholders::_2);
	cout << f(10, 20) << endl;
	function<int(int, int)> f1 = bind(ssub, placeholders::_2, placeholders::_1);
	cout << f1(10, 20) << endl;
	return 0;
}

 我们可以看到当我们将顺序改变后结果就不一样了,那么bind和placeholders是什么呢?

通过查询文档可以知道placeholders是一个命名空间:

 _1,_2这些代表一个个的占位对象,比如我们函数的第一个参数就应该放在_1中,当我们将占位对象的位置发生改变后函数参数的顺序也相应的发生改变。那么有什么作用呢?还记得我们sort这类排序需要仿函数控制,那么我们如果是小于的比较当我们将顺序互换后其实就变成相反的结果了:

int main()
{
	function<bool(int, int)> f2 = bind(less<int>(), placeholders::_1, placeholders::_2);
	cout << f2(1, 2) << endl;
	function<bool(int, int)> f3 = bind(less<int>(), placeholders::_2, placeholders::_1);
	cout << f3(1, 2) << endl;
	return 0;
}

 当然我们在大多数情况下都不会去使用绑定的,所以大家只需要认识一下就可以了。当然对于成员函数我们需要有一些改变,如果我们不想在包装器中传第一个this指针参数的话那么可以直接绑定一下:

int main()
{
	function<int(int, int)> f = bind(&Sub::sub,Sub(), placeholders::_1, placeholders::_2);
	cout << f(10, 20) << endl;
	function<int(int, int)> f1 = bind(&Sub::sub,Sub(), placeholders::_2, placeholders::_1);
	cout << f1(10, 20) << endl;
	return 0;
}

 以上就是包装器和绑定的所有内容了,这部分内容可以了解一下,当需要使用的时候查一下文档即可,因为日常会很少使用。


总结

本篇文章中最重要的部分就是线程库了,学过linux的都知道linux下的线程使用起来是比c++中麻烦的,而c++线程库中将很多东西进行了封装就像简单的锁我们不需要自己去释放,出了作用域这个锁就自动销毁了。

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

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

相关文章

域名解析详解

域名解析 记录类型&#xff1a; 提示&#xff1a; 将域名指向云服务器&#xff0c;选择 A&#xff1b; 将域名指向另一个域名&#xff0c;选择 CNAME&#xff1b; 建立邮箱选择 MX&#xff0c;根据邮箱服务商提供的 MX 记录填写。 记录类型解释A用来指定域名的 IPv4 地址&…

燃气管网监测设备:燃气管网压力在线监测

燃气作为一种重要的能源&#xff0c;广泛用于家庭、工业和商业领域。然而&#xff0c;燃气管网系统在运输和分配过程中可能面临压力波动、管道老化、外部破坏等问题&#xff0c;可能导致燃气泄漏和事故发生。燃气管网压力在线监测是保障燃气管网安全运营的重要手段之一。通过燃…

Linux系统之部署Homepage个人导航页

Linux系统之部署Homepage个人导航页 一、Homepage介绍1.1 Homepage简介1.2 Homepage主要特点 二、本地环境介绍2.1 本地环境规划2.2 本次实践介绍 三、检查本地环境3.1 检查本地操作系统版本3.2 检查系统内核版本3.3 检查系统是否安装Node.js 四、部署Node.js 环境4.1 下载Node…

感谢ChatGPT,救了我狗的命!

前一段时间&#xff0c;国外一位小哥哥在推特上发布了一条消息&#xff0c;声称GPT-4拯救了自家狗狗的性命。 这是怎么一回事呢&#xff1f; 这个小哥哥养了一只两岁的边境牧羊犬&#xff0c;这只牧羊犬被诊断出患有蜱传疾病&#xff0c;这属于一种细菌性传染病。 虽然小哥哥一…

30分钟吃掉DQN算法

表格型方法存储的状态数量有限&#xff0c;当面对围棋或机器人控制这类有数不清的状态的环境时&#xff0c;表格型方法在存储和查找效率上都受局限&#xff0c;DQN的提出解决了这一局限&#xff0c;使用神经网络来近似替代Q表格。 本质上DQN还是一个Q-learning算法&#xff0c;…

金九银十预备秋招: 大厂面试必考点及 Java 面试框架知识点整理

Java 面试 “金九银十”这个字眼对于程序员应该是再熟悉不过的了&#xff0c;每年的金九银十都会有很多程序员找工作、跳槽等一系列的安排。说实话&#xff0c;面试中 7 分靠能力&#xff0c;3 分靠技能&#xff1b;在刚开始的时候介绍项目都是技能中的重中之重&#xff0c;它…

龙膜公益“聚光行动”再起航 为云南山区小学援建绿色电脑教室

中国&#xff0c;上海——近日&#xff0c;全球汽车膜品牌龙膜的公益活动“为山区学校援建绿色电脑教室”在云南泸西县再度起航。为当地的“阿盈里小学”和“歹鲁小学”添置了2间电脑教室&#xff0c;配备了82台再生电脑&#xff0c;为600多名学生提供了数字化设备的使用机会&a…

Android Hilt:强大的依赖注入框架,高级传参解个耦?

作者&#xff1a;J船长 一、Hilt 干依赖注入的 Hilt是干嘛的 Hilt&#xff0c;一个依赖注入框架&#xff0c;谷歌搞出来的&#xff0c;基于Dagger&#xff0c;但是更加简洁易用。 什么是依赖注入 &#xff08;Dependency Injection&#xff09; 依赖注入是一种设计模式。主…

高校劳动积分小程序/基于微信小程序的高校劳动积分系统

摘 要 随着信息技术互联网和小程序的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的微信小程序应运而生&#xff0c;各行各业相继进入信息管理…

系统架构设计师-系统工程与信息系统基础(1)

一、系统工程概念 【系统工程】是一种组织管理技术。 【系统工程】是为了更好的实现系统的目的&#xff0c;对系统的组成要素、组成结构、信息流、控制机构进行分析研究的科学方法。 【系统工程】从整体出发、从系统观念出发、以求【整体最优】 【系统工程】利用计算机作为…

开放式耳机会不会吵到别人?2023年开放式耳机科普!

在了解开放式耳机会不会吵到别人之前&#xff0c;我们先了解下开放式耳机的基本知识&#xff01; 开放式耳机是一种不入耳&#xff0c;没有封闭耳朵的蓝牙耳机&#xff0c;可以听歌的同时接收来自外界声音&#xff0c;安全性高&#xff0c;也减少长期佩戴耳机带来的负担&#…

2023火爆的11门编程语言

2023火爆的11门编程语言 对于我个人来说没有语言偏好&#xff0c;根据不同的应用领域和需求&#xff0c;不同的编程语言都有其独特的优势和适用性。无论使用何种语言只要能更好的实现需求&#xff0c;解决痛点问题&#xff0c;就是好语言。 那么各种语言应用的场景解决了哪些…

项目管理专业人员能力评价CSPM与项目管理PMP对应关系

2021年10月&#xff0c;中共中央、国务院发布的《国家标准化发展纲要》明确提出构建多层次从业人员培养培训体系&#xff0c;开展专业人才培养培训和国家质量基础设施综合教育。建立健全人才的职业能力评价和激励机制。由中国标准化协会&#xff08;CAS&#xff09;组织开展的项…

Solon 成为信通院可信开源社区、可信开源项目

自2021年9月17日成立以来&#xff0c;可信开源社区共同体共有五批新成员加入。在4月21日“OSCAR开源生态建设论坛”上&#xff0c;可信开源社区共同体又迎来2位正式成员和6位预备成员&#xff0c;Solon 为其一&#xff08;图之右下角&#xff09;&#xff1a; 图片引用自CAICT可…

开放式耳机和封闭式耳机的区别?开放式耳机到底有哪些优缺点?

开放式耳机从字面意思可以理解为&#xff1a;开放耳朵&#xff0c;不需要入耳就可以听见声音的耳机&#xff0c;所以它和封闭式耳机的最大区别就是不入耳。这种耳机最大的优点就是不压迫不封闭耳道&#xff0c;而且在听耳机音的同时能够及时注意到周围环境的声音&#xff0c;从…

轻量级日志系统Loki——安装配置

关注“云计算就该这么学”微信公众号&#xff0c;搜索“001”&#xff0c;即可获取氪肝整理的154页Linux学习笔记。 Loki对标EFK/ELK&#xff0c;由于其轻量的设计&#xff0c;备受欢迎&#xff0c;Loki相比EFK/ELK&#xff0c;它不对原始日志进行索引&#xff0c;只对日志的标…

《主责数据保护与流动安全监管框架》重磅发布,美创以“产业研究力量”深入参与

历时四年&#xff0c;聚合行业安全专家智慧&#xff0c;凝炼行业安全最佳实践&#xff0c;数字时代&#xff1a;基于行业最佳实践的《主责数据保护与流动安全监管框架》&#xff08;以下简称“框架”&#xff09;于2023年6月17日第三届数字安全大会上正式发布。 该框架是在中国…

基于SpringBoot+Html的前后端分离的学习平台

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 在知识大爆炸的现代,怎…

一.Elasticsearch快速入门及使用

Elasticsearch快速入门及使用 一.Elasticsearch是什么二.基本概念1.index (索引)2. type (类型)3.Document (文档) 三.为什么Elasticsearch可以从海量数据里快速检索出数据四.Elasticsearch安装1.解压2.运行3.显示以下内容就是启动成功14.Kibana可视化软件安装 五.入门(基本的操…

代码浅析Point-LIO

0. 简介 对于最近出来的Point-LIO(鲁棒高带宽激光惯性里程计)&#xff0c;本人还是非常该兴趣的&#xff0c;为此花了一些时间重点分析了Point-LIO的代码&#xff0c;并研究了它相较于Fast-LIO2的区别 1. laserMapping.cpp 第一部分就是实现对激光雷达视场角的图像分割。首先…