linux篇【11】:linux下的线程<中序>

news2025/7/21 22:09:26

目录

一.线程互斥

1.三个概念

2.互斥

(1)在执行语句的任何地方,线程可能被切换走

(3)抢票场景中的问题

(4)解决方案

3.加锁

(1)加锁介绍

(2)定义/释放 互斥锁

(3)加锁、解锁(使用锁)

(4)使用锁代码

4.加锁方式

(1)基本的传锁(就传不了线程名字了)

(2)既传name又传锁

二.加锁的原理探究 

1.上锁后的临界区内仍可以进程切换。

2.在我被切走的时候,绝对不会有线程进入临界区!

3.加锁是原子的

(1)xchgb 交换是原子的

(2)加锁原理

(3)C++ 加锁

Makefile

Lock.hpp

mythread.cc

(4)C++ RAII加锁

4.可重入VS线程安全

5. 死锁

①两个锁互相锁死的情景 

②一个锁产生死锁的情景 


一.线程互斥

1.三个概念

1.临界资源:多个执行流都能看到并能访问的资源,临界资源
2.临界区:多个执行流代码中有不同的代码,访问临界资源的代码,我们称之为临界区
3.互斥特性:当我们访问某种资源的时候,任何时刻。都只有一个执行流在进行访问,这个就叫做:互斥特性

4.线程互斥:线程互斥 指的是在多个线程间对临界资源进行争抢访问时有可能会造成数据二义,因此通过保证同一时间只有一个线程能够访问临界资源的方式实现线程对临界资源的访问安全性

2.互斥

没有互斥时,以抢票为例,一抢票 票数减1: int tickets;   tickets--;为例

(1)在执行语句的任何地方,线程可能被切换走

int tickets;
tickets--;tickets--是由3条语句完成的:

tickets--:有三步
① load tickets to reg
② reg-- ;
③ write reg to tickets

(2)CPU内的寄存器是被所有的执行流共享的,但是寄存器里面的数据是属于当前执行流的上下文数据。线程被切换的时候,需要保存上下文;线程被换回的时候,需要恢复上下文。 

(3)抢票场景中的问题

情况1:线程A先抢到一张票时,寄存器中tickets 10000——>9999, 还未写回内存,A的时间片到了就被切走了,开始执行线程B了;线程B也抢票,直接抢了9950张,还剩50张,此时B的时间片到了,又切回线程A,又把9999写入内存,就错误了。

        if (tickets > 0)
        {
            usleep(1000);
            cout << name << " 抢到了票, 票的编号: " << tickets << endl;
            tickets--;
            usleep(123); //模拟其他业务逻辑的执行
        }

情况2:或者在抢最后一张时,线程A先抢最后一张票,if (tickets > 0)为真,进入if语句,此时A的时间片到了就被切走了,开始执行线程B了;线程B也抢票,此时显示票数仍是1,if (tickets > 0)为真,进入if语句,并执行tickets--;,tickets变为0,此时B的时间片到了,又切回线程A,线程A又继续执行tickets--;,此时直接把票数减到了负数,就出错了。

(4)解决方案

原子性:一件事要么不做,要么全做完

把tickets--这个临界区设为原子的,使不想被打扰,加锁

3.加锁

(1)加锁介绍

加锁范围:临界区,只要对临界区加锁,而且加锁的力度越细越好

加锁本质:加锁的本质是让线程执行临界区代码串行化

加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加

锁保护的是临界区, 任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁!那这把锁,本身不就也是临界资源吗?锁的设计者早就想到了

pthread_mutex_lock: 竞争和申请锁的过程,就是原子的!申请锁的过程不会中断,不会被打扰。

难度在加锁的临界区里面,就没有线程切换了吗????

mutex简单理解就是一个0/1的计数器,用于标记资源访问状态:

  • 0表示已经有执行流加锁成功,资源处于不可访问,
  • 1表示未加锁,资源可访问。

(2)定义/释放 互斥锁

man pthread_mutex_init

① pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 定义全局/静态的互斥锁,可以用这个宏初始化

② int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);         mutex:锁的地址。attr:锁的属性设为空             

③ int pthread_mutex_destroy(pthread_mutex_t *mutex); 释放锁

(3)加锁、解锁(使用锁)

man pthread_mutex_lock

① int pthread_mutex_lock(pthread_mutex_t *mutex);  加阻塞式锁

