Linux 线程的同步与互斥

news2024/12/25 9:28:22

💓博主CSDN主页:麻辣韭菜💓

⏩专栏分类:Linux初窥门径⏪

🚚代码仓库:Linux代码练习🚚

🌹关注我🫵带你学习更多Linux知识
  🔝 

 

 前言

1.资源共享问题 

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

3.锁 

3.1锁文档说明 

3.2 加锁 解锁操作

3.3 ARII风格锁 

3.4 深入理解锁 

3.5 毁🔒

3.6 可重入&&线程安全

 3.7 常见锁概念

3.7.1死锁 

4.条件变量

4.1同步操作相关函数 


 

 前言

由于线程之间存在竞争,就导致了多线程有的线程涝的涝死,饿的饿死,就需要让线程之间保持某种平衡,让它们被CPU雨露均沾。这就是所谓的同步。由于临界资源只有一份,线程之间同时共享临界资源。为了防止临界资源的安全,线程之间需要互斥。

1.资源共享问题 

在Linux 线程控制​​​​​​文章我们知道了一个进程中的所有线程,在地址空间中的代码区、未初始化区、什么堆区也好、栈区也好,还是共享区也好都是共享的。

就好比下面这个代码

#include <iostream>

int n = 0;

int main()
{
	n++;
	return 0;
}

n是属于main函数栈帧中,如果我们创建线程,那么所有线程都是可以看见它。如果两个线程同时对它++,那么很可能n的值会超过我们的预期。

#include <thread>
#include <iostream>

using namespace std;
int n = 0;

int main()
{
	
	thread t1([]()
		{
			for (int i = 0; i < 100000; i++)
			{
				n++;
			}
		});
	thread t2([]()
		{
			for (int i = 0; i < 100000; i++)
			{
				n++;
			}
		});
	t1.join();
	t2.join();
	cout << n << endl;
	return 0;
}

打印出来的结果确实超出我们预期这是为什么?

n++; 这句代码确实是一句代码,但是对于汇编而是3条指令

也就是说当t1对n进行++时,其实是分为3步,同理t2也是3步。这就导致了++不是原子操作

  t1在执行add这条汇编指令时,也就是说当t1进行++时,调度的时间片到了,没有执行第3条指令mov,而t1被切换走时,会带走自己下上文数据。而这时t2被调度。而这个期间t2一直被调度疯狂的++,且完整的执行完汇编语句,将寄存器的值拷贝到内存中,等t1在被调度回来。t1将自己的上下文数据写回寄存器中,它就会执行第3条mov汇编语句,而不是从新开始。而它的n值是1,拷贝会内存这就出事了。覆盖了t2对n++的值。

结论:多线程场景中对全局变量线程并发访问并不是安全的。 

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

前面的简单实验我们可以得出几个名词

对多线程来说:

n就是临界资源,n++就是临界区 ,两个线程不让同时访问临界资源,叫做互斥。n++这个操作,要么是一次性完成的,要么是未完成的(未开始)那就是原子操作。

总结:

  • 临界资源:多线程执行流共享的资源就叫做临界资源

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

3.锁 

既然多线程之间并发访问,会导致临界资源安全性的问题,互斥能让它们对临界资源起到保护作用。

那利用什么方式能让它们每个时刻只有一个线程访问临界资源?

我们先举个生活中例子 公共厕所。

厕所在社会中属于公共资源,每个人上厕所都会把们关上,且把门上的锁进行反锁。而这个过程就是独享这份"公共资源"。

那对于代码中,如何对临界资源上锁?

Linux中对于锁也是有接口的 我们先用指令了解接口说明文档

3.1锁文档说明 

指令:man 3 pthread_mutex_lock

 

pthread_mutex_lock(pthread_mutex_t *mutex)

  • 功能:锁定由 mutex 引用的互斥锁。
  • 行为:如果互斥锁已经被其他线程锁定,则调用线程将阻塞,直到互斥锁可用。
  • 返回状态:如果成功,函数返回0;互斥锁被锁定,调用线程成为其所有者。

