【多线程】线程互斥 {多执行流并发执行的数据竞争问题,互斥锁的基本用法,pthread_mutex系列函数,互斥锁的原理;死锁;可重入函数和线程安全}

news2024/11/19 13:27:53

一、进程线程间通信的相关概念

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

二、互斥锁

2.1 多执行流并发执行的数据竞争问题

  • 大部分情况,线程使用的数据都是局部变量,变量存储在线程栈空间内。这种情况,变量归属单个线程,其他线程无法访问这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。比如,全局数据、堆空间数据。
  • 多个线程并发的操作共享变量,会带来数据竞争,冲突以及数据不一致等问题。

测试程序:

int tickets = 100; //共有100张票

void *ThreadRoutine(void *name)
{
    while (1)
    {
        if (tickets > 0)
        {
            usleep(rand() % 1000); // 模拟业务过程花费的时间
            printf("%s sells ticket:%d\n", (char *)name, tickets);
            --tickets;
        }
        else
        {
            break;
        }
        usleep(rand() % 1000); // 模拟处理其他业务花费的时间
    }
    return nullptr;
}

int main()
{
    srand((unsigned)time(nullptr));
    pthread_t tid1, tid2, tid3, tid4;
    pthread_create(&tid1, nullptr, ThreadRoutine, (void *)"child thread 1");
    pthread_create(&tid2, nullptr, ThreadRoutine, (void *)"child thread 2");
    pthread_create(&tid3, nullptr, ThreadRoutine, (void *)"child thread 3");
    pthread_create(&tid4, nullptr, ThreadRoutine, (void *)"child thread 4");

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);
    pthread_join(tid4, nullptr);
    return 0;
}

运行结果:

在这里插入图片描述

  1. 同一编号的票被多个线程售出
  2. 某些线程售出了负数编号的票

该程序存在数据竞争的问题,即公共变量tickets被多执行流同时访问和修改。

提示:除了多线程进程外,信号处理函数也是异步执行的(多执行流执行),同样存在数据竞争的问题。

并发运行问题

例如:tickets > 0--tickets操作并不是原子性操作,而是对应三条汇编指令:

  1. 将数据从内存加载到寄存器(当前线程的上下文中)
  2. 进行逻辑运算或算数运算
  3. 将数据写回内存

在这三条步骤的其中任何一步,该线程都有可能被切换,切换前线程上下文会被保存。其他线程在执行时也对tickets进行了访问和修改。当原线程再次被CPU调度执行时,恢复上下文数据,此时的寄存器与内存就会发生数据不一致的错误。

并行运行问题

多核CPU允许多线程并行(同时)运行。在ThreadRoutine函数中,由于没有对访问tickets的操作进行互斥,可能会导致多个线程同时读取和修改tickets变量,从而产生不可预测的结果。

例如:当多个线程同时执行if (tickets > 0)语句时,可能会出现以下情况:

  • 线程A和线程B同时读取tickets的值为1。
  • 线程A先执行--tickets操作,将tickets的值减为0。
  • 线程B再执行--tickets操作,将tickets的值减为-1。

这样,就会出现某些线程售出了负数编号的票。


2.2 互斥锁的基本用法

为了解决数据竞争问题,可以使用互斥锁(Mutex)来保护对tickets变量的访问。互斥锁可以确保在同一时间只有一个线程能够访问临界区(对tickets变量的访问),从而避免数据竞争的发生。

在这里插入图片描述