线程1正在用锁住的代码,那线程2就要阻塞式等待线程1执行完才能使用这个锁(即执行锁住的代码)

② int pthread_mutex_trylock(pthread_mutex_t *mutex); 加非阻塞式锁

线程1正在用这个非阻塞锁(即执行锁住的代码),那线程2就直接返回,只有当没有别的线程用这个锁,自己才能用。

③ int pthread_mutex_unlock(pthread_mutex_t *mutex);  解锁

如果不解锁,比如线程1使用锁后没有解锁就退出了,那么其他线程在竞争使用这个锁时,就会一直处于休眠,等待这个锁被解锁才能继续使用, n = pthread_join(tid4, nullptr); pthread_join就会一直阻塞等待线程退出,所以会显示进程卡住了。

(4)使用锁代码

else那里也要解锁,否则会阻塞:线程1走else使用锁后如果没有解锁就退出了,那么其他线程在竞争使用这个锁时,就会一直处于休眠,等待这个锁被解锁才能继续使用, n = pthread_join(tid4, nullptr); pthread_join就会一直阻塞等待线程退出,所以会显示进程卡住了。

跟上面代码一样(可以忽略):

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h> // 仅仅是了解

// __thread int global_value = 100;

// void *startRoutine(void *args)
// {
//     // pthread_detach(pthread_self());
//     // cout << "线程分离....." << endl;
//     while (true)
//     {
//         // 临界区,不是所有的线程代码都是临界区
//         cout << "thread " << pthread_self() << " global_value: "
//              << global_value << " &global_value: " << &global_value
//              << " Inc: " << global_value++ << " lwp: " << ::syscall(SYS_gettid)<<endl;
//         sleep(1);
//         break;
//     }
//     // 退出进程,任何一个线程调用exit,都表示整个进程退出
//     //exit(1);
//     // pthread_exit()
// }

using namespace std;

// int 票数计数器
// 临界资源
int tickets = 10000; // 临界资源,可能会因为共同访问,可能会造成数据不一致问题。
pthread_mutex_t mutex;

void *getTickets(void *args)
{
    const char *name = static_cast<const char *>(args);

    while (true)
    {
        // 临界区,只要对临界区加锁,而且加锁的粒度约细越好
        // 加锁的本质是让线程执行临界区代码串行化
        // 加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加
        // 锁保护的是临界区, 任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁!
        // 这把锁,本身不就也是临界资源吗?锁的设计者早就想到了
        // pthread_mutex_lock: 竞争和申请锁的过程,就是原子的!
        // 难度在加锁的临界区里面,就没有线程切换了吗????
        pthread_mutex_lock(&mutex);
        if (tickets > 0)
        {
            usleep(1000);
            cout << name << " 抢到了票, 票的编号: " << tickets << endl;
            tickets--;
            pthread_mutex_unlock(&mutex);

            //other code
            usleep(123); //模拟其他业务逻辑的执行
        }
        else
        {
            // 票抢到几张,就算没有了呢?0
            cout << name << "] 已经放弃抢票了,因为没有了..." << endl;
            pthread_mutex_unlock(&mutex);
            break;
        }
    }

    return nullptr;
}

// 如何理解exit?
int main()
{
    pthread_mutex_init(&mutex, nullptr);
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_t tid4;
    pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");
    pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");
    pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");
    pthread_create(&tid4, nullptr, getTickets, (void *)"thread 4");

    // sleep(1);
    // 倾向于:让主线程,分离其他线程

    // pthread_detach(tid1);
    // pthread_detach(tid2);
    // pthread_detach(tid3);

    // 1. 立即分离,延后分离 -- 线程活着 -- 意味着,我们不在关心这个线程的死活。4. 线程退出的第四种方式,延后退出
    // 2. 新线程分离,但是主线程先退出(进程退出) --- 一般我们分离线程,对应的main thread一般不要退出(常驻内存的进程)
    // sleep(1);

    int n = pthread_join(tid1, nullptr);
    cout << n << ":" << strerror(n) << endl;
    n = pthread_join(tid2, nullptr);
    cout << n << ":" << strerror(n) << endl;
    n = pthread_join(tid3, nullptr);
    cout << n << ":" << strerror(n) << endl;
    n = pthread_join(tid4, nullptr);
    cout << n << ":" << strerror(n) << endl;

    pthread_mutex_destroy(&mutex);

    return 0;
}

4.加锁方式

(1)基本的传锁(就传不了线程名字了)