互斥锁类型

  • PTHREAD_MUTEX_NORMAL:不提供死锁检测。重复锁定会导致死锁。解锁未锁定的互斥锁会导致未定义行为。
  • PTHREAD_MUTEX_ERRORCHECK:提供错误检查。重复锁定或解锁未锁定的互斥锁将返回错误。
  • PTHREAD_MUTEX_RECURSIVE:维护一个锁定计数。首次成功获取互斥锁时,锁定计数设置为1。每次重复锁定时,计数增加1;每次解锁时,计数减少1。计数归零时,互斥锁对其他线程可用。解锁未锁定的互斥锁将返回错误。
  • PTHREAD_MUTEX_DEFAULT:默认类型,尝试递归锁定会导致未定义行为。如果解锁的互斥锁不是由调用线程锁定的,或者没有被锁定,将导致未定义行为。

pthread_mutex_trylock(pthread_mutex_t *mutex)

  • 功能:与 pthread_mutex_lock() 相似,但如果互斥锁已经被锁定(包括当前线程),调用将立即返回,而不是阻塞。

pthread_mutex_unlock(pthread_mutex_t *mutex)

  • 功能:释放由 mutex 引用的互斥锁。
  • 释放方式:依赖于互斥锁的类型属性。如果有线程因互斥锁变为可用而被阻塞,调度策略将决定哪个线程获得互斥锁。

信号处理

  • 如果等待互斥锁的线程接收到信号,从信号处理程序返回后,线程将继续等待互斥锁,就像没有被中断一样。

返回值

  • 如果成功,pthread_mutex_lock() 和 pthread_mutex_unlock() 函数返回0;否则,返回错误编号以指示错误。

指令:man 3 pthread_mutex_init

pthread_mutex_destroy()

  • 功能:销毁由 mutex 引用的互斥锁对象,使其变为未初始化状态。
  • 安全:只能销毁未被锁定的互斥锁。尝试销毁一个被锁定的互斥锁将导致未定义行为。
  • 重新初始化:销毁的互斥锁可以通过 pthread_mutex_init() 重新初始化。
  • 错误行为:销毁操作后引用互斥锁将导致未定义行为。

pthread_mutex_init()

  • 功能:使用 attr 指定的属性初始化 mutex 引用的互斥锁。如果 attr 是 NULL,则使用默认属性。
  • 状态:初始化成功后,互斥锁变为已初始化且未锁定状态。
  • 同步使用:只能使用 mutex 本身进行同步操作,不能使用其副本。
  • 重复初始化:尝试重复初始化已初始化的互斥锁将导致未定义行为。

PTHREAD_MUTEX_INITIALIZER

  • 用途:用于静态分配的互斥锁的初始化。效果等同于使用 NULL 作为属性参数调用 pthread_mutex_init(),但不会执行错误检查。

返回值

  • 成功时,pthread_mutex_destroy() 和 pthread_mutex_init() 返回0;失败时,返回错误编号以指示错误。

 文档介绍完毕,我们直接开始代码实操

代码示例 

先来一个没有加锁的模拟抢票的过程。

#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <string>
#include <cstring>
#include <cstdio>
#include <iostream>
using namespace std;
class ThreadData
{
public:
    ThreadData(int number)
    {
        _threadname = "thread-" + to_string(number);
    }
    string GetName()
    {
        return _threadname;
    }

private:
    string _threadname;
};
#define NUM 5
int tickets = 1000; // 火车票
void *GetTickets(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    const char *name = td->GetName().c_str();
    while (true)
    {
        if (tickets > 0)
        {
            usleep(1000);
            printf("线程:%s抢到一张票,tickets剩余:%d\n", name, tickets);
            tickets--;
        }
        else
            break;
        printf("线程%s ... 退出\n", name);
        
    }
    return nullptr;
}
int main()
{
    // 创建多线程
    vector<pthread_t> tids;            // 数组放线程ID
    vector<ThreadData *> thread_datas; // 线程信息
    for (int i = 1; i <= NUM; i++)
    {
        pthread_t tid;
        ThreadData *td = new ThreadData(i);
        thread_datas.emplace_back(td);
        pthread_create(&tid, nullptr, GetTickets, thread_datas[i-1]);
        tids.emplace_back(tid);
    }
    for (auto thread : tids)
    {
        pthread_join(thread, nullptr);
    }
    for (auto td : thread_datas)
    {
        delete td;
    }

    return 0;
}

