Linux系统:线程互斥

news2024/11/17 16:17:23

Linux系统:线程互斥

    • 线程互斥
      • 互斥锁 mutex
      • 互斥锁原理
    • 常见的锁
      • 死锁
      • 自旋锁 spinlock
      • 其它锁


线程互斥

讲解线程互斥前,先看到一个抢票案例:

class customer
{
public:
    int _ticket_num = 0;
    pthread_t _tid;
    string _name;
};

int g_ticket = 10000;

void* buyTicket(void* args)
{
    customer* cust = (customer*)args;

    while(true)
    {
        if(g_ticket > 0)
        {
            usleep(1000);
            cout << cust->_name << " get ticket: " << g_ticket << endl;
            g_ticket--;
            cust->_ticket_num++;
        }
        else
        {
            break;
        }
    }

    return nullptr;
}

int main()
{
    vector<customer> custs(5);

    for(int i = 0; i < 5; i++)
    {
        custs[i]._name= "customer-" + to_string(i + 1);
        pthread_create(&custs[i]._tid, nullptr, buyTicket, &custs[i]);
    }

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

    for(int i = 0; i < 5; i++)
    {
        cout << custs[i]._name << " get tickets: " << custs[i]._ticket_num << endl;
    }

    return 0;
}

这个案例比较复杂,我们的目标是:设定一个全局变量g_ticket,然后派出五个线程来模拟顾客,进行抢票,每次抢票的时候g_ticket--,直到g_ticket <= 0,也就是票被抢光了,就停止抢票。

首先我封装了一个类class customer

class customer
{
public:
    int _ticket_num = 0;
    pthread_t _tid;
    string _name;
};

其代表一个顾客,本质来说是线程模拟的顾客,_ticket_num 表示该顾客抢到的票数,_tid表示这个线程的TID_name则为该顾客的名字。

随后让线程去执行buyTicket函数:

void* buyTicket(void* args)
{
    customer* cust = (customer*)args;

    while(true)
    {
        if(g_ticket > 0)
        {
            usleep(1000);
            cout << cust->_name << " get ticket: " << g_ticket << endl;
            g_ticket--;
            cust->_ticket_num++;
        }
        else
        {
            break;
        }
    }

    return nullptr;
}

一开始线程就进入while循环,只要g_ticket > 0就抢票,让g_ticket--cust->_ticket_num++;,表示总票数减少,自己的票数加一。再输出cust->_name << " get ticket: " << g_ticket,含义为:xxx 抢到了第 xxx 张票

主函数中:

int main()
{
    vector<customer> custs(5);

    for(int i = 0; i < 5; i++)
    {
        custs[i]._name= "customer-" + to_string(i + 1);
        pthread_create(&custs[i]._tid, nullptr, buyTicket, &custs[i]);
    }

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

    for(int i = 0; i < 5; i++)
    {
        cout << custs[i]._name << " get tickets: " << custs[i]._ticket_num << endl;
    }

    return 0;
}

一开始用vector创建了五个customer,第一个for循环将这些customer进行初始化,给他们命名,并创建线程。此时线程就已经开始进行抢票了,随后主线程第二个for循环等待这五个线程。最后一个for循环输出每个线程抢到的票的数目。

输出结果:

在这里插入图片描述

奇怪的事情发生了,我们只有10000张票,最后却抢出了10005张票!最后几个线程,抢到了不存在的0,-1,-2,-3号的票,为什么会多出五张票?

我简化一下模型:

在这里插入图片描述

如图所示,现在有两个线程customer-1customer-2,它们共同争夺g_ticketg_ticket = 1,也就是说只有一个人可以抢到票。它们都执行左侧的代码,只要g_ticket > 0,就g_ticket--减少一个票,然后cust->_ticket_num++,表示自己的票数增加。

现在假设线程customer-1先调度:

在这里插入图片描述

该线程先判断,发现g_ticket > 0,于是进入第一个if语句,进行g_ticket--但是g_ticket--本质上是多条汇编语句,比如下面这样

MOV  eax, [0x1000]  ; 读取 g_ticket 的值
DEC  eax            ;1
MOV  [0x1000], eax  ; 将值写回 g_ticket

也许你看不懂这个指令,我简单讲解一下:第一行MOVE的作用,是把内存中g_ticket 的数据拷贝到CPU中,第二行是将g_ticket - 1,第三行是减法后的结果拷贝回内存中的g_ticket

