Linux——线程的同步与互斥

news2024/11/29 3:50:14

目录

模拟抢火车票的过程

代码示例

thread.cc

Thread.hpp

运行结果

分析原因

tickets减到-2的本质 

解决抢票出错的方案

临界资源的概念

原子性的概念

加锁

定义

初始化

销毁

代码形式如下

代码示例1:

代码示例2:

总结

如何看待锁

申请失败将会阻塞

 pthread_mutex_tyrlock

互斥锁实现原理

 锁是如何实现互斥的?

封装加锁组件(RAII风格)

Mutex.hpp

mythread.cc

RAII风格

可重入与线程的关系(浅谈)

可重入与线程安全联系

可重入与线程安全区别

死锁

概念

提出问题

死锁的四个必要条件

如何破坏死锁

概念:

线程同步

本质

先谈生产者消费者模型

"321"原则

生产者消费者模型的特点

条件变量

概念

定义

具体操作 

所用函数

定义的返回值

代码示例:

 唤醒方式


模拟抢火车票的过程

代码示例

thread.cc


#include <iostream>
#include <pthread.h>
#include <cstdio>
#include <unistd.h>
#include <memory>
#include <cstring>
#include "Thread.hpp"

// pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

// 共享资源, 火车票
int tickets = 10000;
// 就需要尽可能的让多个线程交叉执行
// 多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换
// 线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。
// 线程是什么时候检测上面的问题呢?从内核态返回用户态的时候,线程要对调度状态进行检测
// 如果可以,就发生切换。

// 1、多个执行流进行安全访问的共享资源 - 临界资源?
// 2、我们把多个执行流中,访问临界资源的代码 -- 临界区 -- 往往是线程代码很小的一部分
// 3、想让多个线程串行访问共享资源 -- 互斥
// 4、对一个资源进行访问的时候,要么不做,要么做完 -- 原子性
// 一个对资源进行操作,如果只用一条汇编语句就能完成 -- 原子性
// 反之:不是原子的 -- 当前理解,方便表述

// 解决方案:加锁

void *getTicket(void *args)
{
    // 获取线程的名字
    std::string username = static_cast<const char *>(args);

    while (true)
    {
        // pthread_mutex_lock(&lock);
        if (tickets > 0)
        {
            usleep(1000);

            // 只有票数大于0才值得抢
            std::cout << username << "正在抢票" << tickets-- << std::endl;
            // 用这段时间来模拟真实的抢票要花费的时间
            // pthread_mutex_unlock(&lock);
        }
        else
        {
            // pthread_mutex_unlock(&lock);

            break;
        }
    }
    return nullptr;
}

int main()
{

    std::unique_ptr<Thread> thread1(new Thread(getTicket, (void *)"user1", 1));
    std::unique_ptr<Thread> thread2(new Thread(getTicket, (void *)"user2", 2));
    std::unique_ptr<Thread> thread3(new Thread(getTicket, (void *)"user3", 3));
    std::unique_ptr<Thread> thread4(new Thread(getTicket, (void *)"user4", 4));

    thread1->join();
    thread2->join();
    thread3->join();
    thread4->join();

    return 0;
}

Thread.hpp


#pragma once

#include <iostream>
#include <pthread.h>
#include <functional>
#include <cassert>
#include <cstring>
#include <string>

class Thread;

// 上下文
class Context
{
public:
    Thread *this_;
    void *args_;
public:
    Context():this_(nullptr),args_(nullptr)
    {}
    ~Context()
    {}
};





class Thread
{
public:
    typedef std::function<void *(void *)> func_t;
    const int num = 1024;

public:
    Thread(func_t func, void *args, int number)
        : func_(func), args_(args)
    {

        // name_="thread-";
        // name_+=std::to_string(number);

        char buffer[num];
        snprintf(buffer, sizeof buffer, "thread-%d", number);
        name_ = buffer;
        
        // 异常 == if : 意料之外用异常或者if判断
        // assert:意料之中用assert

        Context *ctx=new Context();

        ctx->this_=this;
        ctx->args_=args_;



        int n = pthread_create(&tid_, nullptr, start_routine,ctx); // TODO
        assert(n == 0);
        (void)n;
        // 编译debug的方式发布的时候是存在的,release方式发布,
        // assert就不存在l,n就是一个定义,
        // 但是没有使用的变量,有些编译器下会有warning
    }

