多线程和线程同步基础篇学习笔记(Linux)

大丙老师教学视频:10-线程死锁_哔哩哔哩_bilibili

目录

大丙老师教学视频:10-线程死锁_哔哩哔哩_bilibili

 线程概念

为什么要有线程

线程和进程的区别

在处理多任务的时候为什么线程数量不是越多越好?

Linux提供的线程API

主要接口

 线程创建 pthread_create()

分别获取主子线程的id pthread_self()

线程退出  pthread_exit()

线程回收pthread_join()

线程分离  pthread_detach()

​编辑

几个不重要的接口

线程取消 pthread_cancel()​编辑

线程Id比较  pthread_equal()

线程同步

同步的方式

互斥锁 ​编辑

​编辑

死锁​编辑

加锁后忘记解锁​编辑

重复加锁,造成死锁

存在多个共享资源,随意加锁造成相互阻塞

那么如何在多线程中避免死锁情况呢?​编辑

读写锁​编辑​编辑​编辑​编辑​编辑

条件变量

条件变量的作用

条件变量和锁的区别

 什么是生产者和消费者模型

条件变量的函数接口

模拟生产者和消费者模型​编辑

信号量


 线程概念

为什么要有线程

为了效率更高.

线程和进程的区别

进程是自愿分配的最小单位,线程是系统调度执行的最小单位.

进程有自己独立的地址空间,就是说一个人住一个房间.线程共享同一个地址空间就像多个人住在同一个房间,节省了系统资源,但是效率却没有下降.

虽然住一个房间,但是并不是所有的东西都是共享的,比如牙刷都是各用各的.每个线程都有自己的栈区和寄存器.

每个进程对应一个虚拟地址空间,一个进程只能抢一个时间片。但是如果这个进程有10个子线程,那么就可以由这10个线程交错去抢这1个时间片,效率并不会低。

下图演示了进程是如何去抢占时间片的:因为多个线程共享一个进程地址空间,所以销毁的时候只销毁一个地址空间就行了,启动快,退出也会,对系统资源的冲击小。

在处理多任务的时候为什么线程数量不是越多越好?

多线程的本地就是多个线程分时复用,来回切换抢时间片。线程切换也是需要消耗时间和资源的.线程的数量太多效率自然就慢了。

Linux提供的线程API

主要接口

 线程创建 pthread_create()

想要创建线程就调用pthread_create()函数

我们可以看见pthread_create()有四个参数。第一个参数是用来返回线程ID的,类型是pthread_t。第二个参数是pthread_attr_t 是一个结构体,用于设置线程属性,例如线程栈的大小、线程的分离状态等。我们用默认的就可以了,一般设置为nullptr.第三个参数是一个函数指针,指向线程启动时调用的函数。函数指针一般都是用来回调用的。我们创建子线程就是为了让子线程去执行任务.这个回调函数就是子线程要执行的任务。第四个参数指向一个 void 的指针,它包含传递给 start_routine 函数的参数。

线程演示:

#include<iostream>
#include<pthread.h>
#include<unistd.h>

void* ThreadRountine(void* args)
{
    while(1)
    {
        std::cout<<"新线程"<<std::endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,ThreadRountine,nullptr);
    
    while(true)
    {
      std::cout<<"主线程"<<std::endl;
      sleep(1);
    }
    
    return 0;
}

如何给线程传参

我们上面说了pthread_create()​​​的第四个参数void *args可以给第三个参数void *(*start_routine)了传参.

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
void* ThreadRountine(void* args)
{
    std::string threadname=(const char*) args;
    while(1)
    {
        std::cout<<threadname<<" "<<"新线程"<<std::endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,ThreadRountine,(void *)"thread1");
    
    while(true)
    {
      std::cout<<"主线程"<<std::endl;
      sleep(1);
    }
    
    return 0;
}

 但是上面那种方法只能传字符串,而我还想传其他东西.比如对象.

 我们可以定义一个类对象,把这个类对象作为pthread_create()的第四个参数传过去:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<functional>