那么假设我们现在执行到汇编的第二条指令:

在这里插入图片描述

现在突然线程customer-1的时间片结束了,要结束调度当前线程,去调度customer-2了。请问当前内存中的g_ticket被修改了吗?不还没有,这是下一条汇编的作用,于是CPU保存当前线程customer-1的上下文,切换调度customer-2

在这里插入图片描述

此时线程customer-2也通过if (g_ticket > 0)检测还有没有票,结果发现还有一张票,于是custome-2也去抢这张票,执行g_ticket--

这下出问题了,刚刚我们的customer-1已经抢了这张票,但是还没来得及把g_ticket变成0,此时customer-2又进来抢了一次票。最后就会出现一张票被两个人抢到的问题!

也就是说,为什么最开始的案例中,会出现10005张票,就是因为最后一张票,被五个线程同时抢到了!当g_ticket已经被抢走时,由于没来得及g_ticket = 0,导致后来的线程以为还有票

我先引入一部分概念,方便大家理解后续知识:

  • 临界资源:以上案例中,g_ticket是共享资源,多个线程共享。我们把这种资源称为临界资源
  • 临界区:访问临界资源的代码,叫做临界区。比如g_ticket--就是临界区代码,以为其访问了临界资源g_ticket
  • 原子性:表示一个操作对外表现只有两种状态:还没开始已经结束

我用刚才的案例帮助大家理解这个原子性的概念:我们说g_ticket--本质上会变成多条汇编语句,也就是说g_ticket--是有过程的,而不是一瞬间完成的。

这就导致在我还没有完成g_ticket--的时候,也就是在g_ticket--过程中,被其他线程打断了。导致其它线程收到错误的信息,抢到不存在的票。

如果说一个线程抢到票后,g_ticket--会立马执行完毕,下一个线程在访问这个g_ticket > 0的时候,一定是在别人已经g_ticket--完毕,而不是在g_ticket--过程中,就可以避免这个问题。这就要求访问g_ticket原子性的,也就是说在别的线程眼中,根本就不存在g_ticket--的过程,要么你没有执行g_ticket--,要么已经执行完毕。

临界区代码只要保证是原子性的,就可以避免这样线程之间错误的抢占资源相同资源的问题

那么要如何保证临界区代码是原子性的呢?此时就需要线程互斥了!

线程互斥指的是在多线程环境中,多个线程访问同一个共享资源时,只允许一个线程访问,其他线程必须等待,直到当前线程访问完成才能继续访问。

线程互斥,是通过来实现的。

锁的规则如下:

  1. 代码必须要有互斥行为:当代码进入临界区时,不允许其它线程进入临界区
  2. 如果多个线程都想执行临界区代码,并且当前临界区没有线程在执行代码,只允许一个线程进入临界区
  3. 线程不能阻止其他线程进入临界区

简单来说就是:任何时候临界区都只能有一个线程执行


互斥锁 mutex

互斥锁pthread库提供的,英文名为mutex(互斥),需要头文件<pthread.h>,先讲解互斥锁的基本创建和销毁方法。

互斥锁的类型是pthread_mutex_t,分为全局互斥锁局部互斥锁,它们的创建方式不同。

全局mutex

想要创建一个全局的互斥锁很简单,直接定义即可:

pthread_mutex_t xxx = PTHREAD_MUTEX_INITIALIZER;

这样就创建了一个名为xxx的变量,类型是pthread_mutex_t,即这个变量是一个互斥锁全局的互斥锁必须用宏PTHREAD_MUTEX_INITIALIZER进行初始化!

另外,全局的互斥锁不需要手动销毁

局部mutex

局部的互斥锁是需要通过接口来初始化与销毁的,接口如下:

pthread_mutex_init

pthread_mutex_init函数用于初始化一个互斥锁,函数原型如下:

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

参数:

  • restrict mutex:类型为pthread_mutex_t *的指针,指向一个互斥锁变量,对其初始化
  • restrict attr:用于设定该互斥锁的属性,一般不用,设为空指针即可

返回值:成功返回0;失败返回错误码


pthread_mutex_destroy

pthread_mutex_destroy函数用于销毁一个互斥锁,函数原型如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数:类型为pthread_mutex_t *的指针,指向一个互斥锁变量,销毁该锁

