【C++11】线程库/异常

一:线程库

    1.1:线程库(thread)

          1.1.1:为什么要有线程库

          1.1.2:thread库中的成员函数

          1.1.3:线程函数参数

    1.2:互斥锁(mutex)

          1.2.1:为什么要有互斥锁

          1.2.2:C++11中的互斥锁

    1.3:原子操作(atomic)

    1.4:条件变量(condition_variable)

二:异常

    2.1:C语言中处理错误的方式

    2.2:异常的概念

    2.3:异常的具体用法

    2.4:异常的重新抛出

    2.5:异常的安全和规范

    2.6: 异常的优缺点

一:线程库

    1.1:线程库(thread)

        1.1.1:为什么要有线程库

关于多线程,我们在学习Linux中重点学习过,,Windows中的线程我们多多少少也有点了解,但是我们能明显的感觉到二者在使用方式上的差别,先不说别的,二者在创建与销毁线程时的方式就非常的不同(Linux下的线程通过在进程中调用pthread_create()函数来创建线程,通过pthread_exit()函数来退出线程,而Windows下的线程则是通过CreateThread()函数来创建线程,通过ExitThread()函数来退出线程);这是为什么呢?

因为Linux下的pthread线程库是POSIX(可移植操作系统接口)的线程标准,一般用于Unix_likePOSIX 系统,而Windows并没有支持POSIX,而是自己搞了一套提供线程相关的接口!!!也就是说,Linux与Windows下的多线程库是不同的,那么Linux下写的多线程程序就不能与Windows下的多线程程序相互适应,可移植性就比较差!那如果我们想让自己写的程序在Linux下能运行,Windows下也能运行,该怎么办呢???

//C++11之前,如何让Linux与Windows支持同一份多线程程序?
//通过条件编译来实现!
int main()
{
#if _WIN32//_WIN32是Windows特有的宏,//如果是在Windows下,那么使用Windows下多线程接口!//CreateThread//创建线程,然后。。。。。#else//如果不在Windows下,那么我们使用Linux下多线程接口!pthread_create//创建线程,然后。。。。。#endifreturn 0;
}

在C++11之前,我们可以通过条件编译,写两份调用不同线程接口的程序来实现!但是这样会非常的麻烦,繁琐!

所以C++11新增封装了一个thread库,可以对线程进行支持,使得C++在并行编程时不需要依赖第三方库,只要所使用的编译器支持C++11,那么无论在什么平台下,都可以使用一样的线程库,提高了程序跨平台的兼容性,同时又避免使用特定于某一平台的函数和接口。

那么thread库是如何做到可以跨平台使用呢?---其实和之前一样,通过条件编译来实现,Linux下用Linux的线程接口,Windows下用Windows的线程接口,只不过条件编译以及调用不同线程接口的过程不需要我们自己编写,而是thread库提前帮我们封装好了!同时这个thread库被封装成了一个面向对象的类!!!

1.1.2:thread库中的成员函数

既然thread库被封装成了一个类,那么我们先来看看这个thread库中比较重要的默认成员函数!

构造函数和移动赋值:

我们可以看到,thread库中有三个构造函数(同时还有一个被delete的拷贝构造(即不允许拷贝构造)),那这些构造函数该如何使用呢?                 

 首先我们先看带有可变参数模板的构造函数

template <class Fn, class... Args>
explicit thread (Fn&& fn, Args&&... args);

 其中,fn:是可调用对象,如函数指针、仿函数、lambda表达式、被包装器包装后的可调用对象等。

        args:可变参数包,传递fn这个可调用对象所需要的如干类型的形参!!!

void func(int x, int n)
{for (int i = 0; i < x; ++i){cout << n << ":" << i << endl;}
}int main()
{//我们可调用对象可以是函数指针,同时,参数可以是任意多个,参数的属性可以是左值/右值!!!thread t1(func, 10, 1+3);//1+3是右值//我们可调用对象也可以是lambda表达式!thread t2([](int x,int n){for (int i = 0; i < x; ++i){cout << n << ":" << i << endl;}}, 20, 2);//main主线程需要将这两个线程回收,否则将会崩溃!t1.join();t2.join();return 0;
}

 这就是使用使用带有可变参数模版的构造函数构造线程的过程而相较与Linux下的pthread_create( 在Linux中,pthread_create创建线程时,线程的调用对象必须是void**类型的,且当要传递的参数>1时,需要将多个参数封装成一个结构体,然后转成void*类型传递过去,再在线程调用对象中将void*强转成结构体,才能拿出多个参数!非常的麻烦,繁琐,下面给出pthread_create函数的具体形式:)

而使用模板构造线程就非常方便,不仅仅可以接受多种类型的可调用对象,而且也可以传递任意多个参数!!!

移动构造:

 thread提供了一个移动构造函数,能够用一个右值线程对象(匿名对象/临时对象)来构造一个线程对象。具体如下所示:

void func(int x, int n)
{for (int i = 0; i < x; ++i){cout << n << ":" << i << endl;}
}
int main()//移动构造:
{thread t3 = thread(func, 10, 1);thread t4 = thread([](int x,int n)//生成的匿名对象移动拷贝给t4这个线程! {for (int i = 0; i < x; ++i){cout << n << ":" << i << endl;}},10,2);t3.join();t4.join();
}

 至于无参的构造函数我这里需要将其与移动赋值搭配在一起,我们先看看默认的operator=:

 这里有一个问题能更好的体验出无参构造和移动赋值:如果我要创建n个线程,每一个线程打印m次(n,m是输入的,且m不同),该如何实现?

具体解答思想:
1:用容器(例如vector)创建n个线程,vector有一个构造常数个无参对象的构造函数!而正好thread支持构造无参对象的构造函数,只不过构建出来的对象什么都不干,那么此时我们就可以通过这个vector的构造函数创建n个无参的线程对象!

2:然后再通过thread的移动赋值将一个个右值对象(匿名对象)移动给构建的n个无参的线程对象,即可完成这个问题!!! 具体如下所示:

//无参构造+移动赋值
int main()
{int n = 0;int m = 0;cin >> n;//要创建n个线程,vector<thread> vtids(n);//此时我们就创建了多个无参的线程对象!vector<int> num;for (int i = 0; i < n; ++i)//每一个线程打印不同的m次{cin >> m;num.emplace_back(m);}for (int i = 0; i < n; ++i){//将lmabda表达式形成的匿名对象移动赋值给vector中的无参线程对象!vtids[i] = thread([i,num](){for (int j = 0; j < num[i]; ++j){cout << i << ":" << j << endl;}});}//最后将多个线程回收:for (auto& e : vtids){e.join();}return 0;
}

 这样,就完美的使用了无参构造和移动赋值,

其实,我们在实现线程池的时候就用到了类似的方法,就是先创建一批线程,但一开始这些线程什么也不做,只是等待任务的到来,当有任务到来时再让这些线程去处理这些任务!

其他成员函数:

至于其他的成员函数,如join(回收线程),detach(分离线程)等成员函数具体如下所示:基本上和我们在Linux学习时的pthread_join,pthread_detach等线程接口差不多,

这里joinable我们需要注意一下:什么时候线程算没有执行完毕,什么时候算线程已经结束了呢?--------如果是以下任意情况,则线程无效:
1:采用无参构造函数构造的线程对象(无参对象没有关联任何线程)
2:线程对象的状态已经转移给其他线程对象(线程通过移动语句交付给另一个线程,此时原线程就无效了)
3:线程已经调用jion或者detach结束(都已经回收或者分离(分离的线程会自己join),肯定就执行完了)


同时,获取线程ID的接口和我们Linux学习中的pthread_self()也有一些不一样!!!就拿我们上面无参构造+移动构造的例子举例,如果我想在可调用对象打印的时候获取线程ID该怎么办:

int main()
{int n = 0;cin >> n;//要创建n个线程,vector<thread> vtids(n);//此时我们就创建了多个无参的线程对象!for (int i = 0; i < n; ++i){//将lmabda表达式形成的匿名对象移动赋值给vector中的无参线程对象!vtids[i] = thread([i](){int m = 5;for (int j = 0; j <m; ++j){//此时这样写会报错,因为线程对象都没有创建好呢,怎么可能用线程对象去调用!//cout << vtids[i].get_id() << endl;cout << i << ":" << j << endl;}cout<<endl;});//我们只能在线程创建好后,在这里通过线程去调用get_id,从而获取线程IDcout << vtids[i].get_id() << endl;}//最后将多个线程回收:for (auto& e : vtids){e.join();}return 0;
}

 但是,我们通常需要在线程关联的线程函数中获取线程ID,而上述这种创建好线程对象,再用线程对象来调用get_id函数获取线程ID的方式就不行了,我们需要换一种!而正好线程库中其他的解决方法:

int main()
{int n = 0;cin >> n;//要创建n个线程,vector<thread> vtids(n);//此时我们就创建了多个无参的线程对象!for (int i = 0; i < n; ++i){//将lmabda表达式形成的匿名对象移动赋值给vector中的无参线程对象!vtids[i] = thread([i](){int m = 5;for (int j = 0; j <m; ++j){//此时我们就可以用this_thread命名空间中的get_id函数//在构造线程对象时打印线程的ID!!!cout << this_thread::get_id() << ":" << j << endl;}cout<<endl;});}//最后将多个线程回收:for (auto& e : vtids){e.join();}return 0;
}

 除了get_id()这个函数之外,这个命名空间里面还封装了另外三个函数,yield()和sleep_until()使用的比较少,slee_for()就像在Linux中使用sleep函数那么常用,至于怎么使用,用的时候直接去Reference - C++ Reference 看例子即可!这里我就不过多的介绍了;

综上,我们对线程的成员函数做出一些总结与注意事项:

1. 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的;

2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。状态!

3. 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:

     函数指针,ambda表达式,函数对象!

4. thread类是防拷贝的,不允许拷贝构造以及拷贝赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的运行。

1.1.3:线程函数参数

我们知道,每一个线程都有自己独立的栈帧!而线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参!!!具体如下所示:

void ThreadFunc1(int& x)
{x += 10;
}//线程函数参数
int main()
{int a = 10;cout << a << endl;// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,//但其实际引用的是线程栈中的拷贝(线程之间各自拥有独立的栈)!thread t1(ThreadFunc1, a);t1.join();cout << a << endl;return 0;
}

如果想要通过形参改变外部实参时,我们该如何做呢???有两种方式,借助std::ref()函数 或者 对要修改的数的地址进行拷贝,具体如下所示:


这样就可以修改 主线程main函数中定义的传给线程的参数!!!

    1.2:互斥锁(mutex)

      1.2.1:为什么要有互斥锁

C++11标准新增了互斥锁(mutex)以及相关的同步原语。互斥锁是多线程编程中的一种同步机制,它可以将某一段代码的访问限制在同一时刻只能由一个线程执行,以避免不同线程之间的竞争和冲突。

多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦,比如。如果我们用两个线程对同一个全局变量++,看看会发生什么?具体如下所示:

我们看到, 同一个线程执行函数中,全局变量的地址是相同的,但是参数n的地址是不同的,为什么呢???---------因为线程之间静态区,常量区,堆区等是线程共享的,但是寄存器和栈帧是独立的!这也印证了我们之前所说的每一个线程都有自己独立的栈帧!!!

同时我们也可以看到运行后的结果确实是100+200=300!符合我们的预期,运行的没有错误呀!

其实不然,因为当多个线程对同一个全局变量进行++时,可能出现同时++的情况,那么最终的结果肯定小于等于预期的结果,这里之所以等于300,是因为数太小,CPU运行的太快了!!!那么我们加大数字再看看,具体如下所示:

我们此时就可以看到,最终运行的结果确实<=预期的结果(也就是说,有相当一部分是两个线程同步重复++的),并且每一次运行的结果都是大不相同的(之所以每一次运行结果不同是跟指令和系统状况相关)!!!

那么线程安全的根本原因到底是什么呢?我以在Linux学习时的笔记对其说明:

在Linux学习笔记中,我们是以对一个全局变量做--操作时进行分析,我们现在这里是++操作,但是不影响,其本质都是一样的!!!

那我们该如何解决这种并发问题,保护共享资源(临界资源)呢???--------通过加锁(互斥锁)实现,通过加锁,可以让某一段代码的访问限制在同一时刻只能由一个线程执行,防止并发问题的出现!!!

1.2.2:c++11中的互斥锁

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

下面,我们分别认识认识这四种互斥锁类!

mutex(h互斥锁类)

C++11提供的最基本的互斥量,先看看这个互斥锁类的构造函数:

 如上所示,互斥锁只支持无参的构造函数,不允许拷贝构造(该类的对象之间不能拷贝,也不能进行移动!);

同时mutex这个类的成员函数并不多,具体如下所示:

 构造函数我们已经看过了,剩下的四个成员函数中,native_handle这个不经常用,剩下的三个成员函数是最常用的!!!

注意:

线程函数调用lock()时,可能会发生以下三种情况:
1:如果此时该互斥量没有被锁住,则调用线程就可以将互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁!
2:如果互斥量已经被其他线程锁住,那么当前调用的线程则会被阻塞住!
3:如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)!