下面是互斥锁的基本使用方法:

  1. 定义互斥锁变量:在使用互斥锁之前,需要先定义一个互斥锁变量。可以使用pthread_mutex_t类型来声明互斥锁变量,例如:pthread_mutex_t mutex;
  2. 初始化互斥锁:在使用互斥锁之前,需要对互斥锁进行初始化。
    • 静态初始化:在定义互斥锁变量时,使用PTHREAD_MUTEX_INITIALIZER宏进行初始化。
    • 动态初始化:可以使用pthread_mutex_init函数来初始化互斥锁,例如:pthread_mutex_init(&mutex, NULL);。第一个参数是要初始化的互斥锁变量,第二个参数是互斥锁的属性,通常使用NULL表示使用默认属性;
  3. 加锁:在访问共享资源之前,需要先加锁。可以使用pthread_mutex_lock函数来加锁,例如:pthread_mutex_lock(&mutex);。如果互斥锁已经被其他线程锁定,那么当前线程会被阻塞,直到互斥锁被解锁。
  4. 访问共享资源:在互斥锁被锁定的情况下,可以安全地串行访问共享资源。
  5. 解锁:在访问共享资源完成后,需要解锁互斥锁,以便其他线程可以继续访问共享资源。可以使用pthread_mutex_unlock函数来解锁,例如:pthread_mutex_unlock(&mutex);
  6. 销毁互斥锁:
    • 不再需要使用互斥锁时,需要将其销毁。可以使用pthread_mutex_destroy函数来销毁互斥锁,例如:pthread_mutex_destroy(&mutex);
    • 静态初始化的互斥锁在程序结束时会自动被系统回收,无需手动销毁。
    • 不要销毁一个已经加锁的互斥量
    • 对于已经销毁的互斥量,要确保后面不会有线程再尝试加锁

我们将上面的售票程序加入互斥锁:

int tickets = 100; //临界资源
// 定义一个全局的互斥锁变量,并利用宏进行初始化(静态初始化)
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

void *ThreadRoutine(void *name)
{
    while (1)
    {
        // 在访问共享资源之前,需要先加锁。
        pthread_mutex_lock(&mtx);
        //临界区
        if (tickets > 0)
        {
            usleep(rand() % 1000); // 模拟业务过程花费的时间
            printf("%s sells ticket:%d\n", (char *)name, tickets);
            --tickets;
            // 在访问共享资源完成后,需要解锁互斥锁。
            pthread_mutex_unlock(&mtx);
        }
        else
        {
            // 在访问共享资源完成后,需要解锁互斥锁。
            pthread_mutex_unlock(&mtx);
            break;
        }
        // 在此处解锁?不行,如果线程执行break,就不会解锁互斥锁。其他线程会被一直阻塞。
        usleep(rand() % 1000); // 模拟处理其他业务花费的时间
    }
    return nullptr;
}

运行结果:

在这里插入图片描述

需要注意的几点:

  1. 在访问共享资源完成后,需要解锁互斥锁。否则,其他线程会被一直阻塞。需要特别注意break, goto等跳转语句跳过解锁函数。

  2. 被互斥锁锁定的临界区只能串行执行(互斥访问),虽然保证了多执行流访问临界资源的安全性,但是会在一定程度上降低程序的效率

  3. 尽量保证被互斥锁锁定的代码都是访问临界资源的代码,不要将其他无关的操作也放入临界区中。因为相比并发或并行执行,临界区串行执行的效率较低。

再次改进上面的代码:

#define THREAD_NUM 5
int tickets = 100;

//声明一个ThreadData类,使线程入口函数的参数更多样化。
class ThreadData
{
public:
    string _tname; //线程名
    pthread_mutex_t *_pmtx; //互斥锁变量的地址

    ThreadData(const string &tname, pthread_mutex_t *pmtx)
        : _tname(tname),
          _pmtx(pmtx){};
};

void *ThreadRoutine(void *arg)
{
    ThreadData *td = (ThreadData *)arg;
    while (1)
    {
        // 在访问临界资源前进行加锁
        pthread_mutex_lock(td->_pmtx);
        if (tickets > 0)
        {
            usleep(rand() % 1000); // 模拟业务过程花费的时间
            printf("%s sells ticket:%d\n", td->_tname.c_str(), tickets);
            --tickets;
            // 不再访问临界资源时需要解锁。
            pthread_mutex_unlock(td->_pmtx);
        }
        else
        {
            // 不再访问临界资源时需要解锁。
            pthread_mutex_unlock(td->_pmtx);
            break;
        }
        usleep(rand() % 1000); // 模拟处理其他业务花费的时间
    }
    delete td; // 释放各自的ThreadData结构空间
    return nullptr;
}