    // 在类内创建线程,想让线程执行对应的方法,需要将方法设置成为static
    // 类内成员,有缺省参数!在第一参数中包含了一个this指针
    static void *start_routine(void *args) 
    {
        Context *ctx=static_cast<Context*>(args);
        
        void *ret=ctx->this_->run(ctx->args_);

        delete ctx;

        return ret;

        // 静态方法不能调用成员方法或者成员变量
        // return func_(args_);

    }

   

    void join()
    {
        int n = pthread_join(tid_, nullptr);
        assert(n == 0);

        (void)n;
    }


    void *run(void *args)
    {
        return func_(args);
    }


    ~Thread()
    {
        // do northing
    }

private:
    std::string name_;
    func_t func_;
    void *args_;

    pthread_t tid_;
};

运行结果

user2正在抢票16
user4正在抢票15
user1正在抢票14
user3正在抢票13
user2正在抢票12
user4正在抢票11
user1正在抢票10
user3正在抢票9
user2正在抢票8
user4正在抢票7
user1正在抢票6
user3正在抢票5
user2正在抢票4
user4正在抢票3
user1正在抢票2
user3正在抢票1
user2正在抢票0
user4正在抢票-1
user1正在抢票-2

分析原因

很明显,我们只有10000张票,可是运行结果中抢到了-2。比如这个那么这多出的3张票对应的人将没地方坐。那么为什么会出现这种问题呢,下面将来解答。

抢票出错的本质:

多个线程交叉执行!

多个线程交叉执行本质:

调度器频繁发生线程调度与切换。

线程一般在什么时候发生切换呢?

1、时间片到了

2、来了更高优先级的线程,线程等待的时候。

线程是什么时候检测上面的问题呢?

从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就发生切换。

判断票是否大于0的本质逻辑是:

if(tickets>0)

1、读取内存数据CPU内的寄存器中
2、进行判断

tickets--的本质逻辑是:

1、读取数据
2、更改数据
3、写回数据

对变量进行++,或者--,在C、C++上,看起来只有一条语句,但是汇编之后至少是三条语句:

1、从内存中读取数据到CPU内的寄存器中
2、在寄存器中让CPU进行对应的算逻运算
3、写回新的结果到内存中变量的位置

tickets减到-2的本质 

 

 

全局变量
int g_val=1000;

寄存器只有一套,因此不同的线程在寄存器的存放的上下文为各个线程私有

线程A在做完 1 和 2 后 带着自己的上下文切走了
1000->999


线程B在做完1 和 2 后也带着自己的上下文切走了        

结果现在线程A切换回来了(切回来之后需要让自己的上下文给重新写入寄存器) 又把数据改为999了。

我们定义的全局变量,在没有保护的时候,往往是不安全的,像上面多个线程在交替执行造成的数据安全问题,发生了数据不一致问题!

解决抢票出错的方案

我们可以使用加锁的方式来避免这种情况发生

临界资源的概念

1、多个执行流进行安全访问的共享资源 - 临界资源
2、我们把多个执行流中,访问临界资源的代码 -- 临界区 -- 往往是线程代码的很小的一部分
3、想让多个线程串行访问资源 -- 互斥
4、对一个资源进行访问的时候,要么不做,要么做完 -- 原子性 

注意:并不是上述所有代码都属于临界区,而是访问临界资源的代码是临界区 。临界区的代码往往粒度(代码长度)很小。

原子性的概念

原子性:一个资源进行的操作,如果只用一条汇编就能完成 -- 就是原子的      反之不是。

加锁

定义

可以把这个锁定义为全局的/静态的 -- 不用初始化和销毁
直接定义为 :
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

初始化

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

销毁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

代码形式如下

加锁
临界区
解锁

加锁和解锁之间的区域是临界区(也就是需要加锁的地方)。

代码示例1:

只展示了getTicket函数部分(包括加锁和解锁)


void *getTicket(void *args)
{
    // 获取线程的名字
    std::string username = static_cast<const char *>(args);

    while (true)
    {
	  // 加锁
        pthread_mutex_lock(&lock);

        // 判断的本质:
        // 1、读取内存数据CPU内的寄存器中
        // 2、进行判断
        if (tickets > 0)
        {
            usleep(1000);

            // 只有票数大于0才值得抢
            std::cout << username << "正在抢票" << tickets << std::endl;

            tickets--;
            // --的本质
            // 1、读取数据
            // 2、更改数据
            // 3、写回数据


            // 用这段时间来模拟真实的抢票要花费的时间
		// 解锁
            pthread_mutex_unlock(&lock);
        }
        else
        {
		// 解锁
            pthread_mutex_unlock(&lock);

            break;
        }
    }
    return nullptr;
}

