C++11—线程库

news2024/11/28 4:46:38

C++线程库

文章目录

  • C++线程库
      • 线程的创建
      • thread提供的成员函数
        • get_id
        • this_thread命名空间
        • 线程的回收策略
          • join
        • detach
      • 线程函数参数
      • mutex的种类
      • lock_guard
      • unique_lock
      • 原子性操作库(atomic)
      • cas操作
      • windows和Linux创建进程的区别

线程的创建

image-20230912193316140

  1. 调用无参的构造函数
thread() noexcept;
#include<iostream>
#include<thread>
using namespace std;
int main()
{
	thread t1;
	return 0;
}
  • thread提供无参构造,创建出来的线程对象没有关联任何的线程函数,即没有启动任何线程。而在Linux中创建线程须调用pthread_create函数,并需要传参 线程执行的函数该函数需要的参数等参数,即程序运行时会启动线程。
  1. 调用带参的构造函数
template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);
  • fn是可调用对象,可以是函数指针、lambda表达式、函数对象。

  • Args是线程调用对象所需要若干个参数。

  • 注意该带参的构造函数用explicit关键字修饰,即该带参的构造函数不支持隐式类型转换。

#include<iostream>
#include<thread>
using namespace std;
void Add(int left, int right)
{
	cout << left + right << endl;
}

int main()
{
	int x = 10;
	int y = 20;
	thread t1(Add,x,y);

	t1.join();
	return 0;
}
  • 把Add函数和整型x、y变量传参給t1线程,当程序运行起来,t1线程会调用Add函数,并把x,y作为参数按顺序传递給left,right。
  1. 线程禁用拷贝函数
  • 禁用拷贝函数的作用在于禁止拷贝构造和拷贝赋值,防止多个线程对同一个线程对象进行复制操作,从而避免潜在的并发、数据不一致问题。其作用主要表现为以下:

    1. 防止不同线程共享相同的线程对象:如果允许线程对象被复制,那么不同的线程可能会共享相同的线程状态,导致无法确定线程的行为。这可能导致竞态条件和数据竞争等并发问题。、

    2. 强制显式管理线程对象的生命周期:禁用拷贝函数迫使程序员显式地管理线程对象的生命周期。这可以确保线程的创建、销毁和使用都是明确的,减少了出现资源泄漏或线程泄漏的可能性。

    3. 提高线程安全性:禁用拷贝函数有助于提高线程安全性,因为不同线程之间不会意外地共享线程对象的状态。这有助于减少并发编程中的错误和难以调试的问题。

  1. 允许使用移动构造函数来构造线程对象
thread (thread&& x) noexcept;

通过使用 std::move 将线程对象从一个地方移到另一个地方,可以避免拷贝线程对象,同时确保线程对象的生命周期正确管理。

#include<iostream>
#include<thread>
using namespace std;

void Add(int left, int right)
{
	cout << left + right << endl;
}

int main()
{
	int x = 10;
	int y = 20;
	thread t1=thread(Add,x,y);
	t1.join();
	return 0;
}
  • 使用带参构造创建一个匿名的线程对象,然后通过移动赋值构造t1对象,匿名对象所管理的资源移动給t1对象管理,且延长了线程对象的生命周期。
#include<iostream>
#include<thread>
using namespace std;

void Add(int left, int right)
{
	cout <<"left+right: " << left + right << endl;
}
class ADD
{
public:
	int operator()()
	{
		int x = 20, y = 10;
		return x + y;
	}
};
int main()
{
	int x = 10;
	int y = 20;
	thread t1(Add,x,y);//可调用对象为函数指针
	thread t2([x, y] {cout <<"x+y: " << x + y << endl; });//可调用对象为lambda表达式
	ADD dd;
	thread t3(dd);//可调用对象为函数对象
	t1.join();
	t2.join();
	t3.join();
	return 0;
}
  • ADD类只含有一个仿函数operator(),说明该类可以像函数一样使用,在main函数中创建了一个ADD对象dd,把函数对象dd作为可调用对象传递給线程t3

thread提供的成员函数

thread中常用的成员函数如下:

