【Linux】线程封装与互斥(万字)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

目录

文章目录

前言

C++多线程的用法

对原生线程进行一次封装

理解pthread线程

Linux线程互斥

进程线程间的互斥相关背景概念

互斥量mutex

操作共享变量会有问题的售票系统代码

互斥量的接口

初始化互斥量

销毁互斥量

互斥量加锁和解锁

改进上面的售票系统:

方法一:定义一个静态或全局的锁变量gmutex

方法二:定义一个局部的锁

方法三: 临时对象, RAII风格的加锁和解锁(构造加锁,析构解锁)

互斥量实现原理探究

可重入VS线程安全

概念

常见的线程不安全的情况

常见的线程安全的情况

常见不可重入的情况

常见可重入的情况

可重入与线程安全联系

可重入与线程安全区别

总结



前言

世上有两种耀眼的光芒,一种是正在升起的太阳,一种是正在努力学习编程的你!一个爱学编程的人。各位看官,我衷心的希望这篇博客能对你们有所帮助,同时也希望各位看官能对我的文章给与点评,希望我们能够携手共同促进进步,在编程的道路上越走越远!


提示:以下是本篇文章正文内容,下面案例可供参考

C++多线程的用法

#include <thread> // C++多线程所对应的头文件
#include <unistd.h>

void threadrun(int num)
{
    while(num)
    {
        std::cout << "I am a thread, num: " << num << std::endl;
        sleep(1);
    }
}
int main()
{
    std::thread t1(threadrun, 10);
    std::thread t2(threadrun, 10);
    std::thread t3(threadrun, 10);
    std::thread t4(threadrun, 10);
    std::thread t5(threadrun, 10);

    while(true)
    {
        std::cout << "I am a main thread "<< std::endl;
        sleep(1);
    }

    t1.join();
    t2.join();
    t3.join();
    t4.join();
    t5.join();

    return 0;
}

对原生线程进行一次封装

C++11的多线程,是对原生线程的封装,所以在编译时,要链接上 -lpthread原生线程库。

为什么要封装呢?

  • 语言的跨平台性。在Linux当中,我们所使用的C++11的多线程,用的是Linux的pthread库;如果是在Windows当中,用的是Windows的原始对应的系统调用创建线程的接口,C++在给Linux和Windows当中提供的标准库是不一样的,C++给我们提供的标准库编译出来,在Windows中是Windows版本的,在Linux中是Linux版本的,所以对应的库不一样,但是代码是一样的。

Windows当中还要不要包含pthread库呢?

  • 不需要。语言具有跨平台性。

其它语言呢?

  • 大部分的语言要在Linux下跑多线程,必须要用原生线程库,因为pthread库是Linux提供多线程的底层唯一方式。
thread.hpp
#ifndef __THREAD_HPP__
#define __THREAD_HPP__

#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>// C++线程的头文件

namespace ThreadModule
{
    // using就相当于typedef,using定义了一个新类型std::function<void(T&)>,是一个新语法
    template<typename T>
    using func_t = std::function<void(T)>;
    // typedef std::function<void(const T&)> func_t;

    template<typename T>
    class Thread
    {
    public:
        void Excute()
        {
            _func(_data);
        }
    public:
        Thread(func_t<T> func, T data, const std::string& name = "none-name")
            : _func(func), _data(data), _threadname(name), _stop(true)
        {}

        // 方法:static修饰的函数中的参数是没有this指针的
        // 因为没有this指针,所以该函数里面也无法调用该类的成员对象了
        static void* threadroutine(void* args) // 类成员函数,形参是有this指针的!!
        {
            Thread<T>* self = static_cast<Thread<T> *>(args);
            self->Excute();
            return nullptr;
        }
        bool Start()
        {
            // pthread_create()函数中的参数3的函数指针,要求的参数类型是void*,
            // 而threadroutine()函数是类成员函数,有一个this指针,所以调不了该函数
            int n = pthread_create(&_tid, nullptr, threadroutine, this);// 把当前对象this传threadroutine()
            if (!n)
            {
                _stop = false;
                return true;
            }
            else
            {
                return false;
            }
        }
        void Detach()
        {
            if (!_stop)
            {
                pthread_detach(_tid);
            }
        }
        void Join()
        {
            if (!_stop)
            {
                pthread_join(_tid, nullptr);
            }
        }
        std::string name()
        {
            return _threadname;
        }
        void Stop()
        {
            _stop = true;
        }
        ~Thread() {}

