百度
360搜索
搜狗搜索

reactor模式详解,java Nio读写为什么是双向详细介绍

本文目录一览: 什么是reactor模式和proactor模式

Reactor模式首先是事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;Service Handler会对输入的请求(Event)进行多路复用,并同步地将它们分发给相应的Request Handler。
Proactor 模式是另一个消息异步通知的设计模式,与 Reactor 的最大区别在于,Proactor 通知的不是就绪事件,而是操作完成事件,这也就是操作系统异步 IO 的主要模型。
扩展资料:
Reactor 在实现上相对比较简单,对于大量对象,频繁从非就绪态触发到就绪态的场景处理十分高效;同时操作系统可以同时去等待多个对象触发,并且可以在事件触发后自由地选择后续执行流程,具有很高的灵活性。
虽然并发编程实现阻塞式同步 IO 也可以实现同时等待多个对象触发的效果,但在编程的复杂度与资源的消耗等方面,Reactor 模式拥有明显的优势。

Netty的Reactor多线程模型,NioEventLoop,ChannelPipeline简介

如果在Google上搜索"Netty 高性能 易用",在找到的一大批文章,你大概率会看到这张图,外加关键字
NIO , Reactor多线程模型 , 异步串行无锁化 , 堆外内存 , pipeline ,翻看完这些文章后可以让你对Netty的原理有大致了解,但是Netty如何实现这些的呢? 本文将尽可能简单的解释Netty中Reactor多线程的实现,如有错误感谢指出.
Selector是NIO的重要组件, Selector上可以注册Channel. Channel在注册的时候会标注自己感兴趣的事件:
Channel,通道,为了便于理解,我把它分为 三类
Reactor多线程模型可以分为三块
mainReactor负责客户端接入
acceptor负责将接入的连接移交给subReactor
subReactor负责连接的读写
关键知识:

运行流程图
关键知识:
ChannelPipeline的设计思想是 责任链设计模式 ,是由ChannelHandlerContext组成的 双向链表 , ,首尾固定为 HeadContext 和 TailContext ,它们作为哨兵存在.当我们添加一个ChannelHandler到ChannelPipeline时,会先 包装成ChannelHandlerContext 再添加进去.
inbound事件传播
客户端向服务端发送消息,这个流向就称为inbound. 消息会从Head开始由左向右传递直到Tail,由Tail进行收尾操作
outbound事件传播
服务端向客户端发送信息,这个流向称为outbound,消息会从Tail开始由右向左传递知道Head,由Head进行收尾操作
异常传递
当某个ChannelHandler操作抛出异常,会从该handler开始向Tail传递.由Tail做收尾操作.
学习Netty,要理解Reactor模型,并把它和Netty的实现结合起来, 我学习Netty的时候就因为这块认识不深刻,浪费了很多时间也没有成效,共勉
https://blog.csdn.net/difffate/article/details/69458588
https://blog.csdn.net/jjzhk/article/details/39553613
https://www.jianshu.com/p/a9b2fec31fd1
https://www.jianshu.com/p/a9d030fec081
https://juejin.im/post/5b4570cce51d451984695a9b
https://www.jianshu.com/p/2461535c38f3
https://juejin.im/post/5a126b146fb9a0450c490201

java Nio读写为什么是双向