返回值:成功返回0;失败返回错误码


创建好互斥锁后,就要使用这个锁,主要是两个操作:申请锁释放锁

三个函数的原型如下:

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_lock:用于申请锁,如果申请失败,就阻塞等待,直到申请到锁;如果申请成功,就执行临界区代码
  • pthread_mutex_trylock:用于申请锁,如果申请失败,直接返回,而不是等待;如果申请成功,就执行临界区代码
  • pthread_mutex_unlock:用于释放锁,表明自己已经访问完毕临界区,其他线程可以来访问了

这三个函数的参数都是pthread_mutex_t *mutex,即指向互斥锁变量的指针,表示要操作哪一个互斥锁。

接下来我们修改一下最初的抢票代码,给它加锁,保证抢票g_ticket--的原子性:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //全局互斥锁

void *buyTicket(void *args)
{
    customer *cust = (customer *)args;

    while (true)
    {
        pthread_mutex_lock(&mutex); // 加锁
        
        if (g_ticket > 0)
        {
            usleep(1000);
            cout << cust->_name << " get ticket: " << g_ticket << endl;
            g_ticket--;
            pthread_mutex_unlock(&mutex); // 解锁
            cust->_ticket_num++;
        }
        else
        {
            pthread_mutex_unlock(&mutex); // 解锁
            break;
        }
    }

    return nullptr;
}

我在此使用的是全局的互斥锁,第一行pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;就是定义了一个全局的互斥锁,并对其初始化。

在访问临界区前,对mutex加锁,在此我在if (g_ticket > 0)前加锁,因为不仅仅是g_ticket--是临界区,if (g_ticket > 0)也是临界区,它们都访问了临界资源g_ticket

if的第一个分支中,当g_ticket--完毕,此时当前线程就不会再访问g_ticket了,于是离开临界区,并对mutex解锁。在第二个分支else中,线程马上要break出循环了,并且退出,此时也要解锁,不然别的线程永远处于阻塞状态了。

可以想象一下,当第一个线程被调度,它要进行抢票,现在先对mutex加锁,然后再去if中访问g_ticket。假如在某个访问临界资源的过程中,CPU调度了其它线程,此时第二个线程进入。第二个线程也想访问g_ticket,于是也对mutex加锁,但是由于锁已经被第一个线程申请走了,此时第二个线程pthread_mutex_lock就会失败,然后阻塞等待

等到第一个线程再次被调度,访问完临界区后,对mutex解锁,此时锁又可以被申请了。于是线程二申请到锁,再去访问g_ticket加锁可以保证,任何时候都只有一个线程访问临界区。当第二个线程访问临界区时,一定是其他线程访问完毕了临界区,或者其它线程还没有访问临界区。这就保证了临界区的原子性,从而维护线程的安全!

输出结果:

在这里插入图片描述

最后的结果中,2158 + 2026 + 1690 + 2018 + 2108 = 10000,不多不少。


互斥锁原理

那么互斥锁是如何做到的呢?

互斥锁的汇编伪代码如下:

加锁lock

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

接下来我讲解一下这个过程:

在这里插入图片描述

如图所示,现在有两个线程thread-1thread-2,它们共同征用内存中的锁mutex。在CPU中有一个寄存器%al,用于存储和锁的值。

现在假设thread-1进行调度执行pthread_mutex_lock

首先执行指令moveb $0, %al,你可以理解为,就是把%al寄存器内部的值变成0

在这里插入图片描述

随后执行xchgb %al, mutex,该过程是让内存中的mutex%al寄存器的值进行交换:

在这里插入图片描述

此时%al寄存器的值变成1mutex的值变成0。随后执行:

if (al寄存器的内容 > 0){
	return 0;
}else
	挂起等待;

也就是说判断当前%al内部的值是0还是大于0,如果大于0那么说明争夺到了锁,此时函数pthread_mutex_lock返回0,表示加锁成功。否则执行else进行挂起等待。

这样一个线程就征用到了一把锁。

现在假设thread-1执行到第一条汇编语句后,%al的值还是0thread-2调度了:

在这里插入图片描述

现在thread-1保存自己的硬件上下文,包括%al = 0在内,随后therad-2进入:

在这里插入图片描述