    private:
        pthread_t _tid;
        std::string _threadname;
        T _data;  // 模板的参数类型T就直接是指针了
        func_t<T> _func;
        bool _stop;
    };
} 

#endif
testThread.cc
using namespace ThreadModule;

void print(int &cnt)
{
    while (cnt)
    {
        std::cout << "hello I am myself thread, cnt: " << cnt-- << std::endl;
        sleep(1);
    }
}

const int num = 10;

int main()
{
    std::vector<Thread<int> > threads;
    // 1. 创建一批线程
    for (int i = 0; i < num; i++)
    {
        std::string name = "thread-" + std::to_string(i + 1);
        threads.emplace_back(print, 10, name);
    }

    // 2. 启动 一批线程
    for (auto &thread : threads)
    {
        thread.Start();
    }

    // 3. 等待一批线程
    for (auto &thread : threads)
    {
        thread.Join();
        std::cout << "wait thread done, thread is: " << thread.name() << std::endl;
    }

    // Thread<int> t1(print, 10);
    // t1.Start();
    // std::cout << "name: " << t1.name() << std::endl;
    // t1.Join();
    return 0;
}

理解pthread线程

  • 二进制代码刚运行时,pthread_t tid;只有一个进程,等执行到pthread_create()代码时,才创建出了新线程,从操作层面上:创建、终止、等待线程的接口都是在库当中实现的,库是将轻量级进程做了封装,所以给上层用户提供的就是库当中的方法,所以把我们用的线程叫做用户级线程。
  • 线程库首先要映射到当前进程的地址空间中(堆栈之间的共享区)!
  • 线程的管理工作要由库来进行管理!

那么库要如何管理线程呢?

  • 先描述,再组织!
  • 库里面要有描述线程的结构体,以及把所有的线程都组织在一起,线程的控制块:struct_pthread,一般我们喜欢将线程的控制块叫做struct_tcb,只不过Linux不提供struct_tcb,创建一个线程就为我们在库当中维护一个控制块结构,而每一个控制块结构的起始地址,就叫做线程的tid。tid的本质就是一个堆栈之间共享区的线程库中的控制块结构的起始地址(虚拟地址)。

  • 线程的整体结构:struct_pthread、线程局部存储、线程栈。
  • 动态库是共享库,多个进程,每一个进程都创建多个线程,每个进程的进程地址空间堆栈之间的共享区都是同一个动态库。
  • 全局变量在进程地址空间当中的已初始化数据区。
  • 线程局部存储:不能用来存储stl的容器数据,只能用来存储内置类型,因为它是一个C语言的库,不认识其它容器的数据。

线程库是不是磁盘当中的一个普通文件呢?

  • 线程库是一个动态库,也是磁盘当中的一个文件。

执行流(task_struct)是如何找到线程的线程栈的?

man clone

轻量级进程是Linux当中线程实现的底层方案,但真正线程实现是在库当中实现的。

Linux线程互斥

进程线程间的互斥相关背景概念

  • 临界资源:多线程执行流共享的资源就叫做临界资源
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
  • 当共享资源做了保护,就叫做临界资源。

互斥量mutex

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

操作共享变量会有问题的售票系统代码

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <unistd.h> 
#include <pthread.h> 
int ticket = 100;
void* route(void* arg)
{
    char* id = (char*)arg;
    while (1) 
    {
        if (ticket > 0) 
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        }
        else 
        {
            break;
        }
    }
}
int main(void)
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, "thread 1");
    pthread_create(&t2, NULL, route, "thread 2");
    pthread_create(&t3, NULL, route, "thread 3");
    pthread_create(&t4, NULL, route, "thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
}