代码示例2:

使用结构体封装了线程名和锁,并且使用了原生的线程创建方式


#include <iostream>
#include <vector>
#include <pthread.h>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <memory>
#include <cstring>
#include "Thread.hpp"

// 共享资源, 火车票
int tickets = 10000;
// 就需要尽可能的让多个线程交叉执行
// 多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换
// 线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。
// 线程是什么时候检测上面的问题呢?从内核态返回用户态的时候,线程要对调度状态进行检测
// 如果可以,就发生切换。

// 1、多个执行流进行安全访问的共享资源 - 临界资源?
// 2、我们把多个执行流中,访问临界资源的代码 -- 临界区 -- 往往是线程代码很小的一部分
// 3、想让多个线程串行访问共享资源 -- 互斥
// 4、对一个资源进行访问的时候,要么不做,要么做完 -- 原子性
// 一个对资源进行操作,如果只用一条汇编语句就能完成 -- 原子性
// 反之:不是原子的 -- 当前理解,方便表述

// 解决方案:加锁


// 使用结构体 对锁和名字进行了封装 使用的时候直接使用结构体对象调用即可
class ThreadData
{
public:
    ThreadData(const std::string &threadname, pthread_mutex_t *mutex_p)
        : threadname_(threadname), mutex_p_(mutex_p)
    {}
    ~ThreadData() {}

public:
    std::string threadname_;
    pthread_mutex_t *mutex_p_;
};

void *getTicket(void *args)
{
    // 获取线程的名字
    // std::string username = static_cast<const char *>(args);

    ThreadData *td = static_cast<ThreadData *>(args);

    while (true)
    {
        //  加锁
        pthread_mutex_lock(td->mutex_p_);

       

        // 判断的本质:
        // 1、读取内存数据CPU内的寄存器中
        // 2、进行判断
        if (tickets > 0)
        {
            usleep(1000);

            // 只有票数大于0才值得抢
            std::cout << td->threadname_ << "正在抢票" << tickets << std::endl;

            tickets--;
            // --的本质
            // 1、读取数据
            // 2、更改数据
            // 3、写回数据

            // 用这段时间来模拟真实的抢票要花费的时间

            // 解锁            				      pthread_mutex_unlock(td->mutex_p_);
        }
        else
        {
            pthread_mutex_unlock(td->mutex_p_);

            // 解锁
            break;
        }
 	  // 抢完票就完了吗? 当然不是
        usleep(1000);// 模拟形成一个订单给用户
    }
    return nullptr;
}

int main()
{
#define NUM 4

    pthread_mutex_t lock;

    pthread_mutex_init(&lock, nullptr);
    std::vector<pthread_t> tids(NUM);


    for (int i = 0; i < NUM; i++)
    {
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "thread %d", i + 1);
        ThreadData *td = new ThreadData(buffer, &lock);

        pthread_create(&tids[i], nullptr, getTicket, td);
    }

    for (const auto &tid : tids)
    {
        pthread_join(tid, nullptr);
    }


    pthread_mutex_destroy(&lock);

    
}

总结

加锁和解锁的过程:

多个线程由并行执行变为了串行执行的,程序变慢了!

锁只规定互斥访问,没有规定必须谁优先执行!

锁就是真正的让多个执行流进行竞争的结果。

执行结果一直是一个线程在抢票原因是:

该线程竞争能力强,其他线程则比较弱,因此每次都是该线程抢到了。

如何看待锁

  1.  锁,本身就是一个共享资源!全局的变量是要被保护的,锁是用来保护全局的资源的,锁本身也是全局资源,锁的安全谁来保护呢?
  2. pthread_mutex_lock、pthread_mutex_unlock:加锁的过程必须是安全的!加锁的过程其实是原子的!要不申请成功,要不申请失败。
  3. 如果申请成功,就继续向后执行,如果申请暂时没有成功,执行流会阻塞 

申请失败将会阻塞

 pthread_mutex_tyrlock