#include <unistd.h>
#include <pthread.h>
#include <iostream>
using namespace std;
int tickets = 1000;
void *startRoutine(void *args)
{
    pthread_mutex_t* mutex_p= static_cast<pthread_mutex_t*>(args);
    while (true)
    {
        pthread_mutex_lock(mutex_p);//如果申请不到,线程阻塞等待
        if (tickets > 0)
        {
            usleep(1000);
            cout << "thread: " << pthread_self() << "get a ticket: " << tickets << endl;
            tickets--;
            pthread_mutex_unlock(mutex_p);
            //做其他的事
            usleep(500);
        }
        else
        {
            pthread_mutex_unlock(mutex_p);
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_t t1, t2, t3, t4;

    static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
    pthread_create(&t1, nullptr, startRoutine, (void *)&mutex);
    pthread_create(&t2, nullptr, startRoutine, (void *)&mutex);
    pthread_create(&t3, nullptr, startRoutine, (void *)&mutex);
    pthread_create(&t4, nullptr, startRoutine, (void *)&mutex);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    pthread_mutex_destroy(&mutex);
    return 0;
}

(2)既传name又传锁

完整版:

#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <iostream>
using namespace std;
int tickets = 1000;
#define NAMESIZE 64
typedef struct threadData
{
    char name[NAMESIZE];
    pthread_mutex_t* mutexp;
}threadData;
void *startRoutine(void *args)
{
    threadData* td= static_cast<threadData*>(args);
    while (true)
    {
        pthread_mutex_lock(td->mutexp);//如果申请不到,线程阻塞等待
        if (tickets > 0)
        {
            usleep(1000);
            cout << "thread: " << td->name << "get a ticket: " << tickets << endl;
            tickets--;
            pthread_mutex_unlock(td->mutexp);
            //做其他的事
            usleep(500);
        }
        else
        {
            pthread_mutex_unlock(td->mutexp);
            break;
        }
    }
    return nullptr;
}

int main()
{
    static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
    pthread_t t1, t2, t3, t4;

    threadData *td1=new threadData();
    threadData *td2=new threadData();
    threadData *td3=new threadData();
    threadData *td4=new threadData();
    strcpy(td1->name,"thread 1");
    strcpy(td2->name,"thread 2");
    strcpy(td3->name,"thread 3");
    strcpy(td4->name,"thread 4");
    td1->mutexp=&mutex;
    td2->mutexp=&mutex;
    td3->mutexp=&mutex;
    td4->mutexp=&mutex;
    pthread_create(&t1, nullptr, startRoutine, (void *)td1);
    pthread_create(&t2, nullptr, startRoutine, (void *)td2);
    pthread_create(&t3, nullptr, startRoutine, (void *)td3);
    pthread_create(&t4, nullptr, startRoutine, (void *)td4);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    pthread_mutex_destroy(&mutex);
    return 0;
}

 

二.加锁的原理探究 

1.上锁后的临界区内仍可以进程切换。

我在临界资源对应的临界区中上锁了,临界区还是多行代码,是多行代码就可以被切换。加锁 不等于 不会被切换。加锁后仍然可以切换进程,因为线程执行的加锁解锁等对应的也是代码,线程在任意代码处都可以被切换,只是线程加锁是原子的——要么你拿到了锁,要么没有

2.在我被切走的时候,绝对不会有线程进入临界区!

——因为每个线程进入临界区都必须先申请锁! !假设当前的锁被A申请走了,即便当前的线程A没有被调度,因为它是被切走的时候是抱着锁走的,其他线程想进入临界区需要先申请锁,但是已经有线程A持有锁了,则其他线程在申请时会被阻塞。即:一旦一个线程持有了锁,该线程根本就不担心任何的切换问题!对于其他线程而言,线程A访问临界区,只有没有进入和使用完毕两种状态
,才对其他线程有意义!即:对于其他线程而言,线程A访问临界区具有一定的原子性
注意:尽量不要在临界区内做耗时的事情!因为只有持有锁的线程能访问,其他线程都会阻塞等待。

3.加锁是原子的

①每一个CPU任何时刻只能有一个线程在跑

②单独的一条汇编代码是具有原子性的

(1)xchgb 交换是原子的

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

(2)加锁原理

mutex中的值默认是1
%al :CPU中的寄存器( 凡是在寄存器中的数据,全部都是线程的内部上下文! !
mutex :内存中的一个变量

 

加锁原理解释:线程A执行 movb $0,%al  :把0放入寄存器%al中。然后执行xchgb %al, mutex:通过一条汇编代码交换 寄存器%al (值是0) 和 变量mutex (值是1)  的值,交换后 寄存器%al中是1, 变量mutex中是0。还未执行判断,此时突然进程切换,线程A会自动带走%al中的上下文数据1,线程B开始执行:线程B执行 movb $0,%al  :把0放入寄存器%al中。然后执行xchgb %al, mutex:通过一条汇编代码交换 寄存器%al (值是0) 和 变量mutex (值是0)  的值,交换后 寄存器%al 和 变量mutex中都是0。再判断——>因为%al是0,不大于0就挂起。此时线程B挂起,该线程A继续执行,线程A会把自己上下文数据恢复到%al中,此时%al=1,该执行判断了——>因为%al是1,就返回。这样就成功做到:多个线程看起来同时在访问寄存器,但是互不影响

lock和unlock的伪代码:

(3)C++ 加锁

Makefile

mythread:mythread.cc
	g++ -o $@ $^ -lpthread -std=c++11

.PHONY:clean
clean:
	rm -f mythread

Lock.hpp

#pragma once

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

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&lock_, nullptr);
    }
    void lock()
    {
        pthread_mutex_lock(&lock_);
    }
    void unlock()
    {
        pthread_mutex_unlock(&lock_);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&lock_);
    }