#include<time.h>

using func_t =std::function<void()>;
class ThreadData
{ 
//构造函数
public:
    ThreadData(const std::string &name,const uint64_t &ctime,func_t f)
    :threadname(name),createtime(ctime),func(f)
    {} 
   
public:
    std::string threadname;
    uint64_t createtime;
    func_t func;
};
void Print()
{
 std::cout<<"我是线程执行的大任务的一部分"<<std::endl;
}

void* ThreadRountine(void* args)
{

    ThreadData *td=static_cast<ThreadData*>(args);
    while(1)
    {
        std::cout<<"thread name: "<<td->threadname<<" "<<"creatime: "<<td->createtime<<std::endl;
        td->func;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    ThreadData *td=new ThreadData("thread1",(uint64_t)time(nullptr),Print);
    pthread_create(&tid,nullptr,ThreadRountine,td);
    
    while(true)
    {
      std::cout<<"主线程"<<std::endl;
      sleep(3);
    }
    
    return 0;
}

如何创建多个线程

可以通过for循环创建多个线程

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<functional>
#include<time.h>
#include<vector>

const int threadnum=5;
using func_t =std::function<void()>;
class ThreadData
{ 
//构造函数
public:
    ThreadData(const std::string &name,const uint64_t &ctime,func_t f)
    :threadname(name),createtime(ctime),func(f)
    {} 
   
public:
    std::string threadname;
    uint64_t createtime;
    func_t func;
};
void Print()
{
 std::cout<<"我是线程执行的大任务的一部分"<<std::endl;
}

void* ThreadRountine(void* args)
{
    std::cout<<"新线程"<<std::endl;
    ThreadData *td=static_cast<ThreadData*>(args);
    while(1)
    {
        std::cout<<"thread name: "<<td->threadname<<" "<<"creatime: "<<td->createtime<<std::endl;
        td->func;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    std::vector<pthread_t> pthreads;
    for(int i=1;i<=threadnum;i++)
    {
        char threadname[64];
        snprintf(threadname,sizeof(threadname),"%s-%d","thread",i);

        ThreadData *td=new ThreadData(threadname,(uint64_t)time(nullptr),Print);
        pthread_create(&tid,nullptr,ThreadRountine,td);

        pthreads.push_back(tid); //每创建成功一个线程就加入到vector里
    }
        while(true)
        {
        std::cout<<"主线程"<<std::endl;
        sleep(1);
        }
    
    return 0;
}

分别获取主子线程的id pthread_self()

每个线程都有一个Id,这个id类型是pthread_t类型的,想要获取当前线程id就调用pthread_self()接口,

ppthread_self()函数用来返回子线程的id

写如下代码

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<functional>
#include<time.h>
#include<vector>

void *threadRoutine(void *args)
{
    const char* name = static_cast<const char*>(args);
    while(true)
    {
        std::cout <<name<<": "  << pthread_self() <<std::endl;
        sleep(1);
    }
}

int main()
{
    pthread_t id;
    const char* threadName = "new thread"; // Correctly passing a const char*
    pthread_create(&id, nullptr, threadRoutine, (void*)threadName);
    
    while(true)
    {
        std::cout << "main thread id: " << id << std::endl;
        sleep(1);
    }
    
    return 0;
}

我们发现,主线程的id和子线程的id值一样,也就是说pthread_self()是一个输出型参数,可以把子线程的id带出去

我们也可以通过pthread_self()把主线程的id也打印出来看看:

int main()
{
    pthread_t id;
    const char* threadName = "new thread"; // Correctly passing a const char*
    pthread_create(&id, nullptr, threadRoutine, (void*)threadName);
    
    while(true)
    {
        std::cout << "main thread sub thread: " << id<<"main thread: "<<pthread_self()<<std::endl;
        sleep(1);
    }
    return 0;
}

线程退出  pthread_exit()

1.我们想让子线程执行完任务后退出不可以直接调用exit()函数来退出.因为exit()函数是用来结束进程的,整个程序就会被结束掉.我们可以用return nullptr的方式结束子线程,也可以调用线程结束函数pthread_exit().

2.子线程退出不会影响主线程,但是主线程1退出会销毁进程地址空间,子线程的资源也会被释放.

观察下列代码和现象,为什么子线程只输出了一次线程id就退出了?

#include<pthread.h>
#include<iostream>

void *callback(void *args)
{
    for(int i=0;i<5;i++)
    {
        std::cout<<"子线程执行"<<i<<std::endl;
    }
    std::cout<<"子线程id: "<<pthread_self()<<std::endl;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,callback,nullptr);
    for(int i=0;i<5;i++)
    {
        std::cout<<"主线程执行"<<i<<std::endl;
    }
    std::cout<<"主线程id: "<<pthread_self()<<std::endl;

    return 0;
}

这是因为主线程先被执行(main函数从上到下依次执行,主线程先抢到了时间片),当主函数在执行的时候子线程去抢时间片,有可能在子线程还没抢到时间片的时候,主线程就执行完就退出了,那么进程地址空间就被销毁了.子线程自然也就结束了.

解决方案

1.让主线程sleep()挂起,等子线程一下.

2.在进程主函数(main())中调用pthread_exit(),只会使主函数所在的线程(可以说是进程的主线程)退出;而如果是return,编译器将使其调用进程退出的代码(如_exit()),从而导致进程及其所有线程结束运行。

理论上说,pthread_exit()和线程宿体函数退出的功能是相同的,函数结束时会在内部自动调用pthread_exit()来清理线程相关的资源。但实际上二者由于编译器的处理有很大的不同。

按照POSIX标准定义,当主线程在子线程终止之前调用pthread_exit()时,子线程是不会退出的。

线程回收pthread_join()

主线程有义务回收子线程结束后释放的资源.可以调用pthread_join()函数来回收资源:

同时主线程还可以获取子线程的返回值.子线程其实就是在执行callback()函数,但是我们知道它是void*类型的,无法返回值.那么主线程如何获取子线程的返回值呢?

我们可以在子线程中调用pthread_exit()函数,我们注意到pthread_exit()函数的参数是一个void  *类型的 名字为retval类型的实参

而pthread_join()函数的第二个参数是void** 类型的同名形参.

二级指针保留一级指针的地址,我们在子线程中把要传递的值的地址传给pthread_exit(),在主线程中调用pthread_join()函数等待子线程结束时回收其资源,如果不获取子线程返回值,第二个参数来获取返回值.

#include<pthread.h>
#include<iostream>
#include<string>

struct Point {
    std::string name;
    int age;
};

void* callback(void* args) {
    struct Point* P = (struct Point*)args;
    P->name = "张三"; // 初始化字符串
    P->age = 18;
    std::cout << "子线程id: " << pthread_self() << std::endl;
    pthread_exit((void*)P); // 返回 Point 结构体指针
}

int main() {
    pthread_t tid;
    struct Point P; // 定义全局或动态分配的结构体
    pthread_create(&tid, nullptr, callback, &P); // 传递 Point 结构体的地址
    void* pt;
    pthread_join(tid, &pt); // 正确地使用 &pt
    struct Point* ans = (struct Point*)pt; // 转换回 Point 结构体指针
    std::cout << ans->name << " " << ans->age << std::endl; // 正确地打印
    return 0;
}

线程分离  pthread_detach()

为什么要分离?

主线程在回收子线程资源的时候需要阻塞等待子线程执行完任务结束(因为主线程不等待一旦退出,那么虚拟进程地址空间就会被销毁,子线程资源也就就被释放了),那么此时主线程就无法再做其他事情了.为了给主线程减负,可以用线程分离技术.把主子线程分离开来,各做各的事情,主线程无需阻塞等待回收子线程资源,自己做完自己的任何自行退出即可.子线程的资源由内核去回收.

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<functional>
#include<time.h>
#include<vector>

void *threadRoutine(void *args)
{
    const char* name = static_cast<const char*>(args);
    for(int i=0;i<2;i++)
       {
        std::cout <<name<<": "  << pthread_self() <<std::endl;
        sleep(1);
    }
    std::cout<<"子线程退出"<<std::endl;
}

int main()
{
    pthread_t id;
    const char* threadName = "子线程"; // Correctly passing a const char*
    pthread_create(&id, nullptr, threadRoutine, (void*)threadName);
    
       for(int i=0;i<1;i++)
       {
       std::cout << "主线程id: " << id << std::endl;
        sleep(1);
       }
 
    
    pthread_detach(id);    //线程分离
      std::cout<<"主线程退出"<<std::endl;
    pthread_exit(nullptr);   //主线程退出不销毁进制地址空间,不影响子线程
  
    
    return 0;
}

几个不重要的接口

线程取消 pthread_cancel()

这个接口用来在主线程中杀死子线程用的.

线程Id比较  pthread_equal()

线程同步

什么是线程同步?为什么要进行线程同步?

在多线程编程中,多个线程可以同时访问共享资源。线程同步是为了保证多个线程之间对共享资源的访问顺序和结果的正确性。当多个线程同时访问共享资源时,如果没有进行适当的同步措施,可能会导致数据不一致、竞争条件和死锁等问题。

总之就是一句话,线程同步就是为了解决数据读取先后顺序问题

举个例子,因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?很难说清楚。因此多线程同步就是要解决这个问题。

那么线程同步就要明确这个答案,就是规定好线程的顺序---先存钱再取钱,这样就只有一个结果-----取钱成功,余额为0.

例如下面就是一个线程异步造成的问题:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<string>
#include<functional>
#include<time.h>
#include<vector>

int number;
void *callA(void* args)
{
   for(int i=0;i<5;i++)
   {
    int cur=number;
    cur++;
    number=cur;
    usleep(5);
    printf("A thread: %lu, %d\n",pthread_self(),number);
     
   }
   return nullptr;
}

void *callB(void* args)
{
   for(int i=0;i<5;i++)
   {
    int cur=number;
    cur++;
   
    number=cur;
    printf("B thread: %lu, %d\n",pthread_self(),number);
     usleep(5);
   }
   return nullptr;
}
int main()
{
    pthread_t t1,t2;
    pthread_create(&t1,nullptr,callA,nullptr);
    pthread_create(&t2,nullptr,callB,nullptr);
    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    return 0;
}

现象1 

 我们发现B线程抢到时间片给Number加上1之后,A线程结束.B线程抢到时间片给number+1,增值完后结果竟然和线程A增值结果一样,而不是比线程A增值后的结果大1.

这个现象问题是典型的多线程并发中的竞态条件(race condition)。在您的代码中,两个线程 t1 和 t2 都在尝试读取、修改并回写全局变量 number。由于线程调度的不确定性,两个线程可能会同时读取到相同的 number 值,然后各自递增,导致最终的 number 值比预期少。

下面是发生竞态条件的步骤:

  1. 线程 A 读取 number 的值,比如当前是 2。
  2. 线程 B 也读取 number 的值,同样是 2。
  3. 线程 A 将其值增加 1 并写回,number 现在是 3。
  4. 线程 B 也将其值增加 1 并写回,number 变成 3(期望是 4)。

由于线程 B 的操作覆盖了线程 A 的结果,所以实际的增量比预期的少。

现象2

线程A打印完是7,线程B打印为什么是9呢?

首先我们要知道number存储在硬盘里,cpu现在想对它做增值操作,需要先读取它,把它从硬盘读取到内存里, 从内存到cpu还需要经过3级缓存和一个寄存器.。

  • 在线程 A 打印 7 之后,线程 B 获得了 CPU 时间,并读取了 number 的值。
  • 线程 B 将 number 递增到 8,但在它有机会打印之前,线程 A 可能已经再次执行,将 number 的值从 8 递增到 9 。但是还没有来得及打印,线程B就又抢到了时间片。
  • 此时number 值为8,线程B对其进行增值变为9,并打印输出。

为了解决上面出现的多个线程可能同时修改同一数据,可能会导致数据处于不一致的状态。以及当多个线程访问共享资源,并且至少有一个线程对资源进行写操作时,如果没有适当的同步机制,就可能出现竞态条件,导致程序行为不可预测。于是有了线程同步这一概念。

同步的方式

常见的线程同步有四种方式:互斥锁,读写锁,条件变量,信号变量。

锁可以保证多个线程按照线程顺序依次执行。加锁就是在临界资源上方枷锁,在临界资源下方解锁。

什么是临界资源?

所谓共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或堆区变量,被称为临界资源,这些变量对应的共享资源也被称之为临界资源。

 什么是临界区?

临界区就是上锁和解锁中间的代码段(不希望被其他线程数据污染的部分都可以放进临界区)

互斥锁 

ps:

  • restrict:这个关键字告诉编译器,mutex指针是访问它所指向的pthread_mutex_t对象的唯一方式。这意味着在pthread_mutex_lock函数执行期间,不会有其他指针指向同一个互斥锁对象。

例如说有一个mutex_t类型的变量: mutex_t mut; 该变量加锁了一个线程。

此时p=mut,p是不能进行对该线程进行解锁的。因为除mutex,arrtr两个互斥锁对象外不能有其他指针指向同一个互斥锁对象。

例如还是上文的代码,我们给线程A和线程B都加上互斥锁后运行结果就达到了我们的预期效果:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <functional>
#include <time.h>
#include <vector>

int number = 0;
pthread_mutex_t lock;

void* callA(void* arg) {
    for (int i = 0; i < 5; ++i) {
        //加锁
        pthread_mutex_lock(&lock);
        number++;
        std::cout << "A thread: " << pthread_self() << ", " << number << std::endl;
        //解锁
        pthread_mutex_unlock(&lock);
    }
    return nullptr;
}

void* callB(void* arg) {
    for (int i = 0; i < 5; ++i) {

        //加锁
        pthread_mutex_lock(&lock);
        number++;
        std::cout << "B thread: " << pthread_self() << ", " << number << std::endl;
        //解锁
        pthread_mutex_unlock(&lock);
    }
    return nullptr;
}

int main() {
    pthread_t t1, t2;

    //初始化互斥锁
    pthread_mutex_init(&lock, nullptr);

    pthread_create(&t1, nullptr, callA, nullptr);
    pthread_create(&t2, nullptr, callB, nullptr);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);

    //释放互斥锁
    pthread_mutex_destroy(&lock);
    return 0;
}