和之前一样如果在实现中,我们这个代码就出问题了。乘客给钱了,但是没有票了。

3.2 加锁 解锁操作

#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <string>
#include <cstring>
#include <cstdio>
#include <iostream>
using namespace std;
class ThreadData
{
public:
    ThreadData(int number)
    {
        _threadname = "thread-" + to_string(number);
    }
    string GetName()
    {
        return _threadname;
    }

private:
    string _threadname;
};
#define NUM 5
int tickets = 1000; // 火车票
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //静态分配。
void *GetTickets(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    const char *name = td->GetName().c_str();
    while (true)
    {
        pthread_mutex_lock(&lock);
        if (tickets >= 0)
        {
            usleep(1000);
            printf("线程:%s抢到一张票,tickets剩余:%d\n", name, tickets);
            tickets--;
            pthread_mutex_unlock(&lock);
        }

        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
        printf("线程:%s ... 退出\n", name);
    }
    return nullptr;
}
int main()
{
    // 创建多线程
    vector<pthread_t> tids;            // 数组放线程ID
    vector<ThreadData *> thread_datas; // 线程信息
    for (int i = 1; i <= NUM; i++)
    {
        pthread_t tid;
        ThreadData *td = new ThreadData(i);
        thread_datas.emplace_back(td);
        pthread_create(&tid, nullptr, GetTickets, thread_datas[i - 1]);
        tids.emplace_back(tid);
    }
    for (auto thread : tids)
    {
        pthread_join(thread, nullptr);
    }
    for (auto td : thread_datas)
    {
        delete td;
    }

    return 0;
}

我使用的是静态的锁,其实也就是个宏 

静态分配 的优点在于 无需手动初始化和手动销毁,锁的生命周期伴随程序,缺点就是定义的 互斥锁 必须为 全局互斥锁 

当然我们也可以使用动态分配,动态分配需要我们手动初始化。

pthread_mutex_t lock; //动态要初始化
pthread_mutex_init(&lock,nullptr);

3.3 ARII风格锁 

 但是使用锁的方式很容易造成死锁的问题,就比如上面的代码需要二次解锁。我们可以利用ARII的思想

#pragma once
#include <pthread.h>
class Mutex
{
public:
    Mutex(pthread_mutex_t *lock)
        : _lock(lock)
    {
    }
    void lock()
    {
        pthread_mutex_lock(_lock);
    }
    void unlock()
    {
        pthread_mutex_unlock(_lock);
    }
    ~Mutex() {}

private:
    pthread_mutex_t *_lock;
};
class lockGudard
{
public:
    lockGudard(pthread_mutex_t *lock) //
        : _mutex(lock)
    {
        _mutex.lock();
    }
    ~lockGudard()
    {
        _mutex.unlock();
    }
private:
    Mutex _mutex;
};
void *GetTickets(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    const char *name = td->GetName().c_str();
    while (true)
    {
        //pthread_mutex_lock(&lock);
        lockGudard lockguard(&lock); //ARII
        if (tickets >= 0)
        {
            usleep(1000);
            printf("线程:%s抢到一张票,tickets剩余:%d\n", name, tickets);
            tickets--;
           // pthread_mutex_unlock(&lock);
        }

        else
        {
            //pthread_mutex_unlock(&lock);
            break;
        }
        printf("线程:%s ... 退出\n", name);
    }
    return nullptr;
}

ARII思想把资源的生命周期交给对象,利用C++类的特性。具体详情请看C++ 智能指针

3.4 深入理解锁 

 从结果来看,加锁之后临界资源确实被保护了,但是对锁来讲这里还有许多细节,我们一一来扣。

细节1:加锁的位置?

 每一个线程访问临界资源前都是要加锁的,本质是对临界区加锁,所以在临界区的代码,有些代码是不涉及临界资源的,例如上图在循环代码之前加锁,也就是说线程要拿到锁才能进入循环。

