【Linux】多线程互斥与同步

文章目录

  • 一、线程互斥
    • 1. 线程互斥的引出
    • 2. 互斥量
    • 3. 互斥锁的实现原理
  • 二、可重入和线程安全
  • 三、线程和互斥锁的封装
    • 1. 线程封装
    • 1. 互斥锁封装
  • 四、死锁
    • 1. 死锁的概念
    • 2. 死锁的四个必要条件
    • 3. 避免死锁
  • 五、线程同步
    • 1. 线程同步的理解
    • 2. 条件变量


一、线程互斥

1. 线程互斥的引出

互斥 指的是一种机制,用于确保在同一时刻只有一个进程或线程能够访问共享资源或执行临界区代码。 互斥的目的是 防止多个并发执行的进程或线程访问共享资源时产生竞争条件,从而保证数据的一致性和正确性,下面我们来使用多线程来模拟实现一个抢票的场景,看看所产生的现象。

#include <iostream>
#include <cstring>
#include <cassert>
#include <pthread.h>
#include <unistd.h>
#include "lockGuard.hpp"
#include "Thread.hpp"
using namespace std;
int tickets = 1000; // 加锁保证共享资源的安全性void* threadRoutine(void* args)
{string name = static_cast<const char*>(args);while(true){if(tickets > 0){usleep(2000); // 模拟抢票花费的时间cout << name << " get a ticket: " << tickets-- << endl;}else{break;}usleep(1000);}return nullptr;
}int main()
{// 创建四个线程pthread_t tids[4];int n = sizeof(tids) / sizeof(tids[0]);for(int i = 0; i < n; i++){char* data = new char[64];snprintf(data, 64, "thread-%d", i + 1);pthread_create(tids + i, nullptr, threadRoutine, data);}for(int i = 0; i < 4; i++){pthread_join(tids[i], nullptr);}return 0;
}

在这里插入图片描述

这里我们可以看到,当全局变量tickets被几个执行流共享时,最后变成了-1,这是因为如果我们如果使用多线程对一个全局变量修改时,线程之间会相互影响,导致线程安全问题。

下面我们来看一下当多个线程对共享变量进行修改时,为什么会发生上述的线程安全问题?

假设有一个全局变量 g_val=100被两个线程,线程A 和 线程B共享,在多线程环境下分别对同一个全局变量g_val进行操作。

当对变量进行操作时会分为三个步骤:

  1. CPU把内存中的数据读到寄存器里
  2. 在寄存器中对数据进行计算
  3. 将修改后的数据从寄存器里写回内存

在这里插入图片描述

下面我们来看一下线程A和线程B对全局变量进行操作时的过程:

  1. 线程A执行g_val- -操作
    在这里插入图片描述
    当线程A执行完第二步时,正准备执行第三步时,时间片到了,线程A需要将自己的上下文和数据带走。
    此时的线程A认为自己已经将数据修改99了,当下一次执行时继续执行步骤三。

  2. 线程B在while中执行g_val- -操作
    在这里插入图片描述

线程B通过while循环了90次将g_val修改成了10,此时时间片到了。因此线程B也将自己的上下文保存了起来。

  1. 继续执行线程A
    在这里插入图片描述

由于上次执行线程A时第3步没有执行,所以线程A继续执行第3步。但是内存中的g_val为上次线程B修改后的值10,所以线程A又将内存中的值改成了99。

因此,一切的原因都是修改全局变量时线程调度切换、并发访问进而导致了数据不一致;想要解决这个问题,我们就需要进行加锁保护。


2. 互斥量

要解决以上问题,需要做到三点:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫 互斥量

在这里插入图片描述

💕 初始化互斥量

  1. 静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  1. 动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
  • 参数:
    mutex:要初始化的互斥量
    attr:NULL

💕 互斥量加锁和解锁

// 加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 返回值:成功返回0,失败返回错误号

💕 销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex)

注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

调用 pthread_ lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁

💕 下面我们来使用互斥锁来改进一下改进上面的售票系统:

在这里插入图片描述

int tickets = 1000; // 加锁保证共享资源的安全性
pthread_mutex_t mutex; // 定义一把锁void* threadRoutine(void* args)
{string name = static_cast<const char*>(args);while(true){pthread_mutex_lock(&mutex);if(tickets > 0){usleep(2000); // 模拟抢票花费的时间cout << name << " get a ticket: " << tickets-- << endl;pthread_mutex_unlock(&mutex);}else{pthread_mutex_unlock(&mutex);break;}usleep(1000);}return nullptr;
}int main()
{pthread_mutex_init(&mutex, nullptr); // 初始化锁// 创建四个线程pthread_t tids[4];int n = sizeof(tids) / sizeof(tids[0]);for(int i = 0; i < n; i++){char* data = new char[64];snprintf(data, 64, "thread-%d", i + 1);pthread_create(tids + i, nullptr, threadRoutine, data);}for(int i = 0; i < 4; i++){pthread_join(tids[i], nullptr);}pthread_mutex_destroy(&mutex);return 0;
}

在这里插入图片描述

因为加锁会导致临界区代码串行访问(互斥),从而导致代码的执行效率减低,因此我们在加锁之后会发现代码的运行速度比不加锁之前慢了许多。因此,进行加锁访问时,保证加锁的粒度越小越好,不要将不访问临界区资源的代码加锁。


3. 互斥锁的实现原理

互斥锁的进一步认识:

  • 加了锁之后,线程在临界区中也会被切换,但这样也不会有问题。因为线程是带着锁进行线程切换的,其余线程是无法申请到锁的,无法进入临界区访问临界资源。
  • 错误的编码方式:线程不申请锁直接访问临界区资源,这样的话,就算别的线程持有锁,该线程也可以进入到临界区。
  • 在没有持有锁的线程看来,对该线程最有意义的情况只用两种:
    1. 线程 1 没有持有锁(什么都没做)
    2. 线程 1 释放锁(做完),此时我可以申请锁。那么在线程 1 持有锁的期间,所做的所有操作在其他线程看来都是原子的!
  • 加锁后,执行临界区的代码一定是串行执行的!
  • 要访问临界资源,每一个线程都必须先申请锁,那么每一个线程都必须先看到同一把锁并访问它,所以锁本身也是一种共享资源。那么锁肯定也要保护起来,为了保护锁的安全,申请和释放锁的操作都必须是原子的!

互斥锁的细节:

  1. 凡是访问同一个临界资源的线程,都要进行加锁保护,而且必须加同一把锁,这个是一个游戏规则,不能有例外。
  2. 每一个线程访问临界区之前,得加锁,加锁本质是给 临界区 加锁,加锁的粒度尽量要细一些
  3. 线程访问临界区的时候,需要先加锁->所有线程都必须要先看到同一把锁->锁本身就是公共资源->锁如何保证自己的安全?-> 加锁和解锁本身就是原子的!
  4. 临界区可以是一行代码,可以是一批代码,
    a. 线程可能被切换吗?当然可能, 不要特殊化加锁和解锁,还有临界区代码。
    b. 此时线程进行切换会有影响吗?不会,因为在我不在期间,任何人都没有办法进入临界区,因为他无法成功的申请到锁!因为锁被我拿走了!
  5. 这也正是体现互斥带来的串行化的表现,站在其他线程的角度,对其他线程有意义的状态就是:锁被我申请(持有锁),锁被我释放了(不持有锁), 原子性就体现在这里
  6. 解锁的过程也被设计成为原子的!

互斥锁的原理:

为了实现互斥锁操作,大多数体系结构都提供了 swapexchange 指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

下面我们来根据lockunlock的伪代码来分析一下加锁和解锁的过程:

在这里插入图片描述

线程A:

  1. movb $0,al 调用线程,向自己的上下文写入0
    在这里插入图片描述

  2. xchgb %al,mutex 将cpu的寄存器中的%al 与 内存中的mutex 进行交换,本质是将共享数据交换到 自己的私有的上下文中。交换只有 一条汇编指令 ,要么没交换,要不就交换完了,即加锁的原子性
    在这里插入图片描述

  3. 判断al寄存器中的内容是否大于0,如果大于0,证明加锁成功。
    在这里插入图片描述

线程B:

  1. 切换成线程B,继续执行前两条指令,先将 al寄存器数据置为0,再将寄存器中的数据 与 内存中的数据进行交换。

在这里插入图片描述

  1. 接着判断al寄存器中的内容是否大于0,发现并不大于0,说明b申请锁失败,紧接着b线程被挂起等待,同时b的上下文随着b的挂起被带走。

  2. 当A线程再次被切换回来时,继续执行上次还未执行的判断,发现al中的数据大于0,加锁成功
    在这里插入图片描述

  3. 线程A释放锁,movb $1,mutex 将内存中mutex的数据置为1,唤醒等待Mutex的线程,此时切换成线程B

  4. 线程B执行lock的前两条指令,此时就可以加锁成功了。在这里插入图片描述


二、可重入和线程安全

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数;否则,是不可重入函数。

💕 常见的线程不安全的情况:

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

💕 常见的线程安全的情况:

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

💕 常见的可重入的情况:

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

💕 常见的不可重入的情况:

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

💕 可重入与线程安全的联系:

  • 函数是可重入的,那就是线程安全的。线程安全的函数,不一定是可重入函数
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题(如:printf 函数是不可重入的,多线程向显示器上打印数据时,数据可能会黏在一起)
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

三、线程和互斥锁的封装

1. 线程封装

💕 Threa.hpp

#pragma once#include <iostream>
#include <cstdlib>
#include <string>
#include <pthread.h>
using namespace std;class Thread
{
public:typedef enum{NEW = 0,RUNNING,EXITED} ThreadStatus;typedef void (*func_t)(void*);public:Thread(int num, func_t func, void* args) :_tid(0), _status(NEW),_func(func),_args(args){char name[128];snprintf(name, 128, "thread-%d", num);_name = name;}int status(){ return _status; }string threadname(){ return _name; }pthread_t get_id(){if(_status == RUNNING)return _tid;elsereturn 0;}static void* thread_run(void* args){Thread* ti = static_cast<Thread*>(args);(*ti)();return nullptr;}void operator()(){if(_func != nullptr)_func(_args);}void run() // 封装线程运行{int n = pthread_create(&_tid, nullptr, thread_run, this);if(n != 0)exit(-1);_status = RUNNING; // 线程状态变为运行}void join() // 疯转线程等待{int n = pthread_join(_tid, nullptr);if(n != 0){cout << "main thread join thread: " << _name << "error" << endl;return;}_status = EXITED;}~Thread(){}
private:pthread_t _tid;string _name;func_t _func; // 线程未来要执行的回调void* _args;ThreadStatus _status;
};

在这里插入图片描述

1. 互斥锁封装

💕 lockGuard.hpp

class Mutex // 自己不维护锁,有外部传入
{
public:Mutex(pthread_mutex_t *mutex):_pmutex(mutex){}void lock(){pthread_mutex_lock(_pmutex);}void unlock(){pthread_mutex_unlock(_pmutex);}~Mutex(){}
private:pthread_mutex_t *_pmutex;
};class LockGuard // 自己不维护锁,有外部传入
{
public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){_mutex.lock();}~LockGuard(){_mutex.unlock();}
private:Mutex _mutex;
};

在这里插入图片描述


四、死锁

1. 死锁的概念

死锁 是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

下面我们通过一个小故事来让大家理解一下死锁:

有两个小朋友张三和李四,共同去了一家商店,想要购买一块1块钱的棒棒糖,但是他们两个各自都只有五毛钱。因此张三想要李四手里的五毛钱去买棒棒糖让自己吃,但这时候李四就不乐意了,他也想想要张三手里的五毛钱去买棒棒糖让自己吃。因此两个人陷入了僵局,因此买棒棒糖吃这件事情就一直无法推进下去。

  • 两个小朋友可以看作是两个线程,两个不同的小朋友可以看作两把不同的锁
  • 棒棒糖是临界资源,老板就是操作系统
  • 想要访问临界资源,必须同时拥有两把锁

在操作系统中我们可以通过两个线程的案例来理解死锁:

在这里插入图片描述

虽然一般来说产生死锁是因为两把及两把以上的锁导致的,但是一把锁也有可能会产生死锁。


2. 死锁的四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

3. 避免死锁

  1. 不加锁
  2. 主动释放锁
    (假设要有两把锁才能获取临界资源,本身有一把锁,在多次申请另一把锁时申请不到,就把自身的锁释放掉)
  3. 按照顺序申请锁
    (假设有线程A和B,线程A申请锁时,必须保持先A再B,线程B申请锁时,也必须保持先A再B
    当线程A申请到A锁时,线程B也申请到A,就不会出现互相申请的情况了)
  4. 控制线程统一释放锁
    (将所有线程 申请的锁 使用一个线程 全部释放掉,就不会出现死锁了)

证明:一个线程申请的锁,可以由另一个线程来释放

#include <iostream>
#include <unistd.h>
#include <pthread.h>using namespace std;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//一个线程加锁, 另一个线程释放锁void* threadRoutine(void* args)
{cout << "I am a new thread" << endl;pthread_mutex_lock(&mutex);cout << "I get a mutex!" << endl;pthread_mutex_lock(&mutex);cout << "I alive again" << endl;return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);sleep(3);cout << "main thread run begin" << endl;pthread_mutex_unlock(&mutex);cout << "main thread unlock..." << endl;sleep(3);return 0;
}

在这里插入图片描述

由运行结果我们就可以看出,说明一个线程申请一把锁,可以由另一个线程释放。


五、线程同步

1. 线程同步的理解

互斥锁存在的两种不合理的情况:

  • 一个线程频繁的申请到锁,别人无法申请到锁,导致别人饥饿的问题
  • 上述的抢票系统,修改一下,当票数为0时,并不会立即退出。而是等待票数的增加,在等待票数增加的过程中,线程会频繁的申请锁和释放锁。这样的情况会导致资源的浪费。

线程同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做线程同步。

当我们访问临界资源前,需要先做临界资源是否存在的检测,检测的本质也是访问临界资源。那么对临界资源的检测也一定要在加锁和解锁之间。常规的方法检测临界资源是否就绪,就注定了我们必须频繁地申请锁和释放锁。


2. 条件变量

想要解决线程频繁申请和释放锁的问题,需要做到以下两点:

  • 不要让线程在频繁的检测资源是否就绪,而是让线程在资源未就绪时进行等待。
  • 当资源就绪的时候,通知等待该资源的线程,让这些线程来进行资源的申请和访问。

达到以上两点要求就是条件变量,条件变量可以通过允许线程阻塞和等待另一个线程发送信号来弥补互斥锁的不足,所以互斥锁和条件变量通常是一起使用的。

条件变量是一种线程同步机制,用于在多线程环境下实现线程间的协调与通信。他在处理竞态条件和线程间的互斥等问题上具有重要作用。

💕 条件变量初始化

// 初始化方式一:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
// 初始化方式二:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

参数

  • cond:要初始化的条件变量
  • attr:NULL

💕 条件变量销毁

int pthread_cond_destroy(pthread_cond_t *cond)

💕 等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

参数:

  • cond:要在这个条件变量上等待
  • mutex:互斥量

💕 唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒全部的线程
int pthread_cond_signal(pthread_cond_t *cond); // 唤醒该条件变量下等待的线程
#include <iostream>
#include <cstdio>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;const int num = 5;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void* active(void* args)
{string name = static_cast<const char*>(args);while(true){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond, &mutex);// pthread_cond_wait,调用的时候,会自动释放锁cout << name << "活动" << endl;pthread_mutex_unlock(&mutex);}
}int main()
{pthread_t tids[num];for(int i = 0; i < num; i++){char* name = new char[32];snprintf(name, 32, "pthread-%d", i + 1);pthread_create(tids + i, nullptr, active, name);}sleep(3);while(true){cout << "main thread wakeup other thread..." << endl;pthread_cond_broadcast(&cond);sleep(1);}for(int i = 0; i < num; i++){pthread_join(tids[i], nullptr);}return 0;
}

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

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

相关文章

卷积网络:实现手写数字是识别50轮准确率97.3%

卷积网络&#xff1a;实现手写数字是识别50轮准确率 1 导入必备库2 torchvision内置了常用数据集和最常见的模型3 数据批量加载4 绘制样例5 创建模型7 设置是否使用GPU8 设置损失函数和优化器9 定义训练函数10 定义测试函数11 开始训练12 绘制损失曲线并保存13 绘制准确率曲线并…

机器人连续位姿同步插值轨迹规划—对数四元数、b样条曲线、c2连续位姿同步规划

简介&#xff1a;Smooth orientation planning is benefificial for the working performance and service life of industrial robots, keeping robots from violent impacts and shocks caused by discontinuous orientation planning. Nevertheless, the popular used quate…

学习记忆——方法篇——连锁拍照、情景故事和逻辑故事法

三大方法速记这些内容 1、连锁拍照法速记重要事件 2、情景故事速记速记购物信息 3、逻辑故事法速记客户档案 一、连锁拍照法速记重要事件 例&#xff1a;女朋友在出差之前嘱咐男朋友几件事 1、把房间收拾干净&#xff0c;最重要的是要把书架整理了&#xff0c;垃圾倒了 2、记…

Spring+MyBatis使用collection标签的两种使用方法

目录 项目场景&#xff1a; 实战操作&#xff1a; 1.创建菜单表 2.创建实体 3.创建Mapper 4.创建xml 属性描述&#xff1a; 效率比较&#xff1a; 项目场景&#xff1a; 本文说明了Spring BootMyBatis使用collection标签的两种使用方法 1. 方法一: 关联查询 2. 方法…

学习Bootstrap 5的第九天

目录 列表组 基础的列表组 实例 活动的列表项 实例 禁用的列表项 实例 链接列表项 实例 移除列表边框 实例 带编号的列表组 实例 水平列表组 实例 多种颜色列表项 实例 多种颜色的链接列表项 实例 带徽章的列表组 实例 列表组案例 实例一 实例二 列表组…

连nil切片和空切片一不一样都不清楚?那BAT面试官只好让你回去等通知了。

连nil切片和空切片一不一样都不清楚&#xff1f;那BAT面试官只好让你回去等通知了。 问题 package mainimport ("fmt""reflect""unsafe" )func main() {var s1 []ints2 : make([]int,0)s4 : make([]int,0)fmt.Printf("s1 pointer:%v, s2 p…

NLP机器翻译全景:从基本原理到技术实战全解析

目录 一、机器翻译简介1. 什么是机器翻译 (MT)?2. 源语言和目标语言3. 翻译模型4. 上下文的重要性 二、基于规则的机器翻译 (RBMT)1. 规则的制定2. 词典和词汇选择3. 限制与挑战4. PyTorch实现 三、基于统计的机器翻译 (SMT)1. 数据驱动2. 短语对齐3. 评分和选择4. PyTorch实现…

本地MQTT服务器搭建(EMQX)

一、下载EMQX 下载地址&#xff1a;EMQ (emqx.com) 打开官网后&#xff0c;选择右边的免费试用按钮 然后单击EMQX Enterprise标签&#xff0c;然后选择下面的EMQX开源版&#xff0c;选择开源版的系统平台为Windows&#xff0c;单击免费下载。 在新页面下单击立即下载 二、安装…

Kotlin(六) 类

目录 创建类 调用类 类的继承------open 构造函数 创建类 创建类和创建java文件一样&#xff0c;选择需要创建的目录New→Kotlin File/Class Kotlin中也是使用class关键字来声明一个类的&#xff0c;这一点和Java一致。现在我们可以在这个类中加入字段和函数来丰富它的功…

循环语句详解

文章目录 循环语句详解1. 循环使用 v-for 指令2. v-for 还支持一个可选的第二个参数&#xff0c;参数值为当前项的索引3. 模板template 中使用 v-for4. v-for 迭代对象-第一个参数为value5. v-for的第二个参数为键名6. v-for的第三个参数为索引7. v-for迭代整数8. computed计算…

leetcode 第454题.四数相加II

给你四个整数数组 nums1、nums2、nums3 和 nums4 &#xff0c;数组长度都是 n &#xff0c;请你计算有多少个元组 (i, j, k, l) 能满足&#xff1a; 0 < i, j, k, l < nnums1[i] nums2[j] nums3[k] nums4[l] 0 454. 四数相加 II - 力扣&#xff08;LeetCode&#xf…

大型语言模型,第 1 部分:BERT

一、介绍 2017是机器学习中具有历史意义的一年&#xff0c;当变形金刚模型首次出现在现场时。它在许多基准测试上都表现出色&#xff0c;并且适用于数据科学中的许多问题。由于其高效的架构&#xff0c;后来开发了许多其他基于变压器的模型&#xff0c;这些模型更专注于特定任务…

rust编译出错:error: failed to run custom build command for `ring v0.16.20`

安装 Visual Studio&#xff0c;确保选择 —.NET 桌面开发、使用 C 的桌面开发和通用 Windows 平台开发。显示已安装的工具链rustup show。然后通过运行更改和设置工具链rustup default stable-x86_64-pc-windows-msvc。 另外是想用clion进行调试rust 需要你按下面配置即可解…

Mental Poker- Part 2

在part-1中&#xff0c;我们梳理了去中心纸牌游戏所面临的挑战&#xff0c;也介绍了一种改进的Barnett-Smart协议&#xff0c;part-2将深入了解该协议背后涉及的算法。 Discrete-log VTMF VTMFs包含4部分&#xff1a;key generation, mask, remask and unmask&#xff0c;这些…

NFTScan 正式上线 TON NFTScan 浏览器!

2023 年 9 月 12 号&#xff0c;NFTScan 团队正式对外发布了 TON NFTScan 基础设施&#xff0c;将为 TON 生态的 NFT 开发者和用户提供简洁高效的 NFT 数据搜索查询服务。NFTScan 作为全球领先的 NFT 数据基础设施服务商&#xff0c;TON 是继 Bitcoin、Ethereum、BNBChain、Pol…

interview3-微服务与MQ

一、SpringCloud篇 &#xff08;1&#xff09;服务注册 常见的注册中心&#xff1a;eureka、nacos、zookeeper eureka做服务注册中心&#xff1a; 服务注册&#xff1a;服务提供者需要把自己的信息注册到eureka&#xff0c;由eureka来保存这些信息&#xff0c;比如服务名称、…

Unity Animation、Animator 的使用(超详细)

文章目录 1. 添加动画2. Animation2.1 制作界面2.2 制作好的 Animation 动画2.3 添加和使用事件 3. Animator3.1 制作界面3.2 一些参数解释3.3 动画参数 4. Animator中相关类、属性、API4.1 类4.2 属性4.3 API4.4 几个关键方法 5. 动画播放和暂停控制 1. 添加动画 选中待提添加…

Sudowrite:基于人工智能的AI写作文章生成工具

【 产品介绍】 名称 Sudowrite 成立/上线时间 2023年 具体描述 Sudowrite是一个基于GPT-3的人工智能写作工具&#xff0c;可以帮助你快速生成高质量的文本内容&#xff0c; 无论是小说、博客、营销文案还是学术论文。 Sudowrite可以根据你的输入和指…

八、MySql表的复合查询

文章目录 一、基本查询回顾二、多表查询&#xff08;一&#xff09;笛卡尔积的初步过滤&#xff08;二&#xff09;例子1.显示部门号为10的部门名&#xff0c;员工名和工资2.显示各个员工的姓名&#xff0c;工资&#xff0c;及工资级别 三、自连接&#xff08;一&#xff09;定…

pkg 打包 nodejs

一、先全局安装pkg npm i -g pkg 二、下载打包所需的 node-v16.16.0-linux-x64 和 node-v16.16.0-win-x64 下载地址&#xff0c;里面选择你需要的版本 三、放到pkg的缓存目录 windows&#xff1a;C:\Users\whh\.pkg-cache\v3.4&#xff0c;&#xff08;把whh替换为你的电脑…