精华内容
下载资源
问答
  • 前文说过,子进程创建之后,父子进程究竟谁先运行是由调度器说了算。 但是,谁先结束呢?一般来说肯定是要让子进程先结束,想一想我们的bash,bash是所有命令的父进程,你总不能让bash先挂吧 其实之所以让子进程先...


    前文说过,子进程被创建之后,父子进程究竟谁先运行是由调度器说了算。
    但是,谁先结束呢?一般来说肯定是要让子进程先结束,想一想我们的bash,bash是所有命令的父进程,你总不能让bash先挂吧
    其实之所以让子进程先退出,是因为父进程容易对子进程进行管理,而且子进程被创建的原因就是因为父进程想要让子进程帮助自己完成一些业务,因此它要拿到结果。因此父进程一般需要等待子进程

    (1)为什么子进程需要被等待

    既然子进程要先退出,那么子进程退出后就变成了僵尸状态,一旦变成僵尸状态,这个子进程就如同僵尸一样,杀也杀不死(因为它已经死了),所以它必须需要让父进程读取到它的状态,回收子进程信息。只有这样,子进程才能得到“救赎”,“魂魄”才能归天

    (2)等待进程的方法

    A:wait方法

    这篇文章中讲过一个例子,通过那个例子我们看到了子进程死去之后变为了僵尸状态

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    int main()
    {
       // printf("还没执行fork函数时的本进程为:%d\n",getpid());
        pid_t ret=fork();//其返回值类型是pid_t型的
        sleep(1);
        if(ret>0)//父进程返回的是子进程ID
        {
          while(1)
          {
            printf("----------------------------------------------------\n");
            printf("父进程一直在运行\n");
            sleep(1);
          }
        }
        else if(ret==0)//子进程fork返回是0
        {
          int count=0;
          while(count<=10)
          {
            printf("子进程已经运行了%d秒\n",count+=1);
            sleep(1);
          }
          exit(0);//让子进程运行10s
        }
        else
          printf("进程创建失败\n");
    
        sleep(1);
        return 0;
    }
    
    
    

    使用一个脚本动态查看

    while :; do ps axj | head -1 && ps axj | grep a.out | grep -v grep;sleep 1;echo "###########";done
    
    

    在这里插入图片描述
    修改代码如下,子进程在10s后退出,父进程在10s后继续运行,运行至第15s,跳出循环,加入语句wailt(NULL)以回收子进程

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    int main()
    {
       // printf("还没执行fork函数时的本进程为:%d\n",getpid());
        pid_t ret=fork();//其返回值类型是pid_t型的
        sleep(1);
        if(ret>0)//父进程返回的是子进程ID
        {
    	  int count=1;
          while(count<=15)
          {
            printf("----------------------------------------------------\n");
            printf("父进程运行了%d秒\n",count);
    		count++;
            sleep(1);
          }
    	  wait(NULL);//回收子进程
        }
        else if(ret==0)//子进程fork返回是0
        {
          int count=1;
          while(count<=10)
          {
            printf("子进程已经运行了%d秒\n",count);
    		count++;
            sleep(1);
          }
          exit(0);//让子进程运行10s
        }
        else
          printf("进程创建失败\n");
    
        sleep(1);
        return 0;
    }
    
    
    

    如下可以发现,当父进程将子进程回收后,僵尸进程也消失了
    在这里插入图片描述
    如果父进程里只写上wait(NULL),那么就表示父进程阻塞在这里,等着子进程死亡,回收子进程

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    int main()
    {
       // printf("还没执行fork函数时的本进程为:%d\n",getpid());
        pid_t ret=fork();//其返回值类型是pid_t型的
        sleep(1);
        if(ret>0)//父进程返回的是子进程ID
        {
    		printf("父进程正在等待子进程死亡\n");
    		wait(NULL);//进程阻塞
    		printf("子进程已经死亡,父进程退出\n");
    		exit(0);
    	}
        else if(ret==0)//子进程fork返回是0
        {
          int count=1;
          while(count<=10)
          {
            printf("子进程已经运行了%d秒\n",count);
    		count++;
            sleep(1);
          }
          exit(0);//让子进程运行10s
        }
        else
          printf("进程创建失败\n");
    
        sleep(1);
        return 0;
    }
    
    
    

    效果如下
    在这里插入图片描述

    B:waitpid方法

    1:基本使用

    这是waitpid的函数原型:pid_ t waitpid(pid_t pid, int *status, int options); 其中形参pid_t pid就是需要等待的子进程的pid(如果设置为-1,表示等待任意一个子进程,其实就等同于了wait),当等待成功时,函数的返回值就是子进程的PID

    如下,修改上面的代码,使用waitpid等待子进程(waitpid后面的两个参数先用NULL和0),然后判断waitpid的返回值和字进程PID是否相等,如果相等,则输出“等待成功”,否则输出“等待失败”。

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    int main()
    {
       // printf("还没执行fork函数时的本进程为:%d\n",getpid());
        pid_t ret=fork();//其返回值类型是pid_t型的
        sleep(1);
        if(ret>0)//父进程返回的是子进程ID
        {
    		printf("父进程正在等待子进程死亡\n");
    		pid_t rec=waitpid(ret,NULL,0);//等待子进程,阻塞
    		if(rec==ret)//如果返回值是子进程id,等待成功
    		{
    			printf("等待成功\n");
    			exit(0);
    		}
    		else
    		{
    			printf("等待失败\n");
    			exit(0);
    		}
    			
    		
    	}
        else if(ret==0)//子进程fork返回是0
        {
          int count=1;
          while(count<=10)
          {
            printf("子进程[%d]已经运行了%d秒\n",getpid(),count);
    		count++;
            sleep(1);
          }
          exit(0);//让子进程运行10s
        }
        else
          printf("进程创建失败\n");
    
        sleep(1);
        return 0;
    }
    
    
    

    在这里插入图片描述
    2:重点理解第二个参数int* status

    首先大家可以发现这个参数比较特殊,需要注意的是这个参数wait也有,它是一个指针,是一个输出型参数。在外面定义一个变量,然后把这个变量的地址传给waitpid(如果传入NULL表示不关心子进程的退出状态信息),然后操作系统会根据这个变量的值,将子进程的退出信息反馈给父进程

    这个status在理解的时候稍微有点难度。对于一个进程来说,如果不要管它结果对不对,那么退出时只有两种状态:一种是正常退出(操作系统没有发送信号)另外一种就是异常退出(操作系统发送了终止信号)
    而这个status参数的目的就是为了判断你这个进程究竟是异常退出还是正常退出,如果是正常退出再看一下你的退出码是否是0,如果不是0,那么根据我业务的相关信息进行判断,报出相应的错误

    在32位环境中,int有4个字节,32个比特位,为了判断状态,只研究status的最后两个字节,也就是低16位。在低16位中,前高8位保存子进程的退出状态,低八位记录接受到的信号值
    在这里插入图片描述

    • 大家还记得kill 的参数列表吗,它的范围是1-64,你可以理解为不管是否正常退出操作系统都在发送信号,只不过正常时发送的是0,异常时发送的是非0
      在这里插入图片描述

    如下程序,子进程设置其退出码为exit(3)

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    int main()
    {
        pid_t ret=fork();
        sleep(1);
        if(ret>0)
        {
    		printf("父进程正在等待子进程死亡\n");
    		int st;//st传地址进去
    		pid_t rec=waitpid(ret,&st,0);//阻塞
    		if(rec==ret)//如果返回值是子进程id,等待成功
    		{
    			printf("等待成功\n");
    			printf("st是%d\n",st);//查看st结果
    			exit(0);
    		}
    		else
    		{
    			printf("等待失败\n");
    			exit(0);
    		}
    			
    		
    	}
        else if(ret==0)//子进程fork返回是0
        {
          int count=1;
          while(count<=10)
          {
            printf("子进程[%d]已经运行了%d秒\n",getpid(),count);
    		count++;
            sleep(1);
          }
          exit(3);//让子进程运行10s
        }
        else
          printf("进程创建失败\n");
    
        sleep(1);
        return 0;
    }
    
    
    

    结果如下
    在这里插入图片描述
    其结果为768,输出的并不是3。我们可以这样想象, st用来判断:进程是否异常退出,如果正常退出结果又是否正确? 所以其传入时低16位默认就将其设为00000000 00000000,也就是默认正常。上面的例子中,并没有出现逻辑上的大问题,只是我的退出码是3,我想要说明这个进程是正常退出的只是结果有误。前面说过,高8位是子进程的退出状态,而3的二进制是11,将其“贴合”在高八位上,就变成了00000011 00000000,而其结果正好就是768。

    那么怎么输出真实的退出码呢,这其实就是C语言的知识了,只需把结果右移8,为了保证结果正确,再和0xff(11111111)进行与运算,也即是st>>8&0xff
    在这里插入图片描述
    现在我们故意再次犯一个“浮点异常”的错误,代码如下

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    int main()
    {
        pid_t ret=fork();
        sleep(1);
        if(ret>0)
        {
    		printf("父进程正在等待子进程死亡\n");
    		int st;//st传地址进去
    		pid_t rec=waitpid(ret,&st,0);//阻塞
    		if(rec==ret)//如果返回值是子进程id,等待成功
    		{
    			printf("等待成功\n");
    			printf("st是%d\n",st);//查看st结果
    			exit(0);
    		}
    		else
    		{
    			printf("等待失败\n");
    			exit(0);
    		}
    			
    		
    	}
        else if(ret==0)//子进程fork返回是0
        {
          int count=1;
          while(count<=10)
          {
            printf("子进程[%d]已经运行了%d秒\n",getpid(),count);
    		count++;
            sleep(1);
          }
          int x=1/0;//浮点异常
          exit(0);
        }
        else
          printf("进程创建失败\n");
    
        sleep(1);
        return 0;
    }
    
    

    结果如下,其状态返回码是8,而之前咋们说过kill -8发送的就是浮点异常错误信息,而8的二进制是1000,也就是一旦出现异常,操作系统发送信号,将这个信号的二进制“贴合”到低8位上,也就是00000000 00001000,于因此退出码是8
    在这里插入图片描述
    所以如果进程不是异常退出的,那么操作系统就不可能给进程发送1-64的信号,其低8位就是0,然后子进程的退出码让其二进制形式“贴合”到高8位,那么要正常输出必须右移8位(虽然进程没有异常退出,但是我们需要获得退出码来验证这个进程的执行结果是否是正确的,依照不同的退出码返回不同的错误信息);如果进程是异常退出的,那么操作系统给进程发送kill信号,让其信号值的二进制“贴合”到低八位,已经异常退出了,所以高8位一定是0,所以输出退出码,不需要右移

    所以我们现在可以去判断进程是否是异常退出的,如果是正常退出,其退出码又是多少。如果是正常退出,那么其低8位一定是0,如果是异常退出其低8位一定不是0,所以我们让st和00000000 11111110(0x7f)进行与运算,如果其结果是0那么一定是正常退出的,反之不是0,那么一定是异常退出

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    int main()
    {;
        pid_t ret=fork();//其返回值类型是pid_t型的
        sleep(1);
        if(ret>0)//父进程返回的是子进程ID
        {
    		printf("父进程正在等待子进程死亡\n");
    		int st=0;
    		pid_t rec=waitpid(ret,&st,0);//阻塞
    		if(rec==ret)//如果返回值是子进程id,等待成功
    		{
    			printf("等待成功\n");
    			if((st&0x7f)==0)
    			{
    				printf("正常退出且其退出码为:%d\n",(st>>8)&0xff);
    			}
    			else
    			{
    				printf("异常退出,退出信号是:%d\n",st&0x7f);
    			}
    			
    		}
    		else
    		{
    			printf("等待失败\n");
    			exit(0);
    		}
    			
    		
    	}
        else if(ret==0)//子进程fork返回是0
        {
          int count=1;
          while(count<=10)
          {
            printf("子进程[%d]已经运行了%d秒\n",getpid(),count);
    		count++;
            sleep(1);
          }
    	  int x=1/0;
          exit(0);
        }
        else
          printf("进程创建失败\n");
    
        sleep(1);
        return 0;
    }
    
    
    
    • 下面进行测试,从上到下分别是正常退出,正常退出但结果不对和异常退出
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    可以看出上面分析过程与代码十分复杂,但是咋们在实际开发时并不这样写,这个逻辑的函数已经封装好了,它是宏函数

    • WIFEXITED(status):判断进程是否异常退出,如果正常退出返回真
    • WEXITSTATUS(status):如果正常退出,看其结果是否正确

    所以代码可以改写如下

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    int main()
    {
        pid_t ret=fork();//其返回值类型是pid_t型的
        sleep(1);
        if(ret>0)//父进程返回的是子进程ID
        {
    		printf("父进程正在等待子进程死亡\n");
    		int st=0;
    		pid_t rec=waitpid(ret,&st,0);//阻塞
    		if(rec==ret)//如果返回值是子进程id,等待成功
    		{
    			printf("等待成功\n");
    			if(WIFEXITED(st))//如果为真,正常退出
    			{
    				printf("正常退出且退出码为%d\n",WEXITSTATUS(st));
    			}
    			else
    			{
    				printf("异常退出,信号值为%d\n",st&0x7f);
    			}
    			
    			
    		}
    		else
    		{
    			printf("等待失败\n");
    			exit(0);
    		}
    			
    		
    	}
        else if(ret==0)//子进程fork返回是0
        {
          int count=1;
          while(count<=10)
          {
            printf("子进程[%d]已经运行了%d秒\n",getpid(),count);
    		count++;
            sleep(1);
          }
          exit(3);
        }
        else
          printf("进程创建失败\n");
    
        sleep(1);
        return 0;
    }
    
    
    

    在这里插入图片描述

    C:进程非阻塞式等待

    前面讲的都是进程的阻塞式等待,也就是父进程遇到wai/waitpid时,父进程就卡在那里,然后静静得看着子进程,子进程不死那么父进程就不向后执行

    进程的非阻塞式等待和waitpid的第三个参数options有关,如果options设置为WNOHANG,表示:如果指定的子进程还没有结束那么就不等待了,返回0,如果结束了就返回该子进程的ID

    下面这段程序中的do while循环表示每次用rec接受waitpid的返回值,由于waitpid设置了WNOHANG,因此如果要等待的子进程还没有结束它就会返回0。一直让这个循环走下去,直到子进程结束,其返回值是子进程的PID,也就是大于0的数,然后循环终止

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    int main()
    {
        pid_t ret=fork();//其返回值类型是pid_t型的
        sleep(1);
        if(ret>0)//父进程返回的是子进程ID
        {
    		printf("父进程正在等待子进程死亡\n");
    		int st=0;
    		pid_t rec=0;
    		
    		do
    		{
    			rec=waitpid(ret,&st,WNOHANG);//如果子进程没有结束返回0
    			if(rec==0)//判断是否结束,
    			{
    				printf("子进程还在运行当中,请稍后再来\n");
    			}
    			else
    			{
    				break;//如果返回的是子进程的PID,那么就跳出循环
    			}
    			sleep(1);
    			
    		}while(1);//直到waitpid返回不是0,也就是子进程结束了
    		
    		
    		if(rec==ret)//如果返回值是子进程id,等待成功
    		{
    			printf("等待成功\n");
    			if(WIFEXITED(st))//如果为真,正常退出
    			{
    				printf("正常退出且退出码为%d\n",WEXITSTATUS(st));
    			}
    			else
    			{
    				printf("异常退出,信号值为%d\n",st&0x7f);
    			}
    			
    			
    		}
    		else
    		{
    			printf("等待失败\n");
    			exit(0);
    		}
    			
    		
    	}
        else if(ret==0)//子进程fork返回是0
        {
          int count=1;
          while(count<=10)
          {
            printf("子进程[%d]已经运行了%d秒\n",getpid(),count);
    		count++;
            sleep(1);
          }
    
          exit(3);
        }
        else
          printf("进程创建失败\n");
    
        sleep(1);
        return 0;
    }
    
    
    

    效果如下
    在这里插入图片描述
    阻塞和非阻塞区别:想象你在电脑上下载游戏,阻塞就是从游戏下载开始我就一直盯着屏幕,它不下载完我就不干别的事情,而非阻塞就是游戏一直下载,我时不时的打开下载器看看进度,没有下载好的话我下次在看,但是在空余时间我可以看看视频,打打字

    展开全文
  • 当父进程发现请求数 >= 子进程数时,父进程创建新的子进程,并把子进程数加1(当然子进程数有个预先上限);当父进程发现子进程数大于请求数加1时,父进程杀死多余的子进程。 总的来说,思想让子进程accept并处理...
  • 我们知道进程是一个可执行程序的运行实例,说白了就是运行着的程序,每一个运行着的程序都可以看做是一个独立的进程,会占用系统资源,在内存中有对应的代码空间和数据空间,操作系统调度运行,创建,分配系统资源...

    1. 回忆进程

      经过前面的学习,我们知道进程是一个可执行程序的运行实例,每一个运行着的程序都可以看做是一个独立的进程,会占用系统资源,在内存中有对应的代码空间和数据空间,由操作系统调度运行,创建,分配系统资源,完成任务并销毁等等。


    2. 什么是线程

      在linux早期还没有线程概念的时候,一个进程就对应一个程序,且一个进程某一时刻只能干一件事情。有了多线程的概念后,一个进程可以包含多个线程,这样进程在某一时刻就能够做很多事情,每个线程处理各自独立的任务。

      操作系统会在多个线程之间调度切换,每个线程都是共享进程中的资源,相对来说占用的资源较少,加入线程的最主要目的就是为了更好地支持多处理器,并且减少进程上下文切换的开销,提高效率。


    3. 线程标识

      就像每个进程都有一个进程id一样,每个线程也有一个线程id,进程id是整个系统唯一的,但线程id不一样,线程id只有在它所属的进程上下文中才有意义。


    4.创建线程

       一般程序运行起来就会产生一个进程,默认情况下该进程中有一个线程,叫做主线程,在创建新的线程前,程序的行为和传统的进程并没什么区别。

       pthread_create函数用于创建一个的线程。

    函数原型:

    #include <pthread.h>
    typedef void *(*stat_routine)(void *);
    int pthread_create(pthread_t *thread , const pthread_attr_t *attr , start_routine th_fn , void *arg);
    

    返回值说明:成功返回0, 失败返回错误号(POSIX线程所有函数返回0表示成功,返回其他值表示失败)。

    参数说明:
      参数thread:保存创建的线程ID,其pthread_t类型在当前Linux中可理解为无符号的长整形,但是在不同linux实现中pthread_t的类型可能存在差异。

      参数attr:指定新线程的各种属性,类型为pthread_attr_t,通常传NULL,表示使用线程默认属性。

      参数th_fn:是一个函数指针,指向线程主函数,start_routine是用来声明线程主函数类型为void* th_fn(void* arg) ,一旦新创建的线程被调度,就会进入该函数开始执行,该函数运行结束,线程也随之结束。

      参数arg:表示线程主函数执行期间的参数,类型为void *,这意味着可以将任意对象的指针传递给线程主函数,如果需要传递多个参数可以将arg设计成一个结构体。


    5. 创建线程实验一

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <stdlib.h>
    
    //线程主控函数
    void *tfn(void *arg){
            puts("I am pthread");
            return NULL;
    }
    
    int main(void) {
            pthread_t tid;
            //创建线程
            int ret = pthread_create(&tid, NULL, tfn, NULL);   
            if(ret != 0){
                    fprintf(stderr , "pthread_create error: %s\n", strerror(ret));
                    exit(-1);
            }
            //sleep的目的是为了防止主控线程先于其他线程退出,导致其他线程也提前结束
            sleep(1);
            puts("I am main pthread");
            return 0;
    }
    

    编译并运行,注意链接pthread库
    gcc -o pthread_create pthread_create.c -lpthread
    ./pthread_create
    

    程序执行结果如图:
    在这里插入图片描述

       有同学可能会很奇怪为什么要sleep 1秒,这是因为:如果任意一个线程调用了exit或_exit,整个进程的所有线程都会终止,从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,会在main函数return之前延时1秒,但这只是一种缓兵之计,即使主线程等待1秒,内核也不一定会调度新创建的工作线程执行


    6. pthread_self和 pthread_equal

      进程内部的每个线程都有一个唯一标识,即线程id,pthread_self函数是用来获取线程id,其作用类似于进程中 getpid() 函数。

    #include <pthread.h>
    pthread_t pthread_self(void);	   
    

    返回值:成功返回0


      线程id是用pthread_t数据类型来表示的,是一个无符号长整形,在可移植的操作系统就不能把它当做整数来处理。

      因为在不同的操作系统中,pthread_t数据类型实现是不一样的,在linux中可能是使用无符号长整形(unsigned long int)表示pthread_t数据类型,但在Solaris中可能使用无符号整型,而FreeBSD可能使用一个指向pthread_t结构的指针类型来表示pthread_t数据类型。

      因此必须使用一个函数来对两个线程id进行比较,pthread_equal函数的作用就是用来比较两个线程的id是否相同。

    #include <pthread.h>
    int pthread_equal(pthread_t tid1 , pthread_t tid2);
    

    返回值:如果两个线程id相同返回非0,不相等返回0

    参数说明:参数tid1和参数tid2为比较的两个线程id


    7. 创建线程实验二

    循环创建多个线程,每个线程打印自己是第几个被创建的线程。

    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <errno.h>
    #include <string.h>
    
    //线程主函数
    void *tfn(void *arg){
            int i = (int)arg;
            sleep(i);
            //依次打印创建的每个线程
            printf("I am pthread%d , pthread_id = %lu\n",i, pthread_self());   
            return NULL;
    }
    
    int main(int argc, char *args[]){
            if(argc < 2){
                    printf("argc < 2\n");
                    return -1;
            }
            pthread_t tid;
            int size = atoi(args[1]);
            int i;
            int ret;
            for(i = 0; i < size; ++i){
                    ret = pthread_create(&tid,NULL,tfn,(void *)i);   //创建多个线程
                    if(ret != 0){
                            //错误处理
                            fprintf(stderr , "pthread_create error",  strerror(ret));
                    }
            }
            sleep(size); 
            //主线程
            printf("I am main pthread , pthread_id = %lu\n" , pthread_self());
            pthread_exit(NULL);
    }
    

    编译并运行:

    gcc -o create_pthread2 create_pthread2.c -lpthread
    ./create_pthread2 6
    

    程序运行结果如图:
    在这里插入图片描述

    8. 多线程传参的问题

       在多线程编程中,如何正确的传参是非常重要和明确的一个问题,因为稍不注意,就会引发不可预料的结果,例如我们来看下面这个例子。

    思考一个问题:将pthread_create函数参4修改为(void )&i, 将线程主函数内改为 i=((int *)arg) 是否可以?


    我们对实验二的代码做以下修改:
    //线程主函数
    void *tfn(void *arg){
    		......
    	 	int i=*((int *)arg);
    	 	......
    }
    
    int main(void)
    {
    	......
    	//对pthread_create函数的参数i取地址
    	ret = pthread_create(&tid,NULL,tfn,(void *)&i);   //创建多个线程
    	......
    }
    

    程序执行结果:
    在这里插入图片描述

       从程序执行结果来看,显然是不行,因为(void *)&i是对i取地址操作,在循环创建线程的时候有可能几乎创建出来多个线程,多个线程读取到的i值是一样的,所以在打印输出的是多个线程id一样的线程(因为线程的创建是非常快的,也就是说第一个进程创建完,第二个进程又接着创建,此时第二个进程读取到的i值和第一个进程的i值是一样的,然后i值才++)。


    在这里插入图片描述

       实际上对i进行取地址操作就是让线程1,线程2 … 线程6指向i的内存空间,换句话说,这些线程是共享同一份数据(变量i),所以当线程创建完毕,i值已经增加到6了,那么这些线程读取到的i值是一样的,所以在打印时i值是一样的。如果改为传递一个普通的变量i值,相当于每个线程的栈空间都有一份数据(变量i)且不共享,每个线程读取到的都是自己的数据,也就不会出现上述的问题了。


    9. 关于pthread_create函数的细节

      pthread_create成功返回后,新创建的线程的id被填写到参数thread所指向的内存单元。线程id的类型是thread_t,它只在当前进程中保证是唯一的,在不同的系统中thread_t这个类型可能有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址。

       另外,线程主函数的函数体返回值是void *,是一个万能指针类型,那么我们必须在线程主控函数必须返回一个指针类型,一般来说直接返回return NULL 实际等效于return (void *)0。


    10. 关于线程的执行顺序

       通过前面两个实验相信你对线程有了一些基本的了解。

       线程是属于进程的,线程运行在进程空间,同一进程所产生的多个线程共享同一进程内存空间,这也是为什么进程间的内存地址空间是独立不共享的,而线程间的内存地址空间是共享的原因。

       当进程退出时,该进程下所属的多线程都会强制清除并退出。

       因为一个进程至少需要一个线程作为执行体,也就是主线程,进程管理的资源(cpu,内存)将线程分配到某个cpu上执行。所以当一个程序运行产生一个进程,但真正运行的实际上是线程而不是进程,进程只是一个空间的概念,相当于一个指挥者的角色,调度管理线程的运行和资源的分配

      所以cpu先把时间片先分配给进程,然后进程再把时间片分配过下属的线程。比如:当前程序运行时,cpu分配了100纳秒的时间片给进程,而进程又把这100纳秒时间片分配给了当前进程下的其他线程(多线程是轮流调度的),因此不管是cpu分配给进程,还是进程分配给其他线程,这个工作是由调度算法程序来做的。


       现在回到实验一为什么sleep的问题。

       pthread_create函数并不保证线程的执行顺序,一般程序运行时都有一个进程,在该进程下还有一个主控线程,主控线程在调用pthread_create创建子线程时,如果子线程一直没有得到cpu的执行权,而主控线程又先于子线程结束,那么该进程下的所有线程都会强制退出,也就没有机会去执行线程主控函数了。所以为了防止这种情况发生,一般来说我们会让主控线程sleep,等待其他线程执行完毕,然后主控线程再退出程序。

    展开全文
  • 例如:迅雷是一个进程,当中的多个下载任务即是多个线程二、线程和进程的区别进程是操作系统资源分配的基本单位,线程是cpu的基本调度单位。一个程序运行后至少有一个进程。一个进程可以包含多个线程,但是至少需要...

    一、什么是线程

    线程,又称轻量级进程。

    程序中的一个顺序控制流程,同时也是cpu的基本调度单位。

    进程由多个线程组成,彼此间完成完成不同的工作,交替执行,称为多线程。

    例如:迅雷是一个进程,当中的多个下载任务即是多个线程

    二、线程和进程的区别

    进程是操作系统资源分配的基本单位,线程是cpu的基本调度单位。

    一个程序运行后至少有一个进程。

    一个进程可以包含多个线程,但是至少需要一个线程

    进程间不能共享数据段地址,但同进程的线程之间可以

    三、线程的组成及特点

    任何一个线程都具有基本的组成部分:

    1.cpu时间片:操作系统会为每个线程分配执行时间。

    2.运行数据:

    堆空间:存储线程需要使用的对象,多个线程可以共享堆中的对象

    栈空间:存储线程需使用的局部编码,每个线程都拥有独立的栈

    3.线程的逻辑代码

    线程的特点:

    1.线程抢占式执行:效率高;可以防止单一线程长时间独占cpu

    2.在单核cpu中,宏观上同时执行,微观上顺序执行

    四、创建线程的方式

    1、继承Thread类 重写run()方法

    获取线程名称:第一种方法:用this.getid() 和  this.getName() 获取线程的ID和名称

    第二种方法:用this.currentThread() 获取当前线程

    线程.setName();给线程名称赋值

    注意:线程的启动 用 start() 方法

    线程的名称可以修改 线程的ID不可修改

    packagecom.monv.Thread;/*** 创建线程类 继承 Thread

    *@authorAdministrator

    **/

    public class MyThread extendsThread{publicMyThread() {//TODO Auto-generated constructor stub

    }publicMyThread(String name){super(name);

    }

    @Overridepublic voidrun() {for(int i = 0;i<100;i++){//1.第一种方法 用this.getid() 和 this.getName() 获取线程的ID和名称//System.out.println("线程ID:"+this.getId()+"线程名称:"+this.getName()+"子线程执行-------"+i);//2.第二种方法 用this.currentThread() 获取当前线程

    System.out.println("线程ID:"+this.currentThread().getId()+" 线程名称:"+this.currentThread().getName()+" 子线程执行------"+i);

    }

    }

    }---------------------------------------------------------

    packagecom.monv.Thread;/*** 测试线程 main是主线程

    *@authorAdministrator

    **/

    public classTestThread {public static voidmain(String[] args) {//1.创建线程

    MyThread testMyThread = new MyThread("这是第一个子线程");//2.启动线程,用Start,不能用Run//修改线程的名称 要写在线程执行前//testMyThread.setName("这是第一个子线程");

    testMyThread.start();

    MyThread testMyThread2= new MyThread("这是第二个子线程");//2.启动线程,用Start,不能用Run//修改线程的名称 要写在线程执行前//testMyThread2.setName("这是第二个子线程");

    testMyThread2.start();//主线程执行

    for (int i = 0; i <100; i++) {

    System.out.println("主线程执行=============="+i);

    }

    }

    }

    2.实现Runnable接口 重写run()方法

    注意:Runable也可以用内部类的方法实现

    packagecom.monv.Thread;public class MyRunnable implementsRunnable{

    @Overridepublic voidrun() {for (int i = 0; i<100 ;i++){

    System.out.println(Thread.currentThread().getName()+"++++++++++"+i);

    }

    }

    }------------------------------------------------------

    packagecom.monv.Thread;public classTestRunnable {public static voidmain(String[] args) {//1.创建MyRunnable对象,表示线程要执行的功能

    MyRunnable runnable = newMyRunnable();//2.创建线程

    Thread thread = new Thread(runnable, "我的线程");//3.执行

    thread.start();for (int i = 0; i < 100;i++){

    System.out.println("main------------"+i);

    }

    }

    }-------------------------------------------------

    -------------匿名内部类方法实现---------------------

    packagecom.monv.Thread;public classTestRunnable {public static voidmain(String[] args) {//创建匿名内部类

    Runnable runnable = newRunnable() {

    @Overridepublic voidrun() {for (int i = 0; i < 100;i++){

    System.out.println(Thread.currentThread().getName()+"++++++++++++"+i);

    }

    }

    };

    Thread thread= new Thread(runnable, "我的线程");

    thread.start();for (int i = 0; i < 100;i++){

    System.out.println("main------------"+i);

    }

    }

    }

    五、线程的状态

    7bbdf4511ac96d62a3d86d88e84429e9.png

    六、常见的方法

    1.休眠:public static void sleep(long millis); 当前线程主动休眠millis毫秒

    2.放弃:public static void yield(); 当前主线程主动放弃时间片,回到就绪状态,竞争下一次时间片

    3.加入:public final void join();允许其他线程加入到当前线程中

    --------1.sleep-----------

    packagecom.monv.Thread;public class SleepThread extendsThread{

    @Overridepublic voidrun() {for (int i =0 ;i<10 ;i++){

    System.out.println(Thread.currentThread().getName()+"............."+i);try{

    Thread.sleep(1000);

    }catch(InterruptedException e) {//TODO Auto-generated catch block

    e.printStackTrace();

    }

    }

    }

    }

    ---------测试-------------packagecom.monv.Thread;public classTestSleep {public static voidmain(String[] args) {

    SleepThread s1= newSleepThread();

    s1.start();

    SleepThread s2= newSleepThread();

    s2.start();

    }

    }--------2.yield-----------

    packagecom.monv.Thread;public class YieldThread extendsThread{

    @Overridepublic voidrun() {for(int i =0;i< 10;i++){

    System.out.println(Thread.currentThread().getName()+"+++++++++++"+i);//主动放弃cpu

    Thread.yield();

    }

    }

    }

    ---------测试------------------packagecom.monv.Thread;public classTestYield {public static voidmain(String[] args) {

    YieldThread y1= newYieldThread();

    YieldThread y2= newYieldThread();

    y1.start();

    y2.start();

    }

    }--------3.join-----------

    packagecom.monv.Thread;public class JoinThread extendsThread{

    @Overridepublic voidrun() {for (int i = 0; i < 30; i++) {

    System.out.println(Thread.currentThread().getName()+"........."+i);try{

    Thread.sleep(500);

    }catch(InterruptedException e) {//TODO Auto-generated catch block

    e.printStackTrace();

    }

    }

    }

    }

    ----------测试---------------packagecom.monv.Thread;public classTestJoin {public static voidmain(String[] args) {

    JoinThread jo1= newJoinThread();

    jo1.start();try{

    jo1.join();//把j1加入当前线程(主线程main),并阻塞当前线程,直到加入线程执行完毕当前线程才会继续执行

    } catch(InterruptedException e1) {//TODO Auto-generated catch block

    e1.printStackTrace();

    }//主线程

    for (int i =0;i<20;i++){

    System.out.println(Thread.currentThread().getName()+"......"+i);try{

    Thread.sleep(300);

    }catch(InterruptedException e) {//TODO Auto-generated catch block

    e.printStackTrace();

    }

    }

    }

    }

    七、线程等待

    c7c5ab784c686e189a2f3b19f5677343.png

    展开全文
  • 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位 一个程序运行后至少有一个进程 一个进程可以包含多个线程,但是至少需要有一个进程 进程间不能共享数据段地址,但同进程的线程之间可以 线程的组成 .....
    ## 线程的简介
    

    什么是线程

    • 线程,又称为轻量级进程(Light Weight Process)程序中的一个顺序控制的流程,同时也是CPU的基本调度单位
    • 进程由多个线程组成,彼此间完成不同的工作,交替执行,称为多线程

    举个栗子
    在这里插入图片描述
    JVM启动是多线程的吗
    是的
    垃圾回收线程也要先启动,否则很容易会出现内存溢出。
    JVM的启动是多线程的,因为它最低有两个线程启动了,主线程和垃圾回收线程。

    线程和进程的区别

    • 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位
    • 一个程序运行后至少有一个进程
    • 一个进程可以包含多个线程,但是至少需要有一个进程
    • 进程间不能共享数据段地址,但同进程的线程之间可以

    线程的组成
    在这里插入图片描述

    线程的创建

    继承Thread类创建线程
    在这里插入图片描述

    • 定义Thread的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务
    • 创建Thread子类的示例(创建该类对象)
    • 启动线程(调用start()方法)

    示例

    //线程类
    public class MyThread extends Thread{
    	//重写run方法
    	@Override
    	public void run() {
    		for(int i=0;i<1000;i++) {
    			System.out.print("MyThread: "+i+"  ");
    			if(i%5==0) {
    				System.out.println();
    			}
    		}
    	}
    }
    //测试类
    public class TestThread {
    	public static void main(String[] args) {
    		//创建线程对象
    		MyThread myThread = new MyThread();
    		//调用start方法
    		myThread.start();
    	}
    }
    

    执行结果
    在这里插入图片描述
    实现Runnable接口创建线程
    在这里插入图片描述

    • 定义Runnable接口的实现类,重写run()方法,这个run()方法体的内容也是线程需要执行的内容
    • 创建Runnable实现类的示例
    • 用该示例作为Thread的target来创建Thread对象(这个Thread对象才是真正的线程对象)
    • 调用start()方法

    示例

    //实现了Runnable接口
    public class MyRunnable implements Runnable{
    	@Override
    	public void run() {
    		for(int i=0;i<1000;i++) {
    			System.out.print("MyRunnable: "+i+"  ");
    			if(i%5==0) {
    				System.out.println();
    			}
    		}
    	}
    }
    //测试类
    public class TestRunnable {
    	public static void main(String[] args) {
    		//创建实现类对象
    		MyRunnable myRunnable = new MyRunnable();
    		//创建线程对象
    		Thread thread = new Thread(myRunnable);
    		thread.start();
    	}
    }
    

    结果
    在这里插入图片描述
    匿名内部类创建线程

    • 匿名内部类本质上也是一个类实现了Runnable接口,重写了run方法,只不过这个类没有名字,直接作为参数传入Thread类,示例代码:
    public class Test {
    	
    	
    		
    		public static void main(String[] args) {
    			new Thread(new Runnable() {
    				//重写run方法
    				public void run() {
    					for (int i = 0; i < 50; i++) {
    						System.out.println(Thread.currentThread().getName() + "执行" + i);
    					}
    				}
    			}).start();
    			for (int i = 0; i < 50; i++) {
    				System.out.println(Thread.currentThread().getName() + "执行" + i);
    			}
    		}
    	
    
    }
    

    Thread 类和 Runnable 接口

    • 在 Thread 类中的 run() 方法调用的是 Runnable 接口中的 run() 方法,也就是说此方法是由 Runnable 子类完成的,所以如果要通过继承 Thread 类实现多线程,则必须覆写 run()。
    • 如果一个类继承 Thread类,则不适合于多个线程共享资源,而实现了 Runnable 接口,就可以方便的实现资源的共享。

    线程的构造方法

    //Thread的构造器
    public Thread(String name) {
        init(null, null, name, 0);
    }
    //无参构造器
    public MyThread() {
    }
    //有参构造器
    public MyThread(String name) {
    	//调用了父类的构造器
    	super(name);
    }
    //创建线程的时候通过构造器,给线程命名
    MyThread mt = new MyThread("mt1 ");
    

    线程的使用

    //线程类
    public class MyThread extends Thread{
    	//重写run方法
    	@Override
    	public void run() {
    		for(int i=0;i<1000;i++) {
    			System.out.println("MyThread: "+i);
    		}
    	}
    }
    //测试类
    public class TestThread {
    	public static void main(String[] args) {
    		MyThread myThread = new MyThread();
    		myThread.start();
    		//在mian函数里写一个循环
    		for(int i=0;i<1000;i++) {
    			System.out.println("Main: "+i);
    		}
    	}
    }
    
    

    执行结果
    在这里插入图片描述

    • 结果我们发现,main里的for循环(main线程)和线程是交替执行的
    • 双方彼此抢占资源,哪个线程对象抢到了 CPU 资源,哪个线程就可以运行,所以程序每次的运行结果肯定是不一样的
    • 在线程启动虽然调用的是 start() 方法,但实际上调用的却是 run() 方法定义的主体(虽然实际上调用的是run()方法,但线程的启动使用的还是start()方法,不可使用run()方法)

    线程的状态

    线程可分为五个状态

    • 创建状态(初始状态)
      在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时它已经有了相应的内存空间和其他资源,但还处于不可运行状态。新建一个线程对象可采用Thread 类的构造方法来实现,例如 “Thread thread=new Thread()”。

    • 就绪状态
      新建线程对象后,调用该线程的 start() 方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待 CPU 服务,这表明它已经具备了运行条件。

    • 运行状态
      当就绪状态被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的 run() 方法。run() 方法定义该线程的操作和功能。

    • 阻塞状态
      一个正在执行的线程在某些特殊情况下,如被人为挂起或需要执行耗时的输入/输出操作,会让 CPU 暂时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep(),suspend(),wait() 等方法,线程都将进入阻塞状态,发生阻塞时线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。

    • 死亡状态(终止状态)
      线程调用 stop() 方法时或 run() 方法执行结束后,即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。

    • 基本状态图
      在这里插入图片描述

    • 等待状态图
      在这里插入图片描述

    • 阻塞状态图
      在这里插入图片描述

    这里说一下Java 的Thread类里面有一个State方法,这个方法里面涵盖了6种线程的状态,如下:

    public enum State {
        // 尚未启动的线程的线程状态。
        // 线程刚被创建,不过还没有被启动(还没有调用start方法)
        NEW,
    
        // 可运行线程的线程状态。
        //处于可运行状态的线程正在Java虚拟机中执行,但是它可能正在等待来自操作系统(例如处理器)的其他资源。
        RUNNABLE,
    
        // 线程的线程状态被阻塞,等待监视器锁定。
        //当一个线程想获取一个对象锁,不过该对象锁被其它的线程持有时,该线程就会进入锁阻塞状态;当该线程持有锁的时候,该线程将会变成可运行的状态。
        BLOCKED,
    
        // 等待线程的线程状态。
        //当一个线程在等待另一个线程执行一个(唤醒)动作时,该线程就会进入无限等待状态。进入这个状态后是不能自动唤醒的,要等待另一个线程调用notify()方法,或notifyall()方法才能够被唤醒。
        WAITING,
    
        // 具有指定等待时间的等待线程的线程状态。
        //类似于无限等待状态,有几个方法有超时参数,如:Thread.sleep、Object.wait方法。调用这些方法,进入计时等待状态。计时等待状态将会一直保持到超时期满或者接收到唤醒通知。
        TIMED_WAITING,
    
        // 终止线程的线程状态。
        //1、因为run方法的正常退出而死亡。
    	//2、因为没有捕获的异常,终止了run方法而死亡。
        TERMINATED;
    }
    

    等待唤醒机制案例

    • 等待
      顾客要去饭店吃饭,自助下单,说明要吃什么,数量是多少。下完单以后,顾客就等待该饭店厨师做饭菜,也就是Waiting状态(无限等待状态)。
    • 唤醒
      厨师收到下单信息,开始做饭菜,做好饭菜,把饭菜递到顾客桌面上,顾客看到饭菜已经来了(notify方法),就可以开吃了(等待唤醒机制)

    总结
    简单的说,就是多个线程在处理同一个资源,但是处理的动作(线程的任务)却不同。如上,厨师线程做饭菜,顾客线程吃饭菜。那为什么要进行线程间的通信呢?多个线程并发执行的时候,在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且希望它们有规律的执行的时候,那么多线程就之间就需要一些协调通信,来达到多线程共同操作一份数据。

    展开全文
  • 浅谈进程和线程

    2021-02-25 11:24:20
    俗称轻量级进程是进程中一条执行路径,cpu的基本调度单位,一个进程由一个多线程组成,每个线程完成不同的工作 多线程实际上宏观上并行,微观上串行 举个例子: JVM一个进程,当中默认包含主线程main,可...
  • 线程库在管理线程时,要为用户线程所属的进程创建专用的线程表(存在于用户空间,不在进程的pcb中)。利用线程表中记录的每个线程的程序计数器、堆栈指针,寄存器和状态等,可以完成在用户线程的切换(包括线程的...
  • 1. 协程介绍 1.1 什么是协程 协程(Coroutine)也叫用户态线程,其通过协作而不是抢占来进行切换。相对于进程或者线程,协程所有的操作都可以... 内核态的线程是由操作系统来进行调度的,在切换线程上下文时,要先...
  • 什么是协程 协程(Coroutine)也叫用户态线程,其通过协作而不是抢占来进行切换。... 内核态的线程是由操作系统来进行调度的,在切换线程上下文时,要先保存上一个线程的上下文,然后执行下一个线程,当...
  • Liunx之线程

    2018-06-14 10:59:26
    多任务可以多进程完成,也可以一个进程内的多线程完成。 线程和进程的对比 进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它 分配资源,然后把该...
  • Java--多线程学习

    2020-07-21 11:04:52
    多线程什么是进程什么是线程进程和线程的区别线程的组成线程的特点创建线程继承Thread获取线程的名称修改线程的名称买票模拟实现Runnable接口 什么是进程 正在运行的程序,系统进行资源分配的基本单位。 目前操作...
  • 内核支持线程和用户级线程

    千次阅读 2017-12-21 09:54:59
     对于通常的进程,无论系统进程还是用户进程进程创建、撤销,以及要求系统设备完成的i/o操作,都利用系统调用而进入内核,再内核中的相应处理程予以完成的进程的切换同样在内核的支持下实现的。...
  • 操作系统复习题

    千次阅读 2019-12-19 16:22:13
    (1)创建到就绪:处于创建状态的进程,当其获得所需的资源以及对其PCB的初始化工作完成后,便由创建状态转入就绪状态。 (2)就绪到执行:处于就绪状态的进程,在调度程序为之分配了处理器之后,该进程就进入执行...
  • 什么是线程? 线程是调度CPU的最小单元,...在多处理器系统上,多线程在多处理器上并行运行,线程的创建调度和管理内核完成,效率比用户级线程要慢,比进程操作快。 什么是线程池? 线程池就是创建若干个可执行的
  • java学习多线程处理

    2013-06-28 08:09:39
    线程的调度管理是由进程完成的。 注意:编程时,必须确保线程不会妨碍同一进程的其他线程 线程的分类:系统级线程(又称为核心级线程,负责管理调度不同进程之间的多个线程,由操作系统直接管理) 用户级线程...
  • JavaSE学习之线程初学

    2020-05-11 00:16:06
    进程由多个线程组成,彼此间完成不同的工作,交替执行,称为多线程。 2.JVM虚拟机一个进程,当中默认包含主线程(Main),可通过代码创建多个独立线程,与Main并发执行。 2、线程的组成 3、线程的状态 4、线
  • 初识java线程池

    2020-10-22 23:40:14
    1.用户线程和内核线程 用户线程(ULT): 用户程序实现,不依赖操作系统核心,应用提供创建...线程的创建调度、和管理内核完成,效率 要比ULT慢,比进程操作快 2.JVM使用的是什么线程模型? java使用的KLT线程模
  •  SessionBean: Stateless Session Bean 的生命周期是由容器决定的,当客户机发出请求要建立一个Bean的实例时,EJB容器不一定要创建一个新的Bean的实例供客户机调用,而是随便找一个现有的实例提供给客户机。...
  • 浅谈微内核

    千次阅读 多人点赞 2018-09-23 18:32:53
    像Linux就是典型的宏内核,它除了时钟中断、进程创建与销毁、进程调度进程间通信外,其他的文件系统、内存管理、输入输出、设备驱动管理都需要内核完成。微内核,又称为微核心,一种内核的设计架构,一群数量...
  • 2.1.5 堆栈溢出一般是由什么原因导致的? 2.1.6 什么函数不能声明为虚函数? 2.1.7 冒泡排序算法的时间复杂度是什么? 2.1.8 写出float x 与“零值”比较的if语句 2.1.9 Internet采用哪种网络协议?该协议的主要...
  • 一个程序作为一个进程运行,程序运行过程中可能会创建多个线程。一个线程在一个时刻只能运行在一个CPU核心上。 为什么使用多线程? ①异步执行 ②利用多CPU资源实现真正意义上的并列执行 线程的应用场景: ①...
  • Unix/Linux 编程实践教程.PDF

    千次下载 热门讨论 2010-09-03 18:34:12
    1.2 什么是系统编程 1.2.1 简单的程序模型 1.2.2 系统模型 1.2.3 操作系统的职责 1.2.4 为程序提供服务 1.3 理解系统编程 1.3.1 系统资源 1.3.2 目标:理解系统编程 1.3.3 方法:通过三个问题来理解 1.4 ...
  • java 面试题 总结

    2009-09-16 08:45:34
    EJB容器EJB组件的代理,EJB组件容器所创建和管理。客户通过容器来访问真正的EJB组件。 21、Static Nested Class 和 Inner Class的不同。 Static Nested Class被声明为静态(static)的内部类,它可以不依赖...
  • 答:启动一个线程调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以JVM调度并执行。这并不意味着线程就会立即运行。run()方法可以产生必须退出的标志来停止一个线程。 40.接口是否可...
  • 6.堆栈溢出一般是由什么原因导致的? 没有回收垃圾资源。 7.什么函数不能声明为虚函数? constructor函数不能声明为虚函数。 8.冒泡排序算法的时间复杂度是什么? 时间复杂度是O(n^2)。 9.写出float x 与“零...
  •  7.2.4 调度进程(Dispatcher Processes,Dnnn)  7.2.5 共享服务器进程(Shared Server Processes,Snnn)  7.3 共享服务器模式(Shared Server Mode)  7.4 共享服务器模式(Shared Server Mode)疑难解析 ...
  • 强大的扫描工具x-scan

    2009-11-13 23:17:02
    Q:扫描一个子网,进程里同时出现10个checkhost.exe的进程是什么原因? A:检测每个主机都会单独起一个Checkhost.exe进程,检测完毕会自动退出。并发主机数量可以通过 图形界面的设置窗口设定,命令行程序通过“-...

空空如也

空空如也

1 2 3 4
收藏数 79
精华内容 31
关键字:

创建进程是由什么调度完成的