一次执行结果:
thread 4 sells ticket : 100
...
thread 4 sells ticket : 1
thread 2 sells ticket : 0
thread 1 sells ticket : -1
thread 3 sells ticket : -2

抢票的结构最终出现了负数,造成了数据不一致,为什么?

  • 因为g_tickets是一个全局的变量,这个全局的变量是没有被保护起来的,并且对全局变量_tickets的判断不是原子的。
  • 当_tickets == 1时,多个线程并发的判断,让很多线程都进入抢票逻辑。
  • 全局变量g_tickets是在内存当中的,当线程1执行if语句时,要进行票数的判断,判断是逻辑运算,必须在CPU内部运行,此时内存中的票数数据拷贝到CPU的寄存器当中,执行到usleep语句时,线程1被切换了出去,因为寄存器只有一套,所以为了保存线程1的上下文数据,数据被线程1带走了;
  • 此时切换到线程2执行判断逻辑,与线程1的情况一样也被切换走了,线程3和4都是如此;
  • 那么当线程1再次切换回来的时候,要重新在内存中读取数据,打印并--操作;
  • _tickets--(不是原子的)等价于_tickets = _tickets - 1;--操作数据改变,会影响原生内存中的数据,因为会写回内存;
  • 那么其它3个线程也都进入了抢票逻辑,所以会读取内存中的数据,打印并--操作,所以会打印出了负数的情况。

-- 操作并不是原子操作,而是对应三条汇编指令:

  • load :将共享变量ticket从内存加载到寄存器中
  • update : 更新寄存器里面的值,执行-1操作
  • store :将新值,从寄存器写回共享变量ticket的内存地址

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

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

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

互斥量的接口

初始化互斥量

初始化互斥量有两种方法:

  • 方法1,如果你定义的锁是静态的或者是全局的:那么这个锁可以不用init初始化和destroy销毁;你可以直接定义一个锁,并用PTHREAD_ MUTEX_ INITIALIZER宏对其进行初始化。
 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER 
  • 方法2,动态分配:

如果这把锁是一个局部的:建议init初始化和destroy销毁

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict 
attr); 
参数: 
mutex:要初始化的互斥量 
attr:NULL 

尝试的去申请锁:

int pthread_mutex_trylock(pthread_mutex_t *mutex);
尝试的去申请锁,跟申请锁成功和函数调用失败是一样的,但是申请锁失败了,不会阻塞,会立马出错返回。

销毁互斥量

销毁互斥量需要注意:

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

互斥量加锁和解锁

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

调用 int pthread_mutex_lock(pthread_mutex_t *mutex) 时,可能会遇到以下情况:

  • 申请成功:函数就会返回,允许你继续向后运行;
  • 申请锁失败:函数就会阻塞,不允许你继续向后运行;
  • 函数调用失败:出错返回,比如:申请锁的对象已经被释放了

改进上面的售票系统:

方法一:定义一个静态或全局的锁变量gmutex

// 抢票逻辑
#include <iostream>
#include <vector>
#include <mutex> // C++11里面锁的头文件
#include "Thread.hpp"

using namespace ThreadModule;

// 数据不一致
int g_tickets = 10000; // 共享资源,没有保护的
// 线程的数据类型
class ThreadData
{
public:
    ThreadData(int& tickets, const std::string& name)
        : _tickets(tickets), _name(name), _total(0))
    {
    }
    ~ThreadData()
    {
    }

public:
    int& _tickets; // 所有的线程,最后都会引用同一个全局的g_tickets
    std::string _name;
    int _total;
};

// 方法一:定义一个静态或全局的锁变量gmutex
// gmutex中的g表示globle的意思
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;// 定义一个锁

 // 线程执行的方法
 void route(ThreadData *td)
 {
     // 加锁
     while (true)
     {
         // 访问临界资源的代码,叫做临界区!
         // 我们加锁,本质就是把多线程的并行执行变为串行执行 --- 加锁的力度要越细越好
         pthread_mutex_lock(&gmutex); // 加锁 : 竞争锁是自由竞争的,竞争锁的能力太强的线程,会导致其他线程抢不到锁 --- 造成了其他线程的饥饿问题!
         
         if (td->_tickets > 0)     
         {
             // 模拟一次抢票的逻辑
             usleep(1000);
             printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets);
             td->_tickets--;  
             pthread_mutex_unlock(&gmutex); // 解锁 法一:
                                          
             td->_total++;
         }
         else
         {
             pthread_mutex_unlock(&gmutex); // 解锁 法一:
             break;
         }
     }
     // 解锁
 }