成员函数功能
join对该线程进行等待,在等待的线程返回之前,调用join函数的线程将会被阻塞
joinable判断该线程是否已经执行完毕,如果是则返回true,否则返回false
detach将该线程与创建线程进行分离,被分离后的线程不再需要创建线程调用join函数对其进行等待
get_id获取该线程的id
swap将两个线程对象关联线程的状态进行交换

另外,joinable函数可以判断线程是否有效的,是以下任意情况则线程无效:

  1. 采用无参构造函数构造的线程对象。
  2. 线程对象的状态已经转移给其他线程对象。
  3. 线程已经调用join或者detach结束。

get_id

  • 通过线程对象来调用thread的成员函数get_id来获取线程id。
  • 如果是在线程对象所关联的线程函数中,可以通过调用this_thread命名空间的get_id函数来获取线程id。
void func()
{
	cout <<"this_thread::get_id: " << this_thread::get_id() << endl;
}
int main()
{
	thread t1(func);
	cout << "t1 id: "<<t1.get_id() << endl;

	t1.join();
	return 0;
}

this_thread命名空间

为了能在线程函数中更好的调用线程的成员函数,库中将几个常用的成员函数包装在this_thread命名空间中。可以在线程所关联的线程函数中通过this_thread::xxx调用。

this_thread命名空间内的成员函数有:

get_id获取该线程的id
yield让出当前线程的执行权,允许系统将执行时间片分配给其他可运行的线程
sleep_until让当前线程休眠到一个具体时间点
sleep_for让当前线程休眠一个时间段

说明一下:

yield函数作用包括:

  1. 放弃当前线程的执行权yield() 函数会主动放弃当前线程的执行权,告诉操作系统该线程愿意将剩余时间片让给其他线程。这样做可以提高系统的多线程并发性能,避免某个线程长时间占用 CPU 资源而导致其他线程被延迟调度的问题。
  2. 控制线程的执行优先级。通过使用 yield() 函数,您可以以某种方式控制线程的执行优先级。当一个线程通过 yield() 放弃执行权时,操作系统可以按照一定的调度算法重新选择下一个要运行的线程。
  3. yield函数只是一个对线程调度的建议,操作系统不一定会按照您的要求进行调度。具体的行为可能受到操作系统和硬件平台的影响。即并不能确保下一个获得执行权的线程一定是其他线程,也有可能是之前的同一个线程继续执行。
  4. 在某些情况下,yield() 可能会导致线程的性能下降,因为线程需要频繁放弃和重新获得执行权。
  5. yield()函数通常用于无锁操作,当当前线程需要切换到另一个线程时,可以调用yield令当前线程放弃时间片,切换上下文,让步給另一个线程占用CPU。

yield让当前线程让步CPU时间片,让其他线程占用CPU,通常用于无锁操作。

总结一下:

  1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
  2. 如果创建线程对象时没有提供线程函数,那么该线程对象实际没有对应任何线程。
  3. 如果创建线程对象时提供了线程函数,那么就会启动一个线程来执行这个线程函数,该线程与主线程一起运行。
  4. thread类是防拷贝的,不允许拷贝构造和拷贝赋值,但是可以移动构造和移动赋值,可以将一个线程对象关联线程的状态转移给其他线程对象,并且转移期间不影响线程的执行。

线程的回收策略

启动一个线程后,该线程会占用一些资源,当这个线程退出时,需要将线程所占用的资源进行回收,否则会导致内存泄漏问题。因此thread库就为我们两种回收线程资源的策略。

join

主线程创建新线程后,需要调用join函数回收新线程,主线程会阻塞在join函数处,直到新线程退出,主线程回收新线程资源成功。

  • 因此主线程调用join函数回收完新线程相关资源后,该新线程对象和刚被销毁的栈帧就没有关系了,因此对同一个线程只能join一次,否则程序会崩溃。
void func()
{
	cout << "this_thread::get_id: " << this_thread::get_id() << endl;
}

int main()
{
	thread t1(func);
	cout << "t1 id: " << t1.get_id() << endl;

	t1.join();
	t1.join();
	return 0;
}
  • 但如果一个线程对象join后,又调用移动赋值函数,将一个右值线程对象的关联线程的状态转移过来了,那么这个线程对象又可以调用一次join
void func()
{
	cout << "this_thread::get_id: " << this_thread::get_id() << endl;
}

