【Linux下】 线程操作及线程互斥

news2024/9/26 1:24:49

【Linux下】 线程操作及线程互斥

线程操作

:以下函数使用时,编译文件时需带上-lpthread选项,因为下面的函数都是线程库里面的函数

pthread_self

#include <pthread.h>
pthread_t pthread_self(void);

**作用:**获取当前线程的tid


1. 创建线程

pthread_create

作用描述

参数:

thread: 输出型参数,传入pthread_t类型的变量,然后获取到线程的id(pthread库里面的id,也是该线程结构体存放的地址(虚拟内存地址))

attr: 线程的属性配置,一般都使用NULL,代表默认属性

start_routine: 线程所要执行的函数 (返回值为void* 参数为void* 的函数指针)

arg: start_routine的函数参数(类型为void*)

返回值:

返回值同其他函数不同,该函数错误时返回的是错误码

例:

void *run_pthread(void *argc)
{
    while(1)
    {
            sleep(1);
            printf("i am a new pthread:%d my name:%s \n",pthread_self(),argc);
   }
}
int main()
{
   pthread_t id=0;
    pthread_create(&id,NULL,run_pthread,"thread 1");
    while(1)
    {
        sleep(1);
            printf("I am main pthread:%u\n",pthread_self());
        }
    return 0;
}

运行结果:


2. 等待线程

为什么需要线程等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内
  • 创建新的线程不会复用刚才退出线程的地址空间

同进程等待一样,线程也是需要被等待的,不然可能会导致“僵尸线程的出现”

操作接口

pthread_join

**作用:**等待指定的线程退出,并可选择获取线程所执行函数的退出码

参数:

thread:我们需要等待的线程id --pthread_create中的第一个参数

retval: 线程所执行函数stashedinrt_routine的返回值,也称为线程退出时的退出码,特殊:当我们的线程被cancel时,返回值会自动被设定成PTHREAD_CANCELED(-1),也是输出型参数

返回值

同pthread_create一样,错误时直接返回错误码

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到所等待的线程的终止状态是不同的,总结如下:

  • 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值
  • 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数**PTHREAD_ CANCELED。 **
  • 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数
  • 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

3. 线程退出

回顾:

我们在之前进程的博客里曾讲过进程退出的3种场景

  • 代码执行完毕,运行结果正确
  • 代码执行完毕,运行结果不正确
  • 代码异常,进程异常终止

之前判断进程正常退出时,运行结果是否正确是通过进程退出码判断的,进程异常可以通过事后调试的方法更加清楚的知道错误在哪里;而我们的线程其实也是存在上面3种情况,也可以设置退出码来判断是否运行结果是否正确

