C++ 多线程编程

1 多线程编程简介

说到多线程编程,就不得不提并行并发,多线程是实现并发和并行的一种手段。

  • 并行指两个或多个独立的操作同时进行

  • 并发指一个时间段内执行多个操作

在单核时代,多个线程是并发的,在一个时间段内轮流执行;在多核时代,多个线程可以实现真正的并行,在多核上真正独立的并行执行。例如现在常见的4核4线程可以并行4个线程;4核8线程则使用了超线程技术,把一个物理核模拟为2个逻辑核心,可以并行8个线程。

并发编程的方法

通常,要实现并发有两种方法:多进程并发和多线程并发。

多进程并发:

使用多进程并发是将一个应用程序划分为多个独立的进程(每个进程只有一个线程),这些独立的进程间可以互相通信,共同完成任务。

由于操作系统对进程提供了大量的保护机制,以避免一个进程修改了另一个进程的数据,使用多进程比多线程更容易写出安全的代码。但这也造就了多进程并发的两个缺点:

  • 在进程件的通信,无论是使用信号、套接字,还是文件、管道等方式,其使用要么比较复杂,要么就是速度较慢或者两者兼而有之。
  • 运行多个线程的开销很大,操作系统要分配很多的资源来对这些进程进行管理。

由于多个进程并发完成同一个任务时,不可避免的是:操作同一个数据和进程间的相互通信,上述的两个缺点也就决定了多进程的并发不是一个好的选择。

多线程并发:

多线程并发指的是在同一个进程中执行多个线程。

有操作系统相关知识的应该知道,线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在。也就是说,同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。这样,同一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。

由于缺少操作系统提供的保护机制,在多线程共享数据及通信时,就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避免**死锁(deadlock)**。

2 std::thread简介

C++11之前,window和linux平台分别有各自的多线程标准,使用C++编写的多线程往往是依赖于特定平台的。

  • Window平台提供用于多线程创建和管理的win32 api;
  • Linux下则有POSIX多线程标准,Threads或Pthreads库提供的API可以在类Unix上运行;

在C++11新标准中,可以简单通过使用thread库,来管理多线程。thread库可以看做对不同平台多线程API的一层包装;因此使用新标准提供的线程库编写的程序是跨平台的。

一个简单的多线程实现

C++11的标准库中提供了多线程库,使用时需要#include <thread>头文件,该头文件主要包含了对线程的管理类std::thread以及其他管理线程相关的类。下面是使用C++多线程库的简单示例:

#include <iostream>
#include <thread>using namespace std;void output(int i)
{cout << i << endl;
}int main()
{for (uint8_t i = 0; i < 4; i++){thread t(output, i);t.detach();	}getchar();return 0;
}

在一个for循环内,创建4个线程分别输出数字0、1、2、3,并且在每个数字的末尾输出换行符。语句thread t(output, i)创建一个线程t,该线程运行output,第二个参数i是传递给output的参数。t在创建完成后自动启动,t.detach表示该线程在后台允许,无需等待该线程完成,继续执行后面的语句。这段代码的功能是很简单的,如果是顺序执行的话,其结果很容易预测得到:

0
1
2
3

但是在并行多线程下,其执行的结果就多种多样了,比如下面就是代码一次运行的结果:

012
3

这就涉及到多线程编程最核心的问题:资源竞争

假设CPU有4核,可以同时执行4个线程,但是控制台却只有一个,同时只能有一个线程拥有这个唯一的控制台,将数字输出。将上面代码创建的四个线程进行编号:t0,t1,t2,t3,分别输出的数字:0,1,2,3。参照上图的执行结果,控制台的拥有权的转移如下:

  • t0拥有控制台,输出了数字0,但是其没有来的及输出换行符,控制的拥有权却转移到了t1;(0)
  • t1完成自己的输出,t1线程完成 (1\n)
  • 控制台拥有权转移给t0,输出换行符 (\n)
  • t2拥有控制台,完成输出 (2\n)
  • t3拥有控制台,完成输出 (3\n)

由于控制台是系统资源,这里控制台拥有权的管理是操作系统完成的。但是,假如是多个线程共享进程空间的数据,这就需要自己写代码控制,每个线程何时能够拥有共享数据进行操作。