int main()
{
	thread t1(func);
	cout << "t1 id: " << t1.get_id() << endl;

	t1.join();
	t1 = thread(func);
	t1.join();
	return 0;
}
  • 当采用join方法回收线程资源时,可能会出现在join之前因为抛异常或其他原因退出当前栈帧而导致join失败。
void func()
{
	cout << "this_thread::get_id: " << this_thread::get_id() << endl;
}

int main()
{
	thread t1(func);
	cout << "t1 id: " << t1.get_id() << endl;

	if (3 != 0)
	{
		return -1;//退出当前栈帧
	}

	t1.join();
	return 0;
}

因此可以采用RAII的方式包装线程对象,利用对象的生命周期控制线程的资源释放,即当退出当前栈帧时自动调用join方法释放线程资源。

class mythread
{
public:
	mythread(thread& t):_t(t)
	{
		cout << "thread id: " << _t.get_id() << endl;
	}

	~mythread()
	{
		cout << "thread id: " << _t.get_id() << " join success" << endl;
		_t.join();
	}

	mythread(mythread const&) = delete;
	mythread& operator=(const mythread&) = delete;
private:
	thread& _t;
};
void func()
{
	cout << "this_thread::get_id: " << this_thread::get_id() << endl;
}
int main()
{
	thread t1(func);
	mythread tt(t1);
	return 0;
}

image-20230913160735032

detach

主线程创建新线程后,也可以调用detach函数将新线程与主线程进行分离,分离后新线程会在后台运行,其所有权和控制权将会交给C++运行库,此时C++运行库会保证当线程退出时,其相关资源能够被正确回收。

  • 使用detach的方式回收线程的资源,一般在线程对象创建好之后就立即调用detach函数。避免在调用detach之前因为某些原因退出当前栈帧导致线程分离失败,从而导致程序崩溃。

线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在 线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。

void ADD(int& num)
{
	num = num + 1;
}

int main()
{
	int num = 10;
	thread t1(ADD,num);
	t1.join();
	cout << "after num: " << num << endl;//10 not 11
	return 0;
}

如果要通过线程函数的形参改变外部的实参,可以参考以下三种方式:

  1. 当想要线程函数的形参引用的是外部传进来的实参时,借助std::ref函数保持对实参的引用特性。
void ADD(int& num)
{
	num = num + 1;
}

int main()
{
	int num = 10;
	thread t1(ADD,ref(num));
	t1.join();
	cout << "after num: " << num << endl;//11
	return 0;
}
  1. 传入实参的地址,线程函数可以通过该地址拿到实参从而进行修改,进而影响外部的实参。
void ADD(int* num)
{
	*num = *num + 1;
}

int main()
{
	int num = 10;
	thread t1(ADD,&num);
	t1.join();
	cout << "after num: " << num << endl;//11
	return 0;
}
  1. 借助lambda表达式,利用lambda表达式的捕捉列表捕捉外部实参的引用,在函数体内对该实参进行修改,进而影响外部的实参。
int main()
{
	int num = 10;
	thread t1([&num] {num = num + 1; });
	t1.join();
	cout << "after num: " << num << endl;//11
	return 0;
}

mutex的种类

在C++11中,Mutex总共包了四个互斥量的种类:

  1. std::mutex
    C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数:

    函数名函数功能
    lock()上锁:锁住互斥量,若该互斥量被其他线程所占有,阻塞等待该锁解锁
    unlock()解锁:释放对互斥量的所有权
    try_lock()尝试锁住互斥量,如果该互斥量已经被其他线程所占有,将直接返回

    注意一下,当线程函数调用lock()时:

    • 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前, 该线程一直拥有该锁。
    • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
    • 如果当前互斥量被当前线程锁住,然后当前现在又通过lock()去申请该互斥量,会导致死锁问题。

    注意一下,当线程函数调用try_lock()时:

    • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量
    • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
    • 如果当前互斥量被当前线程锁住,然后当前线程又通过try_lock去申请该互斥量,若申请互斥量失败会立刻返回,不会造成死锁问题。