int main()
{
    srand((unsigned)time(nullptr));
    // 在主线程栈区创建互斥锁变量
    pthread_mutex_t mtx;
    // 调用pthread_mutex_init初始化互斥锁(动态初始化)
    pthread_mutex_init(&mtx, nullptr);
    // 循环创建子线程
    pthread_t tid[THREAD_NUM];
    for (int i = 0; i < THREAD_NUM; ++i)
    {
        string tmp = "child thread ";
        tmp += to_string(i + 1);
        ThreadData *td = new ThreadData(tmp, &mtx);
        pthread_create(tid + i, nullptr, ThreadRoutine, td); //传入ThreadData对象的指针
    }
    // 循环等待子线程
    for (int i = 0; i < THREAD_NUM; ++i)
    {
        pthread_join(tid[i], nullptr);
    }
    // 在不再需要使用互斥锁时,需要将其销毁。(动态初始化的互斥锁需要进行销毁,而静态初始化不需要)
    pthread_mutex_destroy(&mtx);
    return 0;
}

新的问题:

  1. 加锁了之后,线程在执行临界区代码时,是否会被切换,会有问题吗?
    会被切换,但不会有问题!虽然被切换了,但是你是持有锁被切换的, 所以其他抢票线程要执行临界区代码,也必须先申请锁,而它是无法申请成功的。所以,也不会让其他线程进入临界区,就保证了临界区中数据一致性!

  2. 对于访问临界资源的线程而言,临界区代码要么全部执行成功,要么全部不执行,访问临界资源的操作不可被中断(不能同时执行其他线程的临界区代码),这就是原子性的体现。

  3. 要访问临界资源,每一个线程都必须先申请锁,而锁本身就是一种共享资源,那么谁来保证锁的安全呢?

    所以,为了保证锁的安全,申请和释放锁,必须是原子的!


2.3 互斥锁的原理

  1. 在汇编层面,一条汇编语句要么已经执行完,要么就还没有执行,是原子性的。
  2. 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange汇编指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。即使是多处理器平台(并行),访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
  3. CPU的寄存器数据,本质就是当前执行流的上下文。寄存器的存储空间被所有执行流共享,但是寄存器的内容是被每一个执行流私有的。所以在切换线程时,要将当前线程的寄存器数据保存到其PCB中,并恢复下一个线程的寄存器数据。

以下是加锁的核心汇编伪代码:

lock:
	movb $0, %al // 将数值0,move到al寄存器中
    xchgb %al, mutex //交换al寄存器与mutex变量(内存)的数据
    if(al寄存器的内容 > 0)
    {
        return 0;
    }
	else
        挂起等待;
	goto lock; //跳转到lock标签,再次申请锁
  1. 我们可以将互斥锁变量mutex理解成一个整形变量,值为1表示互斥锁未被线程持有;值为0,表示互斥锁已经被其他线程锁定。创建互斥锁变量并进行初始化后,其默认值为1。
  2. 由于exchange汇编指令是原子的,所以不管线程如何切换,只有一个线程能够将mutex(内存)中的1值交换到自己的寄存器当中,即该线程的上下文中。而线程上下文是线程的私有数据,实现了公有到私有的转换。同时寄存器当中的0值被交换到了mutex中,其他线程再进行交换也只能交换到0。
  3. 在进行if判断时,交换到1值的线程执行return 0,可以安全地进入临界区访问临界资源;而交换到0值的线程阻塞等待,直到互斥锁被解锁,这些线程才会被唤醒,然后再次尝试申请锁。

以下是解锁的核心汇编伪代码:

unlock:
	movb $1, mutex //将数值1,move到mutex变量(内存)
    唤醒等待mutex的线程;
	return 0;
  1. 当持有锁的线程访问完临界资源后,会将mutex变量重新置为1,即解锁互斥锁。
  2. 同时,应该唤醒等待互斥锁解锁的线程,让他们再次竞争申请锁。

回答之前的问题:

  1. 谁来保证锁的安全呢?

    为了保证锁的安全,申请和释放锁,必须是原子的!在设计加锁时,通过一条原子性的exchange指令,保证了加锁和解锁的原子性。

  2. 加锁了之后,线程在临界区中,是否会切换,会有问题吗?

    线程在临界区中也可能会被切换,但他是持有锁被切换的,所谓持有锁切换是指互斥锁的1值保存在当前线程的上下文,被当前线程私有。而其他线程即使被CPU调度执行,也无法抢占互斥锁,也就无法访问临界区代码。所以不会有任何问题。