线程函数调用try_lock()时,可能会发生以下三种情况:
1:如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock释放互斥量!
2:如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞住!
3:如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)!

那么接下来,我们就尝试通过mutex(互斥锁类)解决一开始并发++全局变量的问题,具体如下所示:


 

 我们可以看到,当我们定义一个局部的锁时,结果还是不对,是因为每一个线程都有自己独立的栈,如果此时你定义局部的锁,那么每一个线程里面都存在一个自己的锁,那么这个锁就毫无意义,但是当我们定义的是全局的锁,那么执行++操作时,只有持有锁的线程才能操作,从而避免并发问题的参数!!!

注意:锁放置的位置也值得我们研究,但是在探讨这个问题之前,我们需要先明白一个概念,什么是临界区?

所谓临界区,就是指多个线程同时访问共享资源(如全局变量、文件等)的一段代码(切记,是代码);例如,我们把加锁解锁放到了++x的前后,那么此时临界区中的代码就是++x,如果我们把加锁解锁放到for循环外,那么整个for循环就是临界区!!!

这里就存在着锁放置位置的不同,效率的不同的问题,是把锁加锁解锁在++x(此时两个线程可以看作并行)这个临界区的效率高,还是加锁解锁在for循环(此时两个线程可以看作串行)这个临界区的效率高呢???

这里我就不举例说明了,直接给出结论:并行并不一定比串行快,这里主要看谁消耗的时间短!只要是看并行时临界区(频繁加锁解锁+切换上下文+执行代码)所消耗的时间和串行时临界区(加锁解锁+执行代码)所需要的时间哪一个短,谁短谁效率就相对较高!!!

上面是用函数指针的方式来实现线程的执行函数的,其实在C++11之后,我们经常会使用lambda表达式作为线程的执行函数,具体如下所示:

/// 

(recursive_nutex)递归互斥锁类

递归互斥锁类的成员函数和互斥锁类的成员函数相同,都有(lock,try_lock,unlock等),那如果我们递归++时进行多线程,是不是也有多线程的问题?答案是肯定的,那么此时我们即可通过加锁(mutex)的方式进行解决:

我们确实看到通过加锁解锁(mutex)能解决并发问题,但是临界区只有++x这一句代码,而此时我们对其频繁的加锁解锁 是不是有点降低效率了,我们能不能在递归之后再解锁呢?如果递归之后解锁不可行,我们该怎么办呢?

我们可以看到,用mutex锁,在递归之后解锁是不可行的,会引发死锁问题(可能会重复申请已经申请到但自己还未释放的锁);所以此时我们可以利用递归互斥锁(recursive_mutex),递归互斥锁允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,递归互斥锁(recursive_mutex)的特性和 互斥锁(mutex) 大致相同!!!

(timed_mutex)定时互斥锁类和(recursive_timed_mutex)递归定时互斥锁类

定时互斥锁类(timed_mutex)比互斥锁类(mutex) 多了两个成员函数,try_lock_for(),try_lock_until() 。

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

递归定时互斥锁类(recursive_timed_mutex)就是递归互斥锁类(recursive_mutex)和定时互斥锁类(timed_mutex)的结合,递归定时互斥锁类既支持在递归函数中进行加锁操作,也支持定时尝试申请锁!!!

这四种锁 我们重点学习.使用互斥锁类(mutex)和认识递归互斥锁类(recursive_mutex),至于后面两种互斥锁,用的比较少,这里就不做介绍了,如果到用的时候直接去官网看看例题!

lock_guard和unique_lock

有时候,在我们使用互斥锁进行加锁解锁的过程中,可能会因为在解锁之前提前return 或者出错误,导致没能成功解锁,函数就结束了,那么此后线程再申请这个锁时都会被阻塞住,从而引发死锁问题!!如下所示:

 因此,在使用互斥锁的时候,如果控制不好,出错时不能及时的解锁,就很容易引发死锁问题,那我们该如何避免呢???

为了解决此类问题,有的大佬就通过实现一个类来解决这种死锁问题(主要运用RAII思想),具体类如下所示:

template <class lock>
class LockGuard
{
public:LockGuard(lock& lk)//必须传引用传参,因为mutex锁不支持拷贝构造,赋值,移动语句等,:_lock(lk){_lock.lock();//构造函数时加锁}~LockGuard(){_lock.unlock();//析构函数时解锁}
private:lock& _lock;//同时,这里需要引用,因为构造函数时不支持拷贝!
};
mutex mtx;
void func()
{//那么此时,我们就不用手动的加锁解锁,//当程序出现错误退出或者return时,即出了这个函数的作用域,//那么此时,会知道调用析构函数将锁解开!!!LockGuard<mutex> lock_guard(mtx);//..........int* ptr = (int*)malloc(sizeof(int) * 10);if (ptr == nullptr){exit(-1);}if (rand() % 3 == 0){return;}//.......
}

此时,当这个类定义的对象在哪一个作用域下,那么这个作用域中的代码就受锁的保护,当出来这个作用域,锁会自动调用析构函数进行解锁,从而避免形成死锁问题!!!

同时,如果你不想让这个类对象随着整个函数的作用域创建和销毁,可以通过定义匿名的局部域来控制这个类对象的生命周期。比如:

void func()
{//构建一个匿名的作用域,在这个作用域内定义的类对象,出这个匿名作用域即调用析构函数解锁!!!{LockGuard<mutex> lock_guard(mtx);//..........int* ptr = (int*)malloc(sizeof(int) * 10);if (ptr == nullptr){exit(-1);}if (rand() % 3 == 0){return;}}//从而不影响这个作用域之外的行为!!!//。。。。。。。。。
}

这个LockGuard类并不需要我们自己实现,C++11之后支持了这个类,具体如下所示:

可以看到,lock_guard这个类是一个模板类,其中,它的成员函数只有构造函数和析构函数,即构造函数加锁,析构函数时解锁,用于管理互斥锁的加锁和解锁。

但是lock_guard这个类有一个缺点:太单一,它只能构造函数加锁,析构函数解锁,用户没有办法对该锁进行控制!!!

所以为此。C++11又提供了unique_lock!!!


 

与lock_guard类似,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和unique_lock没有区别!

但是如果我们想在构造函数加锁,析构函数解锁之间,做一些其他的操作(这个操作不需要加锁),那么我们就需要先将锁解开,做完这些操作再将锁加回来,此时lock_hguard就显得捉襟见肘了,unique_lock就正好解决这种问题,具体如下所示:

mutex mtx;
void func1()
{{unique_lock<mutex> unlock(mtx);//..........int* ptr = (int*)malloc(sizeof(int) * 10);if (ptr == nullptr){exit(-1);}//当在构造函数加锁,析构函数解锁之间调用func2!mtx.unlock();//需要先解锁func2();//调用func2;mtx.lock();//在加锁!if (rand() % 3 == 0){return;}}//然后出这个匿名作用域后再调用析构函数解锁!//从而不影响这个作用域之外的行为!!!//。。。。。。。。。
}

1.3:原子操作(atomic)

原子操作:是指不能被中断的操作,即针对某个变量的读取、修改、写入的操作不能被其他线程干扰,必须“原封不动”地执行完毕,即不可被中断的一个或一系列操作。

在多线程编程中,为了保证线程安全和避免数据竞争,通常会使用互斥锁等同步机制来保护共享变量。

但是互斥锁等同步机制的缺点是:在多线程高并发的情况下,会增加线程之间的竞争和上下文切换的开销,如果对锁的控制出现错误,甚至会出现死锁等问题,从而影响程序的执行效率。

所以,C++11中引入了原子操作。C++11引入的原子操作类型,使得线程间数据的同步变得非常高效。原子操作类型如下所示:

 此时,通过atomic,就不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问,我们以之前互斥锁学习时的例子为例:

更为普遍的,我们可以使用atomic类模板,定义出需要的任意原子类型。

那么,上面的atomic_int就可以改成以下的形式:

int main()
{int n = 10000;atomic<int> x = 0;//自己通过atomic模板创建一个atomic类型//mutex mtx;thread t1([&, n]()//混合捕捉:除了n传值捕捉,其他的(x,mtx锁)全部传引用捕捉!!!{//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();cout << x << endl;return 0;
}

同时原子类型不仅仅支持原子的++操作,还支持原子的--、加一个值、减一个值、与、或、异或操作。

但是这里有一个小问题,就是我们自己定义了一个atomic<int>的原子类型,那如果我再将它传递给一个int类型的参数或者按照int类型进行打印时,是会有可能出错的,为了解决这个问题,atomic类中存在一个load函数,可以读到atomic类模板中参数的类型,具体如下所示:

注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。

1.4:条件变量(condition_variable)

条件变量(Conditional Variable)是一种线程同步机制,用于在多线程环境中实现线程间的等待和通知机制。主要用于解决线程间的同步问题,它允许一个或多个线程等待某个条件成立,并在条件成立时通知其他线程或进程继续执行。

条件变量不是线程安全的,所以通常与互斥锁(mutex)一起使用,以实现线程间的安全访问和同步。条件变量提供了一种阻塞等待的方式,当线程执行到等待条件变量的代码时,线程会被阻塞在条件变量上,并且等待其他线程通知。当其他线程或进程结束后会通知在条件变量上等待的线程,此时被阻塞在条件变量上的线程将重新获得执行权并继续执行。

条件变量除了默认的成员函数外,主要提供了wait(阻塞等待)系列接口和notify(唤醒)系列的接口;下面我们先认识认识wait系列的接口:

 wait系列接口一共有三个,最主要的就是wait函数;

 wait函数有两个版本:

第一种wait:当调用这一个wait函数时,需要传入一个锁,通常这个锁需要交付给unique_lock来管理,当线程调用wait函数时,线程将会被阻塞在条件变量上同时,如果此时调用wait函数的线程身上持有锁那么这个线程将会在进入阻塞状态时将身上持有的锁释放掉!不能带着锁去阻塞,如果想让线程脱离阻塞状态,则必须被其他线程唤醒!!!

第二种wait:我们可以看到,第二种wait是带有模板的,当调用时,不仅仅要传入一个被unique_lock所管理的锁,还要传入一个可调用对象!!!

这个可调用对象如果返回的是false,就继续阻塞,如果是true,则从阻塞状态中脱离出来,立即参与到锁的竞争,同时,这个可调用对象将会被一直while循环判断阻塞状况,比较适合用于多生产多消费的场景下!(大概可以理解为是对第一种wait的封装)!

那为什么无论是第一种wait,还是第二种wait,都需要我们在使用时传入一个锁(mutex)呢,而且为什么都要使用unique_lock管理这个锁呢?

答:传锁是防止多个线程同时对共享资源进行操作!而用unique_lock管理锁是因为在线程进入阻塞之前,需要先将身上持有的锁先释放掉,而lock_guard不支持在构造函数加锁,析构函数解锁期间对锁做任何的操作,只有unique_lock支持!所以使用unique_lockj管理锁+条件变量的组合可以避免一些多线程并发的问题,比如死锁,竞态条件等,确保操作的正确性和同步性!

至于wait_for函数和wait_until函数用的比较少,我们大概了解一下即可,

wait_for:表示让线程在某一个时间段内(这个时间段需要我们自己设置)进行阻塞等待,如果超过这个时间段则线程被自动唤醒,也可以在等待的期间通过notify系列接口将其唤醒。

wait_until:表示让线程在某一个时间点(这个时间段需要我们自己设置)之前进行阻塞等待,如果超过这个时间点则线程被自动唤醒,也可以在等待的期间通过notify系列接口将其唤醒。

下面我们再认识一下notify系列接口:

notify_one:唤醒在条件变量阻塞队列中的任意一个线程!如果等待队列为空,则该函数不执行任何操作!

notify_all:唤醒在条件变量阻塞队列中的全部线程!如果等待队列为空,则该函数不执行任何操作!

(因为不止一个线程在条件变量上等待,所以条件变量让这些阻塞线程队列式的排队等待);

为了更好的体现和理解条件变量+互斥锁在多线程下的应用场景,这里就给出一道经典题目:

两个线程交替打印1~100,一个线程打印奇数,一个线程打印偶数,交替运行,该如何实现?

解题思路:线程互斥与同步

1:线程互斥:两个线程都需要打印数据,为了防止线程并发问题,我们需要一把锁,对共享资源加锁进行保护,确保线程打印时数据的安全性!

2:线程同步:需要两个线程交替打印,你打印1,我打印2,你打印3。。。。。;不能一个线程一直运行,我们需要一个线程在打印时,另一个线程停止(阻塞),这个线程打印完,它停下(阻塞),让另一个线程运行,形成交替,所以实现线程停下(阻塞)或者运行(唤醒)我们需要使用条件变量!!!

解题难点:

1:我们要让一个线程打印奇数,那么这个线程该怎么实现,使得它无论在什么情况下,都是它第一个运行,打印1呢?

     我们可不能说先创建的线程就先运行,因为有可能先创建的后运行,后创建的先运行!

2:我们如何控制交替打印,即不能让一个线程运行多次!!!

下面我们先解决第一个难点问题:

这样,即可实现无论在什么情况下,都可以保证线程t1先运行,但是我们看到,此时t2不是阻塞在锁上,就是阻塞在条件变量上,使得t1线程一直运行,我们该如何解决t1线程一直运行的问题呢?

此时,我们即可以让t1线程稳定的总是第一个运行,同时又通过if语句判断+条件变量(wait函数)让t1线程和t2线程交替打印,但是为什么最后的结果也只是两个线程各打印一次,那为什么不接着打印了呢?

答:因为当X=2时,t1线程阻塞在条件变量上,而t2线程执行完没有唤醒t1线程,而是继续运行,那么t2线程出了锁作用域,锁被释放,无人持有锁,

此时X=3了,t1线程还是在条件变量上阻塞着,所以锁还是被t2线程拿到,而t2线程进行if判断时,也调用了wait函数,t2线程也将被阻塞在条件变量上,锁被释放,那么就造成了锁无人持有,t1线程,t2线程都阻塞在条件变量上的局面!!!

所以究其根本,还是在于t2线程执行完没有唤醒t1线程,所以我们只需要在t2线程结束之前唤醒t1即可(形成互相唤醒的场景)!!!具体如下所示:

int main()
{int n = 100;int x = 1;mutex mtx;//定义一个全局的锁condition_variable cv;//定义全局的条件变量thread t1 = thread([&,n](){while(x<n){unique_lock<mutex> lock(mtx);//将锁用unique_lock管理if (x % 2 == 0){cv.wait(lock);}cout << this_thread::get_id() << ":" << x << endl;++x;cv.notify_one();//cv.notify_all();//这里线程互相唤醒时也可以使用notify_all函数,但不是很推荐用!}});thread t2 = thread([&, n](){while (x < n){unique_lock<mutex> lock(mtx);//将锁用unique_lock管理if (x % 2 != 0){cv.wait(lock);}cout << this_thread::get_id() << ":" << x << endl;++x;cv.notify_one();//cv.notify_all();}});t1.join();t2.join();return 0;
}

此时两个线程就可以一个打印奇数,一个打印偶数,交替的打印1~100!!!

但是这里while的循环判断条件x<n可能存在线程安全的问题,所以我们可以将while循环写死,然后在内部判断退出条件(下面将给出);同时wait函数还有另一个版本,带有模板的wait函数,下面我们用带模板的wait函数进行上述功能的实现:

int main()
{int n = 100;int x = 1;mutex mtx;//定义一个全局的锁condition_variable cv;//定义全局的条件变量thread t1 = thread([&,n](){while(1)//写成死循环,在循环内部判断退出条件{unique_lock<mutex> lock(mtx);//将锁用unique_lock管理if (x >= n){break;}//if (x % 2 == 0)//{//	cv.wait(lock);//}//如果可调用对象返回的是false,则阻塞,true则运行!cv.wait(lock, [&x]() {return x % 2 != 0; });cout << this_thread::get_id() << ":" << x << endl;++x;cv.notify_one();//cv.notify_all();//这里线程互相唤醒时也可以使用notify_all函数,但不是很推荐用!}});thread t2 = thread([&, n](){while (1){unique_lock<mutex> lock(mtx);//将锁用unique_lock管理if (x > n){break;}//if (x % 2 != 0)//{//	cv.wait(lock);//}cv.wait(lock, [&x]() {return x % 2 == 0; });cout << this_thread::get_id() << ":" << x << endl;++x;cv.notify_one();//cv.notify_all();}});t1.join();t2.join();return 0;
}

此时,我们就用带有模板的wait函数完成了两个线程交替从1打印到100,并且全部都是线程安全的!!!

综上,这是我对C++11线程库的大致了解,我这里只是介绍了非常常用的接口与场景,至于其他的很多知识点还是边查文档边用吧,毕竟线程库内容还是比较多的,我们现在掌握一些最基本,最常用的即可!

二:异常

2.1:C语言中处理错误的方式

众所周知,C++是在C语言的基础上进行的扩展,所以在了解C++异常之前,我们得先看看C语言中是如何处理代码中存在的错误!

1:当出现的是比较严重的错误,如发生内存错误,除0错误时就可以通过断言(assert)直接终止掉程序,同时,它会告诉你出错的位置,方便我们查错,但是断言(assert)只在Debug版本下生效,在Release版本下无效!

2:当出现的是其他普通的错误,没有危害到程序的运行,此时我们就可以通过返回错误码的方式告知我们程序出现错误的原因,但是返回错误码有一个很大的缺陷:就是我们需要根据返回的错误码去找错误码对应的错误信息描述;这点在我们学习Linux系统编程时有着深刻的体验!!!

所以,实际中C语言基本上都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误!而正是由于c语言处理错误时,返回的信息有限,所以C++才衍生出了异常的概念!!!

2.2:异常的概念

异常是C++中一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。下面给出使用异常时需要用的关键字:
throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
catch: 在您想要处理问题的地方,通过异常处理程序捕获异常,catch 关键字用于捕获异常,可以有多个catch进行捕获。
try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:

try
{// 保护的标识代码
}
catch( ExceptionName e1 )//ExceptionName:通过throw关键字抛出的异常的类型!
{// catch 块:可以在此作用域内,对捕捉到的异常进行处理!
}
catch( ExceptionName e2 )
{// catch 块
}
catch( ExceptionName eN )//可以通过throw关键字抛出n个异常,我们此时就需要n个catch进行捕捉!
{// catch 块
}

2.3:异常的具体用法

异常的抛出和匹配原则:

1:异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。下面我们给出一个示例,具体如下所示:

 2:被选中的处理代码是调用链中与该对象类型匹配离抛出异常位置最近的那一个

在解释这个规则之前,我们需要明白一个概念,什么叫调用链???

搞明白了什么是调用链,我们给出一个例子,方便我们了解第二条规则,具体如下所示:

这里是func函数和main函数都存在try/catch,且类型都匹配,要是类型不匹配或者没有try/catch时,又是什么样呢?

 此时,这里就说明抛异常时存在一个很大的隐患:即执行流乱跳!!!原本Division函数执行完需要接着执行func函数中的代码,但是一旦Division函数抛异常,那么它将跳到具有try/catch捕获异常的main函数中!!与我们学习的函数调用链相违背,提高了调试的难度!!!

3:抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)!