如果不在循环之前加锁而是在if之前加锁,那么所有线程都能进入循环。如果还有其他不涉及临界的代码。这个线程没有拿到锁是不是就可以执行其他代码?

 

建议加锁时,粒度要尽可能的细,因为加锁后区域的代码是串行化执行的,代码量少一些可以提高多线程并发时的效率 

细节2:多线程之间访问同一个临界资源可以不是同一把锁? 

当然不行,多线程之间访问临界资源,如果一个线程自己带锁,那么它就不会阻塞等待。那它就起飞了,没有人管了。只有多线程之间看到同一把互斥锁,才能让它们互斥。

细节3:互斥锁既能是全局的,又能是局部。那它不也是临界资源?它如何保证自己的安全?

加锁 是为了保护 临界资源 的安全,但  本身也是 临界资源,这就像是一个 先有鸡还是先有蛋的问题 的设计者也考虑到了这个问题,于是对于  这种 临界资源 进行了特殊化处理:加锁 和 解锁 操作都是原子的,不存在中间状态,也就不需要保护了 

 我们先来看一段互斥锁伪汇编

lock:
	movb $0, %al
	xchgb %al, mutex
	if(al寄存器里的内容 > 0){
		return 0;
	} else
		挂起等待;
	goto lock;

 假设线程a先拿到锁

 

  1. 将 %al 寄存器的值设置为0。
  2. 使用 xchgb 指令尝试将 %al 与 mutex 原子地交换值。假设mutex 原本为1,%al 将包含旧的 mutex 值(也是1),并且 mutex 现在被设置为0。
  3. 如果 %al 为1,表示当前线程a成功获取了锁,可以继续执行临界区代码。
  4. 线程b申请锁
  5. 检查 %al 寄存器的值。如果它大于0,表示其他线程a已经持有锁,当前线程b应该返回0(通常表示错误或失败)。
  6. 如果锁已被其他线程持有,当前线程将挂起等待,直到锁变为可用状态。
  7. 一旦锁可用,线程再次尝试获取锁,跳转回 lock 标签处继续执行。

 

unlock:
	movb $1, mutex
	唤醒等待 [锁资源] 的线程;
	return

 假设现在线程a解锁:

1. 将mutex的值设置为1

2. 将线程b唤醒,线程b再执行lock那一套逻辑。

3. 从解锁函数返回执行后面代码。

注意:1.xchgb原子操作不会被任何调度打断,要么完成,要么未完成。

           2. 寄存器的值不等于线程上下文的值。

 细节4:一个线程可以一直拿着锁吗?

理论上是可行的,如果一个线程是拿着锁的,如果它的线程调度时间片到了,也是有可能连锁一起带走。为什么这么说

 如果现在这间vip室室免费且小明先拿到🔑进来,但是小明又想出去玩一会,然后小明出门把🔑放在🔒,准备走的时候,发现有很多人,小明心里又不想失去这间vip室。所以小明又重新拿着🔑进入vip室,其他人都没有抢过小明,因为小明离🔑最近。

对于线程来说,那就是拿到🔒意味就能访问临界区。其他线程只能阻塞等待,而解锁的过程就是线程离开临界区,其他线程访问临界区。

3.5 毁🔒

当进程退出我们也是需要对锁资源进行清理,销毁互斥锁可以释放与之关联的系统资源。 

pthread_mutex_destroy(&lock);

就是这么简单的一句。

3.6 可重入&&线程安全

概念:

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

常见的线程不安全的情况 

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

 常见的线程安全的情况

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

常见不可重入的情况 

我们所学的大部分函数都是不可重入的

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

反之其他的函数都是可以重入的

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

可重入与线程安全区别

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

 3.7 常见锁概念

3.7.1死锁 

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

死锁四个必要条件
  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

必要条件只要有一个不成立,都不会出现死锁问题。

避免死锁
  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

担心死锁问题的小伙伴,不用怕,直接无脑ARII锁。 当然也有常见的避免 死锁 问题的算法:死锁检测算法、银行家算法