死锁概念:死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。换言之,一个线程持有资源不释放,向申请不到的资源进行申请,而申请失败处于阻塞等待状态,那么持有的资源将无法释放或者让别的线程使用,该线程就处于死锁状态

  1. std::recursive_mutex
    其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权, 释放互斥量时需要调用与该锁层次深度相同次数的 unlock()。

    除此之外, std::recursive_mutex 的特性和 std::mutex 大致相同。

  2. std::timed_mutex

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

    • try_lock_for()
      接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
    • try_lock_until()
      接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住, 如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。

    除此之外,timed_mutex也提供了locktry_lockunlock成员函数,其的特性与mutex大致相同。

  3. std::recursive_timed_mutex
    recursive_timed_mutex就是recursive_mutex和timed_mutex的结合,recursive_timed_mutex既支持在递归函数中进行加锁操作,也支持定时尝试申请锁。

加锁示例

int gval = 0;
void func1(int val)
{
	for (int i = 0; i < val; i++)
	{
		gval++;
	}
}
int main()
{
	int m = 1000000;
	thread t1(func1,m);
	thread t2(func1,2*m);
	t1.join();
	t2.join();
	
	cout << "val: " << gval << endl;
	return 0;
}
  • 在这个示例中,全局变量gval线程t1和线程t2都能拿到,然后调用同一个函数func对gval进行加加操作。由于没有任何保护措施,会导致程序运行完并不是我们想要的样子。可能当线程t1对gval加加时,线程t2对gval也进行加加操作,那么两个线程对同一个数进行加加后拷贝回去,实际上只对gval加加了一次。
    image-20230913175505706

因此需要用互斥量保护临界区。

int gval = 0;
mutex mut;
void func1(int val)
{
	for (int i = 0; i < val; i++)
	{
		mut.lock();
		gval++;
		mut.unlock();//锁放里面相比于锁放外面会多一个是加锁解锁的消耗二个是切换上下文的消耗
	}	
}

int main()
{
	int m = 1000000;
	thread t1(func1,m);
	thread t2(func1,2*m);
	t1.join();
	t2.join();
	
	cout << "val: " << gval << endl;
	return 0;
}

image-20230913175447968

  • 当两个线程运行到mut.lock()这一行时,两个线程会竞争互斥锁,竞争到的线程能够进入临界区执行相关代码,而竞争失败的线程就阻塞等待。当该互斥锁被持有的线程释放时,该竞争失败的线程才能被唤醒,有机会再次竞争到互斥锁。每次只能有一个线程进入临界区。
  • 实际上将加锁解锁操作放在循环里面,相比于放在循环外面,所消耗的资源会更多。将加锁解锁放在循环里面,一是频繁的加锁解锁有消耗,二是抢不到锁的线程要阻塞休眠,切换上下文。这就会导致多个线程频繁的切换上下文,其二是切换上下文的消耗。
int gval = 0;
mutex mut;
void func1(int val)
{
    mut.lock();
	for (int i = 0; i < val; i++)
	{
		gval++;
	}	
    mut.unlock();
}

int main()
{
	int m = 1000000;
	thread t1(func1,m);
	thread t2(func1,2*m);
	t1.join();
	t2.join();
	
	cout << "val: " << gval << endl;
	return 0;
}

lock_guard

在使用互斥锁时,有可能存在加锁后,在解锁前因为线程对象的生命周期结束或其他原因导致退出当前栈帧,没有完成解锁。那么会导致其他线程在申请当前锁时阻塞住造成死锁问题,因此可以以RAII的方式包装互斥锁,需要用到锁的地方实例化一个lock_guard对象,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。

lock_guard是C++11中的一个模板类,其定义如下:

template <class Mutex>
class lock_guard;
class Lock_guard
{
public:
	Lock_guard(mutex& mut):_mut(mut)
	{
		_mut.lock();
	}
	~Lock_guard()
	{
		_mut.unlock();
	}
    Lock_guard(const Lock_guard&) = delete;
	Lock_guard& operator=(const Lock_guard&) = delete;
private:
	mutex& _mut;
};
int gval = 0;
mutex mut;
void func1(int val)
{	
	{
		Lock_guard lg(mut);
		for (int i = 0; i < val; i++)
		{
			gval++;
		}
	}
}
int main()
{
	int m = 1000000;
	thread t1(func1,m);
	thread t2(func1,2*m);
	t1.join();
	t2.join();
	cout << "val: " << gval << endl;
	return 0;
}
  • lock_guard需要包含一个锁成员变量,该成员变量需要是一个引用类型,在构造函数传参进来一个互斥量mut,用该互斥量来初始化锁成员变量,该成员变量就是lock_guard对象需要维护的互斥锁。
  • 需要用到锁的地方实例化一个lock_guard对象,调用构造函数成功上锁。
  • 出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁。
  • 可以定义一个匿名局部域来控制lock_guard的生命周期,在该局部内的代码都会被lock_guard所保护。
  • 需要删除lock_guard的拷贝构造和拷贝赋值,因为lock_guard内的锁成员变量不支持拷贝。
  • C++库中也有lock_guard,其用法与上面实现的大同小异
  • lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock

unique_lock

  • 与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所 有权的方式管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。
  • 在构造(或移动 (move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。
  • 使用以上类型互斥量实例化 unique_lock的对象时,自动调用构造函数上锁,unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题。

与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:

  1. 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock。
  2. 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)。
  3. 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。

unique_lock其用法和lock_guard相似

mutex mut;
void func()
{
unique_lock<mutex> ul(mut);//调用构造函数加锁
//......
ul.lock();
func1();
//......
ul.unclock();


}//调用析构函数解锁

原子性操作库(atomic)

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

int gval = 0;
void func1(int val)
{
		for (int i = 0; i < val; i++)
		{
			gval++;
		}
}
int main()
{
	int m = 1000000;
	thread t1(func1, m);
	thread t2(func1, 2 * m);
	t1.join();
	t2.join();

	cout << "val: " << gval << endl;
	return 0;
}
  • 上面的代码打印出来的结果会出现少于3000000的情况,其根本原因在于对gval的加加操作不是原子性的。

加加操作分为三步:

  • load:将共享变量gval从内存加载到寄存器中。
  • update:更新寄存器里面的值,执行+1操作。
  • store:将新值从寄存器写回共享变量n的内存地址。
0039310F  mov         eax,dword ptr [gval (03A03D0h)]  
00393114  add         eax,1  
00393117  mov         dword ptr [gval (03A03D0h)],eax
  • 在程序运行完时,导致gval的值不是3000000的原因有很多。在单CPU的情况下,线程t1去到内存中拿到gval的数据,放到CPU的寄存器上,刚完成加加操作的第一步,这时候时间片的时间到了。需要将线程t1切换,OS切换t1的上下文。然后t2可能顺利的完成了加加操作,将加加后的值写回到内存中。这时候切回到线程t1,用之前在内存拿到的值继续完成剩下的加加操作。因此两个线程对同一个变量gval各自加加了一次,实际上gval只被加加了一次。
  • C++98对于这里出现的线程安全问题,会对共享数据使用加锁保护。

虽然加锁可以解决,但是加锁有一个缺陷就是:只要一个线程在对sum++时,其他线程就会被阻塞,会影响程序运行的效率,而且锁如果控制不好,还容易造成死锁。

因此C++11中引入了原子操作。所谓原子操作:即不可被中断的一个或一系列操作,C++11引入 的原子操作类型,使得线程间数据的同步变得非常高效。以下为原子操作类型的名称及其对应的内置类型名称

image-20230913212857136

注意:需要使用以上原子操作变量时,必须添加头文件

#include<atomic>

可以通过atomic类模板,定义出对应内置类型的原子类型

atomic<T> t;
//atomic_int gval=0;
atomic<int>gval=0;
void func1(int val)
{
		for (int i = 0; i < val; i++)
		{
			gval++;
		}
}

int main()
{
	int m = 1000000;
	thread t1(func1, m);
	thread t2(func1, 2 * m);
	t1.join();
	t2.join();

	cout << "val: " << gval << endl;
	return 0;
}
  • 在C++11中,不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问。
  • 原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝。因此在C++11 中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及 operator=等。
  • 为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算 符重载默认删除掉了。

cas操作

CAS:Compare and Swap,即比较再交换。

例如多个线程对同一个数据进行加加操作,线程将数据放到寄存器中,寄存器保存一份原来的数据,然后放数据到cpu上进行加加,通过比对内存中的数据,当保存的原来的数据与此时内存中的数据相同时,就将加加后的数据放回内存;当保存的原来的数据与此时的内存中的数据不相同时,意味着其他线程已经将改变后的数据放回内存了,那么当前线程就不能将数据放回去了。

cas的缺点:CPU开销大。在并发量比较高的情况下,如果许多线程反复尝试更新某一个值,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

windows和Linux创建进程的区别

在上层调用线程库,通过条件编译区分调用windows线程库还是linux线程库。

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

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

相关文章

视频云存储/安防监控/AI分析/视频AI智能分析网关行为分析算法应用场景汇总

随着城市经济的日益发展&#xff0c;城市建设也如火如荼。很多施工人员为了自身方便&#xff0c;无视安全规章制度&#xff0c;不按照安全施工要求进行着装、施工。具有很大的安全隐患&#xff0c;一旦发生事故&#xff0c;就会造成人员损伤和工程进度的挤压。为了改善此问题&a…

QT实战之翻金币游戏【详细过程及介绍】

目录 前言 一、游戏整体分析 二、创建项目 三、添加资源 四、主界面实现 1、设置游戏主场景基本配置 2、设置背景图片 3、创建开始按钮并设置动画 4、创建关卡选择界面并实现主界面与其的切换 五、关卡选择界面实现 1、设置关卡选择场景基本配置 2、设置关卡选择场…

2-2 自动微分机制

神经网络是如何更新网络参数的呢&#xff1f; 为什么需要自动微分机制&#xff1f; 以及什么是自动微分机制&#xff1f; 神经网络通常依赖反向传播求梯度来更新网络参数&#xff0c;求梯度过程通常是一件非常复杂而容易出错的事情。而深度学习框架可以帮助我们自动地完成这种求…

PostgreSQL数据库IPC——SI Message Queue

SI Message Queue代码位于src/backend/storage/ipc/sinvaladt.c和src/backend/storage/ipc/sinval.c文件中&#xff0c;属于PostgreSQL数据库IPC进程间通信的一种方式【之前介绍过PostgreSQL数据库PMsignal——后端进程\Postmaster信号通信也是作为PostgreSQL数据库IPC进程间通…

Linux下Minio分布式存储安装配置(图文详细)

文章目录 Linux下Minio分布式存储安装配置(图文详细)1 资源准备1.1 创建存储目录1.2 获取Minio Server资源1.3 获取Minio Client资源 2 Minio Server安装配置2.1 切换目录2.2 后台启动2.3 查看进程2.4 控制台测试 3 Minio Client安装配置3.1 切换目录3.2 移动mc脚本3.2 运行mc命…

记录一次LiteFlow项目实战

文章目录 学习LiteFlowspring boot整合LiteFlow依赖配置组件定义spring boot配置文件规则文件的定义 执行 组件EL规则串行并行 动态构建组件动态构建chain&#xff08;流程&#xff09;销毁chain高级特性 题外话&#xff1a; 最近喜欢上骑摩托车了&#xff0c;不是多大排量的摩…

C++ if...else 语句

一个 if 语句 后可跟一个可选的 else 语句&#xff0c;else 语句在布尔表达式为假时执行。 语法 C 中 if…else 语句的语法&#xff1a; if(boolean_expression) {// 如果布尔表达式为真将执行的语句 } else {// 如果布尔表达式为假将执行的语句 }如果布尔表达式为 true&…

联想拯救者Lenovo Legion Y7000 2020H(81Y7)原厂Win10系统

链接&#xff1a;https://pan.baidu.com/s/15GwrfjvhK3-_OlhkfPyUbA?pwd4v03 系统自带所有驱动、出厂主题壁纸LOGO、Office办公软件、联想电脑管家等预装程序 所需要工具&#xff1a;16G或以上的U盘 文件格式&#xff1a;ISO 文件大小&#xff1a;10.7GB 注&#xff1a;恢复…

用vb语言编写一个抄底的源代码程序实例

以下是一个基于通达信软件编写的简单抄底源代码程序&#xff0c;用于自动识别股票的底部形态并发出买入信号&#xff1a; vbs 复制 导入通达信软件自带的股票数据接口 Dim TdxApi Set TdxApi CreateObject("TdxApi.TdxLocalAPI") 设置股票代码、周期、数据类型等参…

算法通关村第十九关:白银挑战-动态规划高频问题

白银挑战-动态规划高频问题 1. 最少硬币数 LeetCode 322 https://leetcode.cn/problems/coin-change/description/ 思路分析 尝试用回溯来实现 假如coins[2,5,7]&#xff0c;amount27&#xff0c;求解过程中&#xff0c;每个位置都可以从[2,5,7]中选择&#xff0c;因此可以…

深入理解JVM虚拟机第六篇:内存结构与类加载子系统概述

文章目录 一&#xff1a;内存结构概述 1&#xff1a;运行时数据区 2&#xff1a;运行时数据区简图 3&#xff1a;运行时数据区详细图中英文版 二&#xff1a;类加载器子系统 1&#xff1a;加载 2&#xff1a;连接 3&#xff1a;初始化 一&#xff1a;内存结构概述 1…

CAN基础概念

文章目录 目的控制器、收发器、总线帧格式CAN2.0和CAN-FD波特率与采样点工作模式总结 目的 CAN是非常常用的一种数据总线&#xff0c;被广泛用在各种车辆系统中。大多数时候CAN的控制器和收发器干了比较多的工作&#xff0c;从而对于写代码使用来说比较简单。这篇文章将对CAN使…

经历网数据库共享

经历网&#xff0c;为留住您的经历而生 点击 经历网 进入网站查看当前数据 经历网网址&#xff1a;https://www.jili20.com/ 以下 数据库 数据 截止至 2023年9月13日 1&#xff09;百度网盘 提取 链接&#xff1a;https://pan.baidu.com/s/1WwR4cI9lbSAYTuffo8qmVQ 或点击 此…

微信小程序的在线课外阅读打卡记录系统uniapp

本文从管理员、学生和教师的功能要求出发&#xff0c;中学课外阅读记录系统中的功能模块主要是实现学生、教师、阅读任务、阅读打卡、提醒信息、阅读排行、任务计划、阅读类型、在线考试等。经过认真细致的研究&#xff0c;精心准备和规划&#xff0c;最后测试成功&#xff0c;…

zemax畸变与消畸变

物体不同位置的放大率不同&#xff0c;产生图形变形 这里选择zemax自带的案例&#xff1a; 畸变效果&#xff1a; 明显的负畸变&#xff08;桶形畸变&#xff09; 从场曲畸变图中可以看出&#xff1a; 该系统的最大畸变大约为38% 放入图片观察成像效果&#xff1a; 优化操作数…

GpsAndMap模块开源,欢迎测评

背景 之前的文章有提到&#xff0c;最近在使用folium的过程中&#xff0c;深感对于一个非专业人员来说&#xff0c;GPS坐标以及其所隐含的GPS坐标系&#xff0c;以及不同GPS坐标系之间的相互转换关系&#xff0c;不是一个十分清晰的概念&#xff0c;往往造成在使用GPS坐标在fo…

基本的SELECT语句——“MySQL数据库”

各位CSDN的uu们好呀&#xff0c;好久没有更新小雅兰的MySQL数据库专栏啦&#xff0c;接下来一段时间&#xff0c;小雅兰都会更新MySQL数据库的知识&#xff0c;下面&#xff0c;让我们进入今天的主题吧——基本的SELECT语句&#xff01;&#xff01;&#xff01; SQL概述 SQL语…

Linux - 性能可观察性工具

文章目录 常用的Linux性能可观察性工具图解小结 常用的Linux性能可观察性工具 以下是一些常用的Linux性能可观察性工具&#xff1a; top: 显示实时的系统性能数据&#xff0c;包括CPU使用率、内存使用情况、进程信息等。 htop: 类似于top&#xff0c;但提供了更多的交互式功能…

谷粒商城----rabbitmq

一、 为什么要用 MQ? 三大好处&#xff0c;削峰&#xff0c;解耦&#xff0c;异步。 削峰 比如秒杀&#xff0c;或者高铁抢票&#xff0c;请求在某些时间点实在是太多了&#xff0c;服务器处理不过来&#xff0c;可以把请求放到 MQ 里面缓冲一下&#xff0c;把一秒内收到的…

Arcgis栅格转点时ERROR 999999: 执行函数时出错。 无法创建要素数据集。 执行(RasterToPoint)失败

Arcgis栅格转点时ERROR 999999: 执行函数时出错。 无法创建要素数据集。 执行(RasterToPoint)失败。 问题描述 原因 输出点要素的位置不对 解决方案 点击新建文件地理数据库 然后在该文件地理数据库下输出