当然我们也可以使用 pthread_mutex_tyrlock(pthread_mutex_t *mutex)
函数目的是:试着去加锁
如果我加锁成功了,我会持有锁
如果加锁失败了,会出错返回

如何理解加锁和解锁的本质?

加锁的过程是原子的。

若当前线程1 线程2 线程3 同时访问临界资源的时候,如果线程1,申请锁成功,进入临界资源,正在访问临界资源期间,其他线程在做什么??

阻塞,等待。


如果线程1,申请锁成功,进入临界资源,正在访问临界资源期间,我可不可以被切换呢??

可以

当持有锁的线程被切走的的时候其他线程可以申请锁吗? 

当持有锁的线程被切走的时候,是抱着锁被切走的,即便自己被切走了,其他线程依旧无法申请锁成功,也便无法向后执行!直到我最终释放这个锁!

 

所以,对于其他线程而言,有意义的锁的状态,无非两种

1.申请锁前
2.申请锁后
站在其他线程角度,看待当前线程持有锁的过程就是原子的!!

使用锁注意事项:

1、未来我们在使用锁的时候,一定要尽量保证临界区的粒度(代码长度)非常小。

2、加锁是程序员行为,必须要做到要加都要加!

互斥锁实现原理

i++或者++i都不是原子的。
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。

 锁是如何实现互斥的?

%al:寄存器

交换的本质:共享的数据,交换到我的上下文中!!!

lock:
    movb $0, %al  //将0放到该线程的寄存器中
    xchgb %al, mutex // 寄存器和内存单元的数据相交换
        if(al寄存器的内容 > 0)
        {
            return 0;
        }
        else
            挂起等待;
        goto lock;
unlock:
    movb $1, mutex // 将1移动到内存里面
    唤醒等待Mutex的线程;
    return 0;    


1、CPU内寄存器只有一套被所有执行流共享
2、CPU内寄存器的内容,是每个执行流私有的,运行时上下文

封装加锁组件(RAII风格)

Mutex.hpp


#pragma once

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

// 写了一个锁的类,里面有加锁和解锁的成员函数
// 并且有一个锁的成员变量
class Mutex
{
public:
    Mutex(pthread_mutex_t *lock_p = nullptr) : lock_p_(lock_p)
    {
    }
    void lock()
    {
        if (lock_p_)
            pthread_mutex_lock(lock_p_);
    }

    void unlock()
    {
        if (lock_p_)
            pthread_mutex_unlock(lock_p_);
    }

    ~Mutex()
    {
    }

private:
    pthread_mutex_t *lock_p_; 
};

// 这个类中定义了一个上述类中的对象
// 并且将上述类中的成员函数放进该类中的构造函数和析构函数中
// guard:警卫
class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):mutex_(mutex)
    {
        mutex_.lock(); // 在构造函数中进行加锁
    }
    ~LockGuard()
    {
        mutex_.unlock();// 在析构函数中进行解锁
    }
private:
    Mutex mutex_;

};

mythread.cc



#include <iostream>
#include <vector>
#include <pthread.h>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <memory>
#include <cstring>
#include"Mutex.hpp"
// #include "Thread.hpp"

// // 定义全局变量的
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

// 共享资源, 火车票
int tickets = 10000;


class ThreadData
{
public:
    ThreadData(const std::string &threadname, pthread_mutex_t *mutex_p)
        : threadname_(threadname), mutex_p_(mutex_p)
    {}
    ~ThreadData() {}

public:
    std::string threadname_;
    pthread_mutex_t *mutex_p_;
};

void *getTicket(void *args)
{
    // 获取线程的名字
    std::string username = static_cast<const char *>(args);

   

    while (true)
    {
     LockGuard lockguard(&lock);
        // 将lock传过去之后调用析构函数自动加锁
        // 然后出作用域调用析构函数自动解锁


        if (tickets > 0)
        {
            usleep(1000);


            std::cout << username << "正在抢票" << tickets << std::endl;

            tickets--;

        }
        else
        {
            // pthread_mutex_unlock(&lock);

            // 解锁
            // pthread_mutex_unlock(&lock);

            break;
        }

        // 抢完票就完了吗? 当然不是
        usleep(1000);// 模拟形成一个订单给用户
    }
    return nullptr;
}