共享数据的管理以及线程间的通信,是多线程编程的两大核心。

线程管理

每个应用程序至少有一个进程,而每个进程至少有一个主线程,除了主线程外,在一个进程中还可以创建多个线程。每个线程都需要一个入口函数,入口函数返回退出,该线程也会退出,主线程就是以main函数作为入口函数的线程。

在C++ 11的线程库中,将线程的管理放在了类std::thread中,使用std::thread可以创建、启动一个线程,并可以将线程挂起、结束等操作。

启动一个线程

C++ 11的线程库启动一个线程是非常简单的,只需要创建一个std::thread对象,就会启动一个线程,并使用该std::thread对象来管理该线程。

do_task();
std::thread(do_task);

这里创建std::thread传入的函数,实际上其构造函数需要的是可调用(callable)类型,只要是有函数调用类型的实例都是可以的。所以除了传递函数外,还可以使用:

  • lambda表达式

    使用lambda表达式启动线程输出数字

    for (int i = 0; i < 4; i++)
    {thread t([i]{cout << i << endl;});t.detach();
    }
  • 重载了()运算符的类的实例

    使用重载了()运算符的类实现多线程数字输出

    class Task
    {
    public:void operator()(int i){cout << i << endl;}
    };int main()
    {for (uint8_t i = 0; i < 4; i++){Task task;thread t(task, i);t.detach();	}
    }

把函数对象传入std::thread的构造函数时,要注意一个C++的语法解析错误(C++’s most vexing parse)。向std::thread的构造函数中传入的是一个临时变量,而不是命名变量就会出现语法解析错误。如下代码:

std::thread t(Task());

这里相当于声明了一个函数t,其返回类型为thread,而不是启动了一个新的线程。可以使用新的初始化语法避免这种情况

std::thread t{Task()};

当线程启动后,一定要在和线程相关联的thread销毁前,确定以何种方式等待线程执行结束

C++11有两种方式来等待线程结束:

  • detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。前面代码所使用的就是这种方式。
    • 调用detach表示thread对象和其表示的线程完全分离;
    • 分离之后的线程是不在受约束和管制,会单独执行,直到执行完毕释放资源,可以看做是一个daemon线程;
    • 分离之后thread对象不再表示任何线程;
    • 分离之后joinable() == false,即使还在执行;
  • join方式,等待启动的线程完成,才会继续往下执行。假如前面的代码使用这种方式,其输出就会0,1,2,3,因为每次都是前一个线程输出完成了才会进行下一个循环,启动下一个新线程。
    • 只有处于活动状态线程才能调用join,可以通过joinable()函数检查;
    • joinable() == true表示当前线程是活动线程,才可以调用join函数;
    • 默认构造函数创建的对象是joinable() == false;
    • join只能被调用一次,之后joinable就会变为false,表示线程执行完毕;
    • 调用 ternimate()的线程必须是 joinable() == false;
    • 如果线程不调用join()函数,即使执行完毕也是一个活动线程,即joinable() == true,依然可以调用join()函数;

无论在何种情形,一定要在thread销毁前,调用t.join或者t.detach,来决定线程以何种方式运行。

当使用join方式时,会阻塞当前代码,等待线程完成退出后,才会继续向下执行;

而使用detach方式则不会对当前代码造成影响,当前代码继续向下执行,创建的新线程同时并发执行,这时候需要特别注意:创建的新线程对当前作用域的变量的使用,创建新线程的作用域结束后,有可能线程仍然在执行,这时局部变量随着作用域的完成都已销毁,如果线程继续使用局部变量的引用或者指针,会出现意想不到的错误,并且这种错误很难排查。例如:

auto fn = [](const int *a)
{for (int i = 0; i < 10; i++){cout << *a << endl;}
};[fn]
{int a = 1010;thread t(fn, &a);t.detach();
}();

在lambda表达式中,使用fn启动了一个新的线程,在装个新的线程中使用了局部变量a的指针,并且将该线程的运行方式设置为detach。这样,在lamb表达式执行结束后,变量a被销毁,但是在后台运行的线程仍然在使用已销毁变量a的指针,这样就可能会导致不正确的结果出现。