4.:catch(...)可以捕获任意类型的异常,但是我们也不知道异常错误是什么。下面我们给出一个示例,具体如下所示:

所以一般catch(...)通常放到捕获异常的最后面,捕获任意类型的异常(当然如果有和异常类型相匹配的try/catch,还是优先调用类型匹配的catch捕获,catch(...)只是底线保障!),防止因为一些异常没有被捕获,而导致程序崩溃退出!!!

5:实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获(再加上多态),这个在实际中非常实用,实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家随意抛异常,那么外层的调用者基本就没办法操作了;

所以实际中都会定义一套继承的规范体系。这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了!下面给出一个异常的基类,里面只包括错误码和错误码描述,然后用它派生出不同的子类(比如单门派生一个SQL异常类。网络异常类,缓存异常类等,用来模拟不同的项目模块)!具体如下所示:

class Exception
{
public:Exception(const string& errmsg, int id):_errmsg(errmsg), _id(id){}virtual string what() const//定义一个虚函数,让子类进行重写,以便形成多态!{return _errmsg;}int Get_id(){return _id;}
protected:string _errmsg;int _id;
};class SQL_Exception : public Exception//SQL异常子类
{
public:SQL_Exception(const string& errmsg, int id, const string& sql):Exception(errmsg,id),_sql(sql){}virtual string what() const//子类重写虚函数,{string str = "SQL_Exception:";str += _errmsg;str += " SQL语句:";str += _sql;return str;}
private:string _sql;//SQL语句};
class Cache_Exception : public Exception//缓存异常子类
{
public:Cache_Exception(const string& errmsg, int id):Exception(errmsg, id){}virtual string what() const//子类重写虚函数,{string str = "Cache_Exception:";str += _errmsg;return str;}
};
class Network_Exception : public Exception//网络异常子类
{
public:Network_Exception(const string& errmsg, int id, const string& http):Exception(errmsg, id), _http(http){}virtual string what() const//子类重写虚函数,{string str = "Network_Exception,HTTP:";str += _errmsg;str += _http;return str;}
private:string _http;//http协议
};void SQLMgr()//模拟sql异常,通过生成随机数来throw抛异常
{srand(time(0));if (rand() % 7 == 0){throw SQL_Exception("权限不足", 100, "select * from name = '张三'");}
}
void CacheMgr()//模拟缓存异常
{srand(time(0));if (rand() % 5 == 0){throw Cache_Exception("权限不足", 100);}else if (rand() % 6 == 0){throw Cache_Exception("数据不存在", 101);}SQLMgr();
}
void HttpServer()//模拟网络异常
{// ...srand(time(0));if (rand() % 3 == 0){throw Network_Exception("请求资源不存在", 100, "get");}else if (rand() % 4 == 0){throw Network_Exception("权限不足", 101, "post");}CacheMgr();
}
int main()
{while (1){this_thread::sleep_for(chrono::seconds(1));try {HttpServer();}catch (const Exception& e) // 这里捕获父类对象就可以{// 多态:子类对象通过父类的指针或者引用调用自己重写的虚函数!cout << e.what() << endl;//此时捕捉的是哪一个子类,调用的就是哪一个子类里面的what函数,打印异常信息!}catch (...)//捕捉任意类型的异常!防止某些异常没有被捕获,从而造成程序崩溃!!{cout << "未知异常" << endl;}}return 0;
}

此时我们通过继承和多态就形成了一个自定义的异常体系!那么外层的调用者就可以根据异常信息定位到确切的项目部分,甚至是具体问题!!!

其实,C++标准库当中的异常也是一个基础体系,其中exception就是各个异常类的基类,我们可以在程序中使用这些标准的异常,它们之间的继承关系如下:

下表是对上面继承体系中出现的每个异常的说明:   

2.4 异常的重新抛出

异常的重新抛出指的是在捕获异常后,再次将该异常抛出到上层调用者。这样做的主要目的是为了让更高层的代码能够处理这个异常或者继续将它向上层抛出。因为有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理!如果我们抛异常时,直接让最外层的try/catch捕获,可能会引发一些问题,具体如下所示:

double Division(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Division by zero condition!";}return (double)a / (double)b;
}
void Func()
{int* array = new int[10];int len, time;cin >> len >> time;cout << Division(len, time) << endl;//此时如果 Division函数抛异常,那么new出来的空间将无法delete,可能会造成内存泄漏等问题!cout << "delete []" << array << endl;delete[] array;
}
int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}return 0;
}

 

 所以此时我们需要在func函数中将Division函数抛出的异常捕获,再由func函数重新抛出,但是在重新抛出之前,将new出来的空间delete释放掉即可!这样就防止了内存泄漏等重大问题!