int main()
{



    pthread_mutex_init(&lock, nullptr);
    pthread_t t1, t2, t3, t4;


    pthread_create(&t1,nullptr,getTicket,(void*)"thread 1");
    pthread_create(&t2,nullptr,getTicket,(void*)"thread 2");
    pthread_create(&t3,nullptr,getTicket,(void*)"thread 3");
    pthread_create(&t4,nullptr,getTicket,(void*)"thread 4");

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

    pthread_mutex_destroy(&lock);

    
}

RAII风格

重点:

 LockGuard lockguard(&lock);
 // 将lock传过去之后调用析构函数自动加锁
 // 然后出作用域调用析构函数自动解锁

可重入与线程的关系(浅谈)

可重入与线程安全联系

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

可重入与线程安全区别

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

死锁

概念

一组执行流(无论是进程还是线程),它在持有自己的锁资源的同时,还想方设法的去申请对方的锁资源,因为大家互相持有自己的,还互相申请对方的,因为锁是不可抢占式的锁(我拿了锁,除非我自己主动归还,否则别人是不能直接要我的锁的)。

所以,就是大家互相持有自己的资源,还在等待对方的锁资源而导致代码无法推进的情况,这种情况就叫死锁。

提出问题

1、一把锁,有可能死锁吗?

可能的,比如自己连续申请两次锁
pthread_mutex_lock(&lock);
pthread_mutex_lock(&lock);


2、为什么会有死锁? 逻辑链条

首先一定是用了锁才会发生死锁

3、为什么要用锁呢?

保证临界资源的安全
多线程访问我们可能出现数据不一致的问题
多线程&&全局资源
多线程大部分资源(全局的)是共享的
多线程的特征

引出的大概念

任何技术都有自己的边界,是解决问题的,但有可能在解决问题的同时,一定会可能引入新的问题!

死锁的四个必要条件

1.互斥   --   我们必须得保证访问一份资源是互斥的(这是我们锁基本特性,没有互斥那就是没有加锁)
2.请求与保持   --   我要你的资源这是请求,我还保持我自己的不释放这就是保持,我又要你的资源又不释放我自己的资源,就是请求与保持。
3.不剥夺   --   不能去抢占对方的锁,只能等待对方自动的给你这叫做不剥夺。
4.环路等待条件   --   比如线程A、线程B、线程C。线程A拥有自己的锁它去要线程B的锁,线程B有自己的锁它去要线程C的锁,线程C有自己的锁它去要线程A的锁。形成了一个环路情况。

注意:

只有这四个必要条件同时都满足的情况下才会造成死锁。

如何破坏死锁

概念:

破坏死锁的本质就是破坏这四个条件的至少一个!

做法:

加锁顺序一致
避免锁未释放的场景
资源一次性分配

线程同步

本质

线程运行同步的本质:
当我们在进行临界资源访问安全的前提条件下,让多个线程按照一定的顺序进行资源访问。

先谈生产者消费者模型

生产的过程和消费的过程 - 解耦

生产者跟消费者互不影响。

临时保存产品的场所  --  缓冲区

不符合生产者消费者模型的例子

函数调用:

调用方:生产了数据

形成变量:变量暂时保存数据

目标函数:消费了数据

main函数调用fun函数的时候
正在调用的过程中main正在等待fun函数返回
main函数与fun函数是强耦合关系

"321"原则

3种关系:  生产者和生产者(互斥),消费者和消费者(互斥),生产者和消费者(互斥|[保证共享资源的安全性]/同步)。
-- 产品(数据)

2种角色:  生产者线程,消费者线程

1个交易场所:  一段特定结构的缓冲区。交易的是数据。

注意:
只要我们想写生产消费模型,我们本质工作其实就是维护321原则!

生产者消费者模型的特点

1、生产线程和消费线程进行解耦
2、支持生产和消费的一段时间的忙闲不均的问题 -- 缓冲区的优势
3、提高效率

条件变量

概念

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

定义

是一种数据类型,我们可以使用这个类型来定义变量或者对象。

具体操作 

本质是对线程的PCB做处理。

所用函数

操作系统调用pthread_cond_wait()的时候就是把它放进等待队列里面

操作系统调用pthread_cond_signal()就是它唤醒。

定义的返回值

定义条件变量之前要先初始化。成功返回0,失败返回对应的错误码。

谁排队谁唤醒?

让指定线程在条件变量里面进行排队
主线程进行唤醒。

代码示例:


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