所以在以detach的方式执行线程时,要将线程访问的局部数据复制到线程的空间(使用值传递),一定要确保线程没有使用局部变量的引用或者指针,除非你能肯定该线程会在局部作用域结束前执行结束。

当然,使用join方式的话就不会出现这种问题,它会在作用域结束前完成退出。

异常情况下等待线程完成

当决定以detach方式让线程在后台运行时,可以在创建thread的实例后立即调用detach,这样线程就会后thread的实例分离,即使出现了异常thread的实例被销毁,仍然能保证线程在后台运行。

但线程以join方式运行时,需要在主线程的合适位置调用join方法,如果调用join前出现了异常,thread被销毁,线程就会被异常所终结。为了避免异常将线程终结,或者由于某些原因,例如线程访问了局部变量,就要保证线程一定要在函数退出前完成,就要保证要在函数退出前调用join

void func() 
{thread t([]{cout << "hello C++ 11" << endl;});try{do_something_else();}catch (...){t.join();throw;}t.join();
}

上面代码能够保证在正常或者异常的情况下,都会调用join方法,这样线程一定会在函数func退出前完成。但是使用这种方法,不但代码冗长,而且会出现一些作用域的问题,并不是一个很好的解决方法。

一种比较好的方法是资源获取即初始化(RAII,Resource Acquisition Is Initialization),该方法提供一个类,在析构函数中调用join

class thread_guard
{thread &t;
public :explicit thread_guard(thread& _t) :t(_t){}~thread_guard(){if (t.joinable())t.join();}thread_guard(const thread_guard&) = delete;thread_guard& operator=(const thread_guard&) = delete;
};void func(){thread t([]{cout << "Hello thread" <<endl ;});thread_guard g(t);
}

无论是何种情况,当函数退出时,局部变量g调用其析构函数销毁,从而能够保证join一定会被调用。

向线程传递参数

向线程调用的函数传递参数也是很简单的,只需要在构造thread的实例时,依次传入即可。例如:

void func(int *a,int n){}int buffer[10];
thread t(func,buffer,10);
t.join();

需要注意的是,默认的会将传递的参数以拷贝的方式复制到线程空间,即使参数的类型是引用。例如:

void func(int a,const string& str);
thread t(func,3,"hello");

func的第二个参数是string &,而传入的是一个字符串字面量。该字面量以const char*类型传入线程空间后,在**线程的空间内转换为string**。

如果在线程中使用引用来更新对象时,就需要注意了。默认的是将对象拷贝到线程空间,其引用的是拷贝的线程空间的对象,而不是初始希望改变的对象。如下:

class _tagNode
{
public:int a;int b;
};void func(_tagNode &node)
{node.a = 10;node.b = 20;
}void f()
{_tagNode node;thread t(func, node);t.join();cout << node.a << endl ;cout << node.b << endl ;
}

在线程内,将对象的字段a和b设置为新的值,但是在线程调用结束后,这两个字段的值并不会改变。这样由于引用的实际上是局部变量node的一个拷贝,而不是node本身。在将对象传入线程的时候,调用std::ref,将node的引用传入线程,而不是一个拷贝。例如:thread t(func,std::ref(node));

也可以使用类的成员函数作为线程函数,示例如下

class _tagNode{public:void do_some_work(int a);
};
_tagNode node;thread t(&_tagNode::do_some_work, &node,20);

上面创建的线程会调用node.do_some_work(20),第三个参数为成员函数的第一个参数,以此类推。

转移线程的所有权

thread是可移动的(movable)的,但不可复制(copyable)。可以通过move来改变线程的所有权,灵活的决定线程在什么时候join或者detach。

thread t1(f1);
thread t3(move(t1));

将线程从t1转移给t3,这时候t1就不再拥有线程的所有权,调用t1.joint1.detach会出现异常,要使用t3来管理线程。这也就意味着thread可以作为函数的返回类型,或者作为参数传递给函数,能够更为方便的管理线程。

线程的标识类型为std::thread::id,有两种方式获得到线程的id。

  • 通过thread的实例调用get_id()直接获取
  • 在当前线程上调用this_thread::get_id()获取

3 互斥锁std::mutex

std::mutex 是C++11 中最基本的互斥量,std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁。

