深入浅出入侵式容器

1 篇文章 0 订阅
1 篇文章 0 订阅

记得大学刚毕业那年看了侯俊杰的《深入浅出MFC》,就对深入浅出这四个字特别偏好,并且成为了自己对技术的要求标准——对于技术的理解要足够的深刻以至于可以用很浅显的道理给别人讲明白。以下内容为个人见解,如有雷同,纯属巧合,如有错误,烦请指正。

在说入侵式容器前,先说一说什么是容器,本文提到的容器与docker一点关系都没有,是C++标准库中std::map、std::set、std::list等用来存放数据对象的这些类。在C++中,采用模板实现容器内对象的分类,也就是说通过模板实例化出具体类型对象的容器。

了解了容器之后,再说说入侵式容器,其实这里的入侵式(intrusive )是非常形象的形容词,我们可以通过如下两张链表图的对比来看:

                                                图1                                                                                                图2

我们暂时不要计较两个图的链表是不是需要形成环,我们就假设有一个头指针(head)和尾指针(tail)分别指向了链表的第一个节点和最后一个节点就可以了。上面两个图是非常经典的双向链表管理对象的实现方式,他们两的不同在于链表本身(或链表的一部分内容)是否在存储对象内。图1链表通过一个指针指向对象;图2是把链表放在对象内,再将链表连接起来达到对象的双向连接,我个人把图2所示的双向链表称之为入侵式双向链表,原因就是链表入侵到了对象内部。

现在我们可以对比一下两种方式各有什么优缺点:

  1. 从图上很明显的对比,入侵式双向链表由于省略了obj指针,所以要更节省内存,但这点对于犹如白菜价内存的今天没什么优势可言,当然管理对象的越小、数量越多优势会明显一些;

  2. 在遍历链表并定位具体对象时,非入侵式双向链表由于有指向对象的指针可以直接获取对象,而入侵式双向链表需要通过链表地址减去偏移才能得到对象指针,但这点计算量基本可以忽略;
  3. 非入侵式双向链表的obj指针可以采用无类型指针(void*),这样链表可以连接多种对象(通过指针第一个地址的类型在区分对象),但是这种应用还是比较不多见;同时如果对比std::list和boost::intrusive::list两个链表的话,由于对象类型已经通过模板确定,也就不存在这个优点了(模板类型void*除外哈,怎么感觉除外的内容这么多~);
  4. 入侵式指针如果拥有对象指针,就可以将对象本身从链表中删除,而非入侵式指针由于对象没有指向链表的指针,需要遍历链表才能删除对象;

其他的也看不出来有什么有优缺点,那为什么还要实现这两种链表呢?道理其实很简单,看你主要操作的对象是什么,也就是使用场景:

  1. 非入侵式双向链表:操作的对象主要是链表本身,对于链表管理对象本身并不太多关注,此类应用主要为数据库类型。以redis为例,redis底层数据结构之一的链表为例,如下是4.0.10\adlist.h的源码:
typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

如上源码所示,redis的链表通过无类型指针指向对象。数据库类型应用主要关注的是数据的有效管理,前期不知道所要管理的对象,也就无从谈起入侵式容器了。

  1. 入侵式双向链表:操作的主要以目标主要是对象,仅仅想通过一种方式(如链表)将多个对象有效的管理起来。由于入侵式双向链表可以把自己从一个链表中删除,所以应用中只要传递对象本身即可,无需再传入迭代器(iterator,如果对迭代器不理解的需要先学习一下基础知识);

其实我们正常编写的程序应该主要使用入侵式双向链表,因为我们主要操作的都是我们自定义的对象(例如在linux内核(/include/linux/list.h)中使用非常广泛),那为什么还有很多人不用入侵式双向链表甚至还有人没听说过呢(采用c++ stl编程的人居多,写c代码的人更倾向于使用list_head)?原因在于应用本身对于对象管理的复杂度不高,非入侵式代码可读性更高,实现复杂度更低(主要集中在c++ stl容器类)。

上文主要以双向链表为例说明了入侵式与非入侵式容器的差别,容器的类型还有很多:vector、list、set、map、multiset、multimap,此处就不一一列举了,毕竟不是本文讨论的重点。上面是以c的视角分析了二者的区别,接下来我们以c++的stl视角分析二者的区别。还是一样的,我们以一种容器为例,这回我们用map。