线程退出的情况

  • 程序异常
    • 异常退出,不需要处理,因为我们进程那就已经设计好了处理方案,而我们线程终止就代表其所在的进程也被终止了,所以线程异常终止的情况是是不需要我们用户进行处理的,os会认为是进程异常然后去处理
  • 程序正常退出,通过线程函数返回值判断运行是否正确(程序猿自己定,os中并没有规定
    • 使用pthread_exit函数退出,同return;主线程使用该函数进行退出时,情况会很特殊,会出现主线程呈现僵尸线程的情况,而其他线程仍然在与运行的情况
    • 使用cancel函数取消线程,可以终止其他线程,也可以终结自己
    • 使用return,从子线程中返回–相当于函数返回,这个方法不适用于主线程(在主线程中返回相当于退出整个进程

相关操作接口:

pthread_exit

作用描述:退出当前线程,并返回退出码

参数:

  • retval: 退出码,pthread_join函数中获得的退出码

**注:**pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

pthread_cancel

作用: 取消指定线程

参数:

  • pthread: 我们所需要取消的线程id

返回值

同前面一样,失败返回错误码

**特殊点:**该函数如果成功将目标线程cancel掉,pthread_join中退出码会设置成 **PTHREAD_CANCELED(-1)**为系统定义的宏值

代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
using namespace std;

#define NUM 5
void * pthread_run(void *args)
{
    int id=*(int *)args;
    while(1)
    {
        printf("I am 新线程:[%d]:%p\n",id,pthread_self());
        sleep(5);
        //pthread_cancel(pthread_self());  //pthread_cancel取消成功后给父进程的退出码是-1 
        //pthread_exit((void*)123);
        return (void*)123;
    }
}
int main()
{
    pthread_t tid[NUM];
    for(int i=0;i<NUM;i++)
    {
        printf("I am mainpthread:%d\n",getpid());
        int * id=new int (i);
        pthread_create(&tid[i],nullptr,pthread_run,(void* )id);
    }
    void * code;
    //线程等待
    for(int i=0;i<NUM;i++)
    {
        int res=pthread_join(tid[i],&code);
        printf("res:%d ;退出码为:%d\n",res,code); //等待已经被分离的线程会得到22结果
    }
    return 0;
}

运行结果:

小结:

  • 使用return和pthread_exit的效果是差不多的,可以返回我们所想返回的值
  • 使用pthread_cancel中,取消线程成功的标志是,该线程的退出码为-1–系统中定义的宏

4. 线程分离

线程分离类似于进程当中,子进程退出时会给父进程发送SIGCHID信号,将该信号的处理方法设置为SIG_IGN之后,os会自动回收子进程资源,而不再需要父进程对子进程进行资源回收等工作

线程分离

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

pthread_detach

作用:将线程分离出来,被分离的线程的资源会被系统自动回收,不用在需要我们主线程去回收他

参数:
thread: 我们所要分离的线程的编号


线程tid vs LWP

  • tid: 我们通过pthread_create的第一个参数获得的线程id,进行线程操作时所使用的线程唯一标识
  • LWP: os级别的线程标识,使用于CPU调度线程时,对线程的唯一标识

而这俩者的关系是怎么样的呢?

我们知道我们上面所使用的线程操作函数,实际上都来自pthread线程库,而不是系统调用接口,而线程库就是动态库,我们使用该库时和使用c语言库实际上并没有什么区别;而在动静态库的博客中,我们曾提到过,动态库使用的动态链接,会在需要的时候把所需要的代码映射到我们的程序地址空间的共享区里;当然线程库也不例外

我们之前曾经提过线程是有自己的栈结构的,实际上这个栈结构(也称为用户栈)是在动态库里面就定义好的(如上图),即实际上pthread库中还设计了线程用户级别的描述块,所以说实际上线程tid实际上就是进程地址空间的一个虚拟地址–保存的是当前线程的用户级线程描述块在进程地址空间的地址

所以从这我们就可以得知,tid就是用户级别的线程唯一标识,而这个用户级线程描述块一定是封装了线程os级别的唯一标识LWP,因为调用系统接口操作线程时肯定是通过LWP进行操作的

小结:

  • tid实际上是一个具体的虚拟地址的值,用于操作线程,而LWP是os级别的线程唯一标识
  • 俩者不同主要在于使用场景的不同,tid用于用户操作线程LWP用于os进行调度线程时的标识

相关概念补充

  • 临界资源: 凡是被线程所共享访问的资源都称为临界资源
  • 临界区: 线程的代码中访问临界资源的代码就叫做临界区
  • 对临界区进行保护,实际上就是对临界资源进行保护。 保护方式:互斥或者同步
  • 互斥: 在任何时刻,只允许一个执行流访问某段代码(该代码用来访问临界资源),就称为互斥
  • 同步: 一般而言,在保证访问临界资源是线程安全的前提下,让访问资 源具有一定的顺序性,即让不同的线程更均衡的享受临界资源
  • 原子性: 不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

线程安全问题

我们曾在前面说过,在进程的地址空间中,定义一个全局变量,或者是函数;因为线程之间是共享地址空间的,所以对于当前地址空间的所有线程来说,都是可以操作全局变量或者是调用该函数的;

什么是线程安全问题?

当多个线程同时对一个共享资源进行非原子性操作时,就会出现该数据出现异常情况,这就是线程安全问题。

例子:

我们创建出来5个线程,执行抢票逻辑,5个线程抢1000张票

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <ctime>

int ticket =1000;

void* tickettach(void* argv)
{
    bool ref=true;
    int id=*( (int*)argv);
    delete argv;
    while(ref)
    { 
        ref=t->buytickets();
        if(ticket>0)
        {
            usleep(10000);
            printf("i am[%d]:%u,我正在抢票:%d \n",id,pthread_self(),ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);
        } 
        else{
            printf("票已经被枪光了\n");
            break;
        }
    }
    return (void *)111;
}
//实现多个进程抢票逻辑
int main()
{
    buyticket * t=new buyticket;
    pthread_t pid[5];
    for(int i=0;i<5;++i)
    {
        int *id =new int (i); //这里可能会造成内存泄漏
        pthread_create(pid+i,nullptr,tickettach,(void*)id);
    }

    for(int i=0;i<5;i++)
    {
        pthread_join(pid[i],nullptr);
    }
    return 0;
}

运行结果:

我们会发现:

当多个线程对ticket进行–时,最后ticket明明已经到0了,而线程依旧在抢票,这是为什么呢?

解释:

ticket–在c语言代码中看似是一行代码,但实际上在汇编代码是分成了3条指令的,也就是说ticket–并不是原子性操作–补充:一条汇编指令的操作通常是原子的

  • **load:**将共享变量ticket从内存加载到寄存器中

  • update: 更新寄存器里面的值,执行-1操作

  • **store:**将新值,从寄存器写回共享变量ticket的内存地址

如图:

所以实际上ticket–并不是原子性操作,而我们之前说过一条汇编代码往往是原子的;

解释:为什么ticket最后会出现负值的情况

因为每个线程执行自己的代码都是有时间限制的(时间片),例如:假设现在的ticket为1000,当线程A正要执行上面的ticket–的最后一步将运算结果写回时,线程A的时间片到了,线程A就会将运算好的ticket结果(999)考到自己的上下文数据中,等待下一次时间片的到来;而线程B进行执行抢票代码时,也是先将对应的数据拷贝到CPU的寄存器中,然后对ticket进行运算,而后将运算结果拷回内存中,经过进程B的运算之后,ticket成功减到了10,恰好这时候进程B的时间片到了,进程B就被挂起等待了;而此时又到了进程A的时间片,于是进程A又将自己的上下文数据拷贝到CPU的寄存器中,继续执行上次还未执行完毕的代码,而后将运算结果写回到内存中,此时就会出现矛盾;实际上ticket已经减少到了10,而经过进程A的操作之后,ticket又变成了999;

ticket–实际上就是临界区,访问的是临界资源(全局变量ticket)

如何解决上面出现的问题的呢?

  • 代码必须要有互斥行为:当线程进入临界区执行时,不允许其他线程进入该临界区

  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。

  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

简单图示:

其中就是使用的互斥量保持的代码的互斥性,下面进行详细讲解

线程互斥

互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。

所以就有了互斥量的出现,使用互斥量可以保证每次只有一个线程在访问临界资源;互斥量也被称为锁

互斥量操作接口

初始化互斥量

pthread_mutex_init

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
      const pthread_mutexattr_t *restrict attr);

**作用:**初始化互斥量

参数:

  • **mutex: ** 所需要初始化的锁
  • attr: 通过这个可以设置锁有关的属性 – 一般填nullptr即可

返回值:

成功返回0,失败返回错误码

销毁互斥量

pthread_mutex_destroy

 int pthread_mutex_destroy(pthread_mutex_t *mutex);

**作用:**释放掉互斥量

参数:

  • mutex: 所需要销毁的互斥量

返回值:

成功返回0,失败返回错误码

上锁–获取互斥量

pthrlead_mutex_lock

int pthread_mutex_lock(pthread_mutex_t *mutex);

函数作用: 申请互斥量,只有携带互斥量的线程才能进入临界区

参数:

  • mutex: 所需申请的互斥锁

返回值: 成功返回0,失败返回错误码

解锁–释放互斥量

pthread_mutex_unlock

int pthread_mutex_unlock(pthread_mutex_t *mutex);

函数作用: 释放互斥量,允许其他线程申请互斥量

参数:

  • mutex: 所需释放的互斥量

返回值: 成功返回0,失败返回错误码

线程互斥改良抢票逻辑

通过互斥量实现线程互斥的本质是:

  • 每个线程想要访问临界区资源之前,都要先申请互斥量
  • 只有携带了互斥量的线程,才能进入临界区,对临界资源进行操作
  • 每个进程使用完临界区资源后,都需要将互斥量释放,允许其他的线程对互斥量进行申请,继续访问临界区

基于上面的理论,改良之后的代码:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <string>
#include <cstdlib>
#include <ctime>

int ticket =1000;
pthread_mutex_t mutex;

void* tickettach(void* argv)
{
  //buyticket* t=(buyticket* )argv;
  bool ref=true;
  int id=*( (int*)argv);
  while(ref)
  { 
    //ref=t->buytickets();
    pthread_mutex_lock(&mutex);
    if(ticket>0)
    {
      usleep(10000);
     printf("i am[%d]:%u,我正在抢票:%d \n",id,pthread_self(),ticket);
      ticket--;
      pthread_mutex_unlock(&mutex);
    } 
    else{
     printf("票已经被枪光了\n");
      pthread_mutex_unlock(&mutex);
     break;
    }
  }
  return (void *)111;
}
//实现多个进程抢票逻辑
int main()
{
  //buyticket * t=new buyticket;
  pthread_t pid[5];
  pthread_mutex_init(&mutex,nullptr);
  for(int i=0;i<5;++i)
  {
    int *id =new int (i); //这里可能会造成内存泄漏
    pthread_create(pid+i,nullptr,tickettach,(void*)id);
    //pthread_create(pid+i,nullptr,tickettach,(void*) t);
  }
  
  for(int i=0;i<5;i++)
  {
    pthread_join(pid[i],nullptr);
  }
  pthread_mutex_destroy(&mutex);
  return 0;
}

运行结果:

我们可以发现抢票的逻辑实现是顺畅的,即最后不会出现票被减少到负数的情况

逻辑图理解互斥量实现线程互斥的方法:

互斥量底层

线程要申请互斥量,是不是得先知道互斥量这个变量,也就是说实际上互斥量是先被每个线程“见到”才能申请的,也就是说,实际上互斥量也属于临界资源;

而从上面的认识,我们知道,每个线程都是可以同时访问和申请锁🔒的,而os又是如何保证申请锁资源的这部分的代码的原子性的呢?

如图:

底层互斥锁申请锁伪代码

解释:

lock:
	movb $0 ,%al  //线程将自己的上下文数据加载到CPU的寄存器中(al)
    xchgb %al, mutex  //使用一条汇编指令将寄存器中的数据和内存中的锁的数据进行交换
    if( al寄存器的内容>0 ) {
        return 0;
    }else
        挂起等待;
	goto lock ;
unlock:
	movb $1, mutex  //释放锁资源,将寄存器数据和内存中的锁的数据进行交换
    唤醒等待Mutex的线程;return 0;

所以实际上:

  • 保证线程访问锁🔒资源时,保证线程安全的是,os提供了swap或者是exchange指令,可以实现一条汇编完成内存和寄存器里的数据的交换–通过一条汇编指令保证对🔒资源的操作是原子性的
  • 例子帮助理解:lock变量里面原来存放的值是1,代表该锁目前没有被任何线程所拥有,而这个时候来了俩个线程,分别是线程A,B;俩个线程都是先将自己的数据加载到CPU的上下文中,而后调用第二条指令,进行竞争🔒,而这时候进程A的速度快一些,迅速将1拿到了自己的寄存器,线程B执行第二条指令时,🔒已经被A拿走了,所以B的寄存器中的数据依旧为0–即代表没有抢到锁,所以在下面的分流中,就进入了else,而线程A就拿着🔒进入if里面,开始访问临界资源

理解:当线程A的时间片到了,被CPU挂起等待下次调度,这时候其他线程是否能进入临界区呢?

答案是当然不能,线程A被挂起等待之前,是先将寄存器中的变量(包括🔒)保存到自己的上下文数据中的,而后再被挂起的,也就是说线程A是“抱着🔒”被挂起的,所以没有🔒,其他线程依旧无法进入临界区中,除非线程A将🔒释放了(只可能在线程A执行完了自己的临界区的代码)

所以说:互斥量的设计是能保证线程安全的

补充: 线程运行代码时,CPU寄存器中的临时变量也是属于当前线程的

抽象图理解:


线程互斥的优缺点

优点:

  • 使用线程互斥,可以保证多线程在对共享资源进行访问的线程安全,这是很有意义的–例如:实际中电影院买票,最后买到的是负票,就会出很大问题

缺点:

  • 但是线程互斥带来安全的同时,其实也是做出了效率牺牲的,因为原来线程访问临界资源是并行的,而加了互斥锁之后,访问方式变成了串行–可以观察上面俩个运行动图,明显第二张图的运行速度是要比第一张图要慢的
  • 可能会造成线程饥饿问题,如果某个线程竞争锁资源的能力太强,就会导致其他线程无法享用到临界资源 ,从第二张运行动图看来,实际上有俩个线程是没有参与到抢票中的
  • 加大了编写代码难度
  • 操作不当,可能会造成死锁之类的问题

所以侧面说明:任何东西都是具有俩面性的,我们既要看到其的缺点,也要看到其的优点

而为了解决线程饥饿问题,后面又会引出线程同步进行解决这个问题,下篇博客会进行讲解


可重入和线程安全

概念

  • 线程安全: 多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。

  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入一个函数在重入的情况下运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数

常见的线程不安全的情况

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

常见线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的

  • 类或者接口对于线程来说都是原子操作

  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的

  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构

  • 可重入函数体内使用了静态的数据结构

常见可重入的情况

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

可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的

可重入与线程安全区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的

死锁问题

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

死锁四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用

  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放

  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺

  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁

  • 破坏死锁的四个必要条件

  • 加锁顺序一致

  • 避免锁未释放的场景

  • oc或者new开辟出的空间

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

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

相关文章

淘宝天猫1688京东商品详情API接口,封装接口可高并发

要提供商品详情数据需要知道具体的商品信息&#xff0c;但通常商品详情数据应包括以下内容&#xff1a; 商品名称&#xff1a;商品的名称&#xff0c;以方便顾客对其进行识别和区分。 商品描述&#xff1a;一段让顾客能够全面认识商品的描述。应能够有效地展示商品的特性、功能…

大数据赛项|2023年广东省大学生计算机设计大赛初赛结果公示

2023年广东省大学生计算机设计大赛 暨第16届中国大学生计算机设计大赛 粤港澳大湾区赛初赛结果公示 根据《广东省教育厅关于做好2023年广东省本科高校大学生学科竞赛工作的通知》&#xff0c;广东外语外贸大学承办2023年“广东省大学生计算机设计大赛”。 在广大师生的热情…

迅为国产化RK3588开发平台16G大内存64G存储2路千兆以太网4G/5G通信

iTOP-3588开发板采用瑞芯微RK3588处理器&#xff0c;是全新一代AloT高端应用芯片采用8nmLP制程&#xff0c;搭载八核64位CPU(四核Cortex-A76四核Cortex-A55架构)集成MaliG610MP4四核GPU&#xff0c;内置AI加速器NPU&#xff0c;算力达6Tops&#xff0c;集成独立的8K视频硬件编码…

OpenCV-Python图像阈值

目录 简单阈值 自适应阈值 Otsu的二值化 所谓的图像阈值&#xff0c;就是图像二值化&#xff0c;什么是二值化&#xff0c;就是只有0和1&#xff0c;没有其他的。在OpenCV的图像里面&#xff0c;二值化表示图像的像素为0和255&#xff0c;并没有其他的值&#xff0c;它跟灰度…

js逆向-某东h5st

声明 本文仅供学习参考&#xff0c;如有侵权可私信本人删除&#xff0c;请勿用于其他途径&#xff0c;违者后果自负&#xff01; 如果觉得文章对你有所帮助&#xff0c;可以给博主点击关注和收藏哦&#xff01; 文章标题 声明前言参数分析扣代码算法还原 前言 目标网站&…

SpringCloud项目实例3--通信服务负载均衡

只是在pom.xml文件中添加了spring-cloud-starter-loadbalancer依赖并且在RestTemplate类中添加了一个LoadBalance的注解。这就是另外一种负载均衡的实现方案 Spring Cloud LoadBalancer 介绍 这种方案有什么优点呢&#xff1f; 减少整个系统的复杂度&#xff0c;不需要额外部…

BDD行为驱动开发+Python案例解析

简介&#xff1a;BDD&#xff08;Behavior-Driven Development&#xff0c;行为驱动开发&#xff09;是一种敏捷软件开发方法&#xff0c;它强调软件应该按照预期的行为来开发。BDD的核心理念是使用自然语言编写的可读性强、易于理解的用户故事&#xff08;User Stories&#x…

【前端三剑客之CSS】

目录 1.CSS1.1什么是CSS1.2 引入方式1.2.1内部样式1.2.2 行内样式表1.2.3 外部样式 2.选择器2.1 基础选择器2.1.1 标签选择器2.1.2 类选择器2.1.3 id选择器2.1.4 通配符选择器 2.2 基础选择器小结2.3 复合选择器2.3.1 后代选择器 3.元素属性3.1 字体元素3.2 文本属性3.3 背景属…

滨海县一中学被指为苏醒全国感恩教育巡回演讲营销搭台

梦想与爱同行—苏醒全国感恩教育巡回演讲活动今天下午一点半在滨海西湖路一中操场举行&#xff0c;数千学生和家长参加。 苏醒全国感恩教育巡回演讲开始&#xff0c;苏醒先生先用一口流利的英语做自我介绍&#xff0c;然后又用中文向前来参加的学生和学生家长介绍说&#xff0…

FS4055B是一款3.2V最高3.6V磷酸铁锂充电IC

FS4055B是一款3.2V最高3.6V磷酸铁锂充电IC&#xff0c;输入电源正负极反接保护的单芯片&#xff0c;兼容大小 REV_1.0 是一款完整的单节锂电池充电器&#xff0c;电池正负极反接保护、 3mA-500mA 充电电流。采用涓流、 恒流、恒压控制&#xff0c;SOT23-5 封装与较少的外部元件…

【MySqL】 表的创建,查看,删除

目录 一.使用Cmd命令执行操作 1.使用&#xff08; mysql -uroot -p)命令进入数据库 2.创建表之前先要使用数据库 3.创建表之前要先确定表的名称&#xff0c;列名&#xff0c;以及每一列的数据类型及属性 4.创建表 注意&#xff1a; 5.查看所有已创建的表 6.查看单表 …

H3C防火墙单机旁路部署(网关在防火墙)

防火墙旁路部署在核心交换机上&#xff0c;内网有三个网段vlan 10&#xff1a;172.16.10.1/24、vlan 20&#xff1a;172.16.20.1/24、vlan30&#xff1a;172.16.30.1。要求内网网关在防火墙设备上&#xff0c;由防火墙作为DHCP服务器给终端下发地址&#xff0c;同时由防火墙来控…

算法笔记Day1 | 时间复杂度和空间复杂度的七七八八

文章目录 时间空间复杂度1. 时间空间复杂度的重要性(作用)2. 时间复杂度和大O表示法1&#xff09;算法图解2&#xff09;代码随想录3&#xff09;王道《数据结构》 3. 大O指的是最糟的情形和一般的情形1&#xff09;大O表示的是一般情况&#xff0c;并不是严格的上界2&#xff…

海量请求下,高并发接口的设计思路

1. 背 景 虽然现在很多人&#xff0c;动不动就提什么高并发、请求量多大&#xff0c;数据量多少多少&#xff0c;但我可以很认真地说&#xff0c;那都是他妈的在吹牛&#xff01; 生产环境&#xff0c;真正有大请求量的&#xff0c;就那么几个业务场景&#xff0c;而且多是面…

应急物流 | 灾后早期阶段多目标选址路径问题的混合元启发式算法

解读作者&#xff1a;李奡&#xff0c;闫同仁 A hybrid meta-heuristic algorithm for the multi-objective location-routing problem in the early post-disaster stage Tongren Yan, Fuqiang Lu, Suxin Wang, Leizhen Wang, Hualing Bi Journal of industrial and managem…

云原生应用架构

本博客地址&#xff1a;https://security.blog.csdn.net/article/details/130566883 一、什么是云原生应用架构 成为云原生应用至少需要满足下面几个特点&#xff1a; ● 使用微服务架构对业务进行拆分。单个微服务是个自治的服务领域&#xff0c;对这个领域内的业务实体能够…

Nature | 通用医学人工智能基础模型

编译 | 李汭璨 审稿 | 王建民 今天为大家介绍的是来自Eric J. Topol和 Pranav Rajpurkar研究团队的一篇医学人工智能的综述论文。高度灵活、可重复使用的人工智能模型的极快发展可能会为医学带来新的能力。作者提出了医学人工智能的新范式&#xff0c;称为通用医学人工智能&…

磁盘和文件系统管理

个人简介&#xff1a;云计算网络运维专业人员&#xff0c;了解运维知识&#xff0c;掌握TCP/IP协议&#xff0c;每天分享网络运维知识与技能。座右铭&#xff1a;海不辞水&#xff0c;故能成其大&#xff1b;山不辞石&#xff0c;故能成其高。个人主页&#xff1a;小李会科技的…

Deepnlp EquationGPT公式搜索引擎搜Latex源码和论文

公式搜索引擎常用功能 DeepNLP的EquationGPT用的比较多功能: 网址&#xff1a;EquationGPT Largest Equation Database & Engine powered by GPT 1. 公式搜索引擎&#xff0c;输入公式搜索数学公式的Latex代码和相应论文。 2. 目前覆盖了计算机(机器学习/AI)/ 数学/物理…

【设计模式】总结篇

【C语言部分】总结篇 【操作系统】总结篇 【数据库】总结篇 【计算机网络】总结篇 本文目录 1. 说说什么是单例设计模式&#xff0c;如何实现2. 简述一下单例设计模式的懒汉式和饿汉式&#xff0c;如何保证线程安全3. 说说工厂设计模式&#xff0c;如何实现&#xff0c;以及它的…