现在thread-2执行了两行汇编语句,成功把内存中的mutex与自己的%al交换,申请到了锁,此时thread-1再次调度,thread-2拷贝走自己的硬件上下文:

在这里插入图片描述

恢复硬件上下文后,thread-1%al等于0,执行第二条语句后,%almutex依然是0,这表明锁已经别的线程拿走了,此时在执行if内部的内容,thread-1挂起等待。

可以看到,其实锁的本质,就是保证mutex变量中以及所有访问锁的线程的%al寄存器中,只会有一个非零值。只有拿到非零值的线程才有资格去访问临界资源。其它线程如果要再次申请锁,由于自己的%almutex都是0,就算交换后还是0,也申请不到锁。

并不是谁先调用ptherad_mutex_lock,谁就先抢到锁,而是谁先执行该函数内部的xchgb %al, mutex语句,把非零值放到自己的%al中,谁才抢到锁

再简单看看解锁:

unlock

moveb $1, mutex
唤醒等待mutex的线程;
return 0;

解锁就很简单了,moveb $1, mutex就是把自己的%al中的1还给mutex,然后唤醒所有等待该锁的线程,让它们再次争夺这把锁。最后return 0,也就是pthread_mutex_unlock函数返回0


常见的锁

Linux中不仅仅存在互斥锁这一种锁,还有非常多的锁,接下来我们看看其它的锁。

死锁

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

我简单举一个例子:

现在有两个线程thread-1thread-2,以及两把互斥锁mutex-1mutex-2

在这里插入图片描述

现在要求:一个线程想要访问临界资源,必须同时持有mutex-1mutex-2。随后therad-1去申请了mutex-1thread-2去申请了mutex-2

在这里插入图片描述

thread-1再去申请mutex-2,结果mutex-2已经被therad-2占用了,thread-1陷入阻塞:

在这里插入图片描述

thread-2再去申请mutex-1,结果mutex-1已经被therad-1占用了,thread-2陷入阻塞:

在这里插入图片描述

现在therad-1等待therad-2解锁mutex-2thread-2等待thread-1解锁mutex-1,双方互相等待。由于唤醒thread-2需要therad-1,唤醒therad-1又需要therad-2此时陷入永远的等待状态,这就是死锁

想要造成死锁,有四个必要条件:

  1. 互斥条件:一个资源每次只能被一个执行流使用
  2. 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:一个执行流获得资源后,其它执行流不能强行剥夺
  4. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

以上是比较正式的说法,接下来我从线程角度简单翻译翻译:

  1. 互斥条件临界资源同时只能被一个线程访问=
  2. 请求与保持条件:请求是指:申请对方的锁,保持是指:占着自己有的锁不放
  3. 不剥夺条件:一个线程如果申请锁失败,强行抢走他人的锁
  4. 循环等待条件:以刚刚的死锁为例,therad-1thread-2,而thread-2等待thread-1,形成一个头尾相接的循环

这四个条件都是必要条件,也就是说:

解决死锁,本质就是破坏一个或多个必要条件

主要有以下方式避免死锁:

  1. 破坏互斥条件:不要用锁
  2. 破坏请求与保持条件:如果发现没有申请到锁,立刻释放自己的全部锁
  3. 破坏不剥夺条件:如果发现没有申请到锁,强行释放对方的锁,将其占为己有
  4. 破坏循环等待条件:如果申请多把锁,所有线程都必须按照相同的顺序申请(最简单的方式)

另外的,还有一些死锁的相关算法:死锁检测算法银行家算法,本博客就不做解释了。


自旋锁 spinlock

我们先前讲的锁,其机制是这样的:

在这里插入图片描述

当线程申请一个锁失败,就会阻塞等待当锁被使用完毕,唤醒所有等待该锁的线程

其实锁还有一种不用阻塞等待的策略,而是反复检测的策略,就像这样:

在这里插入图片描述

当线程没有申请到锁,一段时间后再次检测这个锁有没有被释放,一直反复申请这个锁,这个过程叫做自旋。基于这个策略来申请的锁,叫做自旋锁

Linux自带了自旋锁spinlock,类型为pthread_spinlock_t,接口如下:

创建与销毁

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);

加锁与解锁

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

你会发现,这和mutex几乎一摸一样,所以接口也就不讲解了。

