当前位置: 首页 > news >正文

从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)

目录

1. C++多线程

1.1 thread库

1.2 mutex库

1.3 RAII锁

1.4 atomic+CAS

1.5 condition_variable

1.6 分别打印奇数和偶数

2. shared_ptr线程安全

2.1 库里面的shared_ptr使用

2.2 shared_ptr加锁代码

3. 单例模式线程安全

3.1 懒汉模式线程安全问题

3.2 懒汉模式最终代码

3.3 懒汉模式的另一种写法

本篇完。


此篇建议学了Linux系统多线程部分再来看。

1. C++多线程

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。

C++11中最重要的特性就是支持了多线程编程,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。

1.1 thread库

查下文档:

如图所示,C++11提供了thread库,thread是一个类,在使用的时候需要包含头文件pthread。

构造函数:

  • 默认构造函数thread()使用该构造函数创建的线程对象仅是创建对象,线程并没有被创建,也没有允许。
  • thread(Fn&& fn, Args&&... args),这是一个万能引用模板。使用该构造函数时,第一个参数是可调用对象,可以是左值也可以是右值,比如函数指针,仿函数对象,lambda表达式等等。后面的可变参数就是传给线程函数的实参,是一个参数包,也就是可变参数。
  • thread(const thread&) = delete,线程之间是禁止拷贝的。
  • thread(thread&& x),移动构造函数。

成员函数:

  •  get_id,用来获取当前线程的tid值。调用该函数通常都是当前线程,但是当前的从线程从并没有自己的thread对象

所以线程库由提供了一个命名空间,该空间中有上图所示的几个函数,可以通过命名空间来直接调用,如:

this_thread::get_id(); // 获取当前线程tid值

哪个线程执行这条语句就返回哪个线程的tid值,命名空间中的其他几个函数的用法也是这样。

  • yield调用该接口的线程会让其CPU,让CPU调度其他线程。
  • sleep_until调用该接口的线程会延时至一个确定的时间点。
  • sleep_for调用该接口的线程会延时一个时间段,如1s。

  •  operator=(thread&& t),移动赋值。

将一个线程对象赋值给另一个线程对象,通常这么用:

	thread t1; // 仅创建对象,不创建线程t1 = thread(func); // t1线程函数并且执行

此时原本只创建的线程对象就有一个线程在跑了。

注意:只能赋右值,不能赋左值,因为赋值运算符重载被禁掉了,只有移动赋值


  •  join,线程等待,用来回收线程资源。一般主线程会调用该函数,以t.join()的形式,t就是需要被等待的线程对象,此时主线程会阻塞在这里,直到从线程运行结束。

如上面的多线程一样,必须使用join,否则线程资源不会回收,而且如果从线程运行的时间比主线程长的话,主线程会直接运行完并且回收所有资源,导致从线程被强制结束。


  •  joinable,用来判断线程是否有效。

如果是以下任意情况则线程无效:

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

  • detach,线程分离,从线程结束后自动回收资源。

其他的就不介绍了,用到的时候自行查文档即可。

要谨记:thread是禁止拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值


使用一下:

#include <iostream>
#include <thread>
using namespace std;void Print(int n, int& x)
{for (int i = 0; i < n; ++i){cout << this_thread::get_id() << ":" << i << endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));++x;}
}
int main()
{int count = 0;thread t1(Print, 10, ref(count));thread t2(Print, 10, ref(count));t1.join();t2.join();cout << count << endl;return 0;
}

多次运行的结果不一样,可能会出现像第一行一样的抢着打印的问题(学了Linux多线程应该比较清楚),下面就应该想到加锁了。


1.2 mutex库

如上图所示,C++11提供了mutex库,mutex同样是一个类,在使用的时候要包含头文件mutex。

构造函数:

  • 只有默认构造函数mutex(),在创建互斥锁的时候不需要传任何参数。
  • mutex(const mutex&)=delete,禁止拷贝。

其他成员函数:

  • lock(),给临界区加锁,加锁成功继续向下执行,失败则阻塞等待。
  • unlock(),给临界区解锁。
  • try_lock(),给临界区尝试加锁,加锁成功返回true,加锁失败返回false使用try_lock时,如果申请失败则不阻塞,跳过申请锁的部分,执行非临界区代码。

