Linux 线程的同步与互斥

💓博主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 这里进行判断不就是访问临界资源吗?这也是为什么判断会在加锁之后。

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

 

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

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

相关文章

谁拥有数字营销平台即拥有企业竞争力!

掌握数字营销平台&#xff0c;就等于掌握企业竞争力&#xff01; 宝子们&#xff0c;咱今天来聊聊这个事儿哈。在现在这个数字化的时代&#xff0c;拥有像蚓链这样的数字营销平台那可太重要啦&#xff01; 想想看&#xff0c;有了它&#xff0c;企业就能更广、更快地去推广…

创新案例|星巴克中国市场创新之路: 2025目标9000家店的挑战与策略

星巴克创始人霍华德舒尔茨&#xff1a;“为迎接中国市场的全面消费复苏&#xff0c;星巴克2025年推进9000家门店计划&#xff0c;将外卖、电商以及家享和外出场景咖啡业务纳入中国新一轮增长计划中。”在面临中国市场同店增长大幅下滑29%背景下&#xff0c;星巴克通过DTC用户体…

Word中删除空白页

① 文字后面出现的空白页 把鼠标放在空白页的位置&#xff0c;按住Ctrl Delete即可。 ② 表格后面的空白页 把鼠标放在空白页左侧&#xff0c;直到出现一个空白的箭头&#xff0c;点击一下选中空白页&#xff0c;然后再Ctrl D&#xff0c;打开字体选项卡&#xff0c;在效果中…

舔狗日记Puls微信小程序源码

源码介绍&#xff1a; 这是一款舔狗日记Puls微信小程序源码&#xff0c;提供每日一舔的功能&#xff0c;让你舔到最后&#xff0c;什么都有&#xff01; 源码通过API获取一些舔狗日记&#xff0c;内置了100多句舔狗日记&#xff0c;让你摆脱上班摸鱼的无聊时光&#xff0c; …

[240621] Anthropic 发布了 Claude 3.5 Sonnet AI 助手 | Socket.IO 拒绝服务漏洞

目录 Anthropic 发布 Claude 3.5 Sonnet AI 助手Scoket.IO 拒绝服务漏洞&#xff08;CVE-2024-38355&#xff09; Anthropic 发布 Claude 3.5 Sonnet AI 助手 Claude 3.5 Sonnet: 更智能、更快速、更安全的 AI 助手 一、 引言 Anthropic 发布了 Claude 3.5 Sonnet&#xff0…

C语言中操作符详解(一)

众所周知&#xff0c;在我们的C语言中有着各式各样的操作符&#xff0c;并且在此之前呢&#xff0c;我们已经认识并运用了许许多多的操作符&#xff0c;都是诸君的老朋友了昂 操作符作为我们使用C语言的一个非常非常非常重要的工具&#xff0c;诸君一定要加以重视&#xff0c;…

DevOps学习回顾01-技能发展路线-岗位能力-体系认知

事为先&#xff0c;人为重–事在人为 参考来源&#xff1a; 极客时间专栏&#xff1a;DevOps实战笔记&#xff0c;作者&#xff1a;石雪峰 课程链接&#xff1a;https://time.geekbang.org/column/intro/235 时代的典型特征 VUCA VUCA 是指易变性&#xff08;Volatility&…

【Android面试八股文】你能说一说自定义View与ViewGroup的区别

文章目录 Android UI 组件:View 和 ViewGroupViewGroup 的职责View 的职责自定义 View 和 ViewGroup 的区别1. 继承的类不同2. 主要功能不同3. 重写方法不同4. 使用场景不同5. 事件分发方面的区别6. UI 绘制方面的区别Android UI 组件:View 和 ViewGroup 在 Android 开发中,…

AI通用大模型不及垂直大模型?各有各的好

​​​​​​​AI时代&#xff0c;通用大模型和垂直大模型&#xff0c;两者孰优孰劣&#xff0c;一直众说纷纭。 通用大模型&#xff0c;聚焦基础层&#xff0c;如ChatGPT、百度文心一言&#xff0c;科大讯飞星火大模型等&#xff0c;都归属通用大模型&#xff0c;它们可以解答…

使用ASP.NET Core封装接口请求参数格式

