【关于Linux中----信号量及其使用场景】

news2024/11/29 3:53:56

文章目录

  • 一、解释信号量
    • 1.1 概念的引入
    • 1.2 信号量操作和使用接口
  • 二、信号量使用场景
    • 2.1 引入环形队列&&生产消费问题
    • 2.2 代码实现
    • 2.3 对于多生产多消费的情况
    • 2.4 申请信号量和加锁的顺序问题
    • 2.5 多生产多消费的意义


一、解释信号量

1.1 概念的引入

我们知道,一个线程在访问临界资源时,临界资源必须要是满足条件的。但是,在线程访问资源前,无法得知这块资源是否满足生产或消费的条件。所以,线程只能先对这块资源加锁,然后检测其是否满足条件,再进行操作,最后再释放锁。可是,检测的过程本质上也是在访问临界资源

只要一个线程对一块资源加了锁,就默认该线程对这个资源的整体使用。
但实际情况中可能存在,一份公共资源是允许多个线程同时访问其中的不同区域的。所以,在这种情况下,一个线程要访问资源,就必须先申请信号量

信号量的本质是一把衡量临界资源中资源数量多少的计数器,拥有信号量就意味着,在未来一定能够拥有临界资源的一部分。申请信号量的本质是对临界资源中特定某一部分资源的预定机制

所以,有了信号量,就意味着在访问临界资源之前,就可以知道临界资源的使用情况。换言之,如果申请信号量成功,就说明临界资源中一定有可以访问的资源;失败说明不满足条件,必须进行等待。所以,申请信号量成功与否,就能说明是否可以访问临界资源。这样也就不需要先进行判断了。

1.2 信号量操作和使用接口

首先,线程要访问临界资源中的某一部分,就必须先申请信号量。也就是说,信号量要能够被所有线程看到,即信号量本身是公共资源

而因为信号量是衡量资源中资源数量多少的计数器,所以当线程访问资源的时候,它必须进行–操作;当线程归还资源的时候,它必须进行++操作。而为了保证++、–的过程不会被其他线程打断,就必须保证操作的原子性。其中,信号量–的操作叫做P操作,++的操作叫做V操作。而信号量的核心操作就是PV操作

信号量基本使用接口如下:
①初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
②销毁信号量
int sem_destroy(sem_t *sem);
③等待信号量
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
④发布信号量
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()


二、信号量使用场景

2.1 引入环形队列&&生产消费问题

经过前面的铺垫,想必大家已经对信号量和互斥锁适合使用的场景有了大致的轮廓。
互斥锁更适用于一整块的临界资源,而信号量更适用于看似是一块临界资源,但其实是可以分成一个个小部分的资源块的资源。
所以,这里引入一个符合条件的适用于信号量的存储资源的结构----环形队列

环形队列采用数组模拟,用模运算来模拟环状特性
在这里插入图片描述