std::mutex 成员函数

构造函数

std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。

lock()

1、如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
2、 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
3、 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。

unlock()

解锁,释放对互斥量的所有权。

try_lock()

尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况:
1、 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
2、 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
3、 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。(这一条存疑,实测单线程内先lock再trylock,不会死锁,会有1ms以内的停顿,trylock返回false)

std::lock_guard

lock_guard是一个互斥量包装程序,它提供了一种方便的RAII(Resource acquisition is initialization )风格的机制来在作用域块的持续时间内拥有一个互斥量。
创建lock_guard对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开lock_guard对象的作用域时,lock_guard析构并释放互斥量。

1、创建即加锁,作用域结束自动析构并解锁,无需手工解锁
2、不能中途解锁,必须等作用域结束才解锁
3、不能复制

std::unique_lock

unique_lock是lock_guard的升级版,它允许延迟锁定,限时深度锁定,递归锁定,锁定所有权的转移以及与条件变量一起使用。

1、创建时可以不锁定(通过指定第二个参数为std::defer_lock),而在需要时再锁定
2、可以随时加锁解锁
3、作用域规则同 lock_grard,析构时自动释放锁
4、不可复制,可移动
5、条件变量需要该类型的锁作为参数(此时必须使用unique_lock)

lock_guard 和unique_lock 并不管理 std::mutex 对象的生命周期,在使用 lock_guard 的过程中,如果 std::mutex 的对象被释放了,那么在 lock_guard 析构的时候进行解锁就会出现空指针错误。

std::mutex实测代码

#include <mutex>
#include <unistd.h>
std::mutex g_mutex;
pthread_t Tid[10];
void* threadfunc0(void*)
{printf("threadfunc0\n");std::unique_lock<std::mutex> locker(g_mutex);
//    g_mutex.unlock();/*如果执行,则立即打印threadfunc1,after lock,不会等待5秒*/sleep(5);return nullptr;
}void* threadfunc1(void*)
{printf("threadfunc1\n");std::unique_lock<std::mutex> locker(g_mutex);if(g_mutex.try_lock() == true)//不会死锁,返回false,有1ms内的停顿{printf("try_lock\n");g_mutex.unlock();}printf("threadfunc1,after lock\n");return nullptr;
}MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);setbuf(stdout,nullptr);pthread_create(&Tid[0],nullptr,threadfunc0,nullptr);usleep(100);pthread_create(&Tid[1],nullptr,threadfunc1,nullptr);
}



打印:

threadfunc0
threadfunc1
(等待5秒)
threadfunc1,after lock
 

4 std::condition_variable

4.1 介绍:

头文件 #include <condition_variable>

std::condition_variable 是 C++11 提供的一种线程同步原语,它可以用于在线程之间等待和通知事件的发生。

通常情况下,线程需要等待某个条件变量被满足后才能继续执行,这个等待的过程通常使用 std::unique_lock 和std::condition_variable 一起使用。

4.2 std::condition_variable 类提供了两个主要的方法:

  • wait(lock):等待条件变量的通知,如果条件变量没有被满足,线程将被阻塞并释放关联的互斥锁 lock。当条件变量被满足时,线程会重新获取互斥锁并继续执行。
  • notify_one() 或 notify_all():通知等待在条件变量上的线程条件已经被满足,等待的线程将被唤醒,继续执行。

使用 std::condition_variable 的一般流程如下 :

  • 首先定义一个互斥量(std::mutex)和一个条件变量(std::condition_variable),用来保证线程安全和线程之间的同步。
std::mutex _queueMutex;
std::condition_variable _queueCond;
  • 在往任务队列中添加任务时,需要先获取互斥锁,并检查队列是否已满。如果已满,则使用条件变量等待有空闲位置。
// 添加任务
void addTask(Task task) {// 获取互斥锁std::unique_lock<std::mutex> lock(_queueMutex);// 如果队列已满,则等待有空闲位置_queueCond.wait(lock, [this](){ return _taskQueue.size() < _maxQueueSize; });// 将任务添加到队列中_taskQueue.push_back(std::move(task));
}
  • 在从任务队列中取出任务时,也需要获取互斥锁,并检查队列是否为空。如果为空,则使用条件变量等待有任务可取。