来看伪代码:

mutex mtx;if(mtx.try_lock())
{// 临界区代码// ......
}
else
{// 非临界区代码// ......
}

mutex不能递归使用,如下面伪代码所示:

    void Func(int n){lock(); // 加锁// 临界区代码// ......Func(n - 1); // 递归调用unlock(); // 解锁}

在递归中不能使用这样的锁,会造成死锁。正确使用下:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;void Print(int n, int& x, mutex& mtx)
{for (int i = 0; i < n; ++i){mtx.lock();cout << this_thread::get_id() << ":" << i << endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));++x;mtx.unlock();}
}int main()
{mutex m;int count = 0;thread t1(Print, 10, ref(count), ref(m));thread t2(Print, 10, ref(count), ref(m));t1.join();t2.join();cout << count << endl;return 0;
}

后面再来看看怎么实现交错打印的效果,再看看另一种用法:(lambda)

int main()
{mutex mtx;int x = 0;int n = 10;thread t1([&](){for (int i = 0; i < n; ++i){mtx.lock();cout << this_thread::get_id() << ":" << i << endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));++x;mtx.unlock();}});thread t2([&](){for (int i = 0; i < n; ++i){mtx.lock();cout << this_thread::get_id() << ":" << i << endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));++x;mtx.unlock();}});t1.join();t2.join();cout << x << endl;return 0;
}

上面代码的问题:如果加锁解锁之间存在抛异常就死锁了,这时就要用到RAII锁。


1.3 RAII锁

lock_guard是一个类,采用了RAII方式来加锁解锁——将锁的生命周期和对象的生命周期绑定在一起。看下在Linux篇章写的代码:(把锁封装了)

#pragma once
#include <iostream>
#include <pthread.h>class Mutex
{
public:Mutex(pthread_mutex_t* mtx) :_pmtx(mtx){}void lock(){pthread_mutex_lock(_pmtx);std::cout << "进行加锁成功" << std::endl;}void unlock(){pthread_mutex_unlock(_pmtx);std::cout << "进行解锁成功" << std::endl;}~Mutex(){}
protected:pthread_mutex_t* _pmtx;
};class lockGuard // RAII风格的加锁方式
{
public:lockGuard(pthread_mutex_t* mtx) // 因为不是全局的锁,所以传进来,初始化:_mtx(mtx){_mtx.lock();}~lockGuard(){_mtx.unlock();}
protected:Mutex _mtx;
};

看库里的构造函数:

  • lock_guard(mutex_type& m),在创建这个对象的时候需要传入一把锁,在构造函数中,进行了加锁操作。
  • lcok_guard(const lock_guard&)=delete,该对象禁止拷贝,因为互斥锁就不可以拷贝。

析构函数的作用就是将lock_guard对象的资源释放,也就是进行解锁操作。

lock_guard只有构造函数和析构函数,使用该类对象加锁时不需要我们去关心锁的释放,但是它不能在对象生命周期结束之前主动解锁。

看一下unique_lock:

unique_lock也是一种RAII的加锁对象,它和lock_guard的功能一样,将锁的生命周期和对象的生命周期绑定在一起,但是又有区别。

  • unique_lock(mutex_type& m),这个和lock_guard的用法一样,在构造函数中加锁。
  • unique_lock(const unique_lock&)=delete,同样禁止拷贝。

析构函数中和lock_guard一样,也是进行解锁操作。

  • lock,加锁。
  • unlock,解锁。
  • try_lock,尝试加锁。

lock_guard中就没有这几个接口,所以unique_lock可以在析构之前主动解锁,主动解锁后仍然可以再主动加锁,这一点lock_guard是不可以的。

  • try_lock_for,尝试加锁一段时间,时间到后自动解锁。
  • try_lock_until,尝试加锁到指定时间,时间到来后自动解锁。

用法很多,需要使用的时候可以结合库文档来使用。用一下lock_guard+lambda的另一种用法:

int main()
{mutex mtx;int n = 10;int m;cin >> m;vector<thread> v(m);for (int i = 0; i < m; ++i){// 移动赋值给vector中线程对象v[i] = thread([&](){for (int i = 0; i < n; ++i){{lock_guard<mutex> lk(mtx);cout << this_thread::get_id() << ":" << i << endl;}std::this_thread::sleep_for(std::chrono::milliseconds(100));}});}for (auto& t : v){t.join();}return 0;
}

1.4 atomic+CAS

C++11提供了原子操作,我们知道,线程不安全的主要原因就是访问某些公共资源的时候,操作不是原子的,如果让这些操作变成原子的后,就不会存在线程安全问题了。

CAS原理:

原子操作的原理就是CAS(compare and swap)。

  • CAS包含三个操作数:内存位置(V),预期原值(A)和新值(B)。
  • 如果内存位置的值与预期原值相等,那么处理器就会自定将该位置的值更新为新值。
  • 如果内存位置的值与预期原值不相等,那么处理器不会做任何操作。

val是临界资源,两个线程t1和t2同时对这个值进行加加操作,每个线程都是将该值先拿到寄存器eax中。

  • 线程将val值拿到寄存器eax中时,同时将该值放入原值V中。
  • 在修改val值之前,CPU会先判断eax中的值与原值V中的值是否相等,如果相等则修改并且更新值,如果不相等则不修改。

伪代码原理:

while(1)
{eax = val; // 将val值取到寄存器eax中if(eax == V) // 和原值相同可以修改{eax++;V = eax; // 修改原值val = eax; // 修改val值break; // 访问结束,跳出循环}
}
  •  t1和t2虽然同时运行,但是时间粒度划分到极小的时候,CPU仍然是一个个在执行。

t1线程将val值拿到寄存器中,并且赋原值,经过判断发现和原值相同,所以修改val值,并放回到val的地址中。

此时t2线程被唤醒,它将val值拿到寄存器中后与最开始的原值V相比,发现不相同了,所以就不进行修改,而且继续循环,知道寄存器中的值和原值相等才会改变。

  • 原子操作虽然保证了线程安全,但是另一个无法写的的线程会不停的循环,而这也会占用一定的CPU资源。

CAS具体的原理有兴趣可以自行去了解,深入了解后写在简历是加分项。


atomic也是一个类,所以也有构造函数:

 经常使用的是atomic(T val),在创建的时候传入我们想要进行原子操作的变量。

int a = atomic(1);

此时变量a的操作就都成了原子操作了,在多线程访问的时候可以保证线程安全。

成员函数:

该类重载了++,–等运算符,可以直接对变量进行操作。

看看没用atomic也没加锁的:

int main()
{mutex mtx;int x = 0;int n = 100000;int m = 2;vector<thread> v(m);for (int i = 0; i < m; ++i){// 移动赋值给vector中线程对象v[i] = thread([&](){for (int i = 0; i < n; ++i){++x;}});}for (auto& t : v){t.join();}cout << x << endl;return 0;
}

两个线程互相抢着加,就会出现有一个线程没加的情况,看看加锁的:

再看看用atomic的:

和加锁效果一样。


1.5 condition_variable

C++11中同样也有条件变量,用来实现线程的同步。

构造函数:

在创建条件变量的时候不用传入参数,同样是不允许被拷贝的。


其他成员函数:

放入等待队列:

wait(unique_lock<mutex>& lock),该接口是将调用它的线程放入到条件变量的等待队列中。
wait(unique_lock<mutex>& lck, Predicate pred),该接口和上面的作用一样,只是多了一个pred参数,当这个参数为true的话不放入等待队列,为false时放入等待队列。

这里传入的锁是unique_lock而不是lock_guard。

这是因为,当一个线程申请到锁进入临界区,但是条件不满足被放入条件变量的等待队列中时,会将申请到的锁释放。

lock_guard只能在对象生命周期结束时自动释放锁。

unique_lock可以在任意位置释放锁。

如果使用了lock_guard的话就无法在进入等待队列的时候释放锁了。


wait_for和wait_until都是等待指定时间,一个是在等待队列中待指定时间,另一个是在等待队列中带到固定的时间点后自定唤醒。

notify_one唤醒等待队列中的一个线程,notify_all唤醒等待队列中的所有线程。


1.6 分别打印奇数和偶数

写一个程序:支持两个线程交替打印,一个打印奇数,一个打印偶数。

分析:

  • 首先创建一个全局的变量val,让两个线程去访问该变量并且进行加一操作。
  • 考虑到线程安全,所以需要给对应的临界区加互斥锁mutex
  • 又是交替打印,所以要使用条件变量condition_variable来控制顺序,为了方便管理,使用的锁是unique_lock<mutex>

代码实现:

int main()
{int val = 0;int n = 10; // 打印的范围mutex mtx; // 创建互斥锁condition_variable cond; // 创建条件变量thread t1([&](){while (val < n){unique_lock<mutex> lock(mtx); // 加锁while (val % 2 == 0)// 判断是否是偶数{// 是偶数则放入等待队列中等待cond.wait(lock);}// 是奇数时打印cout << "thread1:" << this_thread::get_id() << "->" << val++ << endl;cond.notify_one(); // 唤醒等待队列中的一个线程去打印偶数}});this_thread::sleep_for(chrono::microseconds(100));thread t2([&](){while (val < n){unique_lock<mutex> lock(mtx);while (val % 2 == 1){cond.wait(lock);}cout << "thread2:" << this_thread::get_id() << "->" << val++ << endl;cond.notify_one();//唤醒等待队列中的一个线程去打印奇数}});t1.join();t2.join();return 0;
}

上面代码两个线程执行的函数对象是lambda表达式,所以创建线程对象时,调用的是移动构造函数。

  • wait()的第二个参数是false的时候,该线程被挂起到等待队列中,是true的时候不挂起,而且执行向下执行。
  • 第二个参数的false和true可以是返回值,如代码就是使用的lambda表达式的返回值。

线程t1负责打印奇数,t2负责打印偶数,两个线程通过条件变量的控制交替打印。

还可以这么用:

int main()
{int val = 0;int n = 10; // 打印值的范围mutex mtx;condition_variable cond;bool ready = true;// t1线程打印奇数thread t1([&](){while (val < n){{unique_lock<mutex> lock(mtx);cond.wait(lock, [&ready](){return !ready; });cout << "thread1:" << this_thread::get_id() << "->" << val << endl;val += 1;ready = true;cond.notify_one();}//this_thread::yield();this_thread::sleep_for(chrono::microseconds(10));}});// t2线程打印偶数thread t2([&]() {while (val < n){unique_lock<mutex> lock(mtx);cond.wait(lock, [&ready](){return ready; });cout << "thread2:" << this_thread::get_id() << "->" << val << endl;val += 1;ready = false;cond.notify_one();}});t1.join();t2.join();return 0;
}

成功按照预期打印。


2. shared_ptr线程安全

智能指针复习:从C语言到C++_36(智能指针RAII)auto_ptr+unique_ptr+shared_ptr+weak_ptr-CSDN博客

以前敲的shared_ptr(加一个返回引用计数的接口):

namespace rtx
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr): _ptr(ptr), _pCount(new int(1)){}void Release(){if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数{delete _ptr;delete _pCount;}}~shared_ptr(){Release();}shared_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pCount(sp._pCount){(*_pCount)++;}shared_ptr<T>& operator=(const shared_ptr<T>& sp){//if (this != &sp)if (_ptr != sp._ptr) // 防止自己给自己赋值,注意不能比较this,类似s1 = s2; 再来一次s1 = s2;{                    // 比较_pCount也行//if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数//{//	delete _ptr;//	delete _pCount;//}Release();_ptr = sp._ptr;_pCount = sp._pCount;(*_pCount)++;}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count(){return *_pCount;}protected:T* _ptr;int* _pCount;// 引用计数,有多线程安全问题,学了linux再讲,不能用静态成员};
}

先看看库里面的使用:

int main()
{std::shared_ptr<double> sp1(new double(7.77));std::shared_ptr<double> sp2(sp1);mutex mtx;vector<thread> v(5);int n = 100000;for (auto& t : v){t = thread([&](){for (size_t i = 0; i < n; ++i){// 拷贝是线程安全的std::shared_ptr<double> sp(sp1);// 访问资源不是(*sp)++;}});}for (auto& t : v){t.join();}cout << *sp1 << endl;cout << sp1.use_count() << endl;return 0;
}

2.1 库里面的shared_ptr使用

能指针共同管理的动态内存空间是线程不安全的,访问资源要自己加锁:

再把std换成自己的命名空间:

程序直接崩溃了,因为有时候引用计数不对。

多个线程及主线程中的所有智能指针都共享引用计数,又因为拷贝构造以及析构都不是原子的,所以导致线程不安全问题。

解决办法和Linux中一样,需要加锁:

引用计数加加和减减都要加锁

放个代码:

2.2 shared_ptr加锁代码

namespace rtx
{template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr): _ptr(ptr), _pCount(new int(1)),_pMtx(new mutex){}shared_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pCount(sp._pCount), _pMtx(sp._pMtx){_pMtx->lock();(*_pCount)++;_pMtx->unlock();}shared_ptr<T>& operator=(const shared_ptr<T>& sp){//if (this != &sp)if (_ptr != sp._ptr) // 防止自己给自己赋值,注意不能比较this,类似s1 = s2; 再来一次s1 = s2;{                    // 比较_pCount也行//if (--(*_pCount) == 0) // 防止产生内存泄漏,和析构一样,写成一个函数//{//	delete _ptr;//	delete _pCount;//}Release();_ptr = sp._ptr;_pCount = sp._pCount;_pMtx->lock();(*_pCount)++;_pMtx->unlock();}return *this;}void Release() // 防止产生内存泄漏,和析构一样,写成一个函数{bool flag = false;_pMtx->lock();if (--(*_pCount) == 0){delete _ptr;delete _pCount;flag = true;}_pMtx->unlock();if (flag){delete _pMtx; // new出来的,引用计数为0时要delete}}~shared_ptr(){Release();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count(){return *_pCount;}protected:T* _ptr;int* _pCount;// 引用计数,有多线程安全问题,学了linux再讲,不能用静态成员mutex* _pMtx;};
}int main()
{rtx::shared_ptr<double> sp1(new double(7.77));rtx::shared_ptr<double> sp2(sp1);mutex mtx;vector<thread> v(7);int n = 100000;for (auto& t : v){t = thread([&](){for (size_t i = 0; i < n; ++i){// 拷贝是线程安全的rtx::shared_ptr<double> sp(sp1);// 访问资源不是mtx.lock();(*sp)++;mtx.unlock();}});}for (auto& t : v){t.join();}cout << *sp1 << endl;cout << sp1.use_count() << endl;return 0;
}


3. 单例模式线程安全

单例模式复习:

从C语言到C++_37(特殊类设计和C++类型转换)单例模式-CSDN博客

3.1 懒汉模式线程安全问题

在C++11之后饿汉模式是没有线程安全问题的(做了相关补丁),因为单例对象是在main函数之前就实例化的,而多线程都是在main函数里面启动的。

但是懒汉模式是存在线程安全问题的,当多个线程使用到单例对象时候,在使用GetInstance()获取对象时,用因为调度问题出现误判,导致new多个单例对象。

这里给懒汉模式加个锁:(这里在getInstance这样加锁有没有什么问题?)

此时,每个调用GetInstance()的线程都需要申请锁然后释放锁,对锁的操作也是有开销的,会有效率上的损失。

单例模式在单例一经创建以后就不会再创建了,无论多少线程在访问已经创建的单例对象时都不会再创建,线程就已经安全了。所以在单例对象创建以后,根本没有必要再去申请锁和释放锁。

如果把加锁放在 if 里面呢?这样是不行的,因为第二次线程来的时候单例对象已经不是空的了,所以锁就白加了。

此时就要双检查加锁:

3.2 懒汉模式最终代码

class Singleton
{
public:static Singleton* GetInstance(){// 双检查加锁if (m_pInstance == nullptr) // 保护第一次后,后续不需要加锁{unique_lock<mutex> lock(_mtx); // 加锁,防止new抛异常就用unique_lockif (m_pInstance == nullptr) // 保护第一次时,线程安全{m_pInstance = new Singleton;}}return m_pInstance;}private:Singleton() // 构造函数{}Singleton(const Singleton& s) = delete; // 禁止拷贝Singleton& operator=(const Singleton& s) = delete; // 禁止赋值// 静态单例对象指针static Singleton* m_pInstance; // 单例对象指针static mutex _mtx;
};Singleton* Singleton::m_pInstance = nullptr; // 初始化为空
mutex Singleton::_mtx;int main()
{Singleton* ps = Singleton::GetInstance();//获取单例对象return 0;
}

成功运行。


3.3 懒汉模式的另一种写法

放个代码:

class Singleton
{
public:static Singleton* GetInstance(){// 局部的静态对象,第一次调用时初始化// 在C++11之前是不能保证线程安全的// C++11之前局部静态对象的构造函数调用初始化并不能保证线程安全的原子性。// C++11的时候修复了这个问题,所以这种写法,只能在支持C++11以后的编译器上使用static Singleton _s;return &_s;}private:// 构造函数私有Singleton(){};Singleton(Singleton const&) = delete;Singleton& operator=(Singleton const&) = delete;
};int main()
{Singleton::GetInstance();return 0;
}

C++11之前局部静态对象的构造函数调用初始化并不能保证线程安全的原子性。

C++11的时候修复了这个问题,所以这种写法,只能在支持C++11以后的编译器上使用。


本篇完。

应该算是本专栏的最后一篇了,泪目泪目。道祖且长,行则将至,想再深入学习C++以后就靠自己拓展了。后一部分就是网络和Linux网络的内容了。

相关文章:

从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)

目录 1. C多线程 1.1 thread库 1.2 mutex库 1.3 RAII锁 1.4 atomicCAS 1.5 condition_variable 1.6 分别打印奇数和偶数 2. shared_ptr线程安全 2.1 库里面的shared_ptr使用 2.2 shared_ptr加锁代码 3. 单例模式线程安全 3.1 懒汉模式线程安全问题 3.2 懒汉模式最…...

Angular 指令介绍及使用(三)

Angular 指令概述 在 Angular 中&#xff0c;指令是一种机制&#xff0c;用于扩展和修改组件的行为和外观。指令可以由开发者自定义&#xff0c;也可以是 Angular 框架自带的一些内置指令。通过使用指令&#xff0c;我们可以在 HTML 模板中通过属性或元素名来操作组件。 Angu…...

小学生加减乘除闯关运算练习流量主微信小程序开发

小学生加减乘除闯关运算练习流量主微信小程序开发 经过本次更新&#xff0c;我们增加了新的功能和特性&#xff0c;以提升用户体验和运算练习的趣味性&#xff1a; 能量石与激励视频&#xff1a;用户可以通过观看激励视频来获取能量石&#xff0c;这些能量石可以用于解锁收费…...

普通测径仪升级的智能测径仪 增添11大实用功能!

普通测径仪能对各种钢材进行非接触式的外径及椭圆度在线检测&#xff0c;测量数据准确且无损&#xff0c;可测、监测、超差提示、系统分析等。在此基础上&#xff0c;为测径仪进行了进一步升级制成智能测径仪&#xff0c;为其增添更多智能化模块&#xff0c;让其使用更加方便。…...

vue做的一个一点就转的转盘(音乐磁盘),点击停止时会在几秒内缓慢停止,再次点击按钮可以再次旋转,

先看效果&#xff1a; 代码&#xff1a;主要部分我会红线画出来 css:部分&#xff1a; 源码&#xff1a; vue部分&#xff1a; <template><div class"song-lyric"><div><div class"type"><div class"right">&l…...

Spring6(一):入门案例

文章目录 1. 概述1.1 Spring简介1.2 Spring 的狭义和广义1.3 Spring Framework特点1.4 Spring模块组成 2 入门2.1 构建模块2.2 程序开发2.2.1 引入依赖2.2.2 创建java类2.2.3 创建配置文件2.2.4 创建测试类测试 2.3 程序分析2.4 启用Log4j2日志框架2.4.1 引入Log4j2依赖2.4.2 加…...

Linux中报错no space device解决思路

1&#xff0c;df -h &#xff1a;查看所有文件下的磁盘使用情况。注意&#xff0c;查询的最后一栏属性就是分区所在的目录路径 2&#xff0c;进到具体的文件下&#xff0c;接着命令&#xff1a;du -sh * | grep G 搜索G以上的文本。 没搜到内容的话&#xff0c;使用命令du -sh…...

vue3使用element-plus

安装 # NPM $ npm install element-plus --save# Yarn $ yarn add element-plus# pnpm $ pnpm install element-plus 全局引入 main.js // main.ts import { createApp } from vue import ElementPlus from element-plus//引入ElementPlus所有组件 import element-plus/dis…...

高质量实时渲染笔记

文章目录 Real-time shadows1 自遮挡问题2 解决阴影detach问题&#xff1f;3 Aliasing4 近似积分5 percentage closer soft shadows(PCSS)percenta closer filtering(PCF)PCSS的思想 6 Variance Soft Shadow Mapping (VSSM)步骤Moment Shadow Mapping 7 Distance field shadow …...

云原生下GIS服务规划与设计

作者&#xff1a;lisong 目录 背景云原生环境下GIS服务的相关概念GIS服务在云原生环境下的规划调度策略GIS服务在云原生环境下的调度手段GIS服务在云原生环境下的服务规划调度实践 背景 作为云原生GIS系统管理人员&#xff0c;在面对新建的云GIS系统时&#xff0c;通常需要应对…...

VBA 宏For WPS(完整版)-供大家学习研究参考

VBE7.1安装方法&#xff1a; 适用于安装 WPS 2019 版本的 缺少 VBA 模块的 亲测可用&#xff0c;内含 VBA 7.1 安装顺序1、2、3、4按照顺序安装&#xff1b; 1.安装MSVCRTRedist\Release目录下32位的安装包&#xff0c;此安装包为运行时库 3.安装VBARedist\Release目录下32位的…...

【Linux】八、进程通信

进程通信的介绍 目的 数据传输&#xff1a;一个进程将它的数据发送给另一个进程&#xff1b; 资源共享&#xff1a;多个进程间共享资源&#xff1b; 通知事件&#xff1a;一个进程向另一个或一组进程发送消息&#xff0c;同时事件如&#xff0c;进程终止时要通知父进程&#xf…...

不同类型的软件企业该如何有效的管理好你的软件测试团队?

最近在网上发现一篇记录了2012年《[视频]作为测试经理如何有效管理好你的软件测试团队》的文字内容&#xff0c;感谢记录的人&#xff0c;我也保存一下。顺便将演讲中的PPT重点截图也放上来&#xff0c;一并保存了&#xff01;。由于是现场速记&#xff0c;过度的口语化&#x…...

vue echart 立体柱状图 带阴影

根据一个博主代码改编而来 <template><div class"indexBox"><div id"chart"></div></div> </template><script setup> import * as echarts from "echarts"; import { onMounted } from "vue&…...

vscode远程linux安装codelldb

在windows上使用vscode通过ssh远程连接linux进行c调试时&#xff0c;在线安装codelldb-x86_64-linux.vsix扩展插件失败&#xff0c;原因是linux服务器上的网络问题&#xff0c;所以需要进行手动安装。 首先在windows上下载&#xff1a; codelldb-x86_64-linux.vsix&#xff1b;…...

【中间件篇-Redis缓存数据库08】Redis设计、实现、redisobject对象设计、多线程、缓存淘汰算法

Redis的设计、实现 数据结构和内部编码 type命令实际返回的就是当前键的数据结构类型&#xff0c;它们分别是&#xff1a;string(字符串)hash(哈希)、list(列表)、set(集合)、zset (有序集合)&#xff0c;但这些只是Redis对外的数据结构。 实际上每种数据结构都有自己底层的…...

华为云优惠券介绍、领取入口及使用教程

华为云是华为的云服务品牌&#xff0c;致力于为用户提供一站式云计算基础设施服务。为了吸引用户&#xff0c;华为云经常推出各种优惠活动&#xff0c;其中就包括优惠券的发放&#xff0c;下面将为大家详细介绍华为云优惠券的作用、领取入口以及使用教程。 一、华为云优惠券介绍…...

OPTEE安全通告之CVE-2023-41325(Double free in shdr_verify_signature)

安全之安全(security)博客目录导读 目录 一、受影响版本 二、漏洞描述 三、问题触发 四、官方Patch修复...

第12章 关于 Micro SaaS 的结论

从时间和地点的自由到一种新鲜的独立感,开发 Micro SaaS 应用程序有很多好处。 获得 6 位数的订阅收入。辞掉我朝九晚五的令人丧命的工作。消除毫无意义的会议、办公室政治、混乱和救火。想工作就工作。随时随地使用我想要的任何技术工作。花更多时间陪伴家人。与我开发的应用…...

postman调用接口报{“detail“:“Method \“DELETE\“ not allowed.“}错误, 解决记录

项目是python代码开发, urls.py 路由中访问路径代码如下: urlpatterns [path(reportmanagement/<int:pk>/, views.ReportManagementDetail.as_view(), namereport-management-detail),] 对应view视图中代码如下: class ReportManagementDetail(GenericAPIView):"…...

C++:std::is_convertible

C++标志库中提供is_convertible,可以测试一种类型是否可以转换为另一只类型: template <class From, class To> struct is_convertible; 使用举例: #include <iostream> #include <string>using namespace std;struct A { }; struct B : A { };int main…...

Golang dig框架与GraphQL的完美结合

将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用&#xff0c;可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器&#xff0c;能够帮助开发者更好地管理复杂的依赖关系&#xff0c;而 GraphQL 则是一种用于 API 的查询语言&#xff0c;能够提…...

【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力

引言&#xff1a; 在人工智能快速发展的浪潮中&#xff0c;快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型&#xff08;LLM&#xff09;。该模型代表着该领域的重大突破&#xff0c;通过独特方式融合思考与非思考…...

ffmpeg(四):滤镜命令

FFmpeg 的滤镜命令是用于音视频处理中的强大工具&#xff0c;可以完成剪裁、缩放、加水印、调色、合成、旋转、模糊、叠加字幕等复杂的操作。其核心语法格式一般如下&#xff1a; ffmpeg -i input.mp4 -vf "滤镜参数" output.mp4或者带音频滤镜&#xff1a; ffmpeg…...

【android bluetooth 框架分析 04】【bt-framework 层详解 1】【BluetoothProperties介绍】

1. BluetoothProperties介绍 libsysprop/srcs/android/sysprop/BluetoothProperties.sysprop BluetoothProperties.sysprop 是 Android AOSP 中的一种 系统属性定义文件&#xff08;System Property Definition File&#xff09;&#xff0c;用于声明和管理 Bluetooth 模块相…...

JVM暂停(Stop-The-World,STW)的原因分类及对应排查方案

JVM暂停(Stop-The-World,STW)的完整原因分类及对应排查方案,结合JVM运行机制和常见故障场景整理而成: 一、GC相关暂停​​ 1. ​​安全点(Safepoint)阻塞​​ ​​现象​​:JVM暂停但无GC日志,日志显示No GCs detected。​​原因​​:JVM等待所有线程进入安全点(如…...

基于 TAPD 进行项目管理

起因 自己写了个小工具&#xff0c;仓库用的Github。之前在用markdown进行需求管理&#xff0c;现在随着功能的增加&#xff0c;感觉有点难以管理了&#xff0c;所以用TAPD这个工具进行需求、Bug管理。 操作流程 注册 TAPD&#xff0c;需要提供一个企业名新建一个项目&#…...

CSS | transition 和 transform的用处和区别

省流总结&#xff1a; transform用于变换/变形&#xff0c;transition是动画控制器 transform 用来对元素进行变形&#xff0c;常见的操作如下&#xff0c;它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...

多模态图像修复系统:基于深度学习的图片修复实现

多模态图像修复系统:基于深度学习的图片修复实现 1. 系统概述 本系统使用多模态大模型(Stable Diffusion Inpainting)实现图像修复功能,结合文本描述和图片输入,对指定区域进行内容修复。系统包含完整的数据处理、模型训练、推理部署流程。 import torch import numpy …...

NPOI Excel用OLE对象的形式插入文件附件以及插入图片

static void Main(string[] args) {XlsWithObjData();Console.WriteLine("输出完成"); }static void XlsWithObjData() {// 创建工作簿和单元格,只有HSSFWorkbook,XSSFWorkbook不可以HSSFWorkbook workbook new HSSFWorkbook();HSSFSheet sheet (HSSFSheet)workboo…...