作者:美团技术团队链接:https://zhuanlan.zhihu.com/p/23488863来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。
那么NIO的本质是什么样的呢?它是怎样与事件模型结合来解放线程、提高系统吞吐的呢?
本文会从传统的阻塞I/O和线程池模型面临的问题讲起,然后对比几种常见I/O模型,一步步分析NIO怎么利用事件模型处理I/O,解决线程池瓶颈处理海量连接,包括利用面向事件的方式编写服务端/客户端程序。最后延展到一些高级主题,如Reactor与Proactor模型的对比、Selector的唤醒、Buffer的选择等。
注:本文的代码都是伪代码,主要是为了示意,不可用于生产环境。
传统BIO模型分析
让我们先回忆一下传统的服务器端同步阻塞I/O处理(也就是BIO,Blocking I/O)的经典编程模型:
{ExecutorService executor = Excutors.newFixedThreadPollExecutor(100);//线程池ServerSocket serverSocket = new ServerSocket();serverSocket.bind(8088);while(!Thread.currentThread.isInturrupted()){//主线程死循环等待新连接到来Socket socket = serverSocket.accept();executor.submit(new ConnectIOnHandler(socket));//为新的连接创建新的线程}class ConnectIOnHandler extends Thread{private Socket socket;public ConnectIOnHandler(Socket socket){this.socket = socket;}public void run(){while(!Thread.currentThread.isInturrupted()&&!socket.isClosed()){死循环处理读写事件String someThing = socket.read()....//读取数据if(someThing!=null){......//处理数据socket.write()....//写数据}}}}
这是一个经典的每连接每线程的模型,之所以使用多线程,主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情。其实这也是所有使用多线程的本质:
利用多核。
当I/O阻塞系统,但CPU空闲的时候,可以利用多线程使用CPU资源。
现在的多线程一般都使用线程池,可以让线程的创建和回收成本相对较低。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。
不过,这个模型最本质的问题在于,严重依赖于线程。但线程是很"贵"的资源,主要表现在:
线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态。
容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。
所以,当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的。随着移动端应用的兴起和各种网络游戏的盛行,百万级长连接日趋普遍,此时,必然需要一种更高效的I/O处理模型。
NIO是怎么工作的
很多刚接触NIO的人,第一眼看到的就是Java相对晦涩的API,比如:Channel,Selector,Socket什么的;然后就是一坨上百行的代码来演示NIO的服务端Demo……瞬间头大有没有?
我们不管这些,抛开现象看本质,先分析下NIO是怎么工作的。
常见I/O模型对比
所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。
需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在"干活",而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。
下图是几种常见I/O模型的对比:
以socket.read()为例子:
传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。
对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。
最新的AIO(Async I/O)里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。
换句话说,BIO里用户最关心“我要读”,NIO里用户最关心"我可以读了",在AIO模型里用户更需要关注的是“读完了”。
NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。
如何结合事件模型使用NIO同步非阻塞特性
回忆BIO模型,之所以需要多线程,是因为在进行I/O操作的时候,一是没有办法知道到底能不能写、能不能读,只能"傻等",即使通过各种估算,算出来操作系统没有能力进行读写,也没法在socket.read()和socket.write()函数中返回,这两个函数无法进行有效的中断。所以除了多开线程另起炉灶,没有好的办法利用CPU。
NIO的读写函数可以立刻返回,这就给了我们不开线程利用CPU的最好机会:如果一个连接不能读写(socket.read()返回0或者socket.write()返回0),我们可以把这件事记下来,记录的方式通常是在Selector上注册标记位,然后切换到其它就绪的连接(channel)继续进行读写。
下面具体看下如何利用事件模型单线程处理所有I/O请求:
NIO的主要事件有几个:读就绪、写就绪、有新连接到来。
我们首先需要注册当这几个事件到来的时候所对应的处理器。然后在合适的时机告诉事件选择器:我对这个事件感兴趣。对于写操作,就是写不出去的时候对写事件感兴趣;对于读操作,就是完成连接和系统没有办法承载新读入的数据的时;对于accept,一般是服务器刚启动的时候;而对于connect,一般是connect失败需要重连或者直接异步调用connect的时候。
其次,用一个死循环选择就绪的事件,会执行系统调用(Linux 2.6之前是select、poll,2.6之后是epoll,Windows是IOCP),还会阻塞的等待新事件的到来。新事件到来的时候,会在selector上注册标记位,标示可读、可写或者有连接到来。
注意,select是阻塞的,无论是通过操作系统的通知(epoll)还是不停的轮询(select,poll),这个函数是阻塞的。所以你可以放心大胆地在一个while(true)里面调用这个函数而不用担心CPU空转。
所以我们的程序大概的模样是:
interface ChannelHandler{ void channelReadable(Channel channel); void channelWritable(Channel channel); } class Channel{ Socket socket; Event event;//读,写或者连接 } //IO线程主循环: class IoThread extends Thread{ public void run(){ Channel channel; while(channel=Selector.select()){//选择就绪的事件和对应的连接 if(channel.event==accept){ registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器 } if(channel.event==write){ getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件 } if(channel.event==read){ getChannelHandler(channel).channelReadable(channel);//如果可以读,则执行读事件 } } } Map

handlerMap;//所有channel的对应事件处理器 }这个程序很简短,也是最简单的Reactor模式:注册所有感兴趣的事件处理器,单线程轮询选择就绪事件,执行事件处理器。

优化线程模型

由上面的示例我们大概可以总结出NIO是怎么解决掉线程的瓶颈并处理海量连接的:

NIO由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。

并且由于线程的节约,连接数大的时候因为线程切换带来的问题也随之解决,进而为处理海量连接提供了可能。

单线程处理I/O的效率确实非常高,没有线程切换,只是拼命的读、写、选择事件。但现在的服务器,一般都是多核处理器,如果能够利用多核心进行I/O,无疑对效率会有更大的提高。

仔细分析一下我们需要的线程,其实主要包括以下几种:

事件分发器,单线程选择就绪的事件。

I/O处理器,包括connect、read、write等,这种纯CPU操作,一般开启CPU核心个线程就可以。

业务线程,在处理完I/O后,业务一般还会有自己的业务逻辑,有的还会有其他的阻塞I/O,如DB操作,RPC等。只要有阻塞,就需要单独的线程。

Java的Selector对于Linux系统来说,有一个致命限制:同一个channel的select不能被并发的调用。因此,如果有多个I/O线程,必须保证:一个socket只能属于一个IoThread,而一个IoThread可以管理多个socket。

另外连接的处理和读写的处理通常可以选择分开,这样对于海量连接的注册和读写就可以分发。虽然read()和write()是比较高效无阻塞的函数,但毕竟会占用CPU,如果面对更高的并发则无能为力。

NIO在客户端的魔力

通过上面的分析,可以看出NIO在服务端对于解放线程,优化I/O和处理海量连接方面,确实有自己的用武之地。那么在客户端上,NIO又有什么使用场景呢?

常见的客户端BIO+连接池模型,可以建立n个连接,然后当某一个连接被I/O占用的时候,可以使用其他连接来提高性能。

但多线程的模型面临和服务端相同的问题:如果指望增加连接数来提高性能,则连接数又受制于线程数、线程很贵、无法建立很多线程,则性能遇到瓶颈。

每连接顺序请求的Redis

对于Redis来说,由于服务端是全局串行的,能够保证同一连接的所有请求与返回顺序一致。这样可以使用单线程+队列,把请求数据缓冲。然后pipeline发送,返回future,然后channel可读时,直接在队列中把future取回来,done()就可以了。

伪代码如下:

class RedisClient Implements ChannelHandler{private BlockingQueue CmdQueue;private EventLoop eventLoop;private Channel channel;class Cmd{ String cmd; Future result;}public Future get(String key){ Cmd cmd= new Cmd(key); queue.offer(cmd); eventLoop.submit(new Runnable(){ List list = new ArrayList(); queue.drainTo(list); if(channel.isWritable()){ channel.writeAndFlush(list); } });}public void ChannelReadFinish(Channel channel,Buffer Buffer){ List result = handleBuffer();//处理数据 //从cmdQueue取出future,并设值,future.done();}public void ChannelWritable(Channel channel){ channel.flush();}}这样做,能够充分的利用pipeline来提高I/O能力,同时获取异步处理能力。

多连接短连接的HttpClient

类似于竞对抓取的项目,往往需要建立无数的HTTP短连接,然后抓取,然后销毁,当需要单机抓取上千网站线程数又受制的时候,怎么保证性能呢?

何不尝试NIO,单线程进行连接、写、读操作?如果连接、读、写操作系统没有能力处理,简单的注册一个事件,等待下次循环就好了。

如何存储不同的请求/响应呢?由于http是无状态没有版本的协议,又没有办法使用队列,好像办法不多。比较笨的办法是对于不同的socket,直接存储socket的引用作为map的key。

常见的RPC框架,如Thrift,Dubbo

这种框架内部一般维护了请求的协议和请求号,可以维护一个以请求号为key,结果的result为future的map,结合NIO+长连接,获取非常不错的性能。

NIO高级主题

Proactor与Reactor

一般情况下,I/O 复用机制需要事件分发器(event dispatcher)。 事件分发器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁谁谁的快递到了, 快来拿吧!开发人员在开始的时候需要在分发器那里注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函数;事件分发器在适当的时候,会将请求的事件分发给这些handler或者回调函数。

涉及到事件分发器的两种模式称为:Reactor和Proactor。 Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的。在Reactor模式中,事件分发器等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。

而在Proactor模式中,事件处理者(或者代由事件分发器发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区、读的数据大小或用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分发器得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO操作(称为overlapped技术),事件分发器等IO Complete事件完成。这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。

举个例子,将有助于理解Reactor与Proactor二者的差异,以读操作为例(写操作类似)。

在Reactor中实现读

注册读就绪事件和相应的事件处理器。

事件分发器等待事件。

事件到来,激活分发器,分发器调用事件对应的处理器。

事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。

在Proactor中实现读:

处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下,处理器无视IO就绪事件,它关注的是完成事件。

事件分发器等待操作完成事件。

在分发器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并将结果数据存入用户自定义缓冲区,最后通知事件分发器读操作完成。

事件分发器呼唤处理器。

事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,并将控制权返回事件分发器。

可以看出,两个模式的相同点,都是对某个I/O事件的事件通知(即告诉某个模块,这个I/O操作可以进行或已经完成)。在结构上,两者也有相同点:事件分发器负责提交IO操作(异步)、查询设备是否可操作(同步),然后当条件满足时,就回调handler;不同点在于,异步情况下(Proactor),当回调handler时,表示I/O操作已经完成;同步情况下(Reactor),回调handler时,表示I/O设备可以进行某个操作(can read 或 can write)。

下面,我们将尝试应对为Proactor和Reactor模式建立可移植框架的挑战。在改进方案中,我们将Reactor原来位于事件处理器内的Read/Write操作移至分发器(不妨将这个思路称为“模拟异步”),以此寻求将Reactor多路同步I/O转化为模拟异步I/O。以读操作为例子,改进过程如下:

注册读就绪事件和相应的事件处理器。并为分发器提供数据缓冲区地址,需要读取数据量等信息。

分发器等待事件(如在select()上等待)。

事件到来,激活分发器。分发器执行一个非阻塞读操作(它有完成这个操作所需的全部信息),最后调用对应处理器。

事件处理器处理用户自定义缓冲区的数据,注册新的事件(当然同样要给出数据缓冲区地址,需要读取的数据量等信息),最后将控制权返还分发器。如我们所见,通过对多路I/O模式功能结构的改造,可将Reactor转化为Proactor模式。改造前后,模型实际完成的工作量没有增加,只不过参与者间对工作职责稍加调换。没有工作量的改变,自然不会造成性能的削弱。对如下各步骤的比较,可以证明工作量的恒定:

标准/典型的Reactor:

步骤1:等待事件到来(Reactor负责)。

步骤2:将读就绪事件分发给用户定义的处理器(Reactor负责)。

步骤3:读数据(用户处理器负责)。

步骤4:处理数据(用户处理器负责)。

改进实现的模拟Proactor:

步骤1:等待事件到来(Proactor负责)。

步骤2:得到读就绪事件,执行读数据(现在由Proactor负责)。

步骤3:将读完成事件分发给用户处理器(Proactor负责)。

步骤4:处理数据(用户处理器负责)。

对于不提供异步I/O API的操作系统来说,这种办法可以隐藏Socket API的交互细节,从而对外暴露一个完整的异步接口。借此,我们就可以进一步构建完全可移植的,平台无关的,有通用对外接口的解决方案。

代码示例如下:

interface ChannelHandler{ void channelReadComplate(Channel channel,byte[] data); void channelWritable(Channel channel); } class Channel{ Socket socket; Event event;//读,写或者连接 } //IO线程主循环: class IoThread extends Thread{ public void run(){ Channel channel; while(channel=Selector.select()){//选择就绪的事件和对应的连接 if(channel.event==accept){ registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器 Selector.interested(read); } if(channel.event==write){ getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件 } if(channel.event==read){ byte[] data = channel.read(); if(channel.read()==0)//没有读到数据,表示本次数据读完了 { getChannelHandler(channel).channelReadComplate(channel,data;//处理读完成事件 } if(过载保护){ Selector.interested(read); } } } } Map

handlerMap;//所有channel的对应事件处理器 }Selector.wakeup()

主要作用

解除阻塞在Selector.select()/select(long)上的线程,立即返回。

两次成功的select之间多次调用wakeup等价于一次调用。

如果当前没有阻塞在select上,则本次wakeup调用将作用于下一次select——“记忆”作用。

为什么要唤醒?

注册了新的channel或者事件。

channel关闭,取消注册。

优先级更高的事件触发(如定时器事件),希望及时处理。

原理

Linux上利用pipe调用创建一个管道,Windows上则是一个loopback的tcp连接。这是因为win32的管道无法加入select的fd set,将管道或者TCP连接加入select fd set。

wakeup往管道或者连接写入一个字节,阻塞的select因为有I/O事件就绪,立即返回。可见,wakeup的调用开销不可忽视。

Buffer的选择

通常情况下,操作系统的一次写操作分为两步:

将数据从用户空间拷贝到系统空间。

从系统空间往网卡写。同理,读操作也分为两步:① 将数据从网卡拷贝到系统空间;② 将数据从系统空间拷贝到用户空间。

对于NIO来说,缓存的使用可以使用DirectByteBuffer和HeapByteBuffer。如果使用了DirectByteBuffer,一般来说可以减少一次系统空间到用户空间的拷贝。但Buffer创建和销毁的成本更高,更不宜维护,通常会用内存池来提高性能。

如果数据量比较小的中小应用情况下,可以考虑使用heapBuffer;反之可以用directBuffer。

NIO存在的问题

使用NIO != 高性能,当连接数<1000,并发程度不高或者局域网环境下NIO并没有显著的性能优势。

NIO并没有完全屏蔽平台差异,它仍然是基于各个操作系统的I/O系统实现的,差异仍然存在。使用NIO做网络编程构建事件驱动模型并不容易,陷阱重重。

推荐大家使用成熟的NIO框架,如Netty,MINA等。解决了很多NIO的陷阱,并屏蔽了操作系统的差异,有较好的性能和编程模型。

老司机带你分析SpringMVC框架设计原理与实现

链接:https://pan.baidu.com/s/1cksL0_VmSMdkIXWFSOx19g

密码:57w4

Netty粘包分包现象及解决方案实战,防socket攻击

链接:https://pan.baidu.com/s/1kTF2oqHOqvrPJrKa7TpXOQ

密码:dk9n

大型企业级高并发下数据库水平切分之读写分离技巧详解

链接:https://pan.baidu.com/s/1OrXSGCCboqgVX2vgfC7Z7Q

密码:ri8q

分布式事务出现场景及解决方案详细剖析

链接:https://pan.baidu.com/s/1BBf6cePibN0xawFEY7A6ZA

密码:380p

以上都是小编收集了大神的灵药,喜欢的拿走吧!喜欢小编就轻轻关注一下吧!

如何看懂《Linux多线程服务端编程

一:进程和线程
每个进程有自己独立的地址空间。“在同一个进程”还是“不在同一个进程”是系统功能划分的重要决策点。《Erlang程序设计》[ERL]把进程比喻为人:
每个人有自己的记忆(内存),人与人通过谈话(消息传递)来交流,谈话既可以是面谈(同一台服务器),也可以在电话里谈(不同的服务器,有网络通信)。面谈和电话谈的区别在于,面谈可以立即知道对方是否死了(crash,SIGCHLD),而电话谈只能通过周期性的心跳来判断对方是否还活着。
有了这些比喻,设计分布式系统时可以采取“角色扮演”,团队里的几个人各自扮演一个进程,人的角色由进程的代码决定(管登录的、管消息分发的、管买卖的等等)。每个人有自己的记忆,但不知道别人的记忆,要想知道别人的看法,只能通过交谈(暂不考虑共享内存这种IPC)。然后就可以思考:
·容错:万一有人突然死了
·扩容:新人中途加进来
·负载均衡:把甲的活儿挪给乙做
·退休:甲要修复bug,先别派新任务,等他做完手上的事情就把他重启
等等各种场景,十分便利。
线程的特点是共享地址空间,从而可以高效地共享数据。一台机器上的多个进程能高效地共享代码段(操作系统可以映射为同样的物理内存),但不能共享数据。如果多个进程大量共享内存,等于是把多进程程序当成多线程来写,掩耳盗铃。
“多线程”的价值,我认为是为了更好地发挥多核处理器(multi-cores)的效能。在单核时代,多线程没有多大价值(个人想法:如果要完成的任务是CPU密集型的,那多线程没有优势,甚至因为线程切换的开销,多线程反而更慢;如果要完成的任务既有CPU计算,又有磁盘或网络IO,则使用多线程的好处是,当某个线程因为IO而阻塞时,OS可以调度其他线程执行,虽然效率确实要比任务的顺序执行效率要高,然而,这种类型的任务,可以通过单线程的”non-blocking IO+IO multiplexing”的模型(事件驱动)来提高效率,采用多线程的方式,带来的可能仅仅是编程上的简单而已)。Alan Cox说过:”A computer is a state machine.Threads are for people who can’t program state machines.”(计算机是一台状态机。线程是给那些不能编写状态机程序的人准备的)如果只有一块CPU、一个执行单元,那么确实如Alan Cox所说,按状态机的思路去写程序是最高效的。
二:单线程服务器的常用编程模型
据我了解,在高性能的网络程序中,使用得最为广泛的恐怕要数”non-blocking IO + IO multiplexing”这种模型,即Reactor模式。
在”non-blocking IO + IO multiplexing”这种模型中,程序的基本结构是一个事件循环(event loop),以事件驱动(event-driven)和事件回调的方式实现业务逻辑:
[cpp] view plain copy
//代码仅为示意,没有完整考虑各种情况
while(!done)
{
int timeout_ms = max(1000, getNextTimedCallback());
int retval = poll(fds, nfds, timeout_ms);
if (retval<0){
处理错误,回调用户的error handler
}else{
处理到期的timers,回调用户的timer handler
if(retval>0){
处理IO事件,回调用户的IO event handler
}
}
}
这里select(2)/poll(2)有伸缩性方面的不足(描述符过多时,效率较低),Linux下可替换为epoll(4),其他操作系统也有对应的高性能替代品。
Reactor模型的优点很明显,编程不难,效率也不错。不仅可以用于读写socket,连接的建立(connect(2)/accept(2)),甚至DNS解析都可以用非阻塞方式进行,以提高并发度和吞吐量(throughput),对于IO密集的应用是个不错的选择。lighttpd就是这样,它内部的fdevent结构十分精妙,值得学习。
基于事件驱动的编程模型也有其本质的缺点,它要求事件回调函数必须是非阻塞的。对于涉及网络IO的请求响应式协议,它容易割裂业务逻辑,使其散布于多个回调函数之中,相对不容易理解和维护。
三:多线程服务器的常用编程模型
大概有这么几种:
a:每个请求创建一个线程,使用阻塞式IO操作。在Java 1.4引人NIO之前,这是Java网络编程的推荐做法。可惜伸缩性不佳(请求太多时,操作系统创建不了这许多线程)。
b:使用线程池,同样使用阻塞式IO操作。与第1种相比,这是提高性能的措施。
c:使用non-blocking IO + IO multiplexing。即Java NIO的方式。
d:Leader/Follower等高级模式。
在默认情况下,我会使用第3种,即non-blocking IO + one loop per thread模式来编写多线程C++网络服务程序。
1:one loop per thread
此种模型下,程序里的每个IO线程有一个event loop,用于处理读写和定时事件(无论周期性的还是单次的)。代码框架跟“单线程服务器的常用编程模型”一节中的一样。
libev的作者说:
One loop per thread is usually a good model. Doing this is almost never wrong, some times a better-performance model exists, but it is always a good start.
这种方式的好处是:
a:线程数目基本固定,可以在程序启动的时候设置,不会频繁创建与销毁。
b:可以很方便地在线程间调配负载。
c:IO事件发生的线程是固定的,同一个TCP连接不必考虑事件并发。
Event loop代表了线程的主循环,需要让哪个线程干活,就把timer或IO channel(如TCP连接)注册到哪个线程的loop里即可:对实时性有要求的connection可以单独用一个线程;数据量大的connection可以独占一个线程,并把数据处理任务分摊到另几个计算线程中(用线程池);其他次要的辅助性connections可以共享一个线程。
比如,在dbproxy中,一个线程用于专门处理客户端发来的管理命令;一个线程用于处理客户端发来的MySQL命令,而与后端数据库通信执行该命令时,是将该任务分配给所有事件线程处理的。
对于non-trivial(有一定规模)的服务端程序,一般会采用non-blocking IO + IO multiplexing,每个connection/acceptor都会注册到某个event loop上,程序里有多个event loop,每个线程至多有一个event loop。
多线程程序对event loop提出了更高的要求,那就是“线程安全”。要允许一个线程往别的线程的loop里塞东西,这个loop必须得是线程安全的。
在dbproxy中,线程向其他线程分发任务,是通过管道和队列实现的。比如主线程accept到连接后,将表示该连接的结构放入队列,并向管道中写入一个字节。计算线程在自己的event loop中注册管道的读事件,一旦有数据可读,就尝试从队列中取任务。
2:线程池
不过,对于没有IO而光有计算任务的线程,使用event loop有点浪费。可以使用一种补充方案,即用blocking queue实现的任务队列:
[cpp] view plain copy
typedef boost::functionFunctor;
BlockingQueue taskQueue; //线程安全的全局阻塞队列
//计算线程
void workerThread()
{
while (running) //running变量是个全局标志
{
Functor task = taskQueue.take(); //this blocks
task(); //在产品代码中需要考虑异常处理
}
}
// 创建容量(并发数)为N的线程池
int N = num_of_computing_threads;
for (int i = 0; i < N; ++i)
{
create_thread(&workerThread); //启动线程
}
//向任务队列中追加任务
Foo foo; //Foo有calc()成员函数
boost::function task = boost::bind(&Foo::calc,&foo);
taskQueue.post(task);
除了任务队列,还可以用BlockingQueue实现数据的生产者消费者队列,即T是数据类型而非函数对象,queue的消费者从中拿到数据进行处理。其实本质上是一样的。
3:总结
总结而言,我推荐的C++多线程服务端编程模式为:one (event) loop per thread + thread pool:
event loop用作IO multiplexing,配合non-blockingIO和定时器;
thread pool用来做计算,具体可以是任务队列或生产者消费者队列。
以这种方式写服务器程序,需要一个优质的基于Reactor模式的网络库来支撑,muduo正是这样的网络库。比如dbproxy使用的是libevent。
程序里具体用几个loop、线程池的大小等参数需要根据应用来设定,基本的原则是“阻抗匹配”(解释见下),使得CPU和IO都能高效地运作。所谓阻抗匹配原则:
如果池中线程在执行任务时,密集计算所占的时间比重为 P (0 < P <= 1),而系统一共有 C 个 CPU,为了让这 C 个 CPU 跑满而又不过载,线程池大小的经验公式 T = C/P。(T 是个 hint,考虑到 P 值的估计不是很准确,T 的最佳值可以上下浮动 50%)
以后我再讲这个经验公式是怎么来的,先验证边界条件的正确性。
假设 C = 8,P = 1.0,线程池的任务完全是密集计算,那么T = 8。只要 8 个活动线程就能让 8 个 CPU 饱和,再多也没用,因为 CPU 资源已经耗光了。
假设 C = 8,P = 0.5,线程池的任务有一半是计算,有一半等在 IO 上,那么T = 16。考虑操作系统能灵活合理地调度 sleeping/writing/running 线程,那么大概 16 个“50%繁忙的线程”能让 8 个 CPU 忙个不停。启动更多的线程并不能提高吞吐量,反而因为增加上下文切换的开销而降低性能。
如果 P < 0.2,这个公式就不适用了,T 可以取一个固定值,比如 5*C。
另外,公式里的 C 不一定是 CPU 总数,可以是“分配给这项任务的 CPU 数目”,比如在 8 核机器上分出 4 个核来做一项任务,那么 C=4。
四:进程间通信只用TCP
Linux下进程间通信的方式有:匿名管道(pipe)、具名管道(FIFO)、POSIX消息队列、共享内存、信号(signals),以及Socket。同步原语有互斥器(mutex)、条件变量(condition variable)、读写锁(reader-writer lock)、文件锁(record locking)、信号量(semaphore)等等。
进程间通信我首选Sockets(主要指TCP,我没有用过UDP,也不考虑Unix domain协议)。其好处在于:
可以跨主机,具有伸缩性。反正都是多进程了,如果一台机器的处理能力不够,很自然地就能用多台机器来处理。把进程分散到同一局域网的多台机器上,程序改改host:port配置就能继续用;
TCP sockets和pipe都是操作文件描述符,用来收发字节流,都可以read/write/fcntl/select/poll等。不同的是,TCP是双向的,Linux的pipe是单向的,进程间双向通信还得开两个文件描述符,不方便;而且进程要有父子关系才能用pipe,这些都限制了pipe的使用;
TCP port由一个进程独占,且进程退出时操作系统会自动回收文件描述符。因此即使程序意外退出,也不会给系统留下垃圾,程序重启之后能比较容易地恢复,而不需要重启操作系统(用跨进程的mutex就有这个风险);而且,port是独占的,可以防止程序重复启动,后面那个进程抢不到port,自然就没法初始化了,避免造成意料之外的结果;
与其他IPC相比,TCP协议的一个天生的好处是“可记录、可重现”。tcpdump和Wireshark是解决两个进程间协议和状态争端的好帮手,也是性能(吞吐量、延迟)分析的利器。我们可以借此编写分布式程序的自动化回归测试。也可以用tcpcopy之类的工具进行压力测试。TCP还能跨语言,服务端和客户端不必使用同一种语言。
分布式系统的软件设计和功能划分一般应该以“进程”为单位。从宏观上看,一个分布式系统是由运行在多台机器上的多个进程组成的,进程之间采用TCP长连接通信。
使用TCP长连接的好处有两点:一是容易定位分布式系统中的服务之间的依赖关系。只要在机器上运行netstat -tpna|grep 就能立刻列出用到某服务的客户端地址(Foreign Address列),然后在客户端的机器上用netstat或lsof命令找出是哪个进程发起的连接。TCP短连接和UDP则不具备这一特性。二是通过接收和发送队列的长度也较容易定位网络或程序故障。在正常运行的时候,netstat打印的Recv-Q和Send-Q都应该接近0,或者在0附近摆动。如果Recv-Q保持不变或持续增加,则通常意味着服务进程的处理速度变慢,可能发生了死锁或阻塞。如果Send-Q保持不变或持续增加,有可能是对方服务器太忙、来不及处理,也有可能是网络中间某个路由器或交换机故障造成丢包,甚至对方服务器掉线,这些因素都可能表现为数据发送不出去。通过持续监控Recv-Q和Send-Q就能及早预警性能或可用性故障。以下是服务端线程阻塞造成Recv-Q和客户端Send-Q激增的例子:
[cpp] view plain copy
$netstat -tn
Proto Recv-Q Send-Q Local Address Foreign
tcp 78393 0 10.0.0.10:2000 10.0.0.10:39748 #服务端连接
tcp 0 132608 10.0.0.10:39748 10.0.0.10:2000 #客户端连接
tcp 0 52 10.0.0.10:22 10.0.0.4:55572
五:多线程服务器的适用场合
如果要在一台多核机器上提供一种服务或执行一个任务,可用的模式有:
a:运行一个单线程的进程;
b:运行一个多线程的进程;
c:运行多个单线程的进程;
d:运行多个多线程的进程;
考虑这样的场景:如果使用速率为50MB/s的数据压缩库,进程创建销毁的开销是800微秒,线程创建销毁的开销是50微秒。如何执行压缩任务?
如果要偶尔压缩1GB的文本文件,预计运行时间是20s,那么起一个进程去做是合理的,因为进程启动和销毁的开销远远小于实际任务的耗时。
如果要经常压缩500kB的文本数据,预计运行时间是10ms,那么每次都起进程 似乎有点浪费了,可以每次单独起一个线程去做。
如果要频繁压缩10kB的文本数据,预计运行时间是200微秒,那么每次起线程似 乎也很浪费,不如直接在当前线程搞定。也可以用一个线程池,每次把压缩任务交给线程池,避免阻塞当前线程(特别要避免阻塞IO线程)。
由此可见,多线程并不是万灵丹(silver bullet)。
1:必须使用单线程的场合
据我所知,有两种场合必须使用单线程:
a:程序可能会fork(2);
实际编程中,应该保证只有单线程程序能进行fork(2)。多线程程序不是不能调用fork(2),而是这么做会遇到很多麻烦:
fork一般不能在多线程程序中调用,因为Linux的fork只克隆当前线程的thread of control,不可隆其他线程。fork之后,除了当前线程之外,其他线程都消失了。
这就造成一种危险的局面。其他线程可能正好处于临界区之内,持有了某个锁,而它突然死亡,再也没有机会去解锁了。此时如果子进程试图再对同一个mutex加锁,就会立即死锁。因此,fork之后,子进程就相当于处于signal handler之中(因为不知道调用fork时,父进程中的线程此时正在调用什么函数,这和信号发生时的场景一样),你不能调用线程安全的函数(除非它是可重入的),而只能调用异步信号安全的函数。比如,fork之后,子进程不能调用:
malloc,因为malloc在访问全局状态时几乎肯定会加锁;
任何可能分配或释放内存的函数,比如snprintf;
任何Pthreads函数;
printf系列函数,因为其他线程可能恰好持有stdout/stderr的锁;
除了man 7 signal中明确列出的信号安全函数之外的任何函数。
因此,多线程中调用fork,唯一安全的做法是fork之后,立即调用exec执行另一个程序,彻底隔断子进程与父进程的联系。
在多线程环境中调用fork,产生子进程后。子进程内部只存在一个线程,也就是父进程中调用fork的线程的副本。
使用fork创建子进程时,子进程通过继承整个地址空间的副本,也从父进程那里继承了所有互斥量、读写锁和条件变量的状态。如果父进程中的某个线程占有锁,则子进程同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法知道它占有了哪些锁,并且需要释放哪些锁。
尽管Pthread提供了pthread_atfork函数试图绕过这样的问题,但是这回使得代码变得混乱。因此《Programming With Posix Threads》一书的作者说:”Avoid using fork in threaded code except where the child process will immediately exec a new program.”。
b:限制程序的CPU占用率;
这个很容易理解,比如在一个8核的服务器上,一个单线程程序即便发生busy-wait,占满1个core,其CPU使用率也只有12.5%,在这种最坏的情况下,系统还是有87.5%的计算资源可供其他服务进程使用。
因此对于一些辅助性的程序,如果它必须和主要服务进程运行在同一台机器的话,那么做成单线程的能避免过分抢夺系统的计算资源。

阅读更多 >>>  php获取目录中的所有文件名

3dmax删除网格快捷键是什么,3ds max删除网格快捷键怎么使用的?

3dmax删除栅格快捷键是什么,3ds max删除栅格快捷键怎么用的?掌握max的快捷键是开始学习max的最基础的知识,掌握了快捷键工作效率会大大的提高,下面就来解决你的问题,并且附带max最常用的快捷键。建议保存起来。
3ds max删除栅格快捷键:
3dmax删除栅格的快捷键使用键盘“G”。方法:输入法英文状态下点击栅格按G即可。
3dmax基础常用快捷键大全:

显示降级适配(开关)———O
适应透视图格点———Shift+Ctrl+A
排列———Alt+A
角度捕捉(开关)———A
动画模式———(开关)———N
改变到后视图———K
背景锁定(开关)———Alt+Ctrl+B
前一时间单位———.
下一时间单位———,
改变到上(Top)视图———T
改变到底(Bottom)视图———B
改变到相机(Camera)视图———C
改变到前(Front)视图———F
改变到等大的用户(User)视图———U
改变到右(Right)视图———R
改变到透视(Perspective)图———P
循环改变选择方式———Ctrl+F
默认灯光(开关)———Ctrl+L
删除物体———DEL
当前视图暂时失效———D
是否显示几何体内框(开关)———Ctrl+E
显示第一个工具条———Alt+1
专家模式全屏(开关)———Ctrl+X
暂存(Hold)场景———Alt+Ctrl+H
取回(Fetch)场景———Alt+Ctrl+F
冻结所选物体———6
跳到最后一帧———END
跳到第一帧———HOME
显示/隐藏相机(Cameras)———Shift+C
显示/隐藏几何体(Geometry)———Shift+O
显示/隐藏网格(Grids)———G
显示/隐藏帮助(Helpers)物体———Shift+H
显示/隐藏光源(Lights)———Shift+L
显示/隐藏粒子系统(Particle———Systems)———Shift+P
显示/隐藏空间扭曲(Space———Warps)物体———Shift+W
锁定用户界面(开关)———Alt+0
匹配到相机(Camera)视图———Ctrl+C
材质(Material)编辑器———M
最大化当前视图———(开关)———W
脚本编辑器———F11
新的场景———Ctrl+N
法线(Normal)对齐———Alt+N
向下轻推网格———小键盘-
向上轻推网格———小键盘+
NURBS表面显示方式———Alt+L或Ctrl+4
NURBS调整方格1———Ctrl+1
NURBS调整方格2———Ctrl+2
NURBS调整方格3———Ctrl+3
偏移捕捉———Alt+Ctrl+空格
打开一个MAX文件———Ctrl+O
平移视图———Ctrl+P
交互式平移视图———I
放置高光(Highlight)———Ctrl+H
播放/停止动画———/
快速(Quick)渲染———Shift+Q
回到上一场景*作———Ctrl+A
回到上一视图*作———Shift+A
撤消场景*作———Ctrl+Z
撤消视图*作———Shift+Z
刷新所有视图———1
用前一次的参数进行渲染———Shift+E或F9
渲染配置———Shift+R或F10
在xy/yz/zx锁定中循环改变———F8
约束到X轴———F5
约束到Y轴———F6
约束到Z轴———F7
旋转(Rotate)视图模式———Ctrl+R或V
保存(Save)文件———Ctrl+S
透明显示所选物体(开关)———Alt+X
选择父物体———PageUp
选择子物体———PageDown
根据名称选择物体———H
选择锁定(开关)———空格
减淡所选物体的面(开关)———F2
显示所有视图网格(Grids)(开关)———Shift+G
显示/隐藏命令面板———3
显示/隐藏浮动工具条———4
显示最后一次渲染的图画———Ctrl+I
显示/隐藏主要工具栏———Alt+6
显示/隐藏安全框———Shift+F
*显示/隐藏所选物体的支架———J
显示/隐藏工具条———Y/2
百分比(Percent)捕捉(开关)———Shift+Ctrl+P
打开/关闭捕捉(Snap)———S
循环通过捕捉点———Alt+空格
声音(开关)———\\
间隔放置物体———Shift+I
改变到光线视图———Shift+4
循环改变子物体层级———Ins
子物体选择(开关)———Ctrl+B
帖图材质(Texture)修正———Ctrl+T
加大动态坐标———+
减小动态坐标———-
激活动态坐标(开关)———X
精确输入转变量———F12
全部解冻———7
根据名字显示隐藏的物体———5
刷新背景图像(Background)———Alt+Shift+Ctrl+B
显示几何体外框(开关)———F4
视图背景(Background)———Alt+B
用方框(Box)快显几何体(开关)———Shift+B
打开虚拟现实———数字键盘1
虚拟视图向下移动———数字键盘2
虚拟视图向左移动———数字键盘4
虚拟视图向右移动———数字键盘6
虚拟视图向中移动———数字键盘8
虚拟视图放大———数字键盘7
虚拟视图缩小———数字键盘9
实色显示场景中的几何体(开关)———F3
全部视图显示所有物体———Shift+Ctrl+Z
*视窗缩放到选择物体范围(Extents)———E
缩放范围———Alt+Ctrl+Z
视窗放大两倍———Shift+数字键盘+
放大镜工具———Z
视窗缩小两倍———Shift+数字键盘-
根据框选进行放大———Ctrl+w
视窗交互式放大———[
视窗交互式缩小———]
二、轨迹视图
加入(Add)关键帧———A
前一时间单位———
下一时间单位———
编辑(Edit)关键帧模式———E
编辑区域模式———F3
编辑时间模式———F2
展开对象(Object)切换———O
展开轨迹(Track)切换———T
函数(Function)曲线模式———F5或F
锁定所选物体———空格
向上移动高亮显示———↓
向下移动高亮显示———↑
向左轻移关键帧———←
向右轻移关键帧———→
位置区域模式———F4
回到上一场景*作———Ctrl+A
撤消场景*作———Ctrl+Z
用前一次的配置进行渲染———F9
渲染配置———F10
向下收拢———Ctrl+↓
向上收拢———Ctrl+↑
三、材质编辑器
用前一次的配置进行渲染———F9
渲染配置———F10
撤消场景*作———Ctrl+Z
示意(Schematic)视图
下一时间单位———
前一时间单位———
回到上一场景*作———Ctrl+A
撤消场景*作———Ctrl+Z
Active———Shade
绘制(Draw)区域———D
渲染(Render)———R
锁定工具栏(泊坞窗)———空格
四、视频编辑
加入过滤器(Filter)项目———Ctrl+F
加入输入(Input)项目———Ctrl+I
加入图层(Layer)项目———Ctrl+L
加入输出(Output)项目———Ctrl+O
加入(Add)新的项目———Ctrl+A
加入场景(Scene)事件———Ctrl+s
编辑(Edit)当前事件———Ctrl+E
执行(Run)序列———Ctrl+R
新(New)的序列———Ctrl+N
撤消场景*作———Ctrl+Z
五、NURBS编辑
CV———约束法线(Normal)移动———Alt+N
CV———约束到U向移动———Alt+U
CV———约束到V向移动———Alt+V
显示曲线(Curves)———Shift+Ctrl+C
显示控制点(Dependents)———Ctrl+D
显示格子(Lattices)———Ctrl+L
NURBS面显示方式切换———Alt+L
显示表面(Surfaces)———Shift+Ctrl+s
显示工具箱(Toolbox)———Ctrl+T
显示表面整齐(Trims)———Shift+Ctrl+T
根据名字选择本物体的子层级———Ctrl+H
锁定2D———所选物体———空格
选择U向的下一点———Ctrl+→
选择V向的下一点———Ctrl+↑
选择U向的前一点———Ctrl+←
选择V向的前一点———Ctrl+↓
根据名字选择子物体———H
柔软所选物体———Ctrl+s
转换到Curve———CV———层级———Alt+Shift+Z
转换到Curve———层级———Alt+Shift+C
转换到Imports———层级———Alt+Shift+I
转换到Point———层级———Alt+Shift+P
转换到Surface———CV———层级———Alt+Shift+V
转换到Surface———层级———Alt+Shift+S
转换到上一层级———Alt+Shift+T
转换降级———Ctrl+X
六、FFD
转换到控制点(Control———Point)层级———Alt+Shift+C
到格点(Lattice)层级———Alt+Shift+L
到设置体积(Volume)层级———Alt+Shift+S
转换到上层级———Alt+Shift+T
打开的UVW贴图
进入编辑(Edit)UVW模式———Ctrl+E
调用*.uvw文件———Alt+Shift+Ctrl+L
保存UVW为*.uvw格式的文件———Alt+Shift+Ctrl+S
打断(Break)选择点———Ctrl+B
分离(Detach)边界点———Ctrl+D
过滤选择面———Ctrl+空格
水平翻转———Alt+Shift+Ctrl+B
垂直(Vertical)翻转———Alt+Shift+Ctrl+V
冻结(Freeze)所选材质点———Ctrl+F
隐藏(Hide)所选材质点———Ctrl+H
全部解冻(unFreeze)———Alt+F
全部取消隐藏(unHide)———Alt+H
从堆栈中获取面选集———Alt+Shift+Ctrl+F
从面获取选集———Alt+Shift+Ctrl+V
锁定所选顶点———空格
水平镜象———Alt+Shift+Ctrl+N
垂直镜象———Alt+Shift+Ctrl+M
水平移动———Alt+Shift+Ctrl+J
垂直移动———Alt+Shift+Ctrl+K
平移视图———Ctrl+P
象素捕捉———S
平面贴图面/重设UVW———Alt+Shift+Ctrl+R
水平缩放———Alt+Shift+Ctrl+I
垂直缩放———Alt+Shift+Ctrl+O
移动材质点———Q
旋转材质点———W
等比例缩放材质点———E
焊接(Weld)所选的材质点———Alt+Ctrl+W
焊接(Weld)到目标材质点———Ctrl+W
Unwrap的选项(Options)———Ctrl+O
更新贴图(Map)———Alt+Shift+Ctrl+M
将Unwrap视图扩展到全部显示———Alt+Ctrl+Z
框选放大Unwrap视图———Ctrl+Z
将Unwrap视图扩展到所选材质点的大小———Alt+Shift+Ctrl+Z
缩放到Gizmo大小———Shift+空格
缩放(Zoom)工具———Z
七、反应堆(Reactor)
建立(Create)反应(Reaction)———Alt+Ctrl+C
删除(Delete)反应(Reaction)———Alt+Ctrl+D
编辑状态(State)切换———Alt+Ctrl+s
设置最大影响(Influence)———Ctrl+I
设置最小影响(Influence)———Alt+I
设置影响值(Value)———Alt+Ctrl+V

微服务架构的分布式事务问题如何处理?

可以参考开源分布式事务管理器ByteTCC。ByteTCC特性:
1、支持Spring容器的声明式事务管理;
2、支持普通事务、TCC事务、业务补偿型事务等事务机制;
3、支持多数据源、跨应用、跨服务器等分布式事务场景;
4、支持长事务;
5、支持dubbo服务框架;
6、支持spring cloud;
7、提供框架层面的幂等性解决方案;
1. 性能和时延问题
在服务化之前,业务通常都是本地API调用,本地方法调用性能损耗较小。服务化之后,服务提供者和消费者之间采用远程网络通信,增加了额外的性能损耗:
1) 客户端需要对消息进行序列化,主要占用CPU计算资源。
2) 序列化时需要创建二进制数组,耗费JVM堆内存或者堆外内存。
3) 客户端需要将序列化之后的二进制数组发送给服务端,占用网络带宽资源。
4) 服务端读取到码流之后,需要将请求数据报反序列化成请求对象,占用CPU计算资源。
5) 服务端通过反射的方式调用服务提供者实现类,反射本身对性能影响就比较大。
6) 服务端将响应结果序列化,占用CPU计算资源。
7) 服务端将应答码流发送给客户端,占用网络带宽资源。
8) 客户端读取应答码流,反序列化成响应消息,占用CPU资源。
通过分析我们发现,一个简单的本地方法调用,切换成远程服务调用之后,额外增加了很多处理流程,不仅占用大量的系统资源,同时增加了时延。一些复杂
的应用会拆分成多个服务,形成服务调用链,如果服务化框架的性能比较差、服务调用时延也比较大,业务服务化之后的性能和时延将无法满足业务的性能需求。
1.1 RPC框架高性能设计
影响RPC框架性能的主要因素有三个。
1) I/O调度模型:同步阻塞I/O(BIO)还是非阻塞I/O(NIO)。
2) 序列化框架的选择:文本协议、二进制协议或压缩二进制协议。
3) 线程调度模型:串行调度还是并行调度,锁竞争还是无锁化算法。
1. I/O调度模型
在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术通过把多个I/O的
阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大
优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。
JDK1.5_update10版本使用epoll替代了传统的select/poll,极大地提升了NIO通信的性能,它的工作原理如图1-1所示。
图1-1 非阻塞I/O工作原理
Netty是一个开源的高性能NIO通信框架:它的I/O线程NioEventLoop由于聚合了多路复用器Selector,可以同时并发处理成
百上千个客户端Channel。由于读写操作都是非阻塞的,这就可以充分提升I/O线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起。另外,由于
Netty采用了异步通信模式,一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
Netty被精心设计,提供了很多独特的性能提升特性,使它做到了在各种NIO框架中性能排名第一,它的性能优化措施总结如下。
1) 零拷贝:(1)Netty的接收和发送ByteBuffer采用DIRECT
BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP
BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送
过程中多了一次缓冲区的内存拷贝。(2)Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer
那样方便地对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。(3)Netty的文件传输采用
了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。
2)
内存池:随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区Buffer,情况却稍有不同,特别是对于
堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,Netty提供了基于内存池的缓冲区重用机制。性能测试表明,采用内存池的
ByteBuf相比于朝生夕灭的ByteBuf,性能高23倍左右(性能数据与使用场景强相关)。
3)
无锁化的串行设计:在大多数场景下,并行多线程处理可以提升系统的并发性能。但是,如果对于共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会导
致性能的下降。为了尽可能地避免锁竞争带来的性能损耗,可以通过串行化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多
线程竞争和同步锁。为了尽可能提升性能,Netty采用了串行无锁化设计,在I/O线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行
化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设
计相比一个队列-多个工作线程模型性能更优。
4) 高效的并发编程:volatile的大量、正确使用;CAS和原子类的广泛使用;线程安全容器的使用;通过读写锁提升并发性能。
2. 高性能序列化框架
影响序列化性能的关键因素总结如下。
1) 序列化后的码流大小(网络带宽的占用)。
2) 序列化&反序列化的性能(CPU资源占用)。
3) 是否支持跨语言(异构系统的对接和开发语言切换)。
4) 并发调用的性能表现:稳定性、线性增长、偶现的时延毛刺等。
相比于JSON等文本协议,二进制序列化框架性能更优异,以Java原生序列化和Protobuf二进制序列化为例进行性能测试对比,结果如图1-2所示。
图1-2 序列化性能测试对比数据
在序列化框架的技术选型中,如无特殊要求,尽量选择性能更优的二进制序列化框架,码流是否压缩,则需要根据通信内容做灵活选择,对于图片、音频、有大量重复内容的文本文件(例如小说)可以采用码流压缩,常用的压缩算法包括GZip、Zig-Zag等。
3. 高性能的Reactor线程模型
该模型的特点总结如下。
1) 有专门一个NIO线程:Acceptor线程用于监听服务端,接收客户端的TCP连接请求。
2) 网络I/O操作:读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送。
3) 1个NIO线程可以同时处理N条链路,但是1个链路只对应1个NIO线程,防止产生并发操作。
由于Reactor模式使用的是异步非阻塞I/O,所有的I/O操作都不会导致阻塞,理论上一个线程可以独立处理所有I/O相关的操作,因此在绝大多数场景下,Reactor多线程模型都可以完全满足业务性能需求。
Reactor线程调度模型的工作原理示意如图1-3所示。
图1-3 高性能的Reactor线程调度模型
1.2 业务最佳实践
要保证高性能,单依靠分布式服务框架是不够的,还需要应用的配合,应用服务化高性能实践总结如下:
1) 能异步的尽可能使用异步或者并行服务调用,提升服务的吞吐量,有效降低服务调用时延。
2) 无论是NIO通信框架的线程池还是后端业务线程池,线程参数的配置必须合理。如果采用JDK默认的线程池,最大线程数建议不超过20个。因为JDK的线程池默认采用N个线程争用1个同步阻塞队列方式,当线程数过大时,会导致激烈的锁竞争,此时性能不仅不会提升,反而会下降。
3)
尽量减小要传输的码流大小,提升性能。本地调用时,由于在同一块堆内存中访问,参数大小对性能没有任何影响。跨进程通信时,往往传递的是个复杂对象,如果
明确对方只使用其中的某几个字段或者某个对象引用,则不要把整个复杂对象都传递过去。举例,对象A持有8个基本类型的字段,2个复杂对象B和C。如果明确
服务提供者只需要用到A聚合的C对象,则请求参数应该是C,而不是整个对象A。
4) 设置合适的客户端超时时间,防止业务高峰期因为服务端响应慢导致业务线程等应答时被阻塞,进而引起后续其他服务的消息在队列中排队,造成故障扩散。
5) 对于重要的服务,可以单独部署到独立的服务线程池中,与其他非核心服务做隔离,保障核心服务的高效运行。
6) 利用Docker等轻量级OS容器部署服务,对服务做物理资源层隔离,避免虚拟化之后导致的超过20%的性能损耗。
7) 设置合理的服务调度优先级,并根据线上性能监控数据做实时调整。
2. 事务一致性问题
服务化之前,业务采用本地事务,多个本地SQL调用可以用一个大的事务块封装起来,如果某一个数据库操作发生异常,就可以将之前的SQL操作进行回滚,只有所有SQL操作全部成功,才最终提交,这就保证了事务强一致性,如图2-1所示。
服务化之后,三个数据库操作可能被拆分到独立的三个数据库访问服务中,此时原来的本地SQL调用演变成了远程服务调用,事务一致性无法得到保证,如图2-2所示。
图2-2 服务化之后引入分布式事务问题
假如服务A和服务B调用成功,则A和B的SQL将会被提交,最后执行服务C,它的SQL操作失败,对于应用1消费者而言,服务A和服务B的相关
SQL操作已经提交,服务C发生了回滚,这就导致事务不一致。从图2-2可以得知,服务化之后事务不一致主要是由服务分布式部署导致的,因此也被称为分布
式事务问题。
2.1 分布式事务设计方案
通常,分布式事务基于两阶段提交实现,它的工作原理示意图如图2-3所示。
图2-3 两阶段提交原理图
阶段1:全局事务管理器向所有事务参与者发送准备请求;事务参与者向全局事务管理器回复自己是否准备就绪。
阶段2:全局事务管理器接收到所有事务参与者的回复之后做判断,如果所有事务参与者都可以提交,则向所有事务提交者发送提交申请,否则进行回滚。事务参与者根据全局事务管理器的指令进行提交或者回滚操作。
分布式事务回滚原理图如图2-4所示。
图2-4 分布式事务回滚原理图
两阶段提交采用的是悲观锁策略,由于各个事务参与者需要等待响应最慢的参与者,因此性能比较差。第一个问题是协议本身的成本:整个协议过程是需要加
锁的,比如锁住数据库的某条记录,且需要持久化大量事务状态相关的操作日志。更为麻烦的是,两阶段锁在出现故障时表现出来的脆弱性,比如两阶段锁的致命缺
陷:当协调者出现故障,整个事务需要等到协调者恢复后才能继续执行,如果协调者出现类似磁盘故障等错误,该事务将被永久遗弃。
对于分布式服务框架而言,从功能特性上需要支持分布式事务。在实际业务使用过程中,如果能够通过最终一致性解决问题,则不需要做强一致性;如果能够避免分布式事务,则尽量在业务层避免使用分布式事务。
2.2 分布式事务优化
既然分布式事务有诸多缺点,那么为什么我们还在使用呢?有没有更好的解决方案来改进或者替换呢?如果我们只是针对分布式事务去优化的话,发现其实能改进的空间很小,毕竟瓶颈在分布式事务模型本身。
那我们回到问题的根源:为什么我们需要分布式事务?因为我们需要各个资源数据保持一致性,但是对于分布式事务提供的强一致性,所有业务场景真的都需
要吗?大多数业务场景都能容忍短暂的不一致,不同的业务对不一致的容忍时间不同。像银行转账业务,中间有几分钟的不一致时间,用户通常都是可以理解和容忍
的。
在大多数的业务场景中,我们可以使用最终一致性替代传统的强一致性,尽量避免使用分布式事务。
在实践中常用的最终一致性方案就是使用带有事务功能的MQ做中间人角色,它的工作原理如下:在做本地事务之前,先向MQ发送一个prepare消
息,然后执行本地事务,本地事务提交成功的话,向MQ发送一个commit消息,否则发送一个rollback消息,取消之前的消息。MQ只会在收到
commit确认才会将消息投递出去,所以这样的形式可以保证在一切正常的情况下,本地事务和MQ可以达到一致性。但是分布式调用存在很多异常场景,诸如
网络超时、VM宕机等。假如系统执行了local_tx()成功之后,还没来得及将commit消息发送给MQ,或者说发送出去由于网络超时等原因,MQ
没有收到commit,发生了commit消息丢失,那么MQ就不会把prepare消息投递出去。MQ会根据策略去尝试询问(回调)发消息的系统
(checkCommit)进行检查该消息是否应该投递出去或者丢弃,得到系统的确认之后,MQ会做投递还是丢弃,这样就完全保证了MQ和发消息的系统的
一致性,从而保证了接收消息系统的一致性。
3. 研发团队协作问题
服务化之后,特别是采用微服务架构以后。研发团队会被拆分成多个服务化小组,例如AWS的Two Pizza Team,每个团队由2~3名研发负责服务的开发、测试、部署上线、运维和运营等。
随着服务数的膨胀,研发团队的增多,跨团队的协同配合将会成为一个制约研发效率提升的因素。
3.1 共用服务注册中心
为了方便开发测试,经常会在线下共用一个所有服务共享的服务注册中心,这时,一个正在开发中的服务发布到服务注册中心,可能会导致一些消费者不可用。
解决方案:可以让服务提供者开发方,只订阅服务(开发的服务可能依赖其他服务),而不注册正在开发的服务,通过直连测试正在开发的服务。
它的工作原理如图3-1所示。
图3-1 只订阅,不发布
3.2 直连提供者
在开发和测试环境下,如果公共的服务注册中心没有搭建,消费者将无法获取服务提供者的地址列表,只能做本地单元测试或使用模拟桩测试。
还有一种场景就是在实际测试中,服务提供者往往多实例部署,如果服务提供者存在Bug,就需要做远程断点调试,这会带来两个问题:
1) 服务提供者多实例部署,远程调试地址无法确定,调试效率低下。
2) 多个消费者可能共用一套测试联调环境,断点调试过程中可能被其他消费者意外打断。
解决策略:绕过注册中心,只测试指定服务提供者,这时候可能需要点对点直连,点对点直联方式将以服务接口为单位,忽略注册中心的提供者列表。
3.3 多团队进度协同
假如前端Web门户依赖后台A、B、C和D
4个服务,分别由4个不同的研发团队负责,门户要求新特性2周内上线。A和B内部需求优先级排序将门户的优先级排的比较高,可以满足交付时间点。但是C和
D服务所在团队由于同时需要开发其他优先级更高的服务,因此把优先级排的相对较低,无法满足2周交付。
在C和D提供版本之前,门户只能先通过打测试桩的方式完成Mock测试,但是由于并没有真实的测试过C和D服务,因此需求无法按期交付。
应用依赖的服务越多,特性交付效率就越低下,交付的速度取决于依赖的最迟交付的那个服务。假如Web门户依赖后台的100个服务,只要1个核心服务没有按期交付,则整个进度就会延迟。
解决方案:调用链可以将应用、服务和中间件之间的依赖关系串接并展示出来,基于调用链首入口的交付日期作为输入,利用依赖管理工具,可以自动计算出调用链上各个服务的最迟交付时间点。通过调用链分析和标准化的依赖计算工具,可以避免人为需求排序失误导致的需求延期。
3.4 服务降级和Mock测试
在实际项目开发中,由于小组之间、个人开发者之间开发节奏不一致,经常会出现消费者等待依赖的服务提供者提供联调版本的情况,相互等待会降低项目的研发进度。
解决方案:服务提供者首先将接口定下来并提供给消费者,消费者可以将服务降级同Mock测试结合起来,在Mock测试代码中实现容错降级的业务逻辑(业务放通),这样既完成了Mock测试,又实现了服务降级的业务逻辑开发,一举两得。
3.5 协同调试问题
在实际项目开发过程中,各研发团队进度不一致很正常。如果消费者坐等服务提供者按时提供版本,往往会造成人力资源浪费,影响项目进度。
解决方案:分布式服务框架提供Mock桩管理框架,当周边服务提供者尚未完成开发时,将路由切换到模拟测试模式,自动调用Mock桩;业务集成测试和上线时,则要能够自动切换到真实的服务提供者上,可以结合服务降级功能实现。
3.6 接口前向兼容性
由于线上的Bug修复、内部重构和需求变更,服务提供者会经常修改内部实现,包括但不限于:接口参数变化、参数字段变化、业务逻辑变化和数据表结构变化。
在实际项目中经常会发生服务提供者修改了接口或者数据结构,但是并没有及时知会到所有消费者,导致服务调用失败。
解决方案:
1) 制定并严格执行《服务前向兼容性规范》,避免发生不兼容修改或者私自修改不通知周边的情况。
2) 接口兼容性技术保障:例如Thrift的IDL,支持新增、修改和删除字段,字段定义位置无关性,码流支持乱序等。
分布式系统架构中,分布式事务问题是一个绕不过去的挑战。而微服务架构的流行,让分布式事问题日益突出!
下面我们以电商购物支付流程中,在各大参与者系统中可能会遇到分布式事务问题的场景进行详细的分析!
如上图所示,假设三大参与平台(电商平台、支付平台、银行)的系统都做了分布式系统架构拆分,按上数中的流程步骤进行分析:
1、电商平台中创建订单:预留库存、预扣减积分、锁定优惠券,此时电商平台内各服务间会有分布式事务问题,因为此时已经要跨多个内部服务修改数据;
2、支付平台中创建支付订单(选银行卡支付):查询账户、查询限制规则,符合条件的就创建支付订单并跳转银行,此时不会有分布式事务问题,因为还不会跨服务改数据;
3、银行平台中创建交易订单:查找账户、创建交易记录、判断账户余额并扣款、增加积分、通知支付平台,此时也会有分布式事务问题(如果是服务化架构的话);
4、支付平台收到银行扣款结果:更改订单状态、给账户加款、给积分帐户增加积分、生成会计分录、通知电商平台等,此时也会有分布式事务问题;
5、电商平台收到支付平台的支付结果:更改订单状态、扣减库存、扣减积分、使用优惠券、增加消费积分等,系统内部各服务间调用也会遇到分布式事问题;
如上图,支付平台收到银行扣款结果后的内部处理流程:
1、支付平台的支付网关对银行通知结果进行校验,然后调用支付订单服务执行支付订单处理;
2、支付订单服务根据银行扣款结果更改支付订单状态;
3、调用资金账户服务给电商平台的商户账户加款(实际过程中可能还会有各种的成本计费;如果是余额支付,还可能是同时从用户账户扣款,给商户账户加款);
4、调用积分服务给用户积分账户增加积分;
5、调用会计服务向会计(财务)系统写进交易原始凭证生成会计分录;
6、调用通知服务将支付处理结果通知电商平台;
如上图,把支付系统中的银行扣款成功回调处理流程提取出来,对应的分布式事务问题的代码场景:
/** 支付订单处理 **/
@Transactional(rollbackFor = Exception.class)
public void completeOrder() {
orderDao.update(); // 订单服务本地更新订单状态
accountService.update(); // 调用资金账户服务给资金帐户加款
pointService.update(); // 调用积分服务给积分帐户增加积分
accountingService.insert(); // 调用会计服务向会计系统写入会计原始凭证
merchantNotifyService.notify(); // 调用商户通知服务向商户发送支付结果通知
}
本地事务控制还可行吗?
以上分布式事务问题,需要多种分布式事务解决方案来进行处理。
订单处理:本地事务
资金账户加款、积分账户增加积分:TCC型事务(或两阶段提交型事务),实时性要求比较高,数据必须可靠。
会计记账:异步确保型事务(基于可靠消息的最终一致性,可以异步,但数据绝对不能丢,而且一定要记账成功)
商户通知:最大努力通知型事务(按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对)