三、可重入函数和线程安全

  • 可重入函数:同一个函数被多个执行流同时进入,就叫重入。如果该函数在被重入执行的过程中不会出现任何错误,则被称为可重入函数。反之就是不可重入函数。
  • 线程安全:多个线程并发执行时,在没有锁保护的情况下访问了共享资源(如全局或静态变量,堆区数据等),会出现数据竞争从而导致数据冲突,数据不一致等线程安全问题。

3.1 线程安全的情况

  1. 仅使用本地(局部)数据,或者通过制作全局数据的本地拷贝来保护全局数据。
  2. 使用互斥锁(Mutex)来保护对共享资源的访问。互斥锁可以确保在同一时间只有一个线程能够访问临界区,从而避免数据竞争的发生。
  3. 每个线程对共享资源只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
  4. 不调用线程不安全的函数

3.2 可重入函数的情况

  1. 仅使用本地(局部)数据,或者通过制作全局数据的本地拷贝来保护全局数据。
  2. 使用互斥锁(Mutex)来保护对共享资源的访问。如全局、静态变量或其他共享资源。
  3. 不调用不可重入函数

常见不可重入的情况

  1. 调用了malloc/free函数,因为Linux内核是用全局链表和全局红黑树结构来组织和管理堆空间的。(请看提示)
  2. 调用了标准I/O库函数,因为标准I/O库的很多实现都以不可重入的方式使用了全局数据结构。

提示:

  1. 关于Linux内核中的堆区管理,请阅读:【多线程】线程的概念 {Linux内核中的堆区管理;虚拟地址到物理地址的转换,页,页框,页表,MMU内存管理单元;Linux线程概念,轻量级进程;线程共享进程的资源;线程的优缺点;线程的用途}-CSDN博客
  2. 关于多执行流调用不可重入函数插入链表节点,请阅读:【信号】信号处理 {信号处理的时机;内核态和用户态;信号捕捉的原理;信号处理函数:signal, sigaction;可重入函数;volatile关键字;SIGCHLD信号}-CSDN博客

3.3 区别和联系

联系

  1. 函数是可重入的,那就是线程安全的。
  2. 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。

区别

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

四、死锁

死锁(Deadlock)是指在并发系统中,两个或多个进程(或线程)因为互相等待对方释放资源而无法继续执行的状态。在死锁状态下,进程无法前进,也无法释放资源,导致系统无法正常运行。

死锁通常发生在多个进程(或线程)同时竞争有限的资源时,每个进程都在等待其他进程释放资源,而自己又无法释放已经占有的资源。

在这里插入图片描述

特殊情况:一个执行流,一把互斥锁也可能导致死锁,即加锁后,不解锁,再次申请锁。

死锁的发生需要满足以下四个条件,也被称为死锁的必要条件:

  1. 互斥条件:一个资源每次只能被一个执行流使用(不加锁自然就不会产生死锁)。

  2. 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。

  3. 不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺。

  4. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系,使得每个执行流都在等待下一个执行流所占有的资源。

当这四个条件同时满足时,就可能发生死锁。一旦发生死锁,系统将无法自动解除死锁状态,需要通过人工干预来解决。

为了避免死锁的发生,可以采取以下策略:

  1. 破坏互斥条件:例如,允许多个进程(或线程)同时访问某些资源。

  2. 破坏请求与保持条件:例如,要求进程(或线程)在执行之前一次性获取所有需要的资源,否则在等待资源时释放已经占有的资源。

  3. 破坏不可剥夺条件:例如,允许系统强制剥夺某些进程(或线程)的资源。

  4. 破坏循环等待条件:例如,通过对资源进行排序,按照固定的顺序申请资源,避免交叉申请,循环等待。(T1,T2都先申请R1再申请R2)

  5. 其他方法:精简临界区代码,缩短持有锁的时间;合并临界区,资源一次性分配(一把锁);