我们以分布式计算系统为例,中心需要管理多个节点,每个节点有唯一的名字,那么采用如下定义实现节点管理:

std::map<std::string, std::shared_ptr<Node>>,其中Node为自定义的节点类型。

这样就可以在各个子模块间传递节点名称唯一标识一个节点了。那么接下来需求来了,仅仅通过名字管理节点是无法满足需求的,中心需要知道每个节点的资源负载率,在下次调度计算请求的时候尽量调度到资源负载率最低的节点。对于这个需求,我们有两个实现方案:

  1. 遍历上面提到的map,找到负载率最低的节点;
  2. std::map<Load, std::shared_ptr<Node>>,其中Load为节点负载率类型,中心实时根据节点的负载率进行排序,第一个节点就是负载率最小的节点;

我们来分析两个方案,第一个方案的缺点是需要遍历所有节点,计算量为O(n),但是当节点更新负载率的时候无需任何操作;方案二在查找负载率最低的节点时计算量为O(1),但是节点更新负载率时需要重新调整位置。由于std::map采用树(引用源码class map: public _Tree<_Tmap_traits<_Kty, _Ty, _Pr, _Alloc, false> >)方式管理数据,可以假设计算量为O(log(n))。

我跟人认为方案二相对较好,同时也是我在做分布式计算系统时选择的方案(当初还不知道C++有入侵式容器,后来改用list_head方式自行设计排序,所以每次更新负载率时排序计算量最悲观是O(n))。但是方案对于我来说有一个件事情不能忍,那就是每次更新节点负载率时都要备份以前的负载率,这样才能更新负载率的map(先删除再添加)。过分考虑异常的我总是担心节点中记录的负载率由于某些BUG被修改,这将导致节点在负载率的map无法更新,这是一个非常不安全的事情。在拥有节点指针的情况下从负载率map中删除如果是一个可靠的动作,重新插入的时候依赖的计算的负载率,这样即便负载率存在BUG,只是影响排序问题,问题没那么严重。

此时,入侵式容器发挥价值的时候到了,他完美的解决了我提出的问题。首先,我们需要在Node类型定义中加入容器成员的钩子,如下所示:

class Node

{

    ......

    private:

    boost::intrusive::set_member_hook<> m_resLoadHook;

    ......

}

注:本文入侵式容器采用boost1.61.0版本。

现在是我自有发挥的时候了(也是正经的胡说时间)!set_member_hook,每个单词都已特定的意义:

set:代表是容器类型是set(为什么是set而不是map,后面会说)

member:代表要成为所管理对象的成员,如上面定义的Node一样加入一个该类型的成员变量

hook:这个是钩子,就是把对象通过钩子挂在容器内,有点像windows的钩子函数,把一段代码挂在系统内;

那么我们研究的重点就是这个钩子到底是个啥?接下来就是比较恶心的boost模板代码旅程了,先看boost::intrusive::set_member_hook的定义:

#if defined(BOOST_INTRUSIVE_DOXYGEN_INVOKED) || defined(BOOST_INTRUSIVE_VARIADIC_TEMPLATES)
template<class ...Options>
#else
template<class O1, class O2, class O3, class O4>
#endif
class set_base_hook
   :  public make_set_base_hook<
      #if !defined(BOOST_INTRUSIVE_VARIADIC_TEMPLATES)
      O1, O2, O3, O4
      #else
      Options...
      #endif
      >::type
{
......
}

因为有宏定义BOOST_INTRUSIVE_VARIADIC_TEMPLATES让代码看着有点乱,我们可以假设这个宏是打开的,那么代码就会简洁很多,如下所示:

template<class... Options> class set_member_hook 
: public make_set_base_hook<Options...>::type

上面代码可以看到set_member_hook继承自make_set_base_hook<Options...>::type,那make_set_base_hook::type又是什么呢?

template<class ...Options>
struct make_set_base_hook
{
   typedef typename pack_options < hook_defaults, Options... >::type packed_options;
   typedef generic_hook
   < rbtree_algorithms<rbtree_node_traits<typename packed_options::void_pointer, packed_options::optimize_size> >
   , typename packed_options::tag
   , packed_options::link_mode
   , RbTreeBaseHookId
   > implementation_defined;
   typedef implementation_defined type;
};

make_set_base_hook又把Options选项类加上hook_defaults合并为一个新的packed_options类型。嗯!名字比较形象。make_set_base_hook::type是generic_hook::type加上一大堆模板的类型重定义,所以还要看generic_hook是怎么定义的?