const int num = 4;
int main()
{
    std::cout << "main: &tickets: " << &g_tickets << std::endl;

    // std::mutex mutex;// C++11的做法,不用初始化,因为它有构造函数

    std::vector<Thread<ThreadData*>> threads;
    std::vector<ThreadData*> datas;// 每个线程抢了多少张票
    // 1. 创建一批线程
    for (int i = 0; i < num; i++)
    {
        std::string name = "thread-" + std::to_string(i + 1);
        ThreadData* td = new ThreadData(g_tickets, name);
        threads.emplace_back(route, td, name);
        datas.emplace_back(td);
    }

    // 2. 启动 一批线程
    for (auto& thread : threads)
    {
        thread.Start();
    }

    // 3. 等待一批线程
    for (auto& thread : threads)
    {
        thread.Join();
        std::cout << "wait thread done, thread is: " << thread.name() << std::endl;
    }
    sleep(1);
    // 4. 输出统计数据
    for (auto data : datas)
    {
        std::cout << data->_name << " : " << data->_total << std::endl;
        delete data;
    }

    // pthread_mutex_destroy(&mutex);

    return 0;
}

方法二:定义一个局部的锁

// 抢票逻辑
#include <iostream>
#include <vector>
#include <mutex> // C++11里面锁的头文件
#include "Thread.hpp"

using namespace ThreadModule;

// 数据不一致
int g_tickets = 10000; // 共享资源,没有保护的
// 线程的数据类型
class ThreadData
{
public:
    ThreadData(int& tickets, const std::string& name, pthread_mutex_t &mutex)
        : _tickets(tickets), _name(name), _total(0), _mutex(mutex)
    {
    }
    ~ThreadData()
    {
    }

public:
    int& _tickets; // 所有的线程,最后都会引用同一个全局的g_tickets
    std::string _name;
    int _total;
    pthread_mutex_t& _mutex;
};

 // 线程执行的方法
 void route(ThreadData *td)
 {
     // 加锁
     while (true)
     {       
         // 方法二:加锁
         pthread_mutex_lock(&td->_mutex);
         if (td->_tickets > 0)      
         {
             // 模拟一次抢票的逻辑
             usleep(1000);
             printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets); 
             td->_tickets--;  
                                          
             pthread_mutex_unlock(&td->_mutex); // 解锁 法二:
             td->_total++;
         }
         else
         {
             pthread_mutex_unlock(&td->_mutex); // 解锁 法二:
             break;
         }
     }
     // 解锁
 }

const int num = 4;
int main()
{
    // 方法二:定义一个局部的锁
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, nullptr);// 锁的属性设为nullptr

    // std::mutex mutex;// C++11的做法,不用初始化,因为它有构造函数

    std::vector<Thread<ThreadData*>> threads;
    std::vector<ThreadData*> datas;// 每个线程抢了多少张票
    // 1. 创建一批线程
    for (int i = 0; i < num; i++)
    {
        std::string name = "thread-" + std::to_string(i + 1);
        ThreadData* td = new ThreadData(g_tickets, name, mutex);// 把局部的锁,以参数的形式传递到线程内部,而不是以全局的形式
        threads.emplace_back(route, td, name);
        datas.emplace_back(td);
    }

    // 2. 启动 一批线程
    for (auto& thread : threads)
    {
        thread.Start();
    }

    // 3. 等待一批线程
    for (auto& thread : threads)
    {
        thread.Join();
        std::cout << "wait thread done, thread is: " << thread.name() << std::endl;
    }
    sleep(1);
    // 4. 输出统计数据
    for (auto data : datas)
    {
        std::cout << data->_name << " : " << data->_total << std::endl;
        delete data;
    }

    pthread_mutex_destroy(&mutex);
    return 0;
}