4.条件变量

 从这个结果来看,绝大部分都是线程1在抢票,我的运行结果都没有5号线程和2号线程。

 导致这两个线程饥饿,这并不是我们想要的,能否按照一定的顺序有序的访问。造成这样的原因还是因为线程之间的竞争导致的。

这时我们又要重新说到vip的房间了。小明拿到🔑,下次再申请🔑时候,小明最近。所以其他人是抢不到的,这时管理员看不下去了,对小明说你不能这样干,管理员强行对小明进行限制。

对于线程来说也是同理,我们需要对线程进行一定条件的限制。让其他线程雨露均沾。

我们又要引入一个新的概念条件变量。

  • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
  • 例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

可以把 条件变量 看作一个结构体,其中包含一个 队列 结构,用来存储正在排队等候的线程信息,当条件满足时,就会取 队头 线程进行操作,操作完成后重新进入 队尾 

 

 同步概念与竞态条件

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。

 

4.1同步操作相关函数 

PTHREAD_COND_INITIALIZER 是一个宏,用于在编译时初始化条件变量对象。 

优缺点和PTHREAD_MUTEX_INITIALIZER 一样

条件变量函数 初始化 

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL

销毁  

int pthread_cond_destroy(pthread_cond_t *cond) 

等待条件满足  

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量

唤醒等待 


int pthread_cond_broadcast(pthread_cond_t *cond);
//这个函数的作用是唤醒所有等待指定条件变量 cond 的线程。
int pthread_cond_signal(pthread_cond_t *cond);
//这个函数用于唤醒等待指定条件变量 cond 的一个线程。

 

#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <string>
#include <cstring>
#include <cstdio>
#include <iostream>
using namespace std;
class ThreadData
{
public:
    ThreadData(int number)
    {
        _threadname = "thread-" + to_string(number);
    }
    string GetName()
    {
        return _threadname;
    }

private:
    string _threadname;
};
#define NUM 5
int tickets = 1000;                               // 火车票
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 静态分配。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;   // 条件变量静态全局变量
void *GetTickets(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    const char *name = td->GetName().c_str();
    while (true)
    {
        pthread_mutex_lock(&lock);
        pthread_cond_wait(&cond, &lock);
        if (tickets > 0)
        {
            usleep(1000);
            tickets--;
            printf("线程:%s抢到一张票,tickets剩余:%d\n", name, tickets);
            pthread_mutex_unlock(&lock);
        }

        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
    }

    printf("线程:%s ... 退出\n", name);
    return nullptr;
}
int main()
{
    // 创建多线程
    vector<pthread_t> tids;            // 数组放线程ID
    vector<ThreadData *> thread_datas; // 线程信息
    for (int i = 1; i <= NUM; i++)
    {
        pthread_t tid;
        ThreadData *td = new ThreadData(i);
        thread_datas.emplace_back(td);
        pthread_create(&tid, nullptr, GetTickets, thread_datas[i - 1]);
        tids.emplace_back(tid);
    }
    while (tickets > 0)
    {
        usleep(1000);
        pthread_cond_signal(&cond);
        cout << " 主线程唤醒新线程..." << endl;
    }
    pthread_cond_broadcast(&cond); //这里需要唤醒所有等待的线程
    for (auto thread : tids)
    {
        pthread_join(thread, nullptr);
    }
    for (auto td : thread_datas)
    {
        delete td;
    }

    return 0;
}

 

 问题1:为什么条件变量的等待函数要在锁的后面?

条件不满足时,要去等待队列阻塞等待被唤醒。

一个线程拿到了锁,不解锁去等待,后面的线程不就拿不到锁,不就死锁了吗?

wait函数调用时会自动释放锁,这也是为什么第二个参数是锁。

问题2:我们怎么知道我们要让一个线程去休眠了那?

首先临界资源也是有状态的,要么就绪,要么不就绪。不就绪条件不满足。所以线程回去休眠

问题3:你怎么知道临界资源是就绪还是不就绪的?

很简单 我们 if 这里进行判断不就是访问临界资源吗?这也是为什么判断会在加锁之后。