但是,如果func不仅仅调用了Division函数,也调用了其他的函数,其他的函数内也抛了异常,那么我们是不是需要在func函数内在try/catch捕获呢?

答案是肯定的,那么我们总不能func函数内每多一个异常抛出,我们就在func函数内增加一个try/catch吧!!!所以此时,我们直接在func函数中使用catch(...),将所有的任意的异常捕获,在抛出之前,将该做的事情做完(释放new出来的空间等),然后重新抛出!!!具体如下所示:

double Division(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Division by zero condition!";}return (double)a / (double)b;
}
void Division1()//此时这个函数也可能抛异常!
{if (rand()%2==0){throw 1;}return;
}
void Func()
{// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再 重新抛出去。int* array = new int[10];try {int len, time;cin >> len >> time;cout << Division(len, time) << endl;Division1();//func函数又调用一个可能会发生异常的函数}catch (...)//此时我们无论在func函数中捕获都是任意的异常,都重新抛出去,集中到main函数处理!{cout << "delete []" << array << endl;delete[] array;throw;}
}
int main()
{try{Func();}catch (const char* errmsg){cout << errmsg << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}

 这样不让最外层的函数直接捕获异常,而是经由调用链中的函数将异常重新抛出,不仅保证了程序的安全性,同时还将所有的异常都放到最外层的main函数进行处理,提高代码可靠性和健壮性!

2.5:异常安全与规范

异常安全:
构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化

析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题!


异常规范:
1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。
2. 函数的后面接throw(),表示函数不抛异常。
3. 若无异常接口声明,则此函数可以抛掷任何类型的异常

// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);// 函数后面+throw()就表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;

2.6:异常的优缺点

C++异常的优点:

1.可读性和可维护性:使用异常处理可以使代码更清晰,易于理解和维护。抛出异常可以使错误处理逻辑与正常代码分离,使代码更易于阅读。

2.错误处理:异常处理提供了一种简单有效的方法来管理程序中的错误。当程序遇到错误时,可以抛出异常并捕获它们,以便正确地处理它们。

3.可靠性和稳定性:使用异常处理可以使程序更稳定,因为可以更好地管理错误。异常处理可以使程序更可靠并减少错误的可能性,因为异常处理可以捕获和处理错误。

4.异常安全:使用异常处理可以使程序更加异常安全。异常安全是指程序在遇到异常时不会泄漏资源并保证数据的完整性。使用异常处理可以使程序更容易实现异常安全。

5.多个函数调用:使用异常处理可以在多个函数调用之间传递错误信息,使代码更模块化。当一个函数抛出异常时,可以使用异常处理机制将其传递到调用该函数的上一层函数中。

6.标准化:C++的异常处理是标准化的,并且在大多数系统和编译器上都可用。它是一种被广泛使用的错误处理机制;

C++异常的缺点:

1.性能损失:异常处理需要消耗额外的资源,包括内存和处理时间。在程序遇到异常时,为了适当地处理它们,程序必须执行额外的代码,导致效率下降。

2.代码复杂性:异常处理引入了额外的控制流,可能使代码更难理解和维护。此外,异常处理可能导致代码中的混乱,使得代码逻辑变得复杂。

3.未处理的异常:异常可能会被忽略或未正确处理。如果异常被忽略,程序可能会继续执行,导致错误的结果或系统崩溃。如果异常未正确处理,程序可能会崩溃并可能导致数据损坏。

4.不确定性:异常处理机制可能导致不确定性,即程序可能会产生不同的结果,因为异常可能在不同的时间发生或以不同的顺序发生。

5.可预测性:使用异常处理使得难以预测代码行为,因为异常可能会从任何地方抛出,使得代码执行流程变得不稳定。此外,异常的行为可能因平台或编译器而异。
总结:异常总体而言,利大于弊,所以在项目,工程实践中我们还是积极使用异常的!!!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Apollo新版本Beta技术沙龙

有幸参加Apollo开发者社区于12月2日举办的Apollo新版本(8.0)的技术沙龙会&#xff0c;地址在首钢园百度Apollo Park。由于去的比较早&#xff0c;先参观了一下这面的一些产品&#xff0c;还有专门的讲解&#xff0c;主要讲了一下百度无人驾驶的发展历程和历代产品。我对下面几个…

Java画爱心

Java画爱心代码&#xff0c;每个人都可以被需要 效果图 源代码 package com.example.test; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.Image; import java.awt.Toolkit; import javax.swing.JFrame; class Cardioid extend…

Java+Swing+Mysql实现超市管理系统

一、系统介绍 1.开发环境 操作系统&#xff1a;Win10 开发工具 &#xff1a;IDEA2018 JDK版本&#xff1a;jdk1.8 数据库&#xff1a;Mysql8.0 2.技术选型 JavaSwingMysql 3.功能模块 4.系统功能 1.系统登录登出 管理员可以登录、退出系统 2.商品信息管理 管理员可以对商品信息…

超完整的mysql安装配置方法(包含idea和navicat连接mysql,并实现建表)

mysql安装配置方法 1、下载mysql2、解压到指定的安装目录3、配置初始化文件my.ini4、配置用户变量和系统变量5、初始化mysql6、安装mysql服务并启动修改密码7、使用idea连接mysql8、使用Navicat可视化工具连接mysql&#xff0c;并实现新建数据库&#xff0c;新建表 1、下载mysq…

C语言-详解指针

目录 一.内存 1.内存的定义 2.内存的结构图 二.地址 1.什么是地址 2.什么是变量的地址 三.什么是指针 1.指针的定义 四.如何获取数据存储空间的地址 1.&运算符 五.指针变量 1.什么是指针变量&#xff08;一级指针变量&#xff09; 2.指针变量的定义 3…

<JavaEE> 什么是线程安全?产生线程不安全的原因和处理方式

目录 一、线程安全的概念 二、线程不安全经典示例 三、线程不安全的原因和处理方式 3.1 线程的随机调度和抢占式执行 3.2 修改共享数据 3.3 关键代码或指令不是“原子”的 3.4 内存可见性和指令重排序 四、Java标准库自带的线程安全类 一、线程安全的概念 线程安全是指…

Electron+Ts+Vue+Vite桌面应用系列:sqlite增删改查操作篇

文章目录 1️⃣ sqlite应用1.1 sqlite数据结构1.2 初始化数据库1.3 初始化实体类1.4 操作数据类1.5 页面调用 优质资源分享 作者&#xff1a;xcLeigh 文章地址&#xff1a;https://blog.csdn.net/weixin_43151418/article/details/134692751 ElectronTsVueVite桌面应用系列 &am…

nacos启动报错 java.lang.RuntimeException: [db-load-error]load jdbc.properties error

以standalone mode sh startup.sh -m standalone 为例子 启动nacos 报错&#xff1a; Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcatat org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(To…

Linux中shell的运行原理

在Linux中&#xff0c;每次输入命令时&#xff0c;前面都会出现一串字母&#xff0c;我们称之为命令行提示符 实际上&#xff0c;命令行提示符是一种外壳程序 外壳程序的概念&#xff1a; 前面我们提到过&#xff0c;在Linux中&#xff0c;一切皆文件&#xff0c;所谓的命令就…

C语言 - 字符函数和字符串函数

系列文章目录 文章目录 系列文章目录前言1. 字符分类函数islower 是能够判断参数部分的 c 是否是⼩写字⺟的。 通过返回值来说明是否是⼩写字⺟&#xff0c;如果是⼩写字⺟就返回⾮0的整数&#xff0c;如果不是⼩写字⺟&#xff0c;则返回0。 2. 字符转换函数3. strlen的使⽤和…

批量AI创作文案的工具,批量AI创作文章的软件

人工智能&#xff08;AI&#xff09;的应用不断拓展&#xff0c;其中批量AI创作逐渐成为许多文本创作者和企业编辑的热门选择。面对海量的文章需求&#xff0c;批量AI创作工具能够高效、快速地生成大量文本内容&#xff0c;从而减轻创作者的工作负担。本文将专心分享批量AI创作…

【C语言:自定义类型(结构体、位段、共用体、枚举)】

文章目录 1.结构体1.1什么是结构体1.2结构体类型声明1.3结构体变量的定义和初始化1.4结构体的访问 2.结构体对齐2.1如何对齐2.2为什么存在内存对齐&#xff1f; 3.结构体实现位段3.1什么是位段3.2位段的内存分配3.3位段的跨平台问题3.4位段的应用3.5位段使用注意事项 4.联合体4…

Hadoop学习笔记(HDP)-Part.09 安装OpenLDAP

目录 Part.01 关于HDP Part.02 核心组件原理 Part.03 资源规划 Part.04 基础环境配置 Part.05 Yum源配置 Part.06 安装OracleJDK Part.07 安装MySQL Part.08 部署Ambari集群 Part.09 安装OpenLDAP Part.10 创建集群 Part.11 安装Kerberos Part.12 安装HDFS Part.13 安装Ranger …

【ArcGIS Pro】探索性插值无法覆盖所需shp范围

做个小记录自用&#xff0c;实际不准。 1 看看就行 pro插值 看看过程就行。有详细过程&#xff0c;类似tutorial https://learn.arcgis.com/zh-cn/projects/interpolate-temperatures-using-the-geostatistical-wizard/ 2 注意用投影坐标系 wgs84转投影坐标系 https://blog…

Java常见CodeReview及编码规范

鉴于自己的开发经验,以及常见容易产生bug及性能问题的点做个记录. 1.数据库 如果开发人员的经验不足,Java通过ORM(Mybatis)对数据库的操作的性能问题比较隐蔽.因为不压测或者异常case没发生的时候一般发现不了问题.特别是异常case发生的时候. 除配置表以外的sql都要经过expl…

Mongodb 开启oplog,java监听oplog并写入关系型数据库

开启Oplog windows mongodb bin目录下找到配置文件/bin/mongod.cfg,配置如下&#xff1a; replication:replSetName: localoplogSizeMB: 1024双击mongo.exe 执行 rs.initiate({_id: "local", members: [{_id: 0, host: "localhost:27017"}]})若出现如…

Android : 篮球记分器app _简单应用

示例图&#xff1a; 1.导包 在build.gradle 中 加入 // 使用androidx版本库implementation androidx.lifecycle:lifecycle-extensions:2.1.0-alpha03 2. 开启dataBinding android{...// 步骤1.开启data bindingdataBinding {enabled true}...} 3.写个类继承 ViewModel pac…

消息中间件之间的区别

一.单机吞吐量 ActiveMQ&#xff1a;万级&#xff0c;吞吐量比RocketMQ和Kafka要低了一个数量级 RabbitMQ&#xff1a;万级&#xff0c;吞吐量比RocketMQ和Kafka要低了一个数量级 RocketMQ&#xff1a;10万级&#xff0c;RocketMQ也是可以支撑高吞吐的一种MQ Kafka&#xff…

Linux服务器部署XXL-JOB

参考文档及下载地址&#xff1a;分布式任务调度平台XXL-JOB 1 从git拉取XXL-JOB代码 我们的大部分变动&#xff0c;是发生在xxl-job-admin&#xff0c;最终将这个模块打包成jar包部署在linux服务器上。 2 执行数据库脚本 doc\db\tables_xxl_job.sql 3 修改pom文件&#xff0c…

Siemens-NXUG二次开发-打开与关闭prt文件[Python UF][20231206]

Siemens-NXUG二次开发-打开与关闭prt文件[Python UF][20231206] 1.python uf函数1.1 NXOpen.UF.Part.Open1.2 NXOpen.UF.Part.LoadStatus1.3 NXOpen.UF.Part.Close1.4 NXOpen.UF.Part.AskUnits 2.示例代码3.运行结果3.1 内部模式3.2 外部模式 1.python uf函数 1.1 NXOpen.UF.P…