方法三: 临时对象, RAII风格的加锁和解锁(构造加锁,析构解锁)

LockGuard.hpp
#ifndef __LOCK_GUARD_HPP__
#define __LOCK_GUARD_HPP__

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

class LockGuard
{
public:
    LockGuard(pthread_mutex_t* mutex) :_mutex(mutex)
    {
        pthread_mutex_lock(_mutex); // 构造加锁
    }
    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }
private:
    pthread_mutex_t* _mutex;
};

#endif
void route(ThreadData* td)
{
    while (true)
    {
        {   // 担心就用这个
            LockGuard guard(&td->_mutex); // 临时对象, RAII风格的加锁和解锁(构造加锁,析构解锁)

            // td->_mutex.lock();C++11的做法
            // std::lock_guard<std::mutex> lock(td->_mutex);C++11中也封装了lock_guard
            if (td->_tickets > 0) // 1
            {
                usleep(1000);
                printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets); // 2
                td->_tickets--;                                                           // 3
                td->_total++;
                // td->_mutex.unlock();C++11
            }
            else
            {
                // td->_mutex.unlock();
                break;
            }
        }
    }
}

什么是原子的?

  • 一条语句将来被汇编之后,只有一条汇编。

互斥量实现原理探究

  • 经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
  • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一 个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下

互斥的底层实现:

  1. 假设CPU中有一个寄存器%al,锁相当于内存当中的整型变量;
  2. 假设刚开始把锁初始化为1,线程1此时要申请锁,它把0放入%al的寄存器中,再把寄存器中的值和锁变量中的值进行交换,就完成了加锁,若线程1加锁成功,那么线程1就执行它的代码;
  3. 假设线程1在执行完第二条语句时,线程1被切换成线程2,线程1被切换走时,会把寄存器中的数据带走;
  4. 线程2开始申请锁,线程2调用pthread_mutex_lock()函数从0开始申请锁,将0放入寄存器中,再与内存中的值做交换,因为都是0,所以申请锁失败,挂起等待,将数据带走;
  5. 等线程1回来时,将之前带走的数据恢复到寄存器中,加锁成功;
  6. 成功了之后,还要解锁,将mutex变量重新置为1。

  • 寄存器内部的数据不属于CPU,它属于当前线程的硬件上下文
  • 临界区内部,正在访问临界区的线程,可以被OS切换调度,被切换出去的时候,把锁也带走了。申请锁成功的线程1正在访问临界区,即使线程1被挂起了,其它任何线程都进不来临界区,那么临界区对于其它的线程来说就是原子的,该线程是安全的。
  • 互斥是为了解决数据安全的问题;同步是为了解决资源被充分利用的问题。
  • 线程被切换的时机是随机的。
  • 交换的本质:不是拷贝到寄存器,而是所有线程在争锁的时候,只有一个1。
  • 交换的时候,只有一条汇编 --- 原子的。
  • CPU寄存器硬件只有一套,但是CPU寄存器内部的数据是线程的硬件上下文。
  • 数据在内存里,所有线程都能访问,属于共享的。但是如果转移到CPU内部寄存器中,就属于一个线程私有了。
  • 互斥:任何时刻只允许一个线程进行访问。

线程互斥:

  • 保护并不是把临界资源怎么样,而是保护多个线程都会执行访问临界资源的代码,我们要保护的是临界区。

可重入VS线程安全

概念

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

常见的线程不安全的情况

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

常见的线程安全的情况

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

常见不可重入的情况

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

常见可重入的情况

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

可重入与线程安全联系

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

可重入与线程安全区别

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


总结

好了,本篇博客到这里就结束了,如果有更好的观点,请及时留言,我会认真观看并学习。
不积硅步,无以至千里;不积小流,无以成江海。

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

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

相关文章

数字信号处理教程(2)——时域离散信号与时域离散系统

上回书说到数字信号处理中基本的一个通用模型框架图。今天咱们继续&#xff0c;可以说今天要讲的东西必须是学习数字信号处理必备的观念——模拟与数字&#xff0c;连续和离散。 时域离散序列 由于数字信号基本都来自模拟信号&#xff0c;所以先来谈谈模拟信号。模拟信号就是…