// 取出任务
Task takeTask() {// 获取互斥锁std::unique_lock<std::mutex> lock(_queueMutex);// 如果队列为空,则等待有任务可取_queueCond.wait(lock, [this](){ return !_taskQueue.empty(); });// 从队列中取出一个任务Task task = std::move(_taskQueue.front());_taskQueue.pop_front();return task;
}


通过使用条件变量,往任务队列中添加任务的线程和从任务队列中取出任务的线程可以有效地同步,避免了死锁和竞争条件的问题。同时,也避免了线程空转,减少了资源的浪费。
 

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

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

相关文章

RabbitMQ死信交换机、TTL及延迟队列

一&#xff0c;死信交换机 1&#xff0c;什么是死信交换机 了解死信交换机之前我们先来了解一下什么是死信&#xff0c;当一个队列满足下面的三种情况的时候我们一般称为死信&#xff08;dead letter&#xff09;&#xff1a; 消费者使用basic.reject或 basic.nack声明消费失…

小白入门深度学习 | 6-6:Inception v3 算法原理

一、理论基础 Inceptionv3论文:Rethinking the Inception Architecture for Computer Vision.pdf Inception v3由谷歌研究员Christian Szegedy等人在2015年的论文《Rethinking the Inception Architecture for Computer Vision》中提出。Inception v3是Inception网络系列的第三…

petalinux 无法通过SDK进行TCF调试

IP地址设置没问题 但是无法进行DEBUG 原因是没有开启debug模式&#xff0c;做下图设置重新编译程序生成BOOT.bin即可

追踪 Kubernetes 中的 DNS 查询

在过去的文章中&#xff0c;我们曾 追踪过 Kubernetes 中的网络数据包&#xff0c;这篇文章将追踪 Kubernetes 中的 DNS 查询。 让我们以在 Pod 中解析 Service 完全限定域名&#xff08;FQDN&#xff09; foo.bar.svc.cluster.local 为例。 在开始之前&#xff0c;先回顾下 …

微信小程序的目录解析--【浅入深出系列001外篇】