不过我这里要强调一点,pthread_spin_lock并不是申请失败就返回,而是在pthread_spin_lock内部以自旋的方式申请锁,我们无需手动模拟自旋的过程


其它锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁写锁行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 公平锁非公平锁

以上出现的所有概念,本博客都不讲解。


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

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

相关文章

酒店宾馆民宿预订管理系统(ThinkPHP+uniapp+uView)

便捷高效&#xff0c;轻松管理你的住宿预订&#x1f3e8; 基于ThinkPHPuniappuView开发的多门店民宿酒店预订管理系统&#xff0c;快速部署属于自己民宿酒店的预订小程序&#xff0c;包含预订、退房、WIFI连接、吐槽、周边信息等功能。​​ 一、引言&#xff1a;为何需要民宿…

安卓手机数据快速找回!2个视频恢复大师,助你还原视频

我们的手机成为了储存信息的海洋。但与此同时&#xff0c;也带来了一个不容忽视的问题&#xff1a;一旦手机中的视频资料丢失&#xff0c;我们该如何高效地找回呢&#xff1f;现在很多程序都能够有效地找回手机视频&#xff0c;本文将为您揭示这些视频恢复大师的神奇能力&#…

家用洗地机哪个好?选对洗地机,还你一个清爽的家,快来抄作业!

能够带来高效生活的洗地机已经成为现代家庭清洁的首选。如果你还在犹豫的话&#xff0c;不妨看看这篇文章&#xff0c;让我来带你感受科技清扫的魅力&#xff0c;轻松扫净灰尘污渍&#xff0c;重拾家居整洁。洗地机&#xff0c;不只清洁&#xff0c;更是一种高品质生活态度的选…

【数据结构与算法】对称矩阵,三角矩阵 详解

给出对称矩阵、三角矩阵的节省内存的存贮结构并写出相应的输入、输出算法。 对称矩阵和三角矩阵可以通过特殊的存储结构来节省内存。这种存储结构只存储矩阵的一部分元素&#xff0c;而不是全部元素。 对称矩阵&#xff1a;对于一个n阶对称矩阵&#xff0c;我们只需要存储主对…

大学物理绪论组收集和分析

目录 ​编辑 随机误差的估计 算术平均值的标准偏差 不确定度&#xff08;Uncertainty&#xff09;是测量学中的一个重要概念&#xff0c;用于表示测量结果的可靠程度。它反映了测量值可能偏离真值&#xff08;即被测量的客观真实值&#xff09;的程度。 A类不确定度的计算方…

访问0xdddddddd内存地址引发软件崩溃的实战问题排查

目录 1、问题描述 2、访问空指针或者野指针 3、C程序中常见的异常内存值 4、0xdddddddd内存访问违例问题分析与排查 4.1、初步分析 4.2、CConfMeidaConfigDlg窗口类对象是何时被销毁的&#xff1f; 4.3、为啥会访问到已经释放内存的CConfMeidaConfigDlg类对象&#xff1…

centOS Stream9配置NAT8网络

首先将VMware关机&#xff0c;添加网络适配器 启动虚拟机&#xff0c;查看ens192是否打开连接 安装的图形化需要查看右上角电源处网卡是否连接 最小化安装一般不会出现未连接的状态 使用ip a 查看 配置网卡文件 cd /etc/NetworkManager/system-connections/cd到当前目录下…

四维世景产品及研发部副总经理张静普受邀为第十三届中国PMO大会演讲嘉宾

全国PMO专业人士年度盛会 四维世景科技&#xff08;北京&#xff09;有限公司产品及研发部副总经理张静普先生受邀为PMO评论主办的2024第十三届中国PMO大会演讲嘉宾&#xff0c;演讲议题为“项目管理中的数字化实践及应用”。大会将于6月29-30日在北京举办&#xff0c;敬请关注…

C++11(下):线程库

线程库 1.线程1.1线程类介绍以及简单使用1.2线程函数参数1.3如何获取线程函数返回值 2.锁2.1锁的种类2.2 lock_guard与unique_lock 3.原子库3.1介绍与基本使用3.2CAS&#xff08;原子操作原理&#xff09; 4.条件变量 1.线程 1.1线程类介绍以及简单使用 在C11之前&#xff0c…

Android device/xxx/system/common/overlay编译产物