死锁
加锁后忘记解锁

 在func1()中因为忘记解锁,所以在进入第二层for循环时就进不去了,该线程就会被阻塞在这把互斥锁上。

在fun2()中所以没有忘记解锁,但是在临界区里进行了条件判断,一但满足条件就会立即退出,不再进行解锁操作.那么当下次该线程抢到时间片再进就进不来了。

重复加锁,造成死锁

funcA()中上了两层锁,当上完第一层锁进入临界区后,内部还有一个锁,但是此时已经进不去了(没有进行解锁操作,无钥匙)。

funB()中上了一层锁,也有解锁操作,但是在临界区里调用了funA(),funA()上面说了阻塞,因为funB()进入funA()后也会被阻塞祝,回不来了。

存在多个共享资源,随意加锁造成相互阻塞

 如上图所示,假设线程A对临界资源X进行了加锁。线程B对临界资源Y进行了加锁。

现在,线程A又想对资源Y进行加锁,同时,线程B又想对资源X加锁。那么,两个线程都会被阻塞(假设都用的pthread)_mutex_lock())

那么如何在多线程中避免死锁情况呢?

读写锁

条件变量

条件变量的作用

条件变量是用来阻塞线程的。

条件变量和锁的区别

线程是用来并发线程的,条件变量是用来阻塞线程的。

 什么是生产者和消费者模型