死锁是并发系统中的一个重要问题,对系统的性能和可靠性有很大影响。因此,在设计和实现并发系统时,需要合理地分配和管理资源,以避免死锁的发生。

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

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

相关文章

检测和缓解SQL注入攻击

SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严&#xff0c;攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句&#xff0c;在管理员不知情的情况下实现非法操作&#xff0c;以此来实现欺骗数据库服务器执行非授权的任意查询&#…

一文搞懂 LiveData 粘性事件 和 数据倒灌

文章目录 前言LiveData使用分析粘性事件分析数据倒灌总结 一、前言 在使用LiveData的时候&#xff0c;会有疑问&#xff0c;LiveData 是怎么做到先发送再注册&#xff0c;依然能够接收到事件的。还有就是会碰到切换屏幕&#xff0c;导致重复的操作&#xff0c;也就是所谓的数…

postgresql的windows

1. 资源下载&#xff1a; https://www.postgresql.org/download/windows/ 2. 安装 双击&#xff0c;指定D盘目录&#xff0c;接下来默认安装&#xff0c;一直到出现下面的最后一步。一定要去除勾选复选框。 在最后&#xff0c;点击FINISH。 3. 初始化 4. 检查和修改配置 1&am…

MaxQuant的安装和_使用_(linux)

MaxQuant 是一款定量蛋白质组学软件包&#xff0c;支持多种标记定量和无标定量的质谱数据。 1. 安装 MaxQuant下载方式&#xff1a; 通过官网 下载&#xff0c;包括andromeda&#xff08;搜索引擎&#xff09;viewer&#xff08;检查原始数据、鉴定和定量结果&#xff09;。通…

使用c语言与EASYX实现弹球小游戏

eg1:小球碰到窗体的四个墙实现反弹效果 #include <stdio.h> #include <easyx.h> #include <iostream> #include <math.h> #include <conio.h> #define PI 3.14int main() {initgraph(800, 600);setorigin(400, 300);setaspectratio(1, -1);setb…

MWeb Pro for Mac:博客生成编辑器,助力你的创作之旅

在当今数字化时代&#xff0c;博客已经成为了许多人记录生活、分享知识和表达观点的重要渠道。而要打造一个专业、美观且易于管理的博客&#xff0c;选择一款强大的博客生成编辑器至关重要。今天&#xff0c;我向大家推荐一款备受好评的Mac软件——MWeb Pro。 MWeb Pro是一款专…

从龙湖智创生活入选金钥匙联盟,透视物业服务力竞争风向

假设你是业主&#xff0c;物业“服务”和“管理”&#xff0c;哪个名词看起来更加亲切、讨喜&#xff1f; 站在个人角度&#xff0c;“服务”更让人感受到温度。但对于一个要长期运营下去的住宅或者商企项目来说&#xff0c;整体的管理又必不可少。前者面向人&#xff0c;后者…

Zigbee 模组拉距测试

1.测试方法&#xff1a; 两个同型号Zigbee模组组网&#xff0c;一个作为协调器&#xff0c;一个作为路由器&#xff0c;协调器设备负责组网&#xff0c;其中路由器节点作为被测设备&#xff0c;入网网成功后一分钟开始一轮测试&#xff0c;连续发送100包数据&#xff0c;每包数…

nginx: [emerg] bind() to 0.0.0.0:18888 failed (98: Unknown error)问题解决办法

周末断网&#xff0c;今天来了之后&#xff0c;nginx出现这个问题&#xff0c;本站基本搜索的都是端口被占用问题&#xff0c;我试着杀掉所有占用端口的进程&#xff0c;解决办法 1.killall -9 nginx 2.然后启动(./nginx)nginx&#xff08;PS&#xff1a;不要./nginx -s relo…

echarts 饼图标注 字体修改