MTK 如下代码编译的产物在 framework-res.apk 编译配置文件在device/mediatek/system/common/目录下的Android.bp device/mediatek/system/common/overlay/telephony/frameworks/base/core/res/res/values-mcc655-mnc01/config.xml 在Android U上面还在overlay目录中进行了产…

NSSCTF-Web题目10

目录 [强网杯 2019]随便注 1、题目 2、知识点 3、思路 [GXYCTF 2019]BabyUpload 1、题目 2、知识点 3、思路 [强网杯 2019]随便注 1、题目 2、知识点 数据库注入&#xff0c;堆叠注入&#xff0c;更改表名 3、思路 正常提交查询&#xff0c;看看数据回显 加入单引号…

Ubuntu下FastDDS的源码编译和简单测试

FastDDS是eprosima公司开发的DDS&#xff08;Data Distribution Service&#xff09;库&#xff0c;使用的语言是C&#xff0c;自称是"The Most Complete Open Source DDS Middleware"&#xff0c;其官网是https://eprosima.com/&#xff0c;FastDDS源码在https://gi…

金鸣识别系统:PDF转Excel的高效利器

在日常办公中&#xff0c;我们经常需要将PDF文档转换为Excel表格以便进行数据分析。然而&#xff0c;当有些PDF转换过程可能会变得复杂&#xff0c;因为许多转换工具无法完美处理图片元素&#xff0c;导致转换后的准确率不高或排版错乱。幸运的是&#xff0c;金鸣识别系统以其卓…

细致解析跨境电商多平台搭建利器-179海关接口源码应用方法

介绍 跨境电商已成为当前电商行业的热门发展方向之一。为满足跨境电商的需求&#xff0c;各大平台纷纷推出了多平台搭建利器。其中&#xff0c;179海关接口源码是一款非常实用的工具&#xff0c;本文将对其应用方法进行细致解析。 了解179海关接口源码 179海关接口源码可以帮…

天翼云8080、80端口用不了的问题

天翼云8080、80端口用不了的问题 前言&#xff1a;前段时间天翼云搞了活动&#xff0c;原来公司用的华为云老板说太贵了也快到期了&#xff0c;就换了天翼云的服务器。 排查&#xff1a; 安全组开放 80 8080 防火墙查看 没有问题 nginx nacos dcoker等停了 查看监听端口 发现…

回答网友的一个DBGrid的问题

起因 网友在QQ群里问Text字段的显示问题。 俺回答&#xff1a;百度了一下 方法有 很多很多 。然后给他百度了6种方案。然后告诉他找个顺眼的用。 可是&#xff0c;对方感觉都顺眼。俺就新写了一个 。 代码 创建测试数据 begin ADODataSet1.DisableControls; colcnt:5; …

git 基本命令

列出分支基本命令&#xff1a; git branch 如果我们要手动创建一个分支 。执行 git branch (branchname) 即可&#xff1a; git branch testing 切换到testing分支&#xff1a; git checkout testing 我们也可以使用 git checkout -b (branchname) 命令来创建新分支并立…

斯巴达(Spartanhost)VPS的性能评测

原创原文链接&#xff1a;详细斯巴达&#xff08;Spartanhost&#xff09;VPS的性能和购买价值评测 | BOBO Blog (soulcloser.com)https://www.soulcloser.com/3398/ 引言 最近看了全球的VPS商家&#xff0c;想搞台网站高性能的服务器&#xff0c;发现一个特别有意思的商家竟…

反激开关电源整流桥选型及计算

整流桥的作用就是把输入交流电压整形成直流电压&#xff0c;把正弦波整成馒头波&#xff0c;由于整流管的单向导电 性&#xff0c;在输入电压瞬时值小于滤波电容上电压时整流桥&#xff0c;在这个时候是不导通的&#xff0c;使整流桥的电流变 成2-3ms左右的窄脉冲。为获得所需…

深信服终端安全管理系统EDR版本升级过程

EDR当前版本为3.7.12&#xff0c;具体参考版本详情参数。需要升级到6.0.2R1版本&#xff0c;该版本更改了产品名称&#xff0c;叫做“统一端点安全管理系统aES” 当前版本详情 版本基础信息 软件版本&#xff1a;3.7.12.3829 病毒库版本&#xff1a;20240618174426 漏洞规则库&…