线程的互斥

news2024/10/5 13:41:46

目录

线程互斥

线程互斥的背景知识

多线程抢票

多线程抢票加锁

锁的原理

可重入函数与线程安全

常见的线程安全的情况

常见的不可重入情况

常见的可重入情况

总结


线程互斥

线程互斥的背景知识

临界资源:临界资源就是多个执行流共享的资源就叫做临界资源。

临界区:访问临界资源的代码就叫做临界区。

互斥:就是在任何时刻,只能有一个执行流进入进入临界区然后访问临界资源,可以对临界资源起到保护作用。

原子性:不会被任何调度打断,且只有两态,要么完成,要么未完成。

上一次我们说了关于线程的控制,而我们就是通过多线程来执行一些代码,提高效率(但是不是线程越多越好,而且有些场景不适合多线程)。

而且我们也说了,多线程是比较容易出错的,所以我们通过写代码来发现多线程容易出错的问题,然后来慢慢调整。

如果由多线程访问临界资源然后导致出现问题,那么我们就可以选择对临区进行加锁,来保证多线程执行的正确性。

多线程抢票

下面呢,我们要写一个关于多线程抢票的代码:

我们创建一批线程,然后让这批线程区执行一个抢票(对一个变量进行减减操作):

#define THREAD_NUM 5
​
int tickets = 10000;
​
void *threadRun1(void *args)
{
    while (true)
    {
        if (tickets > 0)
        {
            // 模拟抢票前需要做的事
            usleep(rand() % 3000 + 500);
            
            printf("%d : 抢了第 %d 张票\n", (long long)args, tickets);
            --tickets;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}
​
// 多线程抢票逻辑
void test1()
{
    // 创建多线程
    vector<pthread_t> thread_num(THREAD_NUM);
    for (int i = 0; i < THREAD_NUM; ++i)
    {
        pthread_create(&thread_num[i], nullptr, threadRun1, (void *)(i + 1));
    }
​
    // 循环等待每个线程
    for (int i = 0; i < THREAD_NUM; ++i)
    {
        pthread_join(thread_num[i], nullptr);
    }
}

如果在我们平时的情况下,这个代码抢到 1 就会停下,那么现在我们看一下。

结果:

4 : 抢了第 6 张票
4 : 抢了第 5 张票
3 : 抢了第 4 张票
5 : 抢了第 3 张票
2 : 抢了第 2 张票
4 : 抢了第 1 张票
1 : 抢了第 0 张票
2 : 抢了第 -1 张票
5 : 抢了第 -2 张票
3 : 抢了第 -3 张票
[lxy@hecs-348468 mutex]$ 

这里看到,我们前面的抢票还抢到了负数,那么这是什么情况?

下面我们为大家解释:

我们前面说了,线程事CPU调度的基本单位,由于操作系统中有很多线程,所以为了公平,系统京可能让所有的线程都执行相同的时间,而线程也随时都可能被切换。

虽然说线程被切换,但是其实并没有问题,因为在线程被切换的时候,线程会把CPU中的上下文数据带走,当这个线程回来的时候,会进行上下文恢复,然后继续执行代码。

所以我们知道线程事随时都可能被切换走的。

看这幅图,首先,我们在 if 条件判断那里,判断也是计算的一种,所以需要加载到CPU里面,然后进行判断,如果判断发现大于0,那么就可以进入到 if 里面了,然后执行ticket--操作。

由于这个判断编译成汇编之后,并不是一条指令,所以这个判断也是分为几步的。

由于我们知道,进程随时都可能被切换走,所以如果刚好判断发现大于0,后然后被切换走了,线程此时已经静茹到 if 条件里面了,此时被切换走后,该线程会保护自己的上下文数据,然后也会记录自己执行到哪里了,等下一次回来后接着执行。

然后此时其他的线程又进来了,同样发现大于0,也对 tickets-- 操作,然后经过多次减减操作后,最终减为0,但是此时被切换走的哪个线程又被系统调度了,此时它回复上下文后,就准备执行后续的代码,也就是对 tickets--,但是此时的 tickets 已经被减为0了,不能才减了,也就是票已经售空了,但是被调度的进程在前面已经判断过了,进入了 if 语句,所以此时还是对 tickets 进程 减减操作,此时就出现问题了,线程不安全。

所以就需要对临界区进程加锁。

多线程抢票加锁

这里我们先直接看如何进行加锁,以及加锁的函数以及代码如何编写,后面我们在说一下加锁的原理。

pthread_mutex_init

NAME
       pthread_mutex_destroy, pthread_mutex_init - destroy and initialize a mutex
​
SYNOPSIS
       #include <pthread.h>
​
       int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);
  • 这个函数是加锁,可以让临界区只能由一个执行流进入。

  • 第一个参数是一个 pthread_mutex_t 的指针,也就是我们说的锁,我们需要对锁进行初始化。

  • 第二个参数是一个关于设置锁的属性的变量,我们设置为 nullptr 即可。

  • 但是我们也可以不对锁锁进行这样的初始化,如果是全局的锁,那么我们可以直接使用一个宏来初始化。

  • 全局锁的初始化,可以使用——PTHREAD_MUTEX_INITIALIZER

下面再介绍三个关于锁的常用操作 PV 操作:

NAME
       pthread_mutex_lock, pthread_mutex_trylock, pthread_mutex_unlock - lock and unlock a mutex
​
SYNOPSIS
       #include <pthread.h>
​
       int pthread_mutex_lock(pthread_mutex_t *mutex);
       int pthread_mutex_trylock(pthread_mutex_t *mutex);
       int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 首先第一个函数就是加锁,我们看函数名也可以看出来,该函数如果没有竞争到锁的话,那么就会阻塞住。

  • 第二个函数也是加锁的,但是这个函数如果没有竞争到锁的话,就会返回。

  • 第三个函数就是解锁。

  • 而这三个函数的参数分别是锁的指针。

下面我们写一个代码,对临界区进行加锁:

下面为了简单一点,我们这一次使用全局的锁。

​
// 为了简单一点,这里先使用全局的锁
// 全局的锁可以使用系统中的一个宏来初始化,如果不是全局的,那么就需要使用 pthread_mutex_init 来初始化

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

​
// 加锁条件下测试多线程抢票是否会异常
void *threadRun2(void *args)
{
    while (true)
    {
        pthread_mutex_lock(&mtx);
        if (tickets > 0) // 这里虽然是判断,但是也是计算的一种,所以加锁需要对判断也进行加锁
        {
            printf("%ld : 抢了第 %d 张票\n", (long long)args, tickets);
            int tmp = tickets--;
            --tickets;
            pthread_mutex_unlock(&mtx);
        }
        else
        {
            pthread_mutex_unlock(&mtx);
            break;
        }
        // 这里为了模拟抢票后还需要做的事情,这里就用sleep代替
        usleep(rand() % 1500 + 500);
    }
​
    return nullptr;
}
​

这里我们再 if 之前就需要加锁,为什么?

因为我们前面也说了,判断也是计算,因为后面可能会修改 tickets 所以需要对 if 判断也需要加锁。

那么什么时候解锁呢?

我们可不可以再else 后面解锁?为什么?

我不可以再else后面解锁,因为如果没有走 if 里面呢?而是直接走了else 逻辑,那么此时该进程持有锁,但是它却没有释放,然后直接退出了,这样会导致其他线程竞争锁,然后没有锁就会导致死锁的问题。

如果我们要写到 else 后面,那么我们还需要到 else 的逻辑里面也写一条解锁的代码,这样即使持有锁,那么再走了 else 的逻辑后,还会释放锁。

那么我们为什么不把解锁写到 else 后面呢?

实际上,我们知道,如果加锁了的话,那么此时这段代码就是串行的,那么就是只有一个执行流可以再同一时间执行,所以会导致效率低下,所以加锁的粒度是越细越好。

下面我们看一下试验结果:

1 : 抢了第 7 张票
5 : 抢了第 6 张票
4 : 抢了第 5 张票
1 : 抢了第 4 张票
2 : 抢了第 3 张票
3 : 抢了第 2 张票
5 : 抢了第 1 张票
[lxy@hecs-348468 mutex]$ 
​

这里我们看到结果就正确了。

但是下面我们由几个疑问:

  1. 加锁了,那么加锁的那一段是串行的吗?

  2. 加锁之后,就不会到临界区被切换吗?

  3. 如果被切换,那么是否安全呢?

下面我们就来回答这三个问题。

我们先回答第二个,加锁之后会不会到临界区被切换?

回答:会的! 我们前面也说过,线程随时都可能被切换,那么如果当这个线程执行到临界区,然后时间片到了,那么操作系统一定会把它从CPU上剥下来,然后换另一个线程上去执行,而且不光是时间片到了,如果是抢占式,那么还可能被优先级高的线程给抢占。

回答第三个问题:那么既然可以被抢占,那么切换后,临界资源是否安全呢?

回答:安全! 为什么?我们在进入临界区的时候,我们是先加锁的,那么也就是说,如果进入临界区说明该线程一定是尺有锁的,既然是持有锁,那么说明其他线程是没有锁的,既然如此,那么即使该线程被切换下去,系统将其他线程调度,其他线程因为没有锁而处于阻塞状态,所以也就不会调度其他的线程,等待该线程再一次被调度,首先是恢复上下文,该线程还是持有锁的,所以在此期间,其他的线程都不会访问临界资源,只有持有锁的线程可以访问,所以临界资源是安全的。

回答最后一个(第一个)问题:那么加锁后是串行执行吗?

回答:是的!其实我们回答了上面的两个问题后,我们也就理所当然的知道加锁后是串行执行的,但是串行执行的之哟临界区的代码,其他的代码还是可能是并行的,所以加锁后,临界区的代码执行是串行的。

上一个代码我们使用了全局的锁,这一次我们使用局部的锁,还可以在理解一下加锁:

// 用于将线程的名字和锁都传过去,如果还有其他数据需要传的话,那么就可以写到该结构体中
struct threadDate
{
    threadDate(int _thread_name, pthread_mutex_t* _mutex)
        :thread_name(_thread_name)
        ,mutex(_mutex)
    {}
​
    int thread_name = 0;
    pthread_mutex_t* mutex = nullptr;
};
​
​
// 下面是使用了局部的锁,所以需要加锁的话,那么就需要将锁传过来,可以通过 void* 进程传参
void *threadRun3(void *args)
{
    threadDate* date = reinterpret_cast<threadDate*>(args);// 相似类型转换
    while(true)
    {
        pthread_mutex_lock(date->mutex);
        if(tickets > 0)
        {
            int tmp = tickets--;
            pthread_mutex_unlock(date->mutex);
            printf("%ld 号线程: 抢到了 %d 张票\n", date->thread_name, tmp);
        }
        else 
        {
            pthread_mutex_unlock(date->mutex);
            break;
        }
        usleep(rand() % 1500 + 500);
    }
    delete date;
}
​
// 上面为了方便,使用了全局的锁,下面我们使用局部的锁
void test2()
{
    // 先创建一个局部的锁
    pthread_mutex_t mutex;
    // 需要对锁进程初始化
    pthread_mutex_init(&mutex, nullptr);
    cout << "锁初始化成功" << endl;
​
    // 创建多线程
    vector<pthread_t> thread_num(THREAD_NUM);
    for (int i = 0; i < THREAD_NUM; ++i)
    {   
        // 为了将锁和线程的其他数据传入到回调函数中,我们需要构造一个对象,用来存放每个线程的数据
        threadDate* date = new threadDate(i, &mutex);
        pthread_create(&thread_num[i], nullptr, threadRun3, (void *)date);
    }
​
    // 循环等待每个线程
    for (int i = 0; i < THREAD_NUM; ++i)
    {
        pthread_join(thread_num[i], nullptr);
        // cout << "等待线程 " << i << " 号线程成功" << endl;
    }
​
    // 使用完后需要对锁进行释放
    pthread_mutex_destroy(&mutex);
}

如果我们使用局部的锁,那么如果我们需要在线程中加锁的话,我们就需要把这个锁传入到线程中,那么怎么传入呢?我们在线程创建的时候,最后一个参数是一个 void 的指针,然后我们可以通过这个指针将锁传进去。

但是我们要是还想传入其他的数据呢?

我们可以用一个类/结构体来传入,我们可以将我们想要传入的数据放到一个对象中,然后将该对象的地址传进去。但是如果是局部的锁初始化后,那么我们就需要对这个锁进行释放,所以我们在使用完后还需要进行释放。

这个试验结果我们就不看了,因为这个和上面的结果一样。

锁的原理

上面我们以及使用过锁了,下面我们说一下锁的原理:

在C/C++中,对一个变量进行++/-- 操作的时候,我们看似是一条语句,但是实际上却不是一条语句。

当我们进行++的时候:

  1. 先将变量加载到寄存器中

  2. 然后对寄存器中的数据进程++

  3. 最后将该数据拷贝回内存。

而我们前面说了线程是什么时候都可能被切换的,那么我们可能对一个变量进行加加的时候,可能刚执行完第二步,然后就被切换走了。

而我们前面还说了一个原子性,原子性就是要么完成,要么未完成,而前面的这个加加/减减显然就不是原子的,因为有三条语句,而我们认为如果编译未汇编后只有一条语句,那么该语句就是原子的。

结论:如果是一条汇编,那么就是是原子性。

上面的一个结论以及知道了,下面我们在看一个。

前面我们访问的 tickets 是全局的数据,但是我们访问全局的数据的时候需要进行访问保护,而我们访问保护又需要锁,但是我们加锁又是多线程使用同一把锁,那么这个锁是不是临界资源呢?是的,那么当访问一个临界资源的时候需要加锁,但是锁也是临界资源,那么怎么办呢?

下面我们就谈一下锁的原理:

在谈这个之前,我们先介绍一个背景知识:

我们系统中可能又一条汇编可以让寄存器中的数,直接和内存中的数据交换:

swap / exchange #可以使用一条汇编,就可以将寄存器中的数据和内存中的数据交换

不同的操作系统可能不同,但是一定会有这条汇编,可能是 swap 也可能是 exchange。

那么锁我们应该怎么理解呢?下面我们可以把锁理解为内存中的一个值,我们认为锁就是 1,没有锁就是 0.

如果时 lock 的话,那么时怎么样加锁呢?

lock

lock:
    movb $0,%al
    swap %al,mutex
    if(%al > 0)
    {
        return;
    }
    else
    {
        阻塞...
        goto lock
    }

unlock

unlock:
    movb $1,mutex
    唤醒等待 mutex 的线程
    return

上面我们说了,汇编中有一条语句,可以帮助我们将寄存器中的数据和内存中的数据用一条汇编就可以完成。

我们认为,一条汇编就是原子的,也就是要么完成了,要么未完成。

既然如此那么先看依稀 lock 的代码:

  1. 首先将 0 move 到一个 %al 的寄存器中

  2. 使用一条汇编 swap 将 %al 中的数据和内存中 mutex 中的数据进行交换

  3. 判断 %al 中的数据是否是 1,如果是 1 的话,那么就说明竞争到锁了,那么如果是 0 的话,说明还是没有锁。

  4. 如果没有锁的话,就需要去 else 的逻辑中去阻塞。

  5. 如果有锁了,那么就直接返回,可以执行加锁后的代码。

  6. 既然锁以及被该进程拿走了,此时内存中 mutex 中的值就是 0,此时其他的线程来了,也经过上面的操作,发现内存中的数据是 0,所以就会进入到 else 的逻辑,阻塞起来。

下面看一下 unlock 的代码:

其实 unlock 就比较简单,此时能执行 unlock 一定说明你是加锁了的(在逻辑正确的情况下),那么既然内存中的数据是 0 ,那么就可以将 1 直接放到内存中的 mutex 中,所以这就完成了解锁,解锁之后还需要对阻塞的线程进行唤醒。

可重入函数与线程安全

下面再说一下比较容易混淆的两个概念:可重入与线程安全

线程安全:多线程情况下,同时并发访问梯段代码,不会出现不同的结果,就是说明是线程安全的,一般情况下,如果对全局数据或者静态变量访问了,那么就容易是线程不安全的,当然还可能使 malloc 了,或者其他的操作。

重入:同一个函数被不同的执行流调用,再一个执行流还没有结束的时候,就有其他的执行流执行这段代码,那么就称之为重入,如果重入后,不会对结果产生影响,那么说明该函数时可重入的,如果重入后对结果产生了影响,那么说明该函数时不可重入的。

常见的线程安全的情况
  • 多线程场景下,对全局或者静态的变量具有只读权限,那么一般情况下,这样就是线程安全的。

  • 多线程锁调用的函数都是有原子性的,那么说明时线程安全的。

  • 多线程之间切换,不会导致接口调用出现二义性。

常见的不可重入情况
  • 调用了 malloc/free 等函数...

  • 调用了 IO 类的接口,一般的 IO 都是不可重入的

  • 可重入函数内使用了静态的数据结构

常见的可重入情况
  • 不使用 malloc/free 函数

  • 不使用 IO 类接口

  • 没有使用静态变量,全局变量

  • 没有调用不可重入函数

  • 不返回静态或者全局的数据,使用本地的变量

总结
  • 一般情况下,如果函数是可重入的,那么线程就是安全的。

  • 如果函数是不可重入的,那么说明多线程访问会有问题,所以就不能多线程访问。

  • 线程安全的不一定是可重入函数,而可重入函数一定是线程安全的。

  • 例如:如果对临界资源访问加上锁,那么说明这个是线程安全的,但是如果这个函数是可重入的,那么当锁还未释放的时候就重入了,此时会竞争锁,此时救护产生死锁,所以是不可重入的。

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

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

相关文章

勘察设计考试公共基础之物理篇

2、物理 理想气体的压强P23nw &#xff08;n为分子的密度数&#xff09;&#xff0c;w12mv2 &#xff08;m为分子质量&#xff09;&#xff0c;v2 为分子速率平方的平均值。PnKT&#xff0c;∴w32KT 即&#xff1a;理想气体分子的平均平动动能与气体的温度成正比&#xff0c;气…

【商城更新】神秘市场通行证上架、齿轮头归来//及下架内容

本周商城将于11月8号更新。本次商城除了神秘市场2023通行证上架之外&#xff0c;还有齿轮头黑货箱也会上架藏匿处。随之小兔奇趣齐聚大礼包、危险玩偶大礼包等饰品下架商城。 上架饰品&#xff1a; ▲神秘市场2023通行证 神秘市场2023通行证基础版 售价&#xff1a;1200G-coi…

Xilinx Artix7-100T低端FPGA解码MIPI视频,基于MIPI CSI-2 RX Subsystem架构实现,提供工程源码和技术支持

目录 1、前言免责声明 2、我这里已有的 MIPI 编解码方案3、本 MIPI CSI2 模块性能及其优缺点4、详细设计方案设计原理框图OV5640及其配置权电阻硬件方案MIPI CSI-2 RX SubsystemSensor Demosaic图像格式转换Gammer LUT伽马校正VDMA图像缓存AXI4-Stream toVideo OutHDMI输出 5、…

实验一 Anaconda安装和使用(Python程序设计实验报告)

实验一 Anaconda安装和使用 一、实验环境 Python集成开发环境IDLE/Anaconda 二、实验目的 1&#xff0e;掌握Windows下Anaconda的安装和配置。 2. 掌握Windows下Anaconda的简单使用&#xff0c;包括IDLE、Jupyter Notebook、Spyder工具的使用。 3. 掌握使用pip管理Python扩展库…

中国专利转让数据集(1985-2021年)

专利转让数据追踪和记录专利从一个实体转移到另一个实体的过程。这些数据不仅包括参与转让的申请人和受让人的身份信息&#xff0c;如名字和地址&#xff0c;还涵盖了转让的具体法律细节&#xff0c;包括转让执行日、转让次数、法律状态变更&#xff0c;以及转让登记的相关信息…

FL Studio最新版本号21.2发行更新啦

Image Line宣布发布FL Studio 21.2。更新带来了许多改进&#xff0c;但主要功能是引入了新的词干分离功能和FL Cloud&#xff0c;这是一个新的在线平台&#xff0c;直接与DAW集成&#xff0c;为用户提供从循环和样本到母带和发行功能的一切。 词干分离与FL云 随着最新更新的发…

AI生成PPT工具——Gamma,结合GPT生成不错的效果

AI生成PPT工具——Gamma&#xff0c;结合GPT生成不错的效果 先告诉GPT我现在要参加一个比赛&#xff0c;请他帮忙梳理一下内容。当然整个过程需要不断调整&#xff0c;GPT生成的内容也不是一次就是最好的 不断调整之后让其列出提纲即可&#xff0c;如下&#xff1a; 紧接着我们…

谈谈前端如何防止数据泄露

shigen日更文章的博客写手&#xff0c;擅长Java、python、vue、shell等编程语言和各种应用程序、脚本的开发。记录成长&#xff0c;分享认知&#xff0c;留住感动。 最近突然发现了一个好玩的事情&#xff0c;部分网站进去的时候几乎都是死的&#xff0c;那种死是区别于我们常见…

【Hadoop】MapReduce详解

&#x1f984; 个人主页——&#x1f390;开着拖拉机回家_大数据运维-CSDN博客 &#x1f390;✨&#x1f341; &#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#x1f341; &#x1fa81;&#x1f341;&#x1fa81;&#x1f…

讨论编程建议,生成需求文档:SolidGPT 对话代码,智能互动 | 开源日报 No.73

vuejs/vue Stars: 205.6k License: MIT Vue 是一个用于构建用户界面的渐进式框架。它从头开始设计&#xff0c;可以根据不同的使用情况轻松地在库和框架之间进行扩展。Vue 由一个专注于视图层的核心库组成&#xff0c;并且还有一系列支持性库来帮助您处理大型单页应用程序中的…

哈夫曼树(定义,构造,哈夫曼编码)

目录 1.带权路径长度2.哈夫曼树的定义3.哈夫曼树的构造1.哈夫曼树的特性 4.哈夫曼编码1.编码方式2.应用 1.带权路径长度 ①结点的权:有某种现实含义的数值&#xff08;如:表示结点的重要性等) ②结点的带权路径长度:从树的根到该结点的路径长度&#xff08;经过的边数&#xff…

光明源@智慧公厕是如何提升城市人们生活质量的?

随着城市人口不断增加&#xff0c;城市生活质量成为社会关注的焦点。在城市规划中&#xff0c;智慧公厕作为一项创新的基础设施&#xff0c;正日益受到重视。它们不仅提供卫生便捷的服务&#xff0c;还通过科技的运用&#xff0c;显著提升了城市居民的生活质量。本文将深入探讨…

C++进阶篇4---番外-红黑树

一、红黑树的概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制&#xff0c;红黑树确保没有一条路 径会比其他路径长出俩倍&#xff0…

Springboot项目部署及多环境开发

一、项目部署 我们之前写的代码都是部署在本地的tomcat上&#xff0c;别人是无法访问我们写的程序的。在实际开发中&#xff0c;我们都要将开发完毕的项目部署到公司的服务器上。 我们的代码需要经过编译打包生成一个jar包&#xff0c;这个过程需要借助一个插件来实现。 创建sp…

Linux常用命令——bzip2recover命令

在线Linux命令查询工具 bzip2recover 恢复被破坏的.bz2压缩包中的文件 补充说明 bzip2recover命令可用于恢复被破坏的“.bz2”压缩包中的文件。 bzip2是以区块的方式来压缩文件&#xff0c;每个区块视为独立的单位。因此&#xff0c;当某一区块损坏时&#xff0c;便可利用b…

AIOT数字孪生智慧工地一体化管理平台源码

智慧工地app基于物联网和移动互联网技术&#xff0c;利用各类传感器及终端设备通过与云端服务器的实时数据交互&#xff0c;为施工现场的管理人员提供环境监测、劳务实名制管理、物料管理、巡检记录、设备管理等一系列优质高效的行业解决方案。 一、智能工地应用价值 智慧工地…

有效找回误删照片的 6 种照片数据恢复软件!

照片是珍惜过去珍贵时刻的唯一方式。它们让记忆永存&#xff0c;帮助我们重温生命中最美好的时刻。但是&#xff0c;当这些时刻丢失时会发生什么&#xff1f;您是否曾经因系统崩溃而意外删除或丢失照片&#xff1f;丢失照片可能令人心碎&#xff0c;但仍有希望&#xff0c;因为…

SOLIDWORKS实用技巧之焊件轮廓应用

1.焊件轮廓库官方下载入口 焊件轮廓可以自制&#xff0c;也可以从软件中在线下载获取直接使用&#xff0c;如图1&#xff0c;联网状态按ctrl左键点击下载&#xff0c;解压后获得库文件。 图1 图2 2.库放置的位置和配置 从SOLIDWORKS2014版起&#xff0c;软件焊件轮廓库支持可…

初始MySQL(二)(表的增删查改)

目录 修改表 CRUD(增删改查) insert语句(表中增加数据) update语句(修改表中的数据) delete删除语句 select语句 修改表 添加列 ALTER TABLE tablename ADD (column datatype [DEFAULT expr] [, column datatype] ...); 修改列 ALTER TABLE tablename MODIFY (column …

西门子S7-1500与1200之间PUT/GET无线通讯

本方案搭建的是固定主机1500PLC与两台移动1200PLC之间以太网通讯。 无线通讯网络搭建 首先在固定端主机设备上的西门子S7-1500PLC上搭载一块达泰DTD418MB作为主站。然后在两台移动的西门子S7-1200PLC上分别搭载一块达泰DTD418MB作为从站。由此&#xff0c;便通过DTD418MB搭建…