有些人获取接口请求参数是直接使用数据库实体类来获取的&#xff0c;这种方式虽然写起来很方便&#xff0c;但是会导致swagger接口文档出现很多没用的参数&#xff0c;让人看着不舒服。 比如&#xff0c;新增用户只需要传用户名、密码、邮箱就可以了&#xff0c;但是实体类也包…

@ModelAttribute

基础知识 1.ModelAttribute注解源码&#xff0c;从中可以知道&#xff0c;该注解可以标注在参数上和方法上 2.应用场景&#xff1a;先大致有个概念&#xff0c;可以用来存储项目根路径 3.介绍&#xff1a;ModelAttribute 是 Spring 框架中的一个注解&#xff0c;用于在 Spring …

Go微服务: redis分布式锁在集群中可能遇到的问题及其解决方案

概述 我们的 redis 一般都是集群来给我们程序提供服务的&#xff0c;单体的redis现在也不多见 看到上面是主节点redis和下面是6个重节点redis&#xff0c;主节点和重节点的通讯都是畅通没问题的这个时候&#xff0c;我们有 gorouting 写我们的数据&#xff0c;那它就会用到我们…

labelme 标注岩石薄片数据集流程

labelme 数据标注使用流程 1.打开anaconda环境2.打开labelme工具3.打开数据集文件夹4.开始标注5. 标注完成6. 修改labels.txt文件7. 将标注结果可视化8. 完成json转图片9. 全部命令总结 1.打开anaconda环境 2.打开labelme工具 输入下列两条命令&#xff0c;打开labelme工具 &a…

怎么看电脑实时充电功率

因为我想测试不同的充电器给电脑充电的速度&#xff0c;所以就想找一款软件可以看电脑当前充电功率的软件&#xff0c;我给一个图 直接搜索就可以下载了&#xff0c;charge rate就是功率&#xff0c;这里是毫瓦&#xff0c;换算单位是 1000mw1w 所以我这里充电功率是65w&…

毫秒级响应!清科优能应用 TDengine 建设虚拟电厂运营管理平台

小T导读&#xff1a;在清科优能的虚拟电厂运营管理平台建设中&#xff0c;项目初期预计涉及约一万台设备、总数据采集量达数十万&#xff0c;在数据库选择上&#xff0c;其希望能支持至少两千台设备的并发数据处理。本文介绍了清科优能的数据库选型经验以及最终应用效果&#x…

探索产业园的独特产业定位与价值

数字影像产业园的产业定位独特且全面&#xff0c;涵盖了数字贸易、数字服务、数字文旅和数字基建四大主导产业方向&#xff0c;体现了园区在数字化转型和产业升级方面的前瞻性和创新性。 一、数字贸易的推动者 数字影像产业园致力于推动数字贸易的发展&#xff0c;搭建全球化、…

如何使用nginx部署https网站(亲测可行)

公司本来有网站sqlynx.com是http运行的&#xff0c;但因为产品出海&#xff0c;基本上都要求使用https&#xff0c;但又需要兼容已有的http服务&#xff0c;所以我自己尝试做了一次https的部署&#xff0c;目前是正常可用的。 目录 步骤 1&#xff1a;安装 Nginx 步骤 2&…

一个自定义流程的平台

脚本语言使用的是C#&#xff0c;当用户发布一个新的流程时&#xff0c;会把C#的脚本编译成dll&#xff0c;然后添加到微服务中&#xff0c;因为有了硬编译&#xff0c;所以执行速度是非常快的。逻辑脚本支持调试&#xff0c;可以断点和逐行调试。平台提供了调试工具&#xff0c…

redis高可用-集群部署

一&#xff1a;背景 前面我们实现了redis的主从同步和哨兵模式&#xff0c;解决了单机redis的故障转移和流量分担&#xff0c;但是不管是主从或者哨兵模式都是一个主服务对应一个或者多个从服务&#xff0c;并且主服务和从服务的数据是一样的&#xff0c;这样就实现不了redis大…

从零入手人工智能(4)—— 逻辑回归

1.小故事 一家金融科技公司&#xff0c;公司的首席执行官找到团队提出了一个紧迫的问题&#xff1a;“我们如何提前知道哪些客户可能会违约贷款&#xff1f;” 这让团队陷入了沉思&#xff0c;经过激烈讨论团队中的数据分析师提议&#xff1a;“我们可以尝试使用逻辑回归来预测…