生产者消费者模型-CSDN博客

条件变量的函数接口

模拟生产者和消费者模型

#include<iostream>
#include<cstdio>
#include<pthread.h>
#include<unistd.h>

    pthread_mutex_t mutex;
    pthread_cond_t cond;
struct Node
{
 int value;
 struct Node* next;
};
struct Node* head=nullptr;
//生产者
void* producer(void* args)
{
    while(1)
    {   
        pthread_mutex_lock(&mutex);
   
        //创建新节点
        struct Node* newnode=(struct Node*)malloc(sizeof(struct Node));
        //初始化
        newnode->value=rand()%1000;
        newnode->next=head;
        head=newnode;
        printf("生产者: id:%ld , value:%d\n",pthread_self(),newnode->value);
        pthread_mutex_unlock(&mutex); 
        pthread_cond_broadcast(&cond);
        sleep(rand()%3); //随机休息0~3秒
    }
    return nullptr;
}
//消费者
void* consumer(void* args)
{
    while(1)
    {
        pthread_mutex_lock(&mutex);
         while(head==nullptr)
        {
            pthread_cond_wait(&cond,&mutex);
        }
        struct Node* node=head;
        printf("消费者: id:%ld , value:%d\n",pthread_self(),node->value);
        head=head->next;
        free(node);
        pthread_mutex_unlock(&mutex);
        sleep(rand()%3);
    }
    return nullptr;
}
int main()
{
    pthread_t ptid[5],ctid[3];

    pthread_mutex_init (&mutex,nullptr);
    pthread_cond_init(&cond,nullptr);

    for(int i=0;i<5;i++)  //5个生产者
    {
        pthread_create(&ptid[i],nullptr,producer,nullptr);
    }
    
    for(int i=0;i<3;i++)  //三个消费者
    {
     pthread_create(&ctid[i],nullptr,consumer,nullptr);
    }
  for(int i=0;i<5;i++)  //5个生产者
    {
    pthread_join(ptid[i],nullptr);  //第二个参数用来带出子线程的返回值
    }
     for(int i=0;i<3;i++)  //三个消费者
    {
    pthread_join(ctid[i],nullptr);
    }

    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    return 0;
}