总结:线程同步与互斥 主要讲解了 条件变量 函数接口使用。包括互斥锁的概念、操作、原理,以及多线程与互斥锁的封装;最后简单学习了线程同步相关内容,重点在于对条件变量的理解及使用。至于互斥锁+条件变量的实战:生产者消费者模型将会在下一篇文章中完成

 

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

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

相关文章

JavaScript知识点大总结来了-------这一篇就足够啦!!!

JavaScript基础知识 一、对象的使用 1、创建对象 这里创建对象的方法我们采用最常用的一种&#xff1a; //第一种 <script>var Person {name: "zhangsan",age: "19",weight: "140",hight: "170",print:function(){console.…

让孩子在故事中成长,寓教于乐的趣学伴绘本投影故事机Lite

在早教产品琳琅满目的今天&#xff0c;挑选一款既吸引孩子又能提供真正教育价值的设备&#xff0c;对于家长们来说是一项挑战。我最近发现一种能够通过投影幻灯片讲故事的小工具很有趣&#xff0c;小朋友很喜欢&#xff0c;这款款名为趣学伴绘本投影故事机Lite的设备&#xff0…

NetSuite 不同类型Item的公司间交易科目的设置

我们知道&#xff0c;NetSuite中有Intercompany Preferences的设置&#xff0c;如下所示&#xff0c;分别涉及到公司间应收、公司间应付、公司间收入、公司间费用以及公司间成本共5个科目&#xff0c;非常明确清晰。 最近用户遇到的场景是&#xff0c;如果是Non-Inventory Item…

肾虚学习实验第T1周:实现mnist手写数字识别

>- **&#x1f368; 本文为[&#x1f517;365天深度学习训练营](https://mp.weixin.qq.com/s/0dvHCaOoFnW8SCp3JpzKxg) 中的学习记录博客** >- **&#x1f356; 原作者&#xff1a;[K同学啊](https://mtyjkh.blog.csdn.net/)** 目录 一、前言 作为一名研究牲&#xff0…

webp动图转gif

目录 前言 解决过程 遇到问题 获取duration 前言 上一次我们实现了webp转jpg格式&#xff1a; https://blog.csdn.net/weixin_54143563/article/details/139758200 那么对于含动图的webp文件我们如何将其转为gif文件呢&#xff1f; 之所以会出现这个问题&#xff0c;是因…

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] 5G基站光纤连接问题(200分) - 三语言AC题解(Python/Java/Cpp)

&#x1f36d; 大家好这里是清隆学长 &#xff0c;一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 &#x1f4bb; ACM银牌&#x1f948;| 多次AK大厂笔试 &#xff5c; 编程一对一辅导 &#x1f44f; 感谢大家的订阅➕ 和 喜欢&#x1f497; &#x1f…

容器之工具栏构件演示

代码; #include <gtk-2.0/gtk/gtk.h> #include <glib-2.0/glib.h> #include <gtk-2.0/gdk/gdkkeysyms.h> #include <stdio.h>int main(int argc, char *argv[]) {gtk_init(&argc, &argv);GtkWidget *window;window gtk_window_new(GTK_WINDO…

远程桌面总是连接不上,mstsc远程总是连接不上解决方法

远程桌面连接在日常生活和工作中扮演着至关重要的角色&#xff0c;它允许用户在不同地点和设备之间无缝协作。然而&#xff0c;有时用户可能会遇到MSTSC&#xff08;Microsoft远程桌面连接&#xff09;总是连接不上的问题&#xff0c;这可能是由于多种原因造成的。本文将针对这…

若依 ruoyi 排序 顺序 倒序 的实现

1. table标签新增排序相关属性 // :default-sort"defaultSort" 指定默认排序 // sort-change"handleSortChange" 指定排序点击事件 :default-sort"defaultSort" sort-change"handleSortChange" 2. 列上新增排序相关配置 自定义查询语…

解决File协议导致的CORS限制,用Node.js搭建本地服务器

文章目录 一、前言二、分析报错原因三、如何解决四、具体步骤 你是否曾遇到这样的困境&#xff1a;在本地使用file://协议直接打开HTML文件时&#xff0c;由于现代浏览器的安全限制&#xff0c;无法跨源请求&#xff08;CORS&#xff09;本地资源&#xff1f;尤其是当你试图通过…