option {grid: {// 四周留白区域设置top: 20,right: 20,left: 20,bottom: 20,containLabel: true,},tooltip: {trigger: item,formatter: {b} : {c} ({d}%) // 展示百分比},series: [{type: pie,radius: [40%, 60%],avoidLabelOverlap: false,startAngle: 0, //起始角度data: …

Ajax学习笔记第4天

做决定之前仔细考虑&#xff0c;一旦作了决定就要勇往直前、坚持到底&#xff01; 【1 模仿百度招聘】 整个流程展示&#xff1a; 1.文件目录 2.页面效果展示及代码 data中的page1数据展示 2.1 主页 index.html:index里面代码部分解释 underscore.js :模板页面的相关代码 &…

香橙派OrangePi Zero开发板的WiFi连接

文章目录 调试串口连接连接WIFI设置开机自动连接自定义设置固定IP地址远程SSH连接 调试串口连接 1、准备一个 3.3v 的USB转TTL的模块&#xff0c;将开发板连接到电脑上 注意&#xff1a;引脚连接 a. USB 转 TTL 模块的 GND 接到开发板的 GND 上b. USB 转 TTL 模块的 RX 接到开…

专业课改革,难度陡然提高,专业课122总分390+南京理工大学818南理工818上岸经验分享

今年专业课相对较难&#xff0c;分数122&#xff0c;基本达到预期。南理工818是信号和数电两门课&#xff0c;各站一半。复试时间数电可能要更多一点&#xff0c;也比信号难拿分。今年专业课难度很大&#xff0c;基本超过不少985学校&#xff0c;大家要重视。 有条件的同学建议…

建筑施工二维码预制件管理系统

随着科学经济技术的发展&#xff0c;二维码标识设备越来越受人们的喜爱&#xff0c;不仅仅是在人们日常购物时需要使用到&#xff0c;在建筑用预制构件中使用也非常普遍&#xff0c;在这里二维码相当于预制构件的身份证&#xff0c;替代了传统标识牌。 1、信息管理。通过凡尔码…

倾斜摄影三维模型的顶层合并构建重要性分析

倾斜摄影三维模型的顶层合并构建重要性分析 倾斜摄影超大场景的三维模型的顶层合并对于构建精确、完整且真实的三维模型具有重要的意义和应用价值。本文将从几个方面对其重要性进行浅析。 一、模型完整性与连贯性 倾斜摄影超大场景的三维模型的顶层合并可以将多个倾斜摄影数据…

Tesseract-OCR的安装与环境变量配置

网盘链接&#xff1a;百度网盘 请输入提取码 提取码&#xff1a;gtp1 中文包网盘链接&#xff1a;小蓝枣的资源仓库&#xff0c;提取码&#xff1a;8v8u 安装步骤&#xff1a;解压后直接“NEXT”&#xff0c;“NEXT”注意选择自己的安装路径。 安装后查看是否安装成功&#…

libcurl库的网页爬虫程序

示例代码&#xff1a; #include <curl/curl.h> #include <iostream> ​ int main() {CURL *curl;CURLcode res; ​curl_global_init(CURL_GLOBAL_DEFAULT); ​curl curl_easy_init();if(curl) {curl_easy_setopt(curl, CURLOPT_URL, "/");curl_easy_se…

成本预算管理系统

成本预算管理系统 功能介绍&#xff1a; 一 基本信息&#xff1a; 1、产品设置&#xff1a;产品的长、宽、高及面积计算公式的设置。 2、板材设置&#xff1a;板材类别、厚度、尺寸的设置 3、系统名称&#xff1a;风管系统的类别设置 4、公司信息&#xff1a;本公司的信息…

如何在Android设备上检查应用程序使用情况,包括使用时间

你可能不知道自己花了多少时间在手机上。很可能你一天中有一半的时间都在盯着手机屏幕。如果你怀疑这一事实,你会很快核实的。在这篇文章中,我们将向你介绍如何在Android设备上检查应用程序的使用情况。 如何在Android上检查应用程序电池使用情况 你使用时间最长的应用程序…

为什么会被【禅道】工具的公司提出QQ群的反思…………

周末备份Gitlab的代码库&#xff0c;把Gitlab更新到了最新的16.5。顺带看了禅道官网出了最新版本18.8。但是禅道的升级更新并不顺利…………。 先说一下为什么用禅道这个工具&#xff1a; 再使用禅道这个工具前&#xff0c;使用过的工具有QC(Quality Center)、jira&#xff0…