我们可以看到一个现象:

生产者去生产,生产完了之后全部放到商店。然后消费者去消费,生产者先休息一会。当消费者把商品清空之后生产者接着去生产。 

那么我有一个问题, 消费者在把产品清空时无商品可买会陷入阻塞等待:

那么生产者此时就能把产品生产出来吗?

根据代码上下文来看消费者因为被阻塞没有向下执行解锁,那么此时Mutex这把锁就是上锁状态。生产者也无法进入:


 

 但是根据我们的运行现象来看,生产者在消费者阻塞等待时确实是继续生产产品了。

这是因为消费者在阻塞等待的时候调用了pthread_cond_wait(&cond,&mutex),所以会自动释放该互斥锁的拥有权,第二个参数会自行给mutex解锁。

那么被解锁后,生产者就开始生产,生产完后调用pthread_cond_broadcast()唤醒多个消费者线程。

需要注意的是消费者线程被唤醒之后,会抢时间片,抢到的消费者线程通过pthread_cond_wait(&cond,&mutex)接着加上锁,向下执行,没有加锁成功的消费者线程会继续阻塞。

信号量

信号量也是用来处理生产者和消费者模型的,但是比条件变量更加简单。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

希望十一月对我好一点:C++之多态(2)--多态的原理(部分)

