其实按键精灵多线程有几种实现方法的问题并不复杂,但是又很多的朋友都不太了解线程不建议stop,因此呢,今天小编就来为大家分享按键精灵多线程有几种实现方法的一些知识,希望可以帮助到大家,下面我们一起来看看这个问题的分析吧!
本文目录
线程池处理流程是什么
提示
请带着这些问题继续后文,会很大程度上帮助你更好的理解相关知识点。@pdai
为什么要有线程池?Java是实现和管理线程池有哪些方式?请简单举例如何使用。为什么很多公司不允许使用Executors去创建线程池?那么推荐怎么使用呢?ThreadPoolExecutor有哪些核心的配置参数?请简要说明ThreadPoolExecutor可以创建哪是哪三种线程池呢?当队列满了并且worker的数量达到maxSize的时候,会怎么样?说说ThreadPoolExecutor有哪些RejectedExecutionHandler策略?默认是什么策略?简要说下线程池的任务执行机制?execute–>addWorker–>runworker(getTask)线程池中任务是如何提交的?线程池中任务是如何关闭的?在配置线程池的时候需要考虑哪些配置因素?如何监控线程池的状态?为什么要有线程池线程池能够对线程进行统一分配,调优和监控:
降低资源消耗(线程无限制地创建,然后使用完毕后销毁)提高响应速度(无须创建线程)提高线程的可管理性ThreadPoolExecutor例子Java是如何实现和管理线程池的?
从JDK5开始,把工作单元与执行机制分离开来,工作单元包括Runnable和Callable,而执行机制由Executor框架提供。
WorkerThreadSimpleThreadPool
程序中我们创建了固定大小为五个工作线程的线程池。然后分配给线程池十个工作,因为线程池大小为五,它将启动五个工作线程先处理五个工作,其他的工作则处于等待状态,一旦有工作完成,空闲下来工作线程就会捡取等待队列里的其他工作进行执行。
这里是以上程序的输出。
输出表明线程池中至始至终只有五个名为"pool-1-thread-1"到"pool-1-thread-5"的五个线程,这五个线程不随着工作的完成而消亡,会一直存在,并负责执行分配给线程池的任务,直到线程池消亡。
Executors类提供了使用了ThreadPoolExecutor的简单的ExecutorService实现,但是ThreadPoolExecutor提供的功能远不止于此。我们可以在创建ThreadPoolExecutor实例时指定活动线程的数量,我们也可以限制线程池的大小并且创建我们自己的RejectedExecutionHandler实现来处理不能适应工作队列的工作。
这里是我们自定义的RejectedExecutionHandler接口的实现。
RejectedExecutionHandlerImpl.javaThreadPoolExecutor提供了一些方法,我们可以使用这些方法来查询executor的当前状态,线程池大小,活动线程数量以及任务数量。因此我是用来一个监控线程在特定的时间间隔内打印executor信息。
MyMonitorThread.java这里是使用ThreadPoolExecutor的线程池实现例子。
WorkerPool.java注意在初始化ThreadPoolExecutor时,我们保持初始池大小为2,最大池大小为4而工作队列大小为2。因此如果已经有四个正在执行的任务而此时分配来更多任务的话,工作队列将仅仅保留他们(新任务)中的两个,其他的将会被RejectedExecutionHandlerImpl处理。
上面程序的输出可以证实以上观点。
注意executor的活动任务、完成任务以及所有完成任务,这些数量上的变化。我们可以调用shutdown()方法来结束所有提交的任务并终止线程池。
ThreadPoolExecutor使用详解其实java线程池的实现原理很简单,说白了就是一个线程集合workerSet和一个阻塞队列workQueue。当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中。workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行。
Execute原理当一个任务提交至线程池之后:
线程池首先当前运行的线程数量是否少于corePoolSize。如果是,则创建一个新的工作线程来执行任务。如果都在执行任务,则进入2.判断BlockingQueue是否已经满了,倘若还没有满,则将线程放入BlockingQueue。否则进入3.如果创建一个新的工作线程将使当前运行的线程数量超过maximumPoolSize,则交给RejectedExecutionHandler来处理任务。当ThreadPoolExecutor创建新线程时,通过CAS来更新线程池的状态ctl.
参数corePoolSize线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize,即使有其他空闲线程能够执行新来的任务,也会继续创建线程;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。workQueue用来保存等待被执行的任务的阻塞队列.在JDK中提供了如下阻塞队列:具体可以参考JUC集合:BlockQueue详解ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;LinkedBlockingQueue:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQueue;SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue;PriorityBlockingQueue:具有优先级的无界阻塞队列;LinkedBlockingQueue比ArrayBlockingQueue在插入删除节点性能方面更优,但是二者在put(),take()任务的时均需要加锁,SynchronousQueue使用无锁算法,根据节点的状态判断执行,而不需要用到锁,其核心是Transfer.transfer().
maximumPoolSize线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;当阻塞队列是无界队列,则maximumPoolSize则不起作用,因为无法提交至核心线程池的线程会一直持续地放入workQueue.keepAliveTime线程空闲时的存活时间,即当线程没有任务执行时,该线程继续存活的时间;默认情况下,该参数只在线程数大于corePoolSize时才有用,超过这个时间的空闲线程将被终止;unitkeepAliveTime的单位threadFactory创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名。默认为DefaultThreadFactoryhandler线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:AbortPolicy:直接抛出异常,默认策略;CallerRunsPolicy:用调用者所在的线程来执行任务;DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;DiscardPolicy:直接丢弃任务;当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
三种类型newFixedThreadPool线程池的线程数量达corePoolSize后,即使线程池没有可执行任务时,也不会释放线程。
FixedThreadPool的工作队列为无界队列LinkedBlockingQueue(队列容量为Integer.MAX_VALUE),这会导致以下问题:
线程池里的线程数量不超过corePoolSize,这导致了maximumPoolSize和keepAliveTime将会是个无用参数由于使用了无界队列,所以FixedThreadPool永远不会拒绝,即饱和策略失效newSingleThreadExecutor初始化的线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行.
由于使用了无界队列,所以SingleThreadPool永远不会拒绝,即饱和策略失效
newCachedThreadPool线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列;和newFixedThreadPool创建的线程池不同,newCachedThreadPool在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销;执行过程与前两种稍微不同:
主线程调用SynchronousQueue的offer()方法放入task,倘若此时线程池中有空闲的线程尝试读取SynchronousQueue的task,即调用了SynchronousQueue的poll(),那么主线程将该task交给空闲线程.否则执行(2)当线程池为空或者没有空闲的线程,则创建新的线程执行任务.执行完任务的线程倘若在60s内仍空闲,则会被终止.因此长时间空闲的CachedThreadPool不会持有任何线程资源.关闭线程池遍历线程池中的所有线程,然后逐个调用线程的interrupt方法来中断线程.
关闭方式-shutdown将线程池里的线程状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程.
关闭方式-shutdownNow将线程池里的线程状态设置成STOP状态,然后停止所有正在执行或暂停任务的线程.只要调用这两个关闭方法中的任意一个,isShutDown()返回true.当所有任务都成功关闭了,isTerminated()返回true.
ThreadPoolExecutor源码详解几个关键属性内部状态其中AtomicInteger变量ctl的功能非常强大:利用低29位表示线程池中线程数,通过高3位表示线程池的运行状态:
RUNNING:-1<<COUNT_BITS,即高3位为111,该状态的线程池会接收新任务,并处理阻塞队列中的任务;SHUTDOWN:0<<COUNT_BITS,即高3位为000,该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;STOP:1<<COUNT_BITS,即高3位为001,该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;TIDYING:2<<COUNT_BITS,即高3位为010,所有的任务都已经终止;TERMINATED:3<<COUNT_BITS,即高3位为011,terminated()方法已经执行完成任务的执行execute–>addWorker–>runworker(getTask)
线程池的工作线程通过Woker类实现,在ReentrantLock锁的保证下,把Woker实例插入到HashSet后,并启动Woker中的线程。从Woker类的构造方法实现可以发现:线程工厂在创建线程thread时,将Woker实例本身this作为参数传入,当执行start方法启动线程thread时,本质是执行了Worker的runWorker方法。firstTask执行完成之后,通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;
execute()方法ThreadPoolExecutor.execute(task)实现了Executor.execute(task)
为什么需要doublecheck线程池的状态?在多线程环境下,线程池的状态时刻在变化,而ctl.get()是非原子操作,很有可能刚获取了线程池状态后线程池状态就改变了。判断是否将command加入workque是线程池之前的状态。倘若没有doublecheck,万一线程池处于非running状态(在多线程环境下很有可能发生),那么command永远不会执行。
addWorker方法从方法execute的实现可以看出:addWorker主要负责创建新的线程并执行任务线程池创建新线程执行任务时,需要获取全局锁:
Worker类的runworker方法继承了AQS类,可以方便的实现工作线程的中止操作;实现了Runnable接口,可以将自身作为一个任务在工作线程中执行;当前提交的任务firstTask作为参数传入Worker的构造方法;一些属性还有构造方法:
runWorker方法是线程池的核心:
线程启动之后,通过unlock方法释放锁,设置AQS的state为0,表示运行可中断;Worker执行firstTask或从workQueue中获取任务:进行加锁操作,保证thread不被其他线程中断(除非线程池被中断)检查线程池状态,倘若线程池处于中断状态,当前线程将中断。执行beforeExecute执行任务的run方法执行afterExecute方法解锁操作通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;
getTask方法
下面来看一下getTask()方法,这里面涉及到keepAliveTime的使用,从这个方法我们可以看出线程池是怎么让超过corePoolSize的那部分worker销毁的。
注意这里一段代码是keepAliveTime起作用的关键:
allowCoreThreadTimeOut为false,线程即使空闲也不会被销毁;倘若为ture,在keepAliveTime内仍空闲则会被销毁。
如果线程允许空闲等待而不被销毁timed==false,workQueue.take任务:如果阻塞队列为空,当前线程会被挂起等待;当队列中有任务加入时,线程被唤醒,take方法返回任务,并执行;
如果线程不允许无休止空闲timed==true,workQueue.poll任务:如果在keepAliveTime时间内,阻塞队列还是没有任务,则返回null;
任务的提交submit任务,等待线程池execute执行FutureTask类的get方法时,会把主线程封装成WaitNode节点并保存在waiters链表中,并阻塞等待运行结果;FutureTask任务执行完成后,通过UNSAFE设置waiters相应的waitNode为null,并通过LockSupport类unpark方法唤醒主线程;在实际业务场景中,Future和Callable基本是成对出现的,Callable负责产生结果,Future负责获取结果。
Callable接口类似于Runnable,只是Runnable没有返回值。Callable任务除了返回正常结果之外,如果发生异常,该异常也会被返回,即Future可以拿到异步执行任务各种结果;Future.get方法会导致主线程阻塞,直到Callable任务执行完成;submit方法AbstractExecutorService.submit()实现了ExecutorService.submit()可以获取执行完的返回值,而ThreadPoolExecutor是AbstractExecutorService.submit()的子类,所以submit方法也是ThreadPoolExecutor`的方法。
通过submit方法提交的Callable任务会被封装成了一个FutureTask对象。通过Executor.execute方法提交FutureTask到线程池中等待被执行,最终执行的是FutureTask的run方法;
FutureTask对象publicclassFutureTask<V>implementsRunnableFuture<V>可以将FutureTask提交至线程池中等待被执行(通过FutureTask的run方法来执行)
内部状态内部状态的修改通过sun.misc.Unsafe修改
get方法内部通过awaitDone方法对主线程进行阻塞,具体实现如下:
如果主线程被中断,则抛出中断异常;
判断FutureTask当前的state,如果大于COMPLETING,说明任务已经执行完成,则直接返回;如果当前state等于COMPLETING,说明任务已经执行完,这时主线程只需通过yield方法让出cpu资源,等待state变成NORMAL;通过WaitNode类封装当前线程,并通过UNSAFE添加到waiters链表;最终通过LockSupport的park或parkNanos挂起线程;run方法
FutureTask.run方法是在线程池中被执行的,而非主线程
通过执行Callable任务的call方法;如果call执行成功,则通过set方法保存结果;如果call执行有异常,则通过setException保存异常;任务的关闭shutdown方法会将线程池的状态设置为SHUTDOWN,线程池进入这个状态后,就拒绝再接受任务,然后会将剩余的任务全部执行完
shutdownNow做的比较绝,它先将线程池状态设置为STOP,然后拒绝所有提交的任务。最后中断左右正在运行中的worker,然后清空任务队列。
更深入理解
为什么线程池不允许使用Executors去创建?推荐方式是什么?线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors各个方法的弊端:
newFixedThreadPool和newSingleThreadExecutor:??主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。newCachedThreadPool和newScheduledThreadPool:??主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。推荐方式1首先引入:commons-lang3包
推荐方式2首先引入:com.google.guava包
推荐方式3spring配置线程池方式:自定义线程工厂bean需要实现ThreadFactory,可参考该接口的其它默认实现类,使用方式直接注入bean调用execute(Runnabletask)方法即可
配置线程池需要考虑因素
从任务的优先级,任务的执行时间长短,任务的性质(CPU密集/IO密集),任务的依赖关系这四个角度来分析。并且近可能地使用有界的工作队列。
性质不同的任务可用使用不同规模的线程池分开处理:
CPU密集型:尽可能少的线程,Ncpu+1IO密集型:尽可能多的线程,Ncpu*2,比如数据库连接池混合型:CPU密集型的任务与IO密集型任务的执行时间差别较小,拆分为两个线程池;否则没有必要拆分。监控线程池的状态可以使用ThreadPoolExecutor以下方法:
getTaskCount()Returnstheapproximatetotalnumberoftasksthathaveeverbeenscheduledforexecution.getCompletedTaskCount()Returnstheapproximatetotalnumberoftasksthathavecompletedexecution.返回结果少于getTaskCount()。getLargestPoolSize()Returnsthelargestnumberofthreadsthathaveeversimultaneouslybeeninthepool.返回结果小于等于maximumPoolSizegetPoolSize()Returnsthecurrentnumberofthreadsinthepool.getActiveCount()Returnstheapproximatenumberofthreadsthatareactivelyexecutingtasks.参考文章《Java并发编程艺术》https://www.jianshu.com/p/87bff5cc8d8chttps://blog.csdn.net/programmer_at/article/details/79799267https://blog.csdn.net/u013332124/article/details/79587436https://www.journaldev.com/1069/threadpoolexecutor-java-thread-pool-example-executorservice由于问答代码块插入受限,部分代码未完全展示,若有需要可阅读原文:戳我阅读原文
Linux内核是如何创建线程的,它与windows有哪些不同
谢邀。
其实Linux创建进程,就是创建进程运行所需的内存空间,填充描述进程的task_struct结构体,以及加载进程的程序而已。
Linux内核并无专门创建线程的机制我们之前提到,Linux并不特殊对待线程,在Linux看来,线程不过就是一种特殊的进程而已。那么,Linux是如何创建线程的呢?
线程机制是大多数现代编程语言都会提供的机制,该机制允许在同一进程的共享内存地址空间运行一组“特殊的进程(即线程)”。这些线程不仅共享同一段内存空间,还可以共享已经打开的文件,统计量等其他资源。线程机制支持程序并发运行,在多处理器核心的系统上,该并发机制能够实现多条线程同时运行。
Linux管理线程的方式不同于其他一些经典操作系统,Linux并没有线程的概念,它把线程当作进程的一个子集来管理。因此,Linux内核并未为线程提供额外调度算法,也没有提供额外的数据结构用于描述和存储线程。
就像进程一样,Linux使用task_struct结构体描述和记录线程,每个线程都有唯一属于自己的task_struct结构。从这个角度来看,线程就是一个普通的进程,只不过线程可能和其他进程共享一些资源而已。
以Windows为代表的一些操作系统提供了专门用于创建线程的机制,在这些系统中,线程常常被称作“轻量级进程”,因为相对于进程而言,线程耗费的资源较少,能够较为迅速的创建和投入运行。
但是对于Linux而言,线程不过是进程之间共享资源的一种手段罢了。那么是不是Linux中的线程比Windows中的线程更加“重量级”呢?也不是,因为Linux中的进程本身就很轻量级,Linux创建进程所需时间,并不比Windows创建线程所需时间多多少。
从C语言代码层面来看,假设某个进程包含4个线程,以Windows为代表的一些操作系统一般会有一个包含指向4个不同线程的指针的进程描述符,负责描述地址空间、打开的文件等共享资源,而线程本身再去描述自己独占的资源。
与之对应的,Linux的做法就高雅许多,它仅需为这4个线程创建4个task_struct结构体,然后在task_struct中指定它们共享的资源就可以了。
创建线程看了我最近几篇文章的读者应该已经明白,Linux内核中的线程其实就是进程,因此线程的创建与进程的创建过程是类似的,从C语言源代码层面看,基本上也是通过fork()函数和exec()函数族实现的。只不过在调用clone()函数时需要传递一个参数用于描述共享资源,例如:
上面这行C语言代码和调用fork()函数的结果差不多,只不过输入的几个参数标志位说明了子进程与父进程共享一些资源:地址空间、文件系统、打开的文件、信号处理程序。
对比一下,fork()基本上就相当于clone(SIGCHLD,0),这也是fork()函数创建的子进程之后不再与父进程共享资源的原因。
关于clone()函数的参数标志位,可以在Linux中输入man命令查看。
Linux内核线程就像用户空间的C语言程序开发一样,Linux内核也经常需要在后台处理数据,这时就需要借助内核线程了。Linux的内核线程一般不会独立的地址空间,它们只在内核空间运行,不会切换到用户空间。不过调度是和普通进程一样的,可以被调度和抢占。
Linux创建内核线程由kthread_create()函数实现,它的C语言源代码如下,请看:
可见,kthread_create()函数的C语言代码并不长,而且也可以看出,Linux内核线程是通过kthread_create_info结构体描述的,它的定义C语言代码如下,可见,内核线程的描述和存储也是包含task_struct结构体的:
kthread_create()函数创建名为namefmt的线程,不过线程被创建后是处于不可运行状态的,我们可以通过wake_up_process()函数唤醒它。当然,也可以通过kthread_run()方法实现这一过程,相关的C语言代码如下,请看:
其实就是将kthread_create()函数和wake_up_process()函数组合到一起而已。Linux的内核线程被启动后,会一直运行到调用do_exit()退出。我们也可以调用kthread_stop()函数提前结束它,相关的C语言代码如下,请看:
kthread_stop()函数接收的参数为kthread_create()函数创建的结构体的task_struct成员。从C语言代码可以看出,kthread_stop()其实也是会调用wake_up_process()函数唤醒线程的,它在唤醒线程后,会等待线程函数退出,并不会调用threadfn()函数。
这里需要注意,如果创建的线程函数threadfn()调用了do_exit()函数,最好就不要再调用kthread_stop()函数了。
kthread_stop()函数等待线程退出是通过wait_for_completion()函数实现的,相关的C语言代码如下,请看:
稍稍跟踪一下C语言代码,发现其实这一等待过程是由do_wait_for_common()函数实现的,它的C语言代码如下,请看:
还是比较清晰的,这里就不再赘述了。至此,我们就了解了Linux内核是如何创建线程并投入运行,以及如何结束内核线程的了。
小结本节主要讨论了Linux内核中的线程的创建,应该能够看出,其实核心还是围绕对task_struct结构的管理,这与管理进程并无过多区别。因此,说Linux中的线程只是一种特殊的进程,一点也不为过。
multi行程怎么用
"multi行程"的具体含义不明确,一般需要根据上下文进一步理解。以下是可能的几种情况和对应的解答:
1.如果您在问多段行程如何规划或预订:您可以在旅行社或在线旅游平台上搜索多段行程(multi-stoptrip)或多城市行程(multi-citytrip),然后根据您的需求和预算选择适合的航班和酒店。规划时需要考虑飞行时间、转机时间、时间差、行程安排等因素。
2.如果您在问多个行程如何协调和安排:您可以使用电子日历或待办事项应用程序,将所有行程和活动设置为自己的时间表,并注意调整安排时间,以避免时间冲突和紧迫性。还可以使用各种出行工具和应用程序,例如城市地图、天气预报、语音指导应用程序等。
3.如果您在问多线程编程(multi-threading)的用法:"multi行程"可能指多个线程同时运行,以实现更高效的计算、通信、数据处理等操作。在编写多线程程序时,您需要了解并发编程的各种原理和技术,例如锁定、信号量、条件变量、互斥量等。
请详细描述您的问题,以便我们能够更好地回答你的问题。
按键精灵多线程有几种实现方法
按键精灵提供的多线程功能有5个要用到的命令,下面我们一一介绍:
[DimEnv]
解释:定义环境变量。
备注:用于多线程之间传递变量值功能,仍在试验阶段,不推荐新手使用。
[BeginThread]
格式:{返回线程ID}=BeginThread(过程名)
解释:启动某个线程。
备注:用于多线程功能,仍在试验阶段,不推荐新手使用。
[StopThread]
格式:StopThread{线程ID}
解释:停止某个线程。
备注:用于多线程功能,仍在试验阶段,不推荐新手使用。
[PauseThread]
格式:PauseThread{线程ID}
解释:暂停某个线程。
备注:用于多线程功能,仍在试验阶段,不推荐新手使用。
[ContinueThread]
格式:ContinueThread{线程ID}
解释:继续某个线程。
备注:用于多线程功能,仍在试验阶段,不推荐新手使用。
例子如下
Globalkey,Hwnd1,Hwnd2,Hwnd3
RunApp"notepad.exe"
RunApp"notepad.exe"
RunApp"notepad.exe"
Delay1000
HwndEx=Plugin.Window.Search("记事本")
arr=split(HwndEx,"|")
Hwnd1=Plugin.Window.FindEx(arr(0),0,"Edit",0)
Hwnd2=Plugin.Window.FindEx(arr(1),0,"Edit",0)
Hwnd3=Plugin.Window.FindEx(arr(2),0,"Edit",0)
Do
key=WaitKey()
线程1=BeginThread(按键1)
线程2=BeginThread(按键2)
线程3=BeginThread(按键3)
Loop
Sub按键1
callPlugin.Bkgnd.KeyPress(Hwnd1,key)
EndSub
Sub按键2
callPlugin.Bkgnd.KeyPress(Hwnd2,key)
EndSub
Sub按键3
callPlugin.Bkgnd.KeyPress(Hwnd3,key)
EndSub
OK,本文到此结束,希望对大家有所帮助。