环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态。(这里采用计数器的方式,也就是使用信号量
在这里插入图片描述
具体的细节实现就不解释了,相信大家学到这个程度已经都熟稔于心了。

而这里,我们让生产者和消费者都访问这个环形队列,生产者向队列中写入数据,而消费者从队列中读取数据(相当于把数据弹出),该过程中二者应该是并发的

写代码之前,需要知道环形队列为空和为满的时候,生产者和消费者是在同一个位置的,其他情况下都不在同一位置。

更重要的“游戏规则”是,消费者在队列中的位置一定不能超过生产者(未生产不能消费),生产者不能将消费者“套圈”(队列满了就不能再放入)。而队列为空时,生产者先访问队列,为满时,消费者先访问队列。

所以,只有队列为空和为满的时候,生产者消费者才存在同步和互斥的问题

对于生产者来说,看中的时队列中的剩余空间;对于消费者而言,看中的是放入队列中的数据。所以,在实现代码时,我们应该定义两个信号量,分别用来维护空间资源和数据资源

2.2 代码实现

首先还是老规矩,定义一个环形队列的类,文件为RingQueue.hpp,内容如下:

#pragma once
#include <iostream>
#include <vector>
#include <semaphore.h>
#include <cassert>

static const int gcap=5;

template<class T>
class RingQueue
{
private:
    void P(sem_t& sem)
    {
        int n=sem_wait(&sem);
        assert(n==0);
        (void)n;
    }

    void V(sem_t& sem)
    {
        int n=sem_post(&sem);
        assert(n==0);
        (void)n;
    }
public:
    RingQueue(const int& cap=gcap):_queue(cap),_cap(cap)
    {
        int n=sem_init(&_spaceSem,0,_cap);
        assert(n==0);
        n=sem_init(&_dataSem,0,0);
        assert(n==0);
        _productorStep=_consumerStep=0;
    }

    void Push(const T& in)
    {
        P(_spaceSem);//申请空间信号量成功就一定能进行生产
        _queue[_productorStep++]=in;
        _productorStep%=_cap;
        V(_dataSem);
    }

    void Pop(T* out)
    {
        P(_dataSem);
        *out=_queue[_consumerStep++];
        _consumerStep%=_cap;
        V(_spaceSem);
    }

    ~RingQueue()
    {
        sem_destroy(&_spaceSem);
        sem_destroy(&_dataSem);
    }
private:
    std::vector<T> _queue;
    int _cap;//队列容量
    sem_t _spaceSem;//生产者看重的空间资源信号量
    sem_t _dataSem;//消费者看重的数据资源信号量
    int _productorStep;
    int _consumerStep;
};

然后,在Main.cc中就可以用这个类来完成生产者和消费者各自的任务了,内容如下:

#include "RingQueue.hpp"
#include <pthread.h>
#include <ctime>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>

void* ProductorRoutine(void* rq)
{
    RingQueue<int>* ringqueue=static_cast<RingQueue<int>*>(rq);
    while(true)
    {
        sleep(2);
        int data=rand()%10+1;
        ringqueue->Push(data);
        std::cout<<"生产完成,生产数据: "<<data<<std::endl;
    }
}

void* ConsumerRoutine(void* rq)
{
    RingQueue<int>* ringqueue=static_cast<RingQueue<int>*>(rq);
    while(true)
    {
        int data;
        ringqueue->Pop(&data);
        std::cout<<"消费完成,消费数据: "<<data<<std::endl;
    }
}

int main()
{
    srand((unsigned int)time(nullptr)^getpid()^pthread_self());
    RingQueue<int>* rq=new RingQueue<int>();
    pthread_t c,p;
    pthread_create(&p,nullptr,ProductorRoutine,rq);
    pthread_create(&c,nullptr,ConsumerRoutine,rq);

    pthread_join(p,nullptr);
    pthread_join(c,nullptr);
    delete rq;
    return 0;
}

Makefile内容如下:

ringqueue:Main.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f ringqueue

需要注意的是,上面代码中设置的生产者每一次生产之前都要休眠两秒,而对消费者不做处理。所以代码执行结果一定是生产者每生产一次,消费者就能立刻消费。

运行结果如下:

[sny@VM-8-12-centos circlequeue]$ ./ringqueue
生产完成,生产数据: 7
消费完成,消费数据: 7
生产完成,生产数据: 7
消费完成,消费数据: 7
生产完成,生产数据: 8
消费完成,消费数据: 8
^C

可见,结果和预测相同。

除此之外,我们也可以用在之前的文章中封装过的任务派发类,来给生产者派发任务,而让消费者处理任务。
新建Task.hpp文件内容如下:

#pragma once
#include <iostream>
#include <functional>
#include <cstdio>

class Task
{
    using func_t =std::function<int(int,int,char)>;
public:
    Task()
    {}
    Task(int x,int y,char op,func_t func):_x(x),_y(y),_op(op),_callback(func)
    {}
    std::string operator()()
    {
        int result=_callback(_x,_y,_op);
        char buffer[1024];
        snprintf(buffer,sizeof buffer,"%d %c %d = %d",_x,_op,_y,result);
        return buffer;
    }
    std::string toTaskString()
    {
        char buffer[1024];
        snprintf(buffer,sizeof buffer,"%d %c %d = ?",_x,_op,_y);
        return buffer;
    }
private:
    int _x;
    int _y;
    func_t _callback;
    char _op;
};

const std::string oper="+-*/%";

int mymath(int x,int y,char op)
{
    int result=0;
    switch(op)
    {
    case '+':
        result= x+y;
        break;
    case '-':
        result= x-y;
        break;
    case '*':
        result= x*y;
        break;
    case '/':
        if(y==0)
        {
            std::cerr<<"div zero error!"<<std::endl;
            result=-1;
        } 
        else
            result=x/y;
        break;
    case '%':
        if(y==0)
        {
            std::cerr<<"mod zero error!"<<std::endl;
            result=-1;
        } 
        else
            result=x%y;
        break;
    default:
        break;
    }
    return result;
}

对Main.cc内容稍作修改,如下:

#include "RingQueue.hpp"
#include "Task.hpp"
#include <pthread.h>
#include <ctime>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>

void* ProductorRoutine(void* rq)
{
    RingQueue<Task>* ringqueue=static_cast<RingQueue<Task>*>(rq);
    while(true)
    {
        sleep(2);
        //获取任务
        int x=rand()%1000;
        int y=rand()%1500;
        char op=oper[rand()%oper.size()];
        Task t(x,y,op,mymath);
        //生产任务
        ringqueue->Push(t);
        std::cout<<"生产者派发任务: "<<t.toTaskString()<<std::endl;
    }
}

void* ConsumerRoutine(void* rq)
{
    RingQueue<Task>* ringqueue=static_cast<RingQueue<Task>*>(rq);
    while(true)
    {
        Task t;
        //消费任务
        ringqueue->Pop(&t);
        std::string result=t();
        std::cout<<"消费者消费任务: "<<result<<std::endl;
    }
}

int main()
{
    srand((unsigned int)time(nullptr)^getpid()^pthread_self());
    RingQueue<Task>* rq=new RingQueue<Task>();
    pthread_t c,p;
    pthread_create(&p,nullptr,ProductorRoutine,rq);
    pthread_create(&c,nullptr,ConsumerRoutine,rq);

    pthread_join(p,nullptr);
    pthread_join(c,nullptr);
    delete rq;
    return 0;
}

运行结果如下:

[sny@VM-8-12-centos circlequeue]$ ./ringqueue
生产者派发任务: 912 % 178 = ?
消费者消费任务: 912 % 178 = 22
生产者派发任务: 282 * 951 = ?
消费者消费任务: 282 * 951 = 268182
生产者派发任务: 658 % 173 = ?
消费者消费任务: 658 % 173 = 139
^C

2.3 对于多生产多消费的情况

上面的代码中实现的很明显是单生产单消费的情况,那么如果有多个生产者和多个消费者又该如何实现呢?

要知道的是,不管有多少个生产者和消费者,一次只能有一个生产者和一个消费者访问环形队列。所以,应该让生产者和消费者之间决出一个竞争能力较强的线程,进而又去执行单生产单消费的任务。由于生产者和生产者之间、消费者和消费者之间是互斥的关系,所以一定要有两把锁分别控制生产者和消费者

所以,再对代码做出修改。
Main.cc内容如下:

std::string ThreadName()
{
    char name[128];
    snprintf(name,sizeof(name),"thread[0x%x]",pthread_self());
    return name;
}

void* ProductorRoutine(void* rq)
{
    RingQueue<Task>* ringqueue=static_cast<RingQueue<Task>*>(rq);
    while(true)
    {
        sleep(2);
        //获取任务
        int x=rand()%1000;
        int y=rand()%1500;
        char op=oper[rand()%oper.size()];
        Task t(x,y,op,mymath);
        //生产任务
        ringqueue->Push(t);
        std::cout<<ThreadName()<<",生产者派发任务: "<<t.toTaskString()<<std::endl;
    }
}

void* ConsumerRoutine(void* rq)
{
    RingQueue<Task>* ringqueue=static_cast<RingQueue<Task>*>(rq);
    while(true)
    {
        Task t;
        //消费任务
        ringqueue->Pop(&t);
        std::string result=t();
        std::cout<<ThreadName()<<",消费者消费任务: "<<result<<std::endl;
    }
}

int main()
{
    srand((unsigned int)time(nullptr)^getpid()^pthread_self());
    RingQueue<Task>* rq=new RingQueue<Task>();
    pthread_t c[8],p[4];
    for(int i=0;i<4;i++)
    {
        pthread_create(p+i,nullptr,ProductorRoutine,rq);
    }
    for(int i=0;i<8;i++)
    {
        pthread_create(c+i,nullptr,ConsumerRoutine,rq);
    }
    for(int i=0;i<4;i++)
    {
        pthread_join(p[i],nullptr);
    }
    for(int i=0;i<8;i++)
    {
        pthread_join(c[i],nullptr);
    }
    delete rq;
    return 0;
}

RingQueue.hpp内容如下:

template<class T>
class RingQueue
{
private:
    void P(sem_t& sem)
    {
        int n=sem_wait(&sem);
        assert(n==0);
        (void)n;
    }

    void V(sem_t& sem)
    {
        int n=sem_post(&sem);
        assert(n==0);
        (void)n;
    }
public:
    RingQueue(const int& cap=gcap):_queue(cap),_cap(cap)
    {
        int n=sem_init(&_spaceSem,0,_cap);
        assert(n==0);
        n=sem_init(&_dataSem,0,0);
        assert(n==0);
        _productorStep=_consumerStep=0;
        pthread_mutex_init(&_pmutex,nullptr);
        pthread_mutex_init(&_cmutex,nullptr);
    }

    void Push(const T& in)
    {
        pthread_mutex_lock(&_pmutex);
        P(_spaceSem);//申请空间信号量成功就一定能进行生产
        _queue[_productorStep++]=in;
        _productorStep%=_cap;
        V(_dataSem);
        pthread_mutex_unlock(&_pmutex);
    }

    void Pop(T* out)
    {
        pthread_mutex_lock(&_cmutex);
        P(_dataSem);
        *out=_queue[_consumerStep++];
        _consumerStep%=_cap;
        V(_spaceSem);
        pthread_mutex_unlock(&_cmutex);
    }

    ~RingQueue()
    {
        sem_destroy(&_spaceSem);
        sem_destroy(&_dataSem);
        pthread_mutex_destroy(&_pmutex);
        pthread_mutex_destroy(&_cmutex);
    }
private:
    std::vector<T> _queue;
    int _cap;//队列容量
    sem_t _spaceSem;//生产者看重的空间资源信号量
    sem_t _dataSem;//消费者看重的数据资源信号量
    int _productorStep;
    int _consumerStep;
    pthread_mutex_t _pmutex;
    pthread_mutex_t _cmutex;
};

执行结果如下:

[sny@VM-8-12-centos circlequeue]$ ./ringqueue
thread[0xbaaaa700],生产者派发任务: 730 * 1478 = ?
thread[0xb8aa6700],消费者消费任务: 881 / 481 = 1
thread[0xb82a5700],消费者消费任务: 2 + 874 = 876
thread[0xba2a9700],生产者派发任务: 334 / 1437 = ?
thread[0xb92a7700],消费者消费任务: 334 / 1437 = 0
thread[0xbb2ab700],生产者派发任务: 881 / 481 = ?
thread[0xbaaaa700],生产者派发任务: 990 * 373 = ?
thread[0xb72a3700],消费者消费任务: 990 * 373 = 369270
thread[0xba2a9700],生产者派发任务: 590 + 693 = ?
thread[0xb9aa8700],生产者派发任务: 985 - 912 = ?
thread[0xb72a3700],消费者消费任务: 590 + 693 = 1283
^C

2.4 申请信号量和加锁的顺序问题

现在来谈一下,是先加锁好,还是先申请信号量好?

答案是先申请信号量更好
因为首先申请信号量的过程本来就是原子的,不需要将其放在申请锁之后。
其次,如果先申请锁,那么没有申请到锁的线程什么也干不了,整个过程只有申请到锁的那一个线程在“忙前忙后”。而如果先申请信号量,则申请到信号量的线程可以去申请锁,而其他线程也可以同时在申请信号量,明显提高了效率。

当然,两种方式运行时间的长短,感兴趣的读者可以将上面的代码复制粘贴,然后修改信号量和锁的先后位置,运行观察一下,这里就不演示了。

2.5 多生产多消费的意义

这个话题跟上一篇文章中----阻塞队列中多线程的意义是一样的。
即一个线程在访问队列的时候,其他的线程也可以获取和执行任务,提升了效率。


本篇完,青山不改,绿水长流!

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

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

相关文章

python uiautomator2 环境搭建和基本使用

安装 adb安装和配置 可以百度或者看看这个https://blog.csdn.net/weixin_37600187/article/details/127987889 安装uiautomator2 pip install -i https://mirrors.aliyun.com/pypi/simple -U uiautomator2安装 weditor&#xff08;一般情况会报错&#xff09; pip install…

C#程序设计——面向对象编程基础,设计一个Windows应用程序,模拟一个简单的银行账户管理系统。实现创建账户、取款、存款和查询余额的模拟操作。

一、实验目的 1、理解简单程序中的面向对象思想的应用 2、掌握C#的定义类和创建对象的方法 3、理解方法中参数的传递 二、实验内容 1、设计一个Windows应用程序&#xff0c;模拟一个简单的银行账户管理系统。实现创建账户、取款、存款和查询余额的模拟操…

序列比对算法

案例问题&#xff1a;假设有两个序列&#xff1a;ATGCG 和 ACCG&#xff0c;如何求得它们的最佳匹配方案。 1. Needleman-Wunsch 算法 原理是动态规划&#xff0c;是一个全局比对算法 算法求解步骤&#xff1a; &#xff08;1&#xff09;在它们前面各加上一个 ‘-’ -ATGC…

2023 “认证杯”数学中国数学建模C 题 心脏危险事件 详细思路

2023年认证杯”数学中国数学建模如期开赛&#xff0c;本次比赛与妈杯&#xff0c;泰迪杯时间有点冲突。因此&#xff0c;个人精力有限&#xff0c;有些不可避免地错误欢迎大家指出。为了大家更方便的选题&#xff0c;我将为大家带来C题的详细解析&#xff0c;以方便大家建模分析…

如何有效利用文旅资源

文旅产业是当今世界发展最迅速的行业之一&#xff0c;文化和旅游业的融合已经成为文旅产业发展的趋势。众所周知&#xff0c;文旅资源是我国的宝贵财富&#xff0c;文化遗产、旅游胜地等都是国宝级的文旅资源&#xff0c;从古老的文化遗产到现代的旅游景点&#xff0c;无不体现…

近期CTF web

文章目录NKCTFbaby_phpez_phphard_phpeasy_pmseasy_cmsWebPageTestxiaopiCTFshow愚人赛easy_signineasy_sstiez_flask被遗忘的反序列化easy_php杭师大CTFfindmeez_javaeznodeNKCTF baby_php <?phperror_reporting(0);class Welcome{public $name;public $arg oww!man!!;…

【八】springboot启动源码 - finishRefresh

Last step: publish corresponding event. clearResourceCaches initLifecycleProcessor Initialize lifecycle processor for this context. 从IOC获取LifecycleProcessor设置到applicationContext中,如果从IOC中获取不到会创建DefaultLifecycleProcessor并注册到IOC中 get…

【能力提升】SQL Server常见问题介绍及快速解决建议

前言 本文旨在帮助SQL Server数据库的使用人员了解常见的问题&#xff0c;及快速解决这些问题。这些问题是数据库的常规管理问题&#xff0c;对于很多对数据库没有深入了解的朋友提供一个大概的常见问题框架。 下面一些问题是在近千家数据库用户诊断时发现的常规问题&#xff0…

用Abp实现找回密码和密码强制过期策略

文章目录重置密码找回密码发送验证码校验验证码发送重置密码链接创建接口密码强制过期策略改写接口Vue网页端开发重置密码页面忘记密码控件密码过期提示项目地址用户找回密码&#xff0c;确切地说是 重置密码&#xff0c;为了保证用户账号安全&#xff0c;原始密码将不再以明文…

一篇文章搞定《动手学深度学习》-(李牧)PyTorch版本的所有内容

目录 目录 简介 阅读指南 1. 深度学习简介 2. 预备知识 3. 深度学习基础 4. 深度学习计算 5. 卷积神经网络 6. 循环神经网络 7. 优化算法 8. 计算性能 9. 计算机视觉 10. 自然语言处理 环境 参考&#xff08;大家可以在这里下载代码&#xff09; 原书地址&#…

优思学院|精益生产和精益管理的区别

精益生产和精益管理&#xff0c;这两个概念我们或多或少都听说过。但是&#xff0c;你是否真的明白这两个概念的区别&#xff1f;或者你是否也像我一样&#xff0c;之前把这两个概念混淆在一起呢&#xff1f;今天&#xff0c;我要和大家分享的是&#xff0c;精益生产和精益管理…

用Flutter开发一款音乐App(从0到1开发一款音乐App)

Flutter Music_Listener(flutter音乐播放器) Flutter version 3.9 项目介绍 1、项目整体基于getxretrofitdiojsonserialize开发 2、封装通用控制器BaseController&#xff0c;类似jetpack mvvm框架中的BaseViemodel 3、封装基础无状态基类BaseStatelessWidget&#xff0c;结合…

jmap执行失败了,怎么获取heapdump?

在之前的OOM问题复盘中&#xff0c;我们添加了jmap脚本来自动dump内存现场&#xff0c;方便排查OOM问题。 但当我反复模拟OOM场景测试时&#xff0c;发现jmap有时可以dump成功&#xff0c;有时会报错&#xff0c;如下&#xff1a; 经过网上一顿搜索&#xff0c;发现两种原因可…

来 Azure 学习 OpenAI 三 - 用 Python 调用 Azure OpenAi API

大家好&#xff0c;我是微软学生大使 Jambo。在我们申请好 Azure 和 Azure OpenAI 之后&#xff0c;我们就可以开始使用 OpenAI 模型了。如果你还没有申请 Azure 和 Azure OpenAI&#xff0c;可以参考 注册 Azure 和申请 OpenAI。 本文将会以 Azure 提供的 Openai 端口为例&am…

2023年4月广东省计算机软考中/高级备考班招生简章

软考是全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;简称软考&#xff09;项目&#xff0c;是由国家人力资源和社会保障部、工业和信息化部共同组织的国家级考试&#xff0c;既属于国家职业资格考试&#xff0c;又是职称资格考试。 系统集成…

VS Code 插件开发概览

VS Code 插件开发概览 前言 VS Code作为开发者的代码开发利器&#xff0c;越来越受开发者的喜爱。像我身边的前端&#xff0c;每天80%的开发工作都是在VS Code上完成的。随着人们对它的使用&#xff0c;不再满足简单的优雅&#xff0c;舒服写代码这一基本需求。有些人利用它进…

FA-PEG-MAL,叶酸-聚乙二醇-马来酰亚胺 实验用科研试剂;Folic acid PEG Maleimide

FA-PEG-MAL,叶酸-聚乙二醇-马来酰亚胺 中文名称&#xff1a;叶酸-聚乙二醇-马来酰亚胺 英文名称&#xff1a;Folic acid PEG Maleimide, FA-PEG-MAL 性状&#xff1a;固体或者粘稠液体&#xff0c;取决于分子量大小。 溶剂&#xff1a;溶于水、DMF、DMSO等常规有机溶剂 分…

Redis高级之IO多路复用和epoll(十二)

nginx 的反向代理也是采用了IO多路复用 1.是什么 I/O 网络 I/O 多路 多个客户端连接&#xff08;连接就是套接字描述符&#xff0c;即socket 或者 channel&#xff09;&#xff0c;指的是多条 TCP 连接 复用 用一个进程来处理多条的连接&#xff0c;使用单进程就能实现同时处…

【cmake学习】set_target_properties 常见属性以及获取target 属性

set_target_properties 的作用是设置目标的属性&#xff0c;可以是目标文件输出的名称或者目录、目标文件的版本号。与之对应的&#xff0c;我们可以使用 get_target_properties 来获取目标文件某一属性对应的值。 命令格式如下&#xff1a; set_target_properties(目标文件1…

凌恩生物美文分享|基于宏基因组的氮循环分析内容重磅升级!

元素循环是生物地球化学循环的重要环节&#xff0c;主要涉及碳、氮、磷、硫等元素的循环过程。凌恩生物强势推出基于宏基因组的氮循环研究方案&#xff0c;构建了完整的氮循环循环模式图&#xff0c;对宏基因组数据进行深入挖掘&#xff0c;各部分结果图可直接用于文章发表&…