int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void *start_routine(void *args)
{
    std::string name = static_cast<const char *>(args);

    while (true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex); // 为什么要有mutex 后面会说
        // 判断暂时省略
        std::cout << name << " -> " << tickets << std::endl;
        tickets--;
        pthread_mutex_unlock(&mutex);
    }
    return nullptr;
}

int main()
{
    // 通过条件变量控制线程的执行

    pthread_t t[5];
    for (int i = 0; i < 5; i++)
    {
        char *name = new char[64];
        snprintf(name, 64, "thread %d", i + 1);
        pthread_create(&t[i], nullptr, start_routine, name);
    }

    while (true)
    {
        sleep(1);
        pthread_cond_signal(&cond);
        std::cout << "main thread wakeup one thread" << std::endl;
    }

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

    return 0;
}

 唤醒方式

唤醒单个线程:

int pthread_cond_broadcast(pthread_cond_t *cond);


唤醒多个线程:

int pthread_cond_signal(pthread_cond_t *cond);

运行结果图如下:

 

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

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

相关文章

2.自然语言处理NLP:词映射为向量——词嵌入(word embedding)

1. 什么是词嵌入&#xff08;word2vec&#xff09; &#xff1a; 把词映射为向量&#xff08;实数域&#xff09;的技术 2. 为什么不采用one-hot向量&#xff1a; one-hot词向量无法准确表达不同词之间的相似度&#xff0c;eg&#xff1a;余弦相似度&#xff0c;表示夹角之间的…

创新案例|Amazon如何打造增长飞轮保持每年20%以上的营收增速

作为世界五百强中的头部企业&#xff0c;亚马逊的价值定位经历了三次转变&#xff0c;从成为“地球上最大的书店”&#xff0c;到成为最大的综合网络零售商&#xff0c;再到成为“最以客户为中心的企业”&#xff0c;亚马逊最终以“客户中心”破除了对企业价值定位的束缚&#…

DNS风险分析及安全防护研究(三):DNS缓存投毒及防御策略

在前面章节中&#xff0c;我们简单介绍了DNS系统在协议、软件以及结构中脆弱性&#xff0c;并对DNSSEC协议、去中心化结构等安全增强进行了讨论&#xff0c;接下来针对DNS安全所面临的外部攻击威胁和相应的防御策略做下讨论。 1.DNS缓存投毒攻击 在目前各种DNS攻击手段中&…

安科瑞浅谈集成式电力电容器无功补偿装置的技术特点

安科瑞 徐浩竣 江苏安科瑞电器制造有限公司 zx acrelxhj 摘要&#xff1a;阐述了集成式电力电容器无功补偿装置的组成与应用状况&#xff0e;在与常规电力电容器对比的基础上&#xff0c;分析了集成式电力电容器无功补偿装置的技术特点。通过对集成式无功补偿装置原理结构的…

Linux文件系统、磁盘I/O是怎么工作的?

同CPU、内存一样&#xff0c;文件系统和磁盘I/O&#xff0c;也是Linux操作系统最核心的功能。磁盘为系统提供了最基本的持久化存储。文件系统则在磁盘基础上&#xff0c;提供了一个用来管理文件的树状结构。 目录&#xff1a; 一. 文件系统 1. 索引节点和目录项 2. 虚拟文件系…

提升国际品牌影响力:小企业海外网红营销实战指南

在当今数字化时代&#xff0c;小企业们越来越意识到海外市场的巨大潜力。与此同时&#xff0c;海外网红的崛起也为小企业提供了一个独特的机会&#xff0c;通过与他们合作&#xff0c;迅速拓展国际市场并吸引更多目标受众的关注。然而&#xff0c;对于许多小企业来说&#xff0…

超全性能测试-全链路压测总结,完整一套从环境到脚本详细...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 性能测试&#xf…

类和对象 --- 封装+对象特性

&#x1f442; 快乐E调 - 林澜叶 - 单曲 - 网易云音乐 &#x1f442; Plain Jane (Freestyle) - Ombre2Choc Nation - 单曲 - 网易云音乐 1.5倍速&#xff0c;跟着敲&#xff0c;初识C 目录 &#x1f3c6;封装 &#x1f333;属性和行为作为整体 &#x1f333;案例 -- 设置…

js数组去重与循环对象