浅入深出系列总目录在000集 如何0元学微信小程序–【浅入深出系列000】 文章目录 本系列校训学习资源的选择先说总目录经常碰到的文件(目录&#xff09;最最常见的目录pages次最常用的就是images 目录 操作起来真正的操作 配套资源 本系列校训 用免费公开视频&#xff0c;卷…

Nginx问题汇总

为什么Nginx性能这么高&#xff1f; 主要是因为他的事件处理机制&#xff1a;异步非阻塞事件处理机制&#xff08;事件驱动的异步模型&#xff09;&#xff1a;运用了epoll模型&#xff0c;Nginx 会创建一些事件对象&#xff0c;然后将这些事件对象注册到事件驱动器中。当事件…

串口wifi6+蓝牙二合一系列模块选型参考和外围电路参考设计-WG236/WG237

针对物联网数据传输&#xff0c;智能控制等应用场景研发推出的高集成小尺寸串口WiFi串口蓝牙的二合一组合模块。WiFi符合802.11a/b/g/n无线标准&#xff0c;蓝牙支持低功耗蓝牙V4.2/V5.0 BLE/V2.1和EDR&#xff0c;WiFi部分的接口是UART&#xff0c;蓝牙部分是UART/PCM 接口。模…

3Ds max图文教程:高精度篮球3D建模

推荐&#xff1a; NSDT场景编辑器助你快速搭建可二次开发的3D应用场景 第 1 步。使用以下设置在顶部视口上创建球体&#xff1a; 第 2 步。将球体转换为可编辑的多边形&#xff1a; 第 3 步。转到 Edge 子对象级别并剪切以下边缘&#xff1a; 第 4 步。选择以下边&#xff0c;然…

什么是 Elasticsearch 索引?

作者&#xff1a;David Brimley 索引这个术语在科技界已经被用满了。 如果你问大多数开发人员什么是索引&#xff0c;他们可能会告诉你索引通常指的是关系数据库 (RDBMS) 中与表关联的数据结构&#xff0c;它提高了数据检索操作的速度。 但什么是 Elasticsearch 索引&#xff…

自然语言处理从入门到应用——LangChain:代理(Agents)-[自定义MRKL代理]

分类目录&#xff1a;《自然语言处理从入门到应用》总目录 本文将介绍如何创建自己的自定义MRKL Agent。MRKL Agent由三个部分组成&#xff1a; 工具&#xff1a;代理可用的工具。LLMChain&#xff1a;生成以一定方式解析的文本&#xff0c;以确定要采取哪个动作。代理类本身&…

目标检测YOLO实战应用案例100讲-基于深度学习的雾天交通图像车辆及行人目标检测研究

目录 前言 图像去雾算法 目标检测算法 相关理论及图像去雾实验

k8s-rancher应用

Rancher 管理 Kubernetes 集群 //Rancher 简介 Rancher 是一个开源的企业级多集群 Kubernetes 管理平台&#xff0c;实现了 Kubernetes 集群在混合云本地数据中心的集中部署与管理&#xff0c; 以确保集群的安全性&#xff0c;加速企业数字化转型。超过 40000 家企业每天使用 …

Stable Diffusion配置要求,显卡推荐

Stable Diffusion 是一款流行的人工智能图像生成器&#xff0c;您可以在自己的 PC 上运行。但是运行Stable Diffusion的最低规格是多少&#xff0c;哪些组件最重要&#xff1f; Stable Diffusion需要什么 PC 硬件&#xff1f; Stable Diffusion最关键的一个组件是显卡 (GPU)。…

基于scrcpy的Android群控项目重构,获取Android屏幕元素信息并编写自动化事件

系列文章目录 基于scrcpy的远程调试方案 基于scrcpy的Android群控项目重构 基于scrcpy的Android群控项目重构 进阶版 基于scrcpy的Android群控项目重构&#xff0c;获取Android屏幕元素信息并编写自动化事件&#xff08;视频&#xff09; 基于scrcpy的Android群控项目重构…

大数据技术笔试题库--带答案

一、单选: 1、下列选项中&#xff0c;执行哪一个命令查看Linux系统的IP配置。A A、ipconfig B、find C、ifconfig D、arp -a 2、在MapReduce程序中&#xff0c;map()函数接收的数据格式是&#xff08;D&#xff09;。 A、字符串 B、整型 C、Long D、键值对 3、下列选…

全国节能宣传周丨物通博联智慧能源解决方案助力节能降碳

今年7月10日至16日&#xff0c;为全国第33个节能宣传周。今年全国节能宣传周活动主题是“节能降碳&#xff0c;你我同行”。 全国节能宣传周活动是在1990年国务院第六次节能办公会议上确定的活动周&#xff0c;开展该活动是实施全面节约战略、开展节能降碳宣传教育、推动形成绿…

Vue3组件间的通信方式

目录 1.props父向子组件通信 2.自定义事件 子向父组件通信 3.全局事件总线 4.v-model组件通信&#xff08;父子组件数据同步&#xff09; 绑定单个数据同步 绑定多个数据同步 5.useAttrs组件通信 6.ref与$parent ref获取子组件实例对象 $parent获取父组件实例对象 7.p…

postgresql内核分析 spinlock与lwlock原理与实现机制

​专栏内容&#xff1a; postgresql内核源码分析 手写数据库toadb 并发编程 个人主页&#xff1a;我的主页 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物. 概述 在postgresql 中&#xff0c;有大量的并发同步&#xff0…

260道网络安全工程师面试题汇总(附答题解析+配套资料)

由于我之前写了不少网络安全技术相关的文章和回答&#xff0c;不少读者朋友知道我是从事网络安全相关的工作&#xff0c;于是经常有人私信问我&#xff1a; 我刚入门网络安全&#xff0c;该怎么学&#xff1f; 想找网络安全工作&#xff0c;应该要怎么进行技术面试准备&…

Oracle删除约束条件不会锁表

最近有个需求要删除一个Oracle约束条件&#xff0c;但是由于不知道会不会锁表&#xff0c;所以测试了一下 使用python写了段代码验证下 import cx_Oracle conn cx_Oracle.connect(dba_li/oracle192.168.56.105:1521/orcl) #用自己的实际数据库用户名、密码、主机ip地址 替…