小米MIX Fold 4折叠屏手机背面渲染图曝光

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 更多资源欢迎关注 7 月 3 日消息&#xff0c;消息源 Evan Blass 今天在 X 平台发布推文&#xff0c;分享了小米 MIX Fold 4 折叠屏手机的高清渲染图&#xff08;图片有加工成分在&#xff0c;最终零售版本可能会存在差异…

基于Hadoop平台的电信客服数据的处理与分析③项目开发:搭建基于Hadoop的全分布式集群---任务1:运行环境说明

任务描述 项目的运行环境是基于Hadoop的全分布式模式集群。 任务的主要内容是规划集群节点及网络使用&#xff0c;准备初始环境&#xff0c;关闭防火墙和Selinux。 任务指导 1. 基于Hadoop的全分布式模式集群&#xff0c;如下图所示&#xff1b; 2. 硬软件环境&#xff1a;…

微信小程序转发朋友圈详细教程

微信小程序转发朋友圈功能&#xff0c;官方说的很官方&#xff0c;容易踩坑 官方链接戳这里 想分享朋友圈必须要分享好友 onShareTimeline() { } 想要生效必须先定义 onShareAppMessage() { } /*** 用户点击右上角分享*/onShareAppMessage() { },onShareTimeline() { } 简单…

当《开心消消乐》遇上 AI 推理,我们找到了高质量关卡背后的原因!

随着 AI 热潮席卷各行各业&#xff0c;其落地应用已经成为企业技术研发升级的工作重心。人工智能应用的升级不仅需要软件层面的升级迭代&#xff0c;还需要大规模基础设施的支撑。然而&#xff0c;自行搭建大规模算力、存储基础设施对于大多数企业而言都存在技术难度、人力资源…

OBD诊断(ISO15031) 04服务

文章目录 功能简介ISO 9141-2、ISO 14230-4和SAE J1850的诊断服务定义1、清除/重置与排放相关的诊断信息请求消息定义2、请求与排放相关的DTC响应消息定义3、报文示例 ISO 15765-4的诊断服务定义1、请求与排放相关的DTC请求消息定义2、请求与排放相关的DTC响应消息定义3、否定响…

网络基础:OSPF 协议

OSPF&#xff08;Open Shortest Path First&#xff09;是一种广泛使用的链路状态路由协议&#xff0c;用于IP网络中的内部网关协议&#xff08;IGP&#xff09;。OSPF通过在网络中的所有路由器之间交换路由信息&#xff0c;选择从源到目的地的最优路径。OSPF工作在OSI模型的第…

Python处理excel数据详解

1.导入文件 注意&#xff1a;要把excel放到跟你的python文件在同一个地方 import pandas as pd import numpy as np dfpd.read_excel("鸢尾花训练数据.xlsx",engine"openpyxl") import pandas 先引入 (若没有下载 需要在终端下载 pip install pandas)…

【C语言】enum 关键字

在C语言中&#xff0c;enum关键字用于定义枚举类型。枚举是一种用户自定义的数据类型&#xff0c;由一组命名的整型常量构成。使用枚举可以提高代码的可读性和可维护性&#xff0c;特别是在表示一组相关的常量时。 定义和使用枚举类型 基本定义 要定义一个枚举类型&#xff…

机器人控制系列教程之Delta机器人轨迹规划

并联机器人轨迹规划原理 轨迹规划&#xff1a;并联机器人的轨迹规划相比于串联机器人更加严苛&#xff0c;因为在大多数工作场合都有高速、高精度、轻载的特征。其一般运动轨迹为 Point-To-Point&#xff0c;对这始末两点的轨迹规划在 Delta 机器人乃至所有机器人领域具有重要…

01 数据采集层 流量分发第一步规范采集海量数据