目录 一、数组对象去重 1.1、需要获取重复数据 1.2、直接过滤filterfindIndex 二、循环对象 三、多层数组对象过滤 一、数组对象去重 1.1、需要获取重复数据 let persons [{"name": "yzq","age": 20,"gender": true,"hei…

k8s配置资源管理|secret|configmap

k8s配置资源管理|secret|configmap 一 配置资源管理1 创建 Secret2 使用方式3 将 Secret 导出到环境变量中 二 ConfigMap1 Pod 中使用 ConfigMap2 Pod的创建3 用 ConfigMap 设置命令行参数4 通过数据卷插件使用ConfigMap 一 配置资源管理 //Secret Secret 是用来保存密码、tok…

2023年6月合肥/厦门/长春/深圳DAMA-CDGP数据治理专家认证报名

目前6月18日CDGA&CDGP考试目前开放的城市有&#xff1a;北京、上海、广州(满)、深圳、长沙、呼和浩特、杭州&#xff08;满&#xff09;、南京、济南&#xff08;满&#xff09;、成都、西安、武汉&#xff08;满&#xff09;、天津。 新增了武汉、天津这2个城市。另外合肥…

【Netty】Reactor 模型(十)

文章目录 前言一、传统服务的设计模型二、NIO 分发模型三、Reactor 模型3.1、Reactor 处理请求的流程3.2、Reactor 三种角色 四、单Reactor 单线程模型4.1、消息处理流程4.2、缺点 五、单Reactor 多线程模型5.1、消息处理流程5.2、缺点 六、主从Reactor 多线程模型6.1、Reactor…

Python的一些基础实操练习题

书接上文多看一眼多进步&#xff0c;python入门到放弃&#xff0c;是根据python的知识点的一些基础练习题&#xff0c;说了是基础练习题&#xff0c;基础练习题&#xff0c;基础练习题&#xff0c;水平高的就别看了&#xff0c;平高的就别看了&#xff0c;高的就别看了&#xf…

IP协议-服务类型字段

服务类型&#xff08;Type of Service&#xff09;字段是比较复杂的一个字段&#xff0c;该字段经过多次标准变更。 IPv4报文 一、最初标准&#xff08;RFC 791&#xff09; RFC 791定义TOS字段总共占用8bit&#xff0c;分为IP Precedence优先级&#xff08;3bit&#xff09;、…

Ansys Zemax | 如何将高斯光整形为平顶光

概要 本文展示了如何设计光束整形器将激光器产生的高斯分布的光转换为平顶分布的光输出。&#xff08;联系我们获取文章附件&#xff09; 介绍 光束整形光学元件可以将入射光的光强分布转换为其他特定的分布输出。最常见的例子就是将激光器产生的高斯分布的光转换为平顶&#x…

GMesh的Mesh操作面板介绍

GMesh操作面板介绍 Define 用于控制网格生成过程中各个单元的尺寸大小 “Size at points”选项允许您指定空间中某些点的尺寸大小。这些点可以是模型的几何结构中的点&#xff0c;也可以是在Gmsh中手动定义的点&#xff08;使用“Point”命令&#xff09;。在这种情况下&…

pycharm在终端运行时ps 不显示环境

如果下面显示的是ps ----- 而不是 则需要把这儿修改一下

2023年Java教学大纲!好程序员教你如何快速学会Java!

今天好程序员给大家分享一篇2023年的Java教学大纲&#xff0c;跟着这篇大纲学习&#xff0c;并且熟练掌握该技能&#xff0c;实习轻松月入过万不是梦&#xff01; 一、Java初级程序员必须要掌握的技能&#xff1a; Java基础知识控制声明面向对象的概念数组字符串异常处理输入/输…

Ubuntu22.04安装最新Eigen库

按道理&#xff1a;该方法适用所有Linux&#xff0c;适合安装多版本 本文采用源码 cmake的方法安装&#xff0c;故前置条件&#xff1a; 源码下载&#xff0c;官网下载或GitLab下载安装cmake&#xff08;没有安装cmake&#xff0c;也可以采用其他办法安装&#xff09; 官网下…

解决win无法删除多层嵌套文件夹

起因&#xff1a;昨天研究jpackage工具&#xff0c;不小心搞得一个文件夹里嵌套了好几百个文件夹&#xff0c;用win自己的删除删不掉&#xff0c;shiftdel直接删除也不行&#xff0c;直接弹窗删除错误&#xff1b; 后来用电脑管家下载了个“文件粉碎”&#xff0c;添加目录&am…