private:
    pthread_mutex_t lock_;
};

class LockGuard
{
public:
    LockGuard(Mutex *mutex) : mutex_(mutex)
    {
        mutex_->lock();
        std::cout << "加锁成功..." << std::endl;
    }

    ~LockGuard()
    {
        mutex_->unlock();
        std::cout << "解锁成功...." << std::endl;
    }

private:
    Mutex *mutex_;
};

mythread.cc



int tickets = 1000;
Mutex mymutex;

// 函数本质是一个代码块, 会被多个线程同时调用执行,该函数被重复进入 - 被重入了
bool getTickets()
{
    bool ret = false; // 函数的局部变量,在栈上保存,线程具有独立的栈结构,每个线程各自一份
    LockGuard lockGuard(&mymutex); //局部对象的声明周期是随代码块的!
    if (tickets > 0)
    {
        usleep(1001); //线程切换了
        cout << "thread: " << pthread_self() << " get a ticket: " << tickets << endl;
        tickets--;
        ret = true;
    }
    return ret;
}
void *startRoutine(void *args)
{
    const char *name = static_cast<const char *>(args);
    while(true)
    {
        if(!getTickets())
        {
            break;
        }
        cout << name << " get tickets success" << endl;
        //其他事情要做
        sleep(1);
    }
}

int cnt = 10000;