非关系型数据库NoSQL数据层解决方案 之 redis springboot整合与读写操作 2024详解以及window版redis5.0.14下载百度网盘

redis下载安装以及基本使用 下载地址 链接&#xff1a;百度网盘 请输入提取码 提取码&#xff1a;0410 一个名对应一个数值 内存级 在内存里进行操作 准备启动 我们现在就有一个redis客户端的服务器了 我们再启动一个cmd 操作redis数据库 redis里面的基本数据类型有五种 …

小程序 如何支付后获取 Unionid

接口说明 接口英文名 getPaidUnionid 功能描述 该接口用于在用户支付完成后&#xff0c;获调用本接口前需要用户完成支付&#xff0c;用户支付完成后&#xff0c;取该用户的 UnionId&#xff0c;无需用户授权。本接口支付后的五分钟内有效。 注意事项 调用前需要用户完成…

重构大学数学基础_week05_雅各比矩阵与雅各比行列式

这周来讲一下雅各比矩阵和雅各比行列式。 多元函数的局部线性属性 首先我们来回顾一下向量函数&#xff0c;就是我们输入一个向量&#xff0c;输出也是一个向量&#xff0c;我们假设现在有一个向量函数 这个函数意思就是在说&#xff0c;我们在原来的平面上有一个向量(x,y),经…

收藏||电商数据采集流程||电商数据采集API接口

商务数据分析的流程 第一步&#xff1a;明确分析目的。首先要明确分析目的&#xff0c;并把分析目的分解成若干个不同的分析要点&#xff0c;然后梳理分析思路&#xff0c;最后搭建分析框架。 第二步&#xff1a;数据采集。主流电商API接口数据采集&#xff0c;一般可以通过数…

【芯片知识】QSOP24封装-NRK3502语音识别芯片方案

一、NRK3502语音识别芯片的简介 NRK3502系列芯片是一款蓝牙双模智能语音l0T芯片。芯片集成32位CPU处理器&#xff0c;包含 UART、GPIO、SPI、SD卡、12C、ADC、TouchSensor 等外围接口;内置 NPU、浮点运算单元。 依托于在语音识别技术上的积累和算法的不断优化和创新&#xff0c…

数据库复习——模式分解

模式分解这边主要包括无损分解和保持函数依赖的分解两种形式&#xff0c;简单整理一下。 无损分解 把一个 R R R 分成 ρ { R 1 , R 2 , ⋯ , R k } \rho \{R_1,R_2,\cdots,R_k\} ρ{R1​,R2​,⋯,Rk​}&#xff0c;然后通过自然连接 R 1 ⋈ R 2 ⋈ ⋯ ⋈ R k R_1\bowtie R…

C#和python端通信之使用共享内存

一、前言 本篇主要实验通过使用共享内存实现C#端代码和python端代码之间的通信&#xff0c;主要目的是相较于直接传输较大的数据&#xff08;例如图像数据&#xff09;&#xff0c;该方式更节省时间。 二、代码 C#端&#xff1a; 创建了一个大小为1的共享内存&#xff0c;名为…

Docker容器基础知识,即linux日常运维命令

Docker 是一个流行的用 Go 语言开发的开源项目&#xff0c;基于Linux内核的cgroup、namespace及 AUFS 等技术&#xff0c;对进程进行封装隔离&#xff0c;由 Dotcloud 公司开发。Docker已成为容器行业的事实标准。 小陈发现一个有趣的事情&#xff0c;容器的英文是Container&am…

数据通信与网络(三)

物理层概述&#xff1a; 物理层是网络体系结构中的最低层 它既不是指连接计算机的具体物理设备&#xff0c;也不是指负责信号传输的具体物理介质&#xff0c; 而是指在连接开放系统的物理媒体上为上一层(指数据链路层)提供传送比特流的一个物理连接。 物理层的主要功能——为…

MDK-ARM 编译后 MAP 文件分析

本文配合 STM32 堆栈空间分布 食用更佳&#xff01; 一图胜千言。。。