阅读更多 >>>  handler机制,能讲讲Android的Handler机制吗

《GTFO》Rundown5通关攻略

《GTFO》Rundown5通关攻略不知如何解决,为此小编给大家收集整理《GTFO》Rundown5通关攻略解决办法,感兴趣的快来看看吧。《GTFO》Rundown5通关攻略武器:注意:此游戏是有友伤的,包括你的辅助武器,胶枪和探测仪,锤子除外R5移除了R4的特定武器。手枪:略微增加击退UZI:增加身体伤害,降低携带量,更小的腰射散射DMR:增加身体伤害,增加射速,减少散射,准星符合腰射散射3连发:增加弹容单喷:略微增加弹药量,略微增加有效范围,可以一枪击杀黑刺。连喷:略微增加击退,略微增加身体伤害左轮:略微减少弹药携带量狙:增加弹容,轻微增加弹药携带量,减少射速,减少散射,准星符合腰射散步重型SMG:25发一个弹夹性价比还不错双发步枪:14发一个弹夹 对抗尸潮有奇效R1机枪:伤害从10改成8 增加射速 载弹量备弹现在开箱子可以开出一次性绊雷,然后增加了绊线凝胶雷,和普通诡雷不一样的就是,敌人经过会喷出凝胶。EX扩展新武器不推荐用,整体数值都不是很好建议的武器配置(个人感觉的):版本答案HEL(穿透)武器系列,机枪到了后面的深层稍微皮软,主武器选择建议DMR胶枪:可以封门,粘住并冻住怪物7秒吧好像是(但是注意被冻住的怪物只是没有探测周围而已,别发出会让怪物警报的声音),10发可以封门7发可冻住大怪,触手4发,2发可以冻住远程和近战怪。(客机附带凝胶弱点详细情况看B站视频)生物探测仪:多人游玩带的,可以在怪物警报的时候标记怪物具体位置,可以标记触手,可以用来查看怪物数量。注意生物探测仪是唯一一个无限使用的辅助武器。绊雷:容量从10削弱到8 但是修复了诡雷无法对太多敌人造成伤害的BUG 现在的诡雷简直是TOP1的存在强烈推荐!3个炮台,狙击 全自动 三连发变化不大怪物近战怪每次攻击12血,远程怪每次攻击5血。这个游戏不是说打头就能必死,当你将怪击杀后会中间会出现橙色准星扩散的标志,标志着你已经击杀了你刚才攻击的怪物.钉头怪,可近战可远程,远程攻击发射10发飞弹,每发4血,近战攻击可以像大壮那样锤两下,一下12血,钉头虽然血比较厚但是打出硬直可以打断攻击。母体,会喷迷雾,弱点是身上的孢,个人建议是4个人直接全员shotgun集火秒(不同关卡选择不同),秒不掉出近战小怪很麻烦,移速快,舔一下4滴血,但是数量太多顶不住。用胶枪可以限制一下王后生的小怪。触手处理,多人直接穿透狙或者大狙还有散弹枪一枪打死,不过难度较高一点,然后开团。稳一点就胶雷,胶枪封住 满蓄力锤头锤死。还有一个方法就是跟在触手怪旁边但是别和他体积碰撞了,在触手低头一瞬间满蓄力带走,即使他的触手碰到你了,也完全有足够的时间锤死。当然也可以等着他来送然后蓄力带走。。然后有个绿孢子,一般都会附在墙上和一些毕竟阴的位置,绿孢子是根据你的距离来攻击的,你距离它越近它攻击你的速度越快,远一点你可以听见一点声音,反正迅速反应拿手电照它,它就直接怂了会缩回去,被孢子喷到会加一点感染值和减一点血。(这都不是重点。。。重点是有时候集中注意的时候突然来个孢子喷一下会被吓到。。。)大壮大姐就不给大家介绍了,只要入手了估计也就懂的都懂了。。。LMAO。(使用黄针以后可以在8秒内增加近战伤害,可以在后脑勺满蓄力一击击杀大壮)隐身怪处理,大手电可以照出来,在潜行的时候不建议用大手电,来回切也很麻烦,多人肯定是生物探测仪,有些枪上自带强光手电,可以帮助玩家很好的识别出来。隐身大壮在潜行的时候更难被发现,所以建议潜行的时候用手电照怪物的脚这样既可以帮你识别出哪里有怪,又不会让怪物有所察觉,其实最好用的还是听声音...隐身大壮。速度快,远程舌头速度极快,其他面板和大壮一样。黑刺大壮。伤害和普通大壮相同,速度更快韧性更高,血量厚。隐身的触手,隐身触手可以通过长时间使用手电照射他,可以让他伸出触须来,或者短时间内的冲刺也可以勾引侦察兵伸出触须.黑刺触手 血量应该为120 建议穿透狙单喷 狙击枪背后一枪打死 也可以用HEL GUN速点2枪打背后,如果要暗杀 要么黄针锤 要么2个队友同时锤背后黑刺触手会有一段小硬直时间,也可以3人锤正面tank,近战攻击16血 攻击3次,近战肉搏36血,弱点是背后的孢子,在没有地形的情况下tank处理方法要么用诡雷击杀要么2个队友利用地形耗死。潜行和身法潜行:所有怪(除触手以外,探测半径应该都是一样的),在怪闪光的时候其实就是在探测周围信息,在这个时候如果你处在探测半径里面不能干的事:蹲上蹲下,跳,跑,锤地,锤箱子锁,黑锁失败,或者用右键挡怪。。移动,开灯照怪,即使是蹲着(其实可以蹲着走几步,玩多了就知道了),开枪,体积碰撞或者锤其他的怪,这些行为都会导致怪物发现你然后触发整个房间的怪物,不过这个版本如果怪离你很近,他会先优先攻击你一次,然后再继续下一个行为要么继续攻击要么警报,及时处理掉就不会出现警报。用走的方式前进会引发怪物立刻探测周围,跑则是直接就开始攻击你或者警报了,开灯照怪也会让怪立刻探测周围,可以用这个方式刷新怪的探测频率。如果在怪已经探测到了一些信息的时候会发出心脏跳动的声音,建议这个时候就直接双手离开键盘鼠标就完事了。身法:跑锤在B站上都有相关的视频。溜怪的时候,往怪的侧边跑过基本上是无伤的,就看有没有远程,远程怪的攻击可以通过跑步快速晃动视角改变方向来躲避。(跑扶R4已修改)补给和门补给:4个包:医疗包,弹药包,工具包。以及上个版本加入的消毒包,还有消毒机。医疗包一次回百分之20的血,玩家会自然回血到百分之20。弹药包使用一次是加多少百分比每把枪都是有数值的,具体大家自己去体验。工具包,也有具体数值,游戏里体会。消毒包一次减百分之20感染值,没错你会感染,感染值越高会减少最大生命上限,最高到达85%,因为到达比85%更高的数值他会慢慢的减少到85%。当你听见滋滋滋的声音的时候说明你现在处于毒区,毒区的标志就是很浓的雾,会感染的。消毒机是地图上会自动刷在指定地点,不是每关都有,消毒机是无限使用的。(目前R3版本好像只有B2和D1有消毒机)。2种针,红针回血(10%~12%),黄针(8秒)使用后增加近战伤害(可以满蓄力一击击杀大壮),但是使用了后会涨感染值门:蓝门一般分为2种,一种警报门一种安全门,当你靠近蓝门的时候简单的辨别方法就是看蓝门开关上面有没有红字,有说明是警报门,没有是安全门。现在蓝门上面会告诉你这个蓝门为几级警报门上个版本加入了血门,很明显,因为你靠近血门可以听见里面钉头怪的叫声。开门后会强制触发后门的一小片区域的战斗,甚至有时候会遇到血门后的那个区域刚好就有触手,然后再多打一波怪。。。新版的门上会写难度的自己认真看HIGN难度的门就是最简单的模式,这关最基础的目标,其他难度都是支线R5关卡任务目标A1HIGH找到终端机输入指定的指令然后撤离。EX找到6个PLANTE,运气好只需要开2个紫点门。A2HIGH找到HISEC-CARGO(绿盒子)抱回撤离点。EX输入指定指令之后全图关灯。OVER(必须在抱起主线盒子以后 OVER才能打开)拿到一个HDD硬盘里面有一个母体和一堆母体小怪。B1HIGN找到HSU样本,开启HSU样本门(98或96随机一个门)之后刷无限黑刺大壮,找到HSU样本后占点结束无限黑刺大壮,回到撤离点再次无限黑刺大壮。EX3个血门每一个血门都有一个发电桩,给3个电桩插上电池。OVER reactor(反应堆)验证 7波。B2HIGN找到9个GLP。EX 2台终端机验证6波,验证完后无限警报(必须打了EX以后才能选择打不打OVER)。OVER找到5个OSIP。B3HIGN找到 UNS设施 并将绿盒子放上去结束无限黑刺警报(开局开了无限警报门后,去右边的四级门里面 2个血门有一个刷钥匙 钥匙可开启 UNS门。EX 输入指定指令后触发大紫点防守站,3个方向的血门全部自动打开,左边血门刷黑刺大壮,右边血门刷顶头怪,正前方血门刷TANK+黑刺。C1HIGN找到2台终端机验证每台7波 输入完后无限尸潮。EX找到另一台终端机验证10波,验证时刷隐身怪,验证完后无限尸潮改成刷新隐身大壮(EX区域有一个沉睡母体)C2HIGN为中央发电机插上5个电池每插一个降一层雾。EX是reactor(反应堆)验证 6波。OVER和B3一样 触发大紫点防守具体打法B站有强打流和卡上限溜。D1HIGN找到reactor后输入代码 占完点后刷无限隐身怪。EX找到终端机输入指定指令后全图开始起毒雾。(建议最后撤离的时候再去输入EX的代码)

3dmax切角的英文是什么呢?

这个是3DMAX常用中英文翻译对照表大全,自己找一下,还有问题都可以在溜溜自学网提问!
File——【文件】
New——【新建】
Reset——【重置】

Open——【打开】
Save——【保存】
Save As——【保存为】
Save selected——【保存选择】
XRef Objects——【外部引用物体】
XRef Scenes——【外部引用场景】
Merge——【合并】
Merge Animation——【合并动画动作】
Replace——【替换】
Import——【输入】
Export——【输出】
Export Selected——【选择输出】
Archive——【存档】
Summary Info——【摘要信息】
File Properties——【文件属性】
View Image File——【显示图像文件】
History——【历史】
Exit——【退出】
Edit——【菜单】
Undo or Redo——【取消/重做】
Hold and fetch——【保留/引用】
Delete——【删除】
Clone——【克隆】
Select All——【全部选择】
Select None——【空出选择】
Select Invert——【反向选择】
Select By——【参考选择】
Color——【颜色选择】
Name——【名字选择】
Rectangular Region——【矩形选择】
Circular Region——【圆形选择】
Fabce Region——【连点选择】
Lasso Region——【套索选择】
Region:——【区域选择】
Window——【包含】
Crossing——【相交】
Named Selection Sets——【命名选择集】
Object Properties——【物体属性】
Tools——【工具】
Transform Type-In——【键盘输入变换】
Display Floater——【视窗显示浮动对话框】
Selection Floater——【选择器浮动对话框】
Light Lister——【灯光列表】
Mirror——【镜像物体】
Array——【阵列】
Align——【对齐】
Snapshot——【快照】
Spacing Tool——【间距分布工具】
Normal Align——【法线对齐】
Align Camera——【相机对齐】
Align to View——【视窗对齐】
Place Highlight——【放置高光】
Isolate Selection——【隔离选择】
Rename Objects——【物体更名】
Group——【群组】
Group——【群组】
Ungroup——【撤消群组】
Open——【开放组】
Close——【关闭组】
Attach——【配属】
Detach——【分离】
Explode——【分散组】
Views——【查看】
Undo View Change/Redo View change——【取消/重做视窗变化】
Save Active View/Restore Active View——【保存/还原当前视窗】
Viewport Configuration——【视窗配置】
Grids——【栅格】
Show Home Grid——【显示栅格命令】
Activate Home Grid——【活跃原始栅格命令】
Activate Grid Object——【活跃栅格物体命令】
Activate Grid to View——【栅格及视窗对齐命令】
Viewport Background——【视窗背景】
Update Background Image——【更新背景】
Reset Background Transform——【重置背景变换】
Show Transform Gizmo——【显示变换坐标系】
Show Ghosting——【显示重橡】
Show Key Times——【显示时间键】
Shade Selected——【选择亮显】
Show Dependencies——【显示关联物体】
Match Camera to View——【相机与视窗匹配】
Add Default Lights To Scene——【增加场景缺省灯光】
Redraw All Views——【重画所有视窗】
Activate All Maps——【显示所有贴图】
Deactivate All Maps——【关闭显示所有贴图】
Update During Spinner Drag——【微调时实时显示】
Adaptive Degradation Toggle——【绑定适应消隐】
Expert Mode——【专家模式】
Create——【创建】
Standard Primitives——【标准图元】
Box——【立方体】
Cone——【圆锥体】
Sphere——【球体】
GeoSphere——【三角面片球体】
Cylinder——【圆柱体】
Tube——【管状体】
Torus——【圆环体】
Pyramid——【角锥体】
Plane——【平面】
Teapot——【茶壶】
Extended Primitives——【扩展图元】
Hedra——【多面体】
Torus Knot——【环面纽结体】
Chamfer Box——【斜切立方体】
Chamfer Cylinder——【斜切圆柱体】
Oil Tank——【桶状体】
Capsule——【角囊体】
Spindle——【纺锤体】
L-Extrusion——【L形体按钮】
Gengon——【导角棱柱】
C-Extrusion——【C形体按钮】
RingWave——【环状波】
Hose——【软管体】
Pri *** ——【三棱柱】
Shapes——【形状】
Line——【线条】
Text——【文字】
Arc——【弧】
Circle——【圆】
Donut——【圆环】
Ellipse——【椭圆】
Helix——【螺旋线】
NGon——【多边形】
Rectangle——【矩形】
Section——【截面】
Star——【星型】
Lights——【灯光】
Target Spotlight——【目标聚光灯】
Free Spotlight——【自由聚光灯】
Target Directional Light——【目标平行光】
Directional Light——【平行光】
Omni Light——【泛光灯】
Skylight——【天光】
Target Point Light——【目标指向点光源】
Free Point Light——【自由点光源】
Target Area Light——【指向面光源】
IES Sky——【IES天光】
IES Sun——【IES阳光】
SuNLIGHT System and Daylight——【太阳光及日光系统】
Camera——【相机】
Free Camera——【自由相机】
Target Camera——【目标相机】
Particles——【粒子系统】
Blizzard——【暴风雪系统】
PArray——【粒子阵列系统】
PCloud——【粒子云系统】
Snow——【雪花系统】
Spray——【喷溅系统】
Super Spray——【超级喷射系统】
Modifiers——【修改器】
Selection Modifiers——【选择修改器】
Mesh Select——【网格选择修改器】
Poly Select——【多边形选择修改器】
Patch Select——【面片选择修改器】
Spline Select——【样条选择修改器】
Volume Select——【体积选择修改器】
FFD Select——【自由变形选择修改器】
NURBS Surface Select——【NURBS表面选择修改器】
Patch/Spline Editing——【面片/样条线修改器】:
Edit Patch——【面片修改器】
Edit Spline——【样条线修改器】
Cross Section——【截面相交修改器】
Surface——【表面生成修改器】
Delete Patch——【删除面片修改器】
Delete
Spline——【删除样条线修改器】
Lathe——【车床修改器】
Normalize Spline——【规格化样条线修改器】
Fillet/Chamfer——【圆切及斜切修改器】
Trim/Extend——【修剪及延伸修改器】
Mesh Editing——【表面编辑】
Cap Holes——【顶端洞口编辑器】
Delete Mesh——【编辑网格物体编辑器】
Edit Normals——【编辑法线编辑器】
Extrude——【挤压编辑器】
Face Extrude——【面拉伸编辑器】
Normal——【法线编辑器】
Optimize——【优化编辑器】
Smooth——【平滑编辑器】
STL Check——【STL检查编辑器】
Symmetry——【对称编辑器】
Tessellate——【镶嵌编辑器】
Vertex Paint——【顶点着色编辑器】
Vertex Weld——【顶点焊接编辑器】
Animation Modifiers——【动画编辑器】
Skin——【皮肤编辑器】
Morpher——【变体编辑器】
Flex——【伸缩编辑器】
Melt——【熔化编辑器】
Linked XForm——【连结参考变换编辑器】
Patch Deform——【面片变形编辑器】
Path Deform——【路径变形编辑器】
Surf Deform——【表面变形编辑器】
* Surf Deform——【空间变形编辑器】
UV Coordinates——【贴图轴坐标系】
UVW Map——【UVW贴图编辑器】
UVW Xform——【UVW贴图参考变换编辑器】
Unwrap UVW——【展开贴图编辑器】
Camera Map——【相机贴图编辑器】
* Camera Map——【环境相机贴图编辑器】
Cache Tools——【捕捉工具】
Point Cache——【点捕捉编辑器】
Subdivision Surfaces——【表面细分】
MeshSmooth——【表面平滑编辑器】
HSDS Modifier——【分级细分编辑器】
Free Form Deformers——【自由变形工具】
FFD 2×2×2/FFD 3×3×3/FFD 4×4×4——【自由变形工具2×2×2/3×3×3/4×4×4】
FFD Box/FFD Cylinder——【盒体和圆柱体自由变形工具】
Parametric Deformers——【参数变形工具】
Bend——【弯曲】
Taper——【锥形化】
Twist——【扭曲】
Noise——【噪声】
Stretch——【缩放】
Squeeze——【压榨】
Push——【推挤】
Relax——【松弛】
Ripple——【波纹】
Wave——【波浪】
Skew——【倾斜】
Slice——【切片】
Spherify——【球形扭曲】
Affect Region——【面域影响】
Lattice——【栅格】
Mirror——【镜像】
Displace——【置换】
XForm——【参考变换】
Preserve——【保持】
Surface——【表面编辑】
Material——【材质变换】
Material By Element——【元素材质变换】
Disp Approx——【近似表面替换】
NURBS Editing——【NURBS面编辑】
NURBS Surface Select——【NURBS表面选择】
Surf Deform——【表面变形编辑器】
Disp Approx——【近似表面替换】
Radiosity Modifiers——【光能传递修改器】
Subdivide——【细分】
* Subdivide——【超级细分】
Character——【角色人物】
Create Character——【创建角色】
Destroy Character——【删除角色】
Lock/Unlock——【锁住与解锁】
Insert Character——【插入角色】
Save Character——【保存角色】
Bone Tools——【骨骼工具】
Set Skin Pose——【调整皮肤姿势】
Assume Skin Pose——【还原姿势】
Skin Pose Mode——【表面姿势模式】
Animation——【动画】
IK Solvers——【反向动力学】
HI Solver——【非历史性控制器】
HD Solver——【历史性控制器】
IK Limb Solver——【反向动力学肢体控制器】
SplineIK Solver——【样条反向动力控制器】
Constraints——【约束】
Attachment Constraint——【附件约束】
Surface Constraint——【表面约束】
Path Constraint——【路径约束】
Position Constraint——【位置约束】
Link Constraint——【连结约束】
LookAt Constraint——【视觉跟随约束】
Orientation Constraint——【方位约束】
Transform Constraint——【变换控制】
Link Constraint——【连接约束】
Position/Rotation/Scale——【PRS控制器】
Transform script——【变换控制脚本】
Position Controllers——【位置控制器】
Audio——【音频控制器】
Bezier——【贝塞尔曲线控制器】
expression_r——【表达式控制器】
Linear——【线性控制器】
Motion Capture——【动作捕捉】 Noise——【燥波控制器】
Quatermion(TCB)——【TCB控制器】
Reactor——【反应器】
Spring——【弹力控制器】
script——【脚本控制器】
XYZ——【XYZ位置控制器】
Attachment Constraint——【附件约束】
Path Constraint——【路径约束】
Position Constraint——【位置约束】
Surface Constraint——【表面约束】
Rotation Controllers——【旋转控制器】
Scale Controllers——【比例缩放控制器】
Custom Attribute——【加入用户属性】
Wire Parameters——【参数绑定】
Wire Parameters——【参数绑定】
Parameter Wiring Dialog——【参数绑定对话框】
Make Preview——【创建预视】
View Preview——【观看预视】
Rename Preview——【重命名预视】
Graph Editors——【图表编辑器】
Track View-Curve Editor——【轨迹窗曲线编辑器】
Track View-Dope Sheet——【轨迹窗拟定图表编辑器】
NEW Track View——【新建轨迹窗】
Delete Track View——【删除轨迹窗】
Saved Track View——【已存轨迹窗】
New Schematic View——【新建示意观察窗】
Delete Schematic View——【删除示意观察窗】
Saved Schematic View——【显示示意观察窗】
Rendering——【渲染】
Render——【渲染】
Environment——【环境】
Effects——【效果】
Advanced Lighting——【高级光照】
Render To Texture——【贴图渲染】
Raytracer Settings——【光线追踪设置】
Raytrace Global Include/Exclude——【光线追踪选择】
Activeshade Floater——【活动渲染窗口】
Activeshade Viewport——【活动渲染视窗】
Material Editor——【材质编辑器】
Material/Map
Browser——【材质/贴图浏览器】
Video Post——【视频后期制作】
Show Last Rendering——【显示最后渲染图片】
RAM Player——【RAM播放器】
Customize——【用户自定义】
Customize——【定制用户界面】
Load Custom UI Scheme——【加载自定义用户界面配置】
Save Custom UI Scheme——【保存自定义用户界面配置】
Revert to Startup Layout——【恢复初始界面】
Show UI——【显示用户界面】
mand Panel——【命令面板】
Toolbars Panel——【浮动工具条】
Main Toolbar——【主工具条】
Tab Panel——【标签面板】
Track Bar——【轨迹条】
Lock UI Layout——【锁定用户界面】
Configure Paths——【设置路径】
Units Setup——【单位设置】
Grid and Snap Settings——【栅格和捕捉设置】
Viewport Configuration——【视窗配置】
Plug-in Manager——【插件管理】
Preferences——【参数选择】
MAXscript——【MAX脚本】
New script——【新建脚本】
Open script——【打开脚本】
Run script——【运行脚本】
MAXscript Listener——【MAX脚本注释器】
Macro Recorder——【宏记录器】
Visual MAXscript Editer——【可视化MAX脚本编辑器】
Help——【帮助】
User Referebce——【用户参考】
MAXscript Referebce——【MAX脚本参考】
Tutorials——【教程】
Hotkey Map——【热键图】
Additional Help——【附加帮助】
3ds max on the Web——【3ds max网页】
Plug——【插件信息】
Authorize 3ds max——【授权】

阅读更多 >>>  linux固定中断号如何分配

3dmax的命令大全是什么?

字母键:
  A键:激活角度捕捉开关
  B键:切换到底视图
  C键:切换摄像机视图

  D键:当前视图暂时失效
  E键:旋转
  F键:切换到主视图(前视图)
  G键:切换视窗网格的显示
  H键:按名称选择物体
  I键:按鼠标所在的方向进行移动
  J键:切换物体被选择的外框(一种显示方式)
  K键:改变到后视图
  L键:切换到左视图
  M键:材质编辑器
  N键:激活动画记录器(自动记录关键帧)
  O键:在旋转视图时是否以方体方式显示
  P键:切换到透视图
  Q键:选择
  R键:缩放
  S键:激活捕捉按钮
  T键:切换到顶视图
  U键:切换到用户视图
  V键:视图选择菜单
  W键:移动
  X键:坐标手柄的显示
  Z键:使被选或未选物体以最佳角度显示
  --------------------------------------------------------------------
  数字键:
  1-6只有在加了可编辑命令情况下切换命令的子层级(此属于命令快捷键)
  6键:打开PF Source粒子设置面版
  7键:显示场景中物体的块面
  8键:打开环境面版
  9键:打开渲染面版
  0键:打开烘培面版
  
  F键:
  F1键:帮助
  F2键:被选择的面是否以线框或面的形式来显示
  F3键:物体切换线框或实体
  F4键:增强显示(物体外围是否显示线框)
  F5键:切换到坐标轴X
  F6键:切换到坐标轴Y
  F7键:切换到坐标轴Z
  F8键:切换XY,XZ,YZ坐标轴
  F9键:渲染
  F10键:渲染输出面版
  F11键:脚本编写框
  F12键:移动的属性面版
  --------------------------------------------------------------------
  其他单独按键:
  -键:缩小坐标轴(视觉效果)
  +键:放大坐标轴(视觉效果)
  Delete键:删除所选物体
  /键:播放动画
  
<键:后一帧
  >键:前一帧

  "键:激活动画记录器(手动记录关键帧)

  Home键:切换到第一帧

  End键:切换到最后一帧

  空格:对被选择的物体进行锁定

  [键:放大视图

  ]键:缩小视图

  介绍完了主界面各个键盘的功能后,下面再具体来说说3dmax软件中各大试图模块的快捷键:

  1、轨迹视图

  加入(Add)关键帧【A】

  前一时间单位【<】

  下一时间单位【>】

  编辑(Edit)关键帧模式【E】

  编辑区域模式【F3】

  编辑时间模式【F2】

  展开对象(Object)切换【O】

  展开轨迹(Track)切换【T】

  函数(Function)曲线模式【F5】或【F】

  锁定所选物体【空格】

  向上移动高亮显示【↓】

  向下移动高亮显示【↑】

  向左轻移关键帧【←】

  向右轻移关键帧【→】

  位置区域模式【F4】

  回到上一场景*作【Ctrl】+【A】

  撤消场景*作【Ctrl】+【Z】

  用前一次的配置进行渲染【F9】

  渲染配置【F10】

  向下收拢【Ctrl】+【↓】

  向上收拢【Ctrl】+【↑】

  

  2、材质编辑器

  用前一次的配置进行渲染【F9】

  渲染配置【F10】

  撤消场景*作【Ctrl】+【Z】

  --------------------------------------------------------------------

  3、示意(Schematic)视图

  下一时间单位【>】

  前一时间单位【<】

  回到上一场景制作【Ctrl】+【A】

  撤消场景制作【Ctrl】+【Z】

  绘制(Draw)区域【D】

  渲染(Render)【R】

  锁定工具栏(泊坞窗)【空格】

  --------------------------------------------------------------------

  4、视频编辑

  加入过滤器(Filter)项目【Ctrl】+【F】

  加入输入(Input)项目【Ctrl】+【I】

  加入图层(Layer)项目【Ctrl】+【L】

  加入输出(Output)项目【Ctrl】+【O】

  加入(Add)新的项目【Ctrl】+【A】

  加入场景(Scene)事件【Ctrl】+【s】

  编辑(Edit)当前事件【Ctrl】+【E】

  执行(Run)序列【Ctrl】+【R】

  新(New)的序列【Ctrl】+【N】

  撤消场景*作【Ctrl】+【Z】

  --------------------------------------------------------------------

  5、NURBS编辑

  CV约束法线(Normal)移动【Alt】+【N】

  CV约束到U向移动【Alt】+【U】

  CV约束到V向移动【Alt】+【V】

  显示曲线(Curves)【Shift】+【Ctrl】+【C】

  显示控制点(Dependents)【Ctrl】+【D】

  显示格子(Lattices)【Ctrl】+【L】

  NURBS面显示方式切换【Alt】+【L】

  显示表面(Surfaces)【Shift】+【Ctrl】+【s】

  显示工具箱(Toolbox)【Ctrl】+【T】

  显示表面整齐(Trims)【Shift】+【Ctrl】+【T】

  根据名字选择本物体的子层级【Ctrl】+【H】

  锁定2D所选物体【空格】

  选择U向的下一点【Ctrl】+【→】

  选择V向的下一点【Ctrl】+【↑】

  选择U向的前一点【Ctrl】+【←】

  选择V向的前一点【Ctrl】+【↓】

  根据名字选择子物体【H】

  柔软所选物体【Ctrl】+【s】

  转换到CurveCV层级【Alt】+【Shift】+【Z】

  转换到Curve层级【Alt】+【Shift】+【C】

  转换到Imports层级【Alt】+【Shift】+【I】

  转换到Point层级【Alt】+【Shift】+【P】

  转换到SurfaceCV层级【Alt】+【Shift】+【V】

  转换到Surface层级【Alt】+【Shift】+【S】

  转换到上一层级【Alt】+【Shift】+【T】

  转换降级【Ctrl】+【X】

  

  6、FFD

  转换到控制点(ControlPoint)层级【Alt】+【Shift】+【C】

  到格点(Lattice)层级【Alt】+【Shift】+【L】

  到设置体积(Volume)层级【Alt】+【Shift】+【S】

  转换到上层级【Alt】+【Shift】+【T】

  --------------------------------------------------------------------

  7、UVW贴图

  进入编辑(Edit)UVW模式【Ctrl】+【E】

  调用*.uvw文件【Alt】+【Shift】+【Ctrl】+【L】

  保存UVW为*.uvw格式的文件【Alt】+【Shift】+【Ctrl】+【S】

  打断(Break)选择点【Ctrl】+【B】

  分离(Detach)边界点【Ctrl】+【D】

  过滤选择面【Ctrl】+【空格】

  水平翻转【Alt】+【Shift】+【Ctrl】+【B】

  垂直(Vertical)翻转【Alt】+【Shift】+【Ctrl】+【V】

  冻结(Freeze)所选材质点【Ctrl】+【F】

  隐藏(Hide)所选材质点【Ctrl】+【H】

  全部解冻(unFreeze)【Alt】+【F】

  全部取消隐藏(unHide)【Alt】+【H】

  从堆栈中获取面选集【Alt】+【Shift】+【Ctrl】+【F】

  从面获取选集【Alt】+【Shift】+【Ctrl】+【V】

  锁定所选顶点【空格】

  水平镜象【Alt】+【Shift】+【Ctrl】+【N】

  垂直镜象【Alt】+【Shift】+【Ctrl】+【M】

  水平移动【Alt】+【Shift】+【Ctrl】+【J】

  垂直移动【Alt】+【Shift】+【Ctrl】+【K】

  平移视图【Ctrl】+【P】

  象素捕捉【S】

  平面贴图面/重设UVW【Alt】+【Shift】+【Ctrl】+【R】

  水平缩放【Alt】+【Shift】+【Ctrl】+【I】

  垂直缩放【Alt】+【Shift】+【Ctrl】+【O】

  移动材质点【Q】

  旋转材质点【W】

  等比例缩放材质点【E】

  焊接(Weld)所选的材质点【Alt】+【Ctrl】+【W】

  焊接(Weld)到目标材质点【Ctrl】+【W】

  Unwrap的选项(Options)【Ctrl】+【O】

  更新贴图(Map)【Alt】+【Shift】+【Ctrl】+【M】

  将Unwrap视图扩展到全部显示【Alt】+【Ctrl】+【Z】

  框选放大Unwrap视图【Ctrl】+【Z】

  将Unwrap视图扩展到所选材质点的大小【Alt】+【Shift】+【Ctrl】+【Z】

  缩放到Gizmo大小【Shift】+【空格】

  缩放(Zoom)工具【Z】

  --------------------------------------------------------------------

  8、反应堆(Reactor)

  建立(Create)反应(Reaction)【Alt】+【Ctrl】+【C】

  删除(Delete)反应(Reaction)【Alt】+【Ctrl】+【D】

  编辑状态(State)切换【Alt】+【Ctrl】+【s】

  设置最大影响(Influence)【Ctrl】+【I】

  设置最小影响(Influence)【Alt】+【I】

?
</键:后一帧

网站数据信息

"reactor模式详解,java Nio读写为什么是双向"浏览人数已经达到25次,如你需要查询该站的相关权重信息,可以点击进入"Chinaz数据" 查询。更多网站价值评估因素如:reactor模式详解,java Nio读写为什么是双向的访问速度、搜索引擎收录以及索引量、用户体验等。 要评估一个站的价值,最主要还是需要根据您自身的需求,如网站IP、PV、跳出率等!