int main()
{
    pthread_t t1, t2, t3, t4;

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

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

(4)C++ RAII加锁

通过RAII思想,创建对象时加锁,出代码块时解锁

    {
        //临界资源
        LockGuard LockGuard(&mymutex);
        cnt++;
        ...
        ...
        ...
    }

4.可重入VS线程安全

(1)概念

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

(2)####常见的线程不安全的情况

不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
(3)常见的线程安全的情况
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
(4)常见不可重入的情况
调用了 malloc/free 函数,因为 malloc 函数是用全局链表来管理堆的
调用了标准 I/O 库函数,标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
(5)常见可重入的情况
不使用全局变量或静态变量
不使用用 malloc 或者 new 开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
(6)可重入与线程安全联系
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
(7)可重入与线程安全区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

5. 死锁

(1)概念

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

(2)死锁四个必要条件(有一个条件不满足,死锁就不成立)

互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。(即:保持着自己的锁,还要要对方的锁)
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
(3)避免死锁
破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配
(4)避免死锁算法
死锁检测算法(了解)
银行家算法(了解)
7. Linux线程同步
###条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情
况就需要用到条件变量。

①两个锁互相锁死的情景 

#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include "Lock.hpp"
#include <mutex>

using namespace std;

pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;

void *startRoutine1(void *args)
{
    while (true)
    {
        pthread_mutex_lock(&mutexA);
        sleep(1);
        pthread_mutex_lock(&mutexB);

        cout << "我是线程1,我的tid: " << pthread_self() << endl;

        pthread_mutex_unlock(&mutexA);
        pthread_mutex_unlock(&mutexB);
    }
}
void *startRoutine2(void *args)
{
    while (true)
    {
        pthread_mutex_lock(&mutexB);
        sleep(1);
        pthread_mutex_lock(&mutexA);

        cout << "我是线程2, 我的tid: " << pthread_self() << endl;

        pthread_mutex_unlock(&mutexB);
        pthread_mutex_unlock(&mutexA);
    }
}

int main()
{
    pthread_t t1, t2;

    pthread_create(&t1, nullptr, startRoutine1, nullptr);
    pthread_create(&t2, nullptr, startRoutine2, nullptr);

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

    return 0;
}

②一个锁产生死锁的情景 

申请锁申请了两次

请求与保持条件:你拿着这把锁,还要继续要这把锁,因为这个锁不会解锁,就会死锁

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

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

相关文章

STC 51单片机39——汇编语言 按钮流水灯

每按一下按钮&#xff0c;灯就移动一个 ORG 0000H LJMP MAIN ORG 0003H ;中断矢量 LJMP INT MAIN:SETB EA ;开总中断允许“开关” SETB EX0 ;开分中断允许“开关” SETB PX0 ;高优先级 SETB IT0 ;边沿触发 MOV A,#0FEH ;给…

Linux系统编程(二)——Linux系统IO函数

在第一篇的时候写到了系统环境的搭建以及各种调试的方法&#xff0c;接下来讲述关于系统函数的使用。 目录 0x01 标准C库IO函数和Linux系统IO函数对比 一、标准C库IO函数操作流程 二、标准C库IO和Linux系统IO的关系 三、虚拟地址空间 0x02 LinuxIO函数实例 一、open()、…

dubbo以xml方式操作和新版dubbo-admin安装

文章目录1 dubbo xml配置1.1 提供者1.1.1 提供者接口1.1.2 提供者实现类1.1.2.1 项目结构图示1.1.2.2 pom.xml1.1.2.3 实现类接口1.1.2.4 配置文件1.1.2.4.1 xml配置1.1.2.4.2 结合注解方式1.1.2.5 启动类1.1.2.5.1 直接读取配置文件1.1.2.5.2 Main.main启动1.1.3 其他方式配置…

最新持续更新Crack:LightningChart 行业使用大全

LightningChart .NET 和 JavaScript 解决方案旨在通过彻底的图表优化、最小的延迟和流畅的呈现来满足行业最苛刻的数据可视化要求。LightningChart .NET 和 JavaScript 直观的 API 使用户能够使用适用于任何桌面、移动和平板设备的鼠标和触摸屏交互功能轻松操作最复杂的图表。 …

JSP JAVA javaweb企业仓库库存管理系统(仓库进销存管理系统ssm库存管理系统仓库管理系统)

JSP 企业仓库库存管理系统&#xff08;仓库进销存管理系统ssm库存管理系统仓库管理系统&#xff09;

【附源码】计算机毕业设计JAVA疫情下的进出口食品安全信息管理系统

【附源码】计算机毕业设计JAVA疫情下的进出口食品安全信息管理系统 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&…

Django Cookie 与 Session 对比

文章目录原理比较语法比较Cookie 示例创建 Cookie更新 Cookie删除 CookieSession 示例创建 session查询 session删除一组键值对删除 session参考文档本文通过示例演示 Django 中如何创建、查询、删除 Cookie 与 Session。 原理比较 在Web开发中&#xff0c;使用 session 来完成…

无线监控摄像头使用什么样的物联网卡?

无线监控摄像头使用什么样的物联网卡&#xff1f; 随着安全行业的发展和进步&#xff0c;无线监控摄像头的种类越来越多&#xff0c;但主要的网络形式大致可以分为两种形式&#xff0c;一种是连接WiFi使用远程监控&#xff0c;另一种是插入物联网卡的远程监控。随着物联网的快…

初阶数据结构学习记录——열 二叉树(3)链式

链式二叉树是由指针形成的二叉树&#xff0c;之前写的二叉树是由数组组成的&#xff0c;链式由链表来做。链式二叉树每个节点有两个指针&#xff0c;指向两边。以往二叉树&#xff0c;栈&#xff0c;队列等等都需要增删查改&#xff0c;但链式二叉树则不是这样&#xff0c;是因…

深入理解java虚拟机:虚拟机字节码执行引擎(1)

文章目录1. 概述2. 运行时栈帧结构2.1 局部变量表2.2 操作数栈2.3 动态连接2.4 方法返回地址2.5 附加信息1. 概述 代码编译的结果是从 本地机器码 转变为 字节码 &#xff0c;是存储格式发展的一小步&#xff0c;却是编程语言发展的一大步。 执行引擎 是Java虚拟机最核心的组…

pmp是什么意思啊?

PMP是一个证书&#xff0c;项目管理类的专业认证考试&#xff0c;从国外引进大陆已经很多年了&#xff0c;反响也不错。 以前&#xff0c;大陆每年报考PMP的人很少&#xff0c;那时的思维观念&#xff0c;更多的认为有了这个PMP证书&#xff0c;代表着你很上进&#xff0c;学习…

Terraform 初始化慢~配置本地离线源

解决Terraform初始化慢~配置本地离线源 - 知乎 这里不再介绍Terraform是啥了&#xff0c;可以参考最近上线的课程。直奔主题&#xff0c;配置一个离线的源。需要手动或者terraform init一次下载&#xff0c; 然后缓存。后续直接使用缓存。 本次实践使用的是Linux/Mac 系统&am…

【App自动化测试】(十二)App异常弹框处理

目录1. app弹框异常处理——递归方式1.1 黑名单弹框异常处理逻辑1.2 实现代码1.3 方法缺点2. app弹框异常处理——装饰器版本2.1 装饰器的优势2.2 实现代码前言&#xff1a; 本文为在霍格沃兹测试开发学社中学习到的一些技术写出来分享给大家&#xff0c;希望有志同道合的小伙伴…

计算机毕业设计之java+ssm交通信息网上查询系统

项目介绍 随着交通交通管理需求和在线交通管理渗透率的提升&#xff0c;中国交通管理在线市场将释放巨大潜力&#xff0c;交通管理系统的建设和发展成为业界广泛关注的重点&#xff0c;本文将对此进行分析&#xff0c;以期为我国交通管理电子商务的发展提供参考。交通管理业对…

石化能源行业工业互联网智能工厂解决方案

随着时代的发展&#xff0c;中国的工业企业逐渐进入了一个“新常态”&#xff1a;生产效率提升&#xff0c;非计划停运或检修造成的生产损失更为昂贵&#xff1b;高盈利的要求&#xff0c;需要更加关注能源使用效率&#xff1b;法律法规对于人员安全及环保合规要求更为严格&…

基于ffmpeg开发的多音频文件音量均衡程序

前言 audio_balance ✨ 基于ffmpeg开发的多音频文件音量均衡程序 ✨ 项目地址 GitHub&#xff1a;https://github.com/Ikaros-521/audio_balance gitee&#xff1a;https://gitee.com/ikaros-521/audio_balance 使用说明 Python&#xff1a;3.9 程序依赖 ffmpeg实现。请先安…

Centernet 生成高斯热图

写在前面的话 最近学校阳了&#xff0c;宿舍给封了&#xff0c;宿舍网络不好远程跑不了实验&#xff0c;随缘写一下对CenterNet源码的一个解读&#xff0c;之前写论文的那段时间留下来的工作&#xff0c;respect&#xff01; 这个文章主要是对CenterNet中生成高斯核的部分代码…

皕杰报表之语义层

1 语义层定义 语义层——是处于数据源与报表之间的一个概念&#xff0c;是用户和数据库之间的一个代码翻译层&#xff0c;通俗的讲是将数据库中的比较凌乱、复杂的数据对象&#xff08;例如&#xff1a;存储在table中的各个字段的记录&#xff09;按预先定义好的规则&#xff…

权限管理框架Shiro renren-security权限管理结构

权限管理框架Shiro&#xff1a; 一直在做项目&#xff0c;由于是二次开发的项目&#xff0c;今天才发现自己连权限控制都没有搞懂。二次开发的是基于renren开源的一个项目。 链接&#xff1a;https://gitee.com/renrenio/renren-security 这个项目主要使用shiro权限管理框架来…

31、Java高级特性——Math类、Random类、String类、StringBuffer类、StringBuilder类

目录 一、Math类 1、Math类中的方法 1.1 圆周率&#xff1a;PI 1.2 绝对值&#xff1a;abs() 1.3 返回最小近似值:ceil() 1.4 返回最大近似值 1.5 四舍五入&#xff1a;round() 1.6 最大值和最小值&#xff1a;max()/min() 1.7 求指定次幂 &#xff1a;po…