template
   < class NodeAlgorithms
   , class Tag
   , link_mode_type LinkMode
   , base_hook_type BaseHookType
   >
class generic_hook
   : public detail::if_c
      < detail::is_same<Tag, member_tag>::value
      , typename NodeAlgorithms::node
      , node_holder<typename NodeAlgorithms::node, Tag, BaseHookType>
      >::type
   , public hook_tags_definer
      < generic_hook<NodeAlgorithms, Tag, LinkMode, BaseHookType>
      , detail::is_same<Tag, dft_tag>::value*BaseHookType>
{
    ......
}

看到上面代码还没有崩溃的算是能抗的(我不是天生好强,只是我比较能抗),不佩服写boost的大神都不行,这模板用的真6,6到基本看不懂。我们从代码是能够发现内容,generic_hook是继承自两个类型:

detail::if_c
      < detail::is_same<Tag, member_tag>::value
      , typename NodeAlgorithms::node
      , node_holder<typename NodeAlgorithms::node, Tag, BaseHookType>
      >::type
hook_tags_definer
      < generic_hook<NodeAlgorithms, Tag, LinkMode, BaseHookType>
      , detail::is_same<Tag, dft_tag>::value*BaseHookType>

我们先从第一个类型分析:

template<bool C, typename T1, typename T2>
struct if_c
{
   typedef T1 type;
};

上面的代码可以看出detail::if_c<#$$%%^%^&>::type=NodeAlgorithms::node(至于为啥要绕这么大圈我们在其他的文章在说明哈),而NodeAlgorithms是构造generic_hook的模板,终于看到点曙光了~好了,我们再回头看一下代码:

struct make_set_base_hook
{
   typedef typename pack_options < hook_defaults, Options... >::type packed_options;
   typedef generic_hook
   < rbtree_algorithms<rbtree_node_traits<typename packed_options::void_pointer, packed_options::optimize_size> >
   , typename packed_options::tag
   , packed_options::link_mode
   , RbTreeBaseHookId
   > implementation_defined;
   typedef implementation_defined type;
};

NodeAlgorithms= rbtree_algorithms<rbtree_node_traits<typename packed_options::void_pointer, packed_options::optimize_size> >,顺着思路我们可以继续探索rbtree_algorithms<@#$%&>::node究竟为何方神圣:

template<class NodeTraits>
class rbtree_algorithms
   #ifndef BOOST_INTRUSIVE_DOXYGEN_INVOKED
   : public bstree_algorithms<NodeTraits>
   #endif
{
   public:
   typedef NodeTraits                          	node_traits;
   typedef typename NodeTraits::node           	node;
   typedef typename NodeTraits::node_ptr       	node_ptr;
   typedef typename NodeTraits::const_node_ptr 	const_node_ptr;
   typedef typename NodeTraits::color          	color;
   ......
}

rbtree_algorithms<@#$%&>::node是NodeTraits::node的重定义,再返回上面的代码可知传入的模板为:

rbtree_node_traits<typename packed_options::void_pointer, packed_options::optimize_size>

那就在看看rbtree_node_traits是怎么定义的吧?

template<class VoidPointer, bool OptimizeSize = false>
struct rbtree_node_traits
   :  public rbtree_node_traits_dispatch
         < VoidPointer
         ,  OptimizeSize &&
           (max_pointer_plus_bits
            < VoidPointer
            , detail::alignment_of<compact_rbtree_node<VoidPointer> >::value
            >::value >= 1)
         >
{};

又是一大堆~我们直接奔主要内容,rbtree_node_traits继承自rbtree_node_traits_dispatch:

struct rbtree_node_traits_dispatch
   :  public default_rbtree_node_traits_impl<VoidPointer>
{};

rbtree_node_traits_dispatch又继承自default_rbtree_node_traits_impl:

template<class VoidPointer>
struct default_rbtree_node_traits_impl
{
   typedef rbtree_node<VoidPointer> node;
   typedef typename node::node_ptr        node_ptr;
   typedef typename node::const_node_ptr  const_node_ptr;

   typedef typename node::color color;
   .......
}

终于找到了这个真正的node的定义了,他是rbtree_node<VoidPointer>的重定义:

template<class VoidPointer>
struct rbtree_node
{
   typedef rbtree_node<VoidPointer> node;
   typedef typename pointer_rebind<VoidPointer, node >::type         node_ptr;
   typedef typename pointer_rebind<VoidPointer, const node >::type   const_node_ptr;

   enum color { red_t, black_t };
   node_ptr parent_, left_, right_;
   color color_;
};

看到了么?rbtree_node有父节点、左节点、右节点的指针以及当前节点的颜色,同时将VoidPointer和rbtree_node的绑定。从中我们可以得到以下几个信息:

  1. 每个节点是一个红黑树节点,至于什么是红黑树自己去百度吧(我发现只要不知道的就可以说不是此处讨论重点,自行去百度~)
  2. 每个节点包含一个void*类型的指针,这个可以从VoidPointer名称得知,严谨的可以沿着模板继承路线找到哈,这个指针就是容器管理对象的指针;

那么,我们之前定义的计算节点类型就可以看做为(因为我们没有传递任何选项,所有的就是默认选项,读者可以自行了解每个选项是什么意思):

class Node 
{
......
private:
    rbtree_node	m_resLoadHook;
......
}

大概的图形如下所示,仅仅是示意,没有画出全部内容:

上图和我们开篇说的入侵式双向链表非常像,在拥有对象指针时,将对象从容器中删除不再需要通过key查找了,把钩子从容器中移除就可以了。上面说了好多代码,这仅仅是开始,我们还有好多问题没有解答:容器怎么定义?容器内节点如何排序?我们先来看看入侵式set如何定义的:

template<class T, class ...Options>
class set
   :  public make_set<T,
   Options...
   >::type
{
......
}

set类继承自make_set::type:

template<class T, class ...Options>
struct make_set
{
   typedef typename pack_options
      < rbtree_defaults,
      Options...
      >::type packed_options;

   typedef typename detail::get_value_traits
      <T, typename packed_options::proto_value_traits>::type value_traits;

   typedef set_impl
         < value_traits
         , typename packed_options::key_of_value
         , typename packed_options::compare
         , typename packed_options::size_type
         , packed_options::constant_time_size
         , typename packed_options::header_holder_type
         > implementation_defined;
   typedef implementation_defined type;
};

上面的代码可以看出,make_set::type=set_impl<$$%%^&*&*>:

#if defined(BOOST_INTRUSIVE_DOXYGEN_INVOKED)
template<class T, class ...Options>
#else
template<class ValueTraits, class VoidOrKeyOfValue, class Compare, class SizeType, bool ConstantTimeSize, typename HeaderHolder>
#endif
class set_impl
#ifndef BOOST_INTRUSIVE_DOXYGEN_INVOKED
   : public bstree_impl<ValueTraits, VoidOrKeyOfValue, Compare, SizeType, ConstantTimeSize, RbTreeAlgorithms, HeaderHolder>
#endif
{
......
}

上面的代码可以看出无论宏定义BOOST_INTRUSIVE_DOXYGEN_INVOKED是否打开,set_impl都需要ValueTraits,VoidOrKeyOfValue,Compare,SizeType,ConstantTimeSize,HeaderHolder这几个类作为模板参数构造类型。在回头看看make_set中对于set_impl传入的模块:

ValueTraits:detail::get_value_traits<T, typename packed_options::proto_value_traits>::type

VoidOrKeyOfValue: packed_options::key_of_value

Compare:packed_options::compare

SizeType:packed_options::size_type

ConstantTimeSize:packed_options::constant_time_size

HeaderHolder:packed_options::header_holder_type

其中packed_options是我们传入的选项类加上红黑树的默认选项组合而成,如代码所示:

typedef typename pack_options
      < rbtree_defaults,
      Options...
      >::type packed_options;

ValueTraits是我们传入的容器管理对象的类型加上一些选项而成,此处不细说这个是怎么来的,至少我们知道构造入侵式容器需要提供管理对象的类型,这个和std::set一样。关键的是这些选项类我们应该传入什么?而且我们前期自己定义的Node类型中的钩子怎么使用?我们再来看看set_impl模板参数:

  1. VoidOrKeyOfValue:字面上意思是空或者值的key,这个有点意思哈!我们不管空,值的key作为容器本身自己不知道么?还要我们传递一个选项进来?我们假定set的实现是利用红黑树(前面已经好多地方提到了红黑树~),对于一棵树管理对象的key应该是什么?其实就是rbtree_node这个拥有指向对象的东东,那么我们就联想到我们以前定义那个钩子的用处了,需要把它传入到这里,嗯,应该是这么个意思~
  2. Compare:比较函数,set毕竟是有序的,比较函数还是很有必要的,容器需要知道怎么比较对象,同时也可以自定比较函数;