多态的原理 虚函数表指针 下⾯编译为32位程序的运⾏结果是什么&#xff08;&#xff09; A. 编译报错 B.运⾏报错C.8 D.12 class Base{public:virtual void Func1(){cout << "Func1()" << endl;}protected:int _b 1;char _ch x;};int main(){Base b…

Java: 遍历 Map

Java: 遍历 Map package animals;import java.util.HashMap; import java.util.Iterator; import java.util.Map;/*** Description :** author : HMF* Date : Created in 15:33 2024/11/1* version :*/ public class Test002 {public static void main(String[] args){Map<S…

基于vue+neo4j 的中药方剂知识图谱可视化系统

前言 历时一周时间&#xff0c;中药大数据R02系统中药开发完毕&#xff0c;该系统通过scrapy工程获取中药数据&#xff0c;使用python pandas预处理数据生成知识图谱和其他相关数据&#xff0c;利用vuespringbootneo4jmysql 开发系统&#xff0c;具体功能请看本文介绍。 简要…

01.如何用DDD重构老项目

学习资料来源&#xff1a;DDD独家秘籍视频合集 https://space.bilibili.com/24690212/channel/collectiondetail?sid1940048&ctype0 文章目录 动机DDD与重构实践重构? 重写从一开始就采用DDD重构步骤1. 添加领域模块2.分离出有价值的代码3.迁移到领域模块4.重复2,3 动机 …

WPF+MVVM案例实战(十八)- 自定义字体图标按钮的封装与实现(ABD类)

文章目录 1、案例效果1、按钮分类2、ABD类按钮实现描述1.文件创建与代码实现2、样式引用与控件封装3、按钮案例演示1、页面实现与文件创建2、运行效果如下3、总结4、源代码获取1、案例效果 1、按钮分类 在WPF开发中,最常见的就是按钮的使用,这里我们总结以下大概的按钮种类,…

Python 5个数据容器

列表&#xff08;list&#xff09; 列表的定义 定义空列表&#xff1a; 变量名 [] 或 变量名 list() 定义变量&#xff1a; 变量名 [元素1&#xff0c;元素2&#xff0c;元素3&#xff0c;... ] 取出列表元素 列表 [下标索引] 从前向后&#xff0c;从0开始&#xff…

使用语言模型进行文本摘要的五个级别(llm)

视频链接&#xff1a;5 Levels Of LLM Summarizing: Novice to Expert

Qt5.15.x源码编译

介绍&#xff1a; QT5.15以上版本已经不提供现成的集成软件了。所以当我们项目中需要用到5.15以上的版本时&#xff0c;只能自己对源码进行编译来生成一个环境了&#xff08;Qt提供了在线升级&#xff0c;但是在线升级中没有MinGW版本了&#xff09; 背景&#xff1a; 我们想要…

Ubuntu 系统、Docker配置、Docker的常用软件配置(下)

前言 书接上文&#xff0c;现在操作系统已经有了&#xff0c;作为程序的载体Docker也安装配置好了&#xff0c;接下来我们需要让Docker发挥它的法力了。 Docker常用软件的安装 1.Redis 缓存安装 1.1 下载 docker pull redis:7.4.1 #可改为自己需要的版本 1.2 创建本地目录存储…

Redis全系列学习基础篇之位图(bitmap)常用命令的解析

文章目录 描述常用命令及解析常用命令解析 应用场景统计不确定时间周期内用户登录情况思路分析实现 统计某一特定时间内活跃用户(登录一次即算活跃)的数量思路分析与实现 描述 bitmap是redis封装的用于针对位(bit)的操作,其特点是计算效率高&#xff0c;占用空间少,常被用来统计…

面试题:JVM(四)

new对象流程&#xff1f;&#xff08;龙湖地产&#xff09; 对象创建方法&#xff0c;对象的内存分配。&#xff08;360安全&#xff09; 1. 对象实例化 创建对象的方式有几种&#xff1f; 创建对象的步骤 指针碰撞&#xff1a;以指针为分界线&#xff0c;一边是已连续使用的…

手写实现call,apply,和bind方法

手写实现call&#xff0c;apply和bind方法 call&#xff0c;apply和bind方法均是改变this指向的硬绑定方法&#xff0c;要想手写实现此三方法&#xff0c;都要用到一个知识点&#xff0c;即对象调用函数时&#xff0c;this会指向这个对象&#xff08;谁调用this就指向谁&#…

【python ASR】win11-从0到1使用funasr实现本地离线音频转文本

文章目录 前言一、前提条件安装环境Python 安装安装依赖,使用工业预训练模型最后安装 - torch1. 安装前查看显卡支持的最高CUDA的版本&#xff0c;以便下载torch 对应的版本的安装包。torch 中的CUDA版本要低于显卡最高的CUDA版本。2. 前往网站下载[Pytorch](https://pytorch.o…

AI驱动无人驾驶:安全与效率能否兼得?

内容概要 如今&#xff0c;人工智能正以其神奇的魔力驱动着无人驾驶的浪潮&#xff0c;带来了无数令人兴奋的可能性。这一领域的最新动态显示&#xff0c;AI技术在车辆的决策过程和实时数据分析中发挥着重要作用&#xff0c;帮助车辆更聪明地应对复杂的交通环境。通过实时监测…

从头开始学PHP之面向对象

首先介绍下最近情况&#xff0c;因为最近入职了且通勤距离较远&#xff0c;导致精力不够了&#xff0c;而且我发现&#xff0c;人一旦上了班&#xff0c;下班之后就不想再进行任何脑力劳动了&#xff08;对大部分牛马来说&#xff0c;精英除外&#xff09;。 话不多说进入今天的…

Systemd:现代 Linux 系统服务管理的核心

Systemd&#xff1a;现代 Linux 系统服务管理的核心 引言 Systemd 是一种现代的系统和服务管理器&#xff0c;用于在 Linux 系统启动时初始化用户空间&#xff0c;并通过服务管理和资源控制实现系统的自动化管理。自发布以来&#xff0c;Systemd 已逐渐取代传统的 SysVinit 和…

Linux初阶——线程(Part3):POSIX 信号量 CP 模型变体

一、什么是 POSIX 信号量 信号量本质就是一个统计资源数量的计数器。​​​​​​​ 1、PV 操作 pv操作就是一种让信号量变化的操作。其中 P 操作可以让信号量减 1&#xff08;如果信号量大于 0&#xff09;&#xff0c;V 操作可以让信号量加 1. 2、信号量类型——sem_t 3…

《女巫攻击:潜伏在网络背后的隐秘威胁与防御策略》

目录 引言 一、基本概念 二、攻击机制 三、Sybil攻击类型 1、直接通信 2、间接通信 3、伪造身份 4、盗用身份 5、同时攻击 6、非同时攻击 四、攻击影响 五、防御措施 总结 引言 随着区块链技术和去中心化网络的迅速发展&#xff0c;网络安全问题也愈发引起关注。其…

Mybatis-plus入门教程

注意版本 jdk 18 springboot 3.1.0 mybatis 3.0.3 mybatisplus 3.5.5 快速入门 构建模块 导入依赖 <properties><maven.compiler.source>18</maven.compiler.source><maven.compiler.target>18</maven.compiler.target><project.build…

插件式模块化软件框架的思想图解一(框架篇)

插件式模块化软件框架的思想图解一&#xff08;框架篇&#xff09; Chapter1 插件式模块化软件框架的思想图解一&#xff08;框架篇&#xff09;一、前述二、模块化原则1、高度独立2、接口规范 三、从管理需求出发四、框架雏形五、接口引用规定六、子模块与代码模板七、把优秀当…