《易经》&#xff1a;“初九&#xff1a;潜龙勿用”。潜龙的意思是隐藏&#xff0c;阳气潜藏&#xff0c;阳爻位于最下方称为“初九”&#xff0c;龙潜于渊&#xff0c;是学而未成的阶段&#xff0c;此时需要打好基础。 而模块一我们就是讲解推荐系统有关的概念、基础数据体系…

2024年7月2日 (周二) 叶子游戏新闻

老板键工具来唤去: 它可以为常用程序自定义快捷键&#xff0c;实现一键唤起、一键隐藏的 Windows 工具&#xff0c;并且支持窗口动态绑定快捷键&#xff08;无需设置自动实现&#xff09;。 卸载工具 HiBitUninstaller: Windows上的软件卸载工具 经典名作30周年新篇《恐怖惊魂夜…

VirtualBox Ubuntu Sever配置双网卡

Ubuntu 版本&#xff1a;Ubuntu Server 2404 vitrualBox 网卡配置&#xff1a; 如上配置后&#xff0c;ifconfig 只能看到 网卡1 应用了。要应用 网卡2 需要更改文件 /etc/netplan/50-cloud-init.yaml&#xff08;不同的ubuntu版本这个文件名可能不同&#xff09; 首先 ifcon…

《昇思25天学习打卡营第7天|函数式自动微分》

文章目录 今日所学&#xff1a;一、函数与计算图二、微分函数与梯度计算三、Stop Gradient四、Auxiliary data五、神经网络梯度计算总结 今日所学&#xff1a; 今天我学习了神经网络训练的核心原理&#xff0c;主要是反向传播算法。这个过程包括将模型预测值&#xff08;logit…

PyCharm远程开发配置(2024以下版本)

目录 PyCharm远程开发配置 1、清理远程环境 1.1 点击Setting 1.2 进入Interpreter 1.3 删除远程环境 1.4 删除SSH 2、连接远程环境 2.1 点击Close Project 2.2 点击New Project 2.3 项目路径设置 2.4 SSH配置 2.5 选择python3解释器在远程环境的位置 2.6 配置远程…

EXCEL返回未使用数组元素(未使用值)

功能简介&#xff1a; 在我们工作中&#xff0c;需要在EXCEL表列出哪些元素&#xff08;物品或订单&#xff09;已经被使用了&#xff08;或使用了多少次&#xff09;&#xff0c;哪些没有被使用。 当数量过于庞大时人工筛选或许不是好办法&#xff0c;我们可以借助公式&…

鸿蒙数据防泄漏(DLP)【Data Loss Prevention Kit简介】

Data Loss Prevention Kit简介 Data Loss Prevention Kit&#xff08;数据防泄漏服务&#xff0c;简称为DLP&#xff09;&#xff0c;是系统提供的系统级的数据防泄漏解决方案&#xff0c;提供文件权限管理、加密存储、授权访问等能力&#xff0c;数据所有者可以基于帐号认证对…

Python基础小知识问答系列-可迭代型变量赋值

1. 问题&#xff1a; 怎样简洁的把列表中的元素赋值给单个变量&#xff1f; 当需要列表中指定几个值时&#xff0c;剩余的变量都收集在一起&#xff0c;该怎么进行变量赋值&#xff1f; 当只需要列表中指定某几个值&#xff0c;其他值都忽略时&#xff0c;该怎么…

【数据分享】《中国金融年鉴》1986-2020年PDF版

而今天要免费分享的数据就是1986-2020年间出版的《中国金融年鉴》并以多格式提供免费下载。&#xff08;无需分享朋友圈即可获取&#xff09; 数据介绍 《中国金融年鉴》自1986年起&#xff0c;逐年记录着中国金融领域的发展历程、政策变化和市场动态。这部年鉴不仅是金融专业…

PD虚拟机和VirtualBox有什么区别?Parallels Desktop 19.1.1 破解版

随着计算机技术的不断发展&#xff0c;虚拟机软件在现代信息技术领域中扮演着越来越重要的角色。虚拟机不仅可以帮助用户在一台物理机器上运行多个操作系统&#xff0c;还能有效隔离不同环境&#xff0c;提升系统安全性。在众多的虚拟机软件中&#xff0c;PD虚拟机&#xff08;…