其他的选项我就不细说了,读者可以自行了解,因为以上的内容就够我用啦~~~~现在我们就看看怎么定义这个入侵式set:

using boost::intrusive
typedef member_hook<Node,set_member_hook<>, &Node::m_resLoadHook>    HookOption;
typedef set<Node, HookOption,compare<CompareResLoad>>	             ResLoadSet;

ResLoadSet就是我们定义出来的管理Node对象的集合,其中HookOption要告诉容器值的key用的是set_member_hook<>类型,叫m_resLoadHook这个名字(采用&Node::m_resLoadHook告诉key在对象的地址偏移的方式),最后就是排序函数,自己按照函数标准实现就可以了。至此我们就可以使用ResLoadSet来管理Node,这个容器里的每个节点都是按照负载率排好序的。使用入侵式容器必须要注意一下几点:

  1. 容器本身只需要把对象的钩子通过比较函数挂到指定位置就行了,容器操作的是对象的钩子成员变量,使用者需要自行管理对象本身内存。这一点与非入侵式容器不同,非入侵式容器是在内部管理对象内存的。这就要求不能向入侵式容器中插入局部变量,也就意味着所有的对象都是new出来的指针(全局变量也可以,如果所有对象都能够用全局变量定义了,那还用容器干啥呢?)
  2. 我们操作对象都是通过指针的,因为是new出来的,但是入侵式容器各种接口函数要求传入对象的引用~这一点我只能认为实现者考虑我们使用非入侵式容器需要传入对象引用的习惯,还是不错的,只是知道原理后感觉怪怪的。

现在我来解释一下为什么前面用map,到了入侵式容器我们选择了set,如果我说入侵式容器没有map你会不会打我?其实我们细想想确实入侵式map没有存在的可能,原因如下:

  1. map<Key,Node>,key定义在其他对象内,入侵式也就无从谈起;
  2. 如果把Key定义在Node里面,那不就是入侵式set么?

其实入侵式容器应用在一个对象需要通过多个容器进行管理的时候非常好用,比如Node分为CPU、内存、GPU、网络等几个负载率。当需要将节点删除时,由于Node内有所有容器的钩子,删除非常方便,但是如果是通用的map,很有可能存在获取相应map的key值比较麻烦的情况。这一点linux内核用list_head管理各种对象,对象从一种状态切换到另一种状态,就需要将对象从若干链表移除放入其他链表,task管理就是一个比较典型的例子。

文章最后,我们来看下boost官方文档是如何介绍入侵式容器的:

Boost.Intrusive is a library presenting some intrusive containers to the world of C++. Intrusive containers are special containers that offer better performance and exception safety guarantees than non-intrusive containers (like STL containers).

The performance benefits of intrusive containers makes them ideal as a building block to efficiently construct complex containers like multi-index containers or to design high performance code like memory allocation algorithms.

While intrusive containers were and are widely used in C, they became more and more forgotten in C++ due to the presence of the standard containers which don't support intrusive techniques.Boost.Intrusive wants to push intrusive containers usage encapsulating the implementation in STL-like interfaces. Hence anyone familiar with standard containers can easily use Boost.Intrusive.

大概意思是:入侵式容器相比于非入侵式容器拥有更好的性能和异常安全保证,入侵式容器的高性能广泛应用在多索引容器或者类似内存分配算法这样的高性能程序上。入侵式容器在C程序中比较普遍,人们渐渐在C++中忘记入侵式容器是因为标准库中不支持入侵式容器......后面的就不废话了。最重要的一点是入侵式容器好用并且性能高,为什么性能好?我认为有以下几点:

  1. 管理对象的内存是用户自己管理的,容器只负责对象内的钩子成员变量的操作,省去了对象内部管理的部分,也就省去了对象拷贝的过程。当然,如果容器的模板传入的是对象指针类型(如set<Node*>)就另当别论了;
  2. 删除对象的时候复杂度为常数,如O(1),而非入侵式容器复杂度是O(log(n))或O(n);
  3. 查找性能与删除原理一样,所以就不多说了;

 

 

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: PCS7是西门子公司生产的一种工业自动化控制系统,是一种功能强大且易于使用的平台,用于控制和监控工业过程中的各种设备和系统。PCS7深入浅出 V8是PCS7的一个版本,它在之前的版本基础上进行了技术升级和功能扩展。 PCS7深入浅出 V8拥有更加直观和用户友好的界面,使操作更加简单易学。它采用了全新的图形组织和设计方,使得用户可以更轻松地理解和使用系统。同时,它还提供了更多的可定制性,可以根据用户的需求进行个性化配置和设置。 在功能方面,PCS7深入浅出 V8具有更高的可靠性和稳定性。它采用了分布架构,可以同时控制多个设备和系统,实现更加复杂的工艺过程控制。同时,它还具备故障诊断和报警功能,可以及时发现和解决问题,确保工业生产的连续性和稳定性。 PCS7深入浅出 V8还拥有更强大的数据处理和分析能力。它可以实时采集和存储大量的工业数据,并通过先进的算法和模型进行分析和预测。这样,用户可以根据数据的分析结果进行调整和优化,提高生产效率和质量。 总的来说,PCS7深入浅出 V8是一款功能强大且易于使用的工业自动化控制系统。通过它,用户可以更加轻松地控制和监控工业过程中的各种设备和系统,实现更高效、更可靠的工业生产。 ### 回答2: PCS7是西门子工业自动化控制系统中的一款重要软件,它的最新版本是V8。PCS7深入浅出 V8指的是对于PCS7 V8版本的深入理解和简单易懂的介绍。 PCS7 V8是在之前版本的基础上进行了升级和改进,以提供更高效、更稳定、更灵活的自动化控制系统。它具有以下几个重要特点: 首先,PCS7 V8在硬件和软件方面都进行了优化。在硬件方面,它支持多种不同类型的通信协议,可以与各种工业设备进行连接,实现数据的传输和控制。在软件方面,PCS7 V8增加了许多新功能和模块,包括智能监控、故障诊断、过程数据分析等,进一步提高了系统的自动化程度和控制精度。 其次,PCS7 V8具有良好的可扩展性和灵活性。它支持分布控制架构,可以将控制任务分散到不同的控制器上,实现系统的并行运行和负载均衡。同时,PCS7 V8的功能模块和监控界面都支持自定义设置,可以根据用户的需求进行个性化定制,满足不同行业和企业的特殊需求。 再次,PCS7 V8具备强大的数据处理和分析能力。它可以实时采集和存储大量的过程数据,支持数据的查询、统计和报表生成等功能,帮助用户对生产过程进行全面监控和分析。同时,PCS7 V8还可以与企业级信息系统进行集成,实现生产数据的共享和优化,提高企业的生产效率和竞争力。 综上所述,PCS7 V8是一款功能强大、性能优越的自动化控制系统软件,它通过深入浅出的方,为用户提供了更简单易懂的操作界面和更全面精细的控制功能。无论是在工业生产还是其他领域,PCS7 V8都能帮助用户实现自动化控制和智能化管理,提高生产效率和产品质量。 ### 回答3: PCS7是西门子(Siemens)公司推出的一款自动化系统软件,它能够广泛应用于工业自动化领域的控制系统中。PCS7深入浅出 v8 是PCS7软件的一个新版本,它在前一版本的基础上进行了一些改进和升级。 首先,PCS7深入浅出 v8在系统可靠性方面进行了改进。它采用了分布控制技术,可以将控制任务分配给多个控制单元,提高了系统的容错性和可靠性。同时,它还具备故障自诊断功能,能够实时检测系统中的故障并给出相应的报警信息,方便工程师进行故障处理。 其次,PCS7深入浅出 v8在网络通信方面进行了优化。它支持多种网络通信协议,能够与其他设备和系统进行无缝集成。同时,它还具备远程监控和控制功能,使得工程师可以通过互联网对远程设备进行监控和控制,提高了生产效率和管理效果。 此外,PCS7深入浅出 v8还具备强大的数据处理能力。它可以实时采集、存储和分析系统中的各种数据,对生产过程进行监控和优化。同时,它还提供了丰富的数据展示和报表生成功能,方便用户进行数据分析和决策。 综上所述,PCS7深入浅出 v8是一款功能强大、性能稳定的工业自动化系统软件。它的改进和升级使得工业生产过程更加可靠、高效、智能化。通过使用PCS7深入浅出 v8,企业可以提高生产效率,降低运营成本,提高竞争力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值