计算机基础-进程and线程

参考文章:https://blog.csdn.net/feiBlog/article/details/85397287、
java线程池:https://www.jianshu.com/p/7726c70cdc40
管道:https://www.cnblogs.com/tp-16b/p/8886378.html
https://www.cnblogs.com/zgq0/p/8780893.html
https://blog.csdn.net/sophia__yu/article/details/82390048?depth_1-

1. 进程线程的区别:

参考文章:https://www.cnblogs.com/fubaizhaizhuren/p/7501403.html

  • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
  • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。

一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程

  • 执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  • 线程是处理器调度的基本单位,但是进程不是。
  • 两者均可并发执行。

优缺点:

  • 线程执行开销小,但是不利于资源的管理和保护。线程适合在SMP机器(双CPU系统)上运行。
  • 进程执行开销大,但是能够很好的进行资源管理和保护。进程可以跨机器前移。

2. 进程的七种通信方式

参考文章:https://blog.csdn.net/weixin_42197191/article/details/82867154

  • 全双工: [1] 指可以同时(瞬时)进行信号的双向传输(A→B且B→A)。指A→B的同时B→A,是瞬时同步的。
  • 半双工:指一个时间内只有一个方向的信号传输(A→B或B→A)。
  • 传统的通信方式:
    命名管道
    无名管道
    信号
  • IPC通信:
    消息队列
    共享内存
    信号灯
  • BSD:
    socket

管道

  1. 定义:管道是最早出现的进程间通信的手段。在 shell 中执行命令时,经常将上一个命令的输出作为下一个命令的输入,由多个命令配合完成一件事情。而这就是通过管道来实现的。
    在这里插入图片描述
  2. 管道如何实现进程间的通信
    (1)父进程创建管道,得到两个⽂件描述符指向管道的两端

(2)父进程fork出子进程,⼦进程也有两个⽂件描述符指向同⼀管道。

(3)父进程关闭fd[0],子进程关闭fd[1],即⽗进程关闭管道读端,⼦进程关闭管道写端(因为管道只支持单向通信)。⽗进程可以往管道⾥写,⼦进程可以从管道⾥读,管道是⽤环形队列实现的,数据从写端流⼊从读端流出,这样就实现了进程间通信。
在这里插入图片描述

2.1无名管道(匿名管道)

定义:无名管道是一种特殊类型的文件,在内核空间中对应的资源即是一段内存空间,内核在这段空间以循环队列的方式临时存入一个进程发送给另外一个进程的信息,这段内核空间完全由操作系统管理和维护,应用程序只需要,也能通过系统调用来访它。

无名管道和普通的文件有很大的差异:无名管道的内核资源在通信两进程退出后会自动释放。跟普通文件不同,但是编程方式,具有和普通文件一样的特点,可以使用read/write等函数进行读写操作,只是注意:特殊的文件只能用文件IO操作

特点

1、使用条件:只能用于具有亲缘关系(父子进程,兄弟进程等)的进程之间的通信
2、通信模式:半双工模式,fd[0]作为读端,fd[1]作为写端
3、读写方式:对于它的读写采用文件IO(不支持lseek函数)(面向字节流:数据无规则,没有明显边界,收发数据比较灵活:对于用户态,可以一次性发送也可以分次发送,当然接受数据也如此;而面向数据报:数据有明显边界,数据只能整条接受 )
4、读操作会阻塞(等待):在管道中无数据情况下。
写操作会阻塞(等待):当管道被写满时,无名管道的大小为64K
5、管道破裂:管道读端关闭,再向管道中写数据时。
6、大小有限制(一般是65536)
7 、无名管道只有所有进程都结束的时候,管道内的资源才完全释放,否则一直存在

匿名管道读写规则:

1.管道无数据读取(read): 如果描述符是默认的阻塞特性,读取将会阻塞挂起等待,直到管道有数据
2.管道被写满(write):如果描述符是默认的阻塞特性,写入操作会阻塞挂起等到,直到有数据被取走;如果描述符被设置为非阻塞特性,写入操作不具备条件,直接报错返回-1,错误码:EAGAIN。
3.如果所有管道写入端对应的文件描述符被关闭,则读取完管道中数据,然后read返回0;
4.如果所有管道读取端对应的文件描述符被关闭,则write操作会触发异常(因为没有人读数据),操作系统会给进程发送SIGPIPE,进程收到这个进程会退出;
5.当要写入的数据量不大于PIPE_BUF(512字节)时,linux将保证写入的原子性(操作不会被打断,一步完成);
6.当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

2.2命名管道(FIFO)

  1. 定义:有名管道:有自己的名字,但是有名管道名称保存在磁盘上,但是内容保存在内核中,有名管道和普通的文件一样具有磁盘存放路径,文件的权限和其他的属性信息,但是有名管道和普通文件又有区别,有名管道没有在磁盘上存真正的信息,而是在内存中存放,2个进程结束后自动丢失,通信结束后有名管道的文件路径本身存在,这是和无名管道区别的地方。

特点:

特点:
1、有名管道可以使互不相关的两个进程互相通信。
2、有名管道可以通过路径名来指出,并且在文件系统中可见。
3、读写方式:对于它的读写采用文件IO(不支持lseek函数)
4、其它与无名管道一样
5.有名管道读端写端不固定。一段读,另一端写。当然某一端既可以读也可以写,要用父子进程实现。

命名管道打开特性:

  1. 如果用只读打开命名管道,open函数将阻塞等待直至有其他进程以写的方式打开这个命名管道,如果没有进程以写的方式发开这个命名管道,程序将停在此处
  2. 如果用只写打开命名管道,open函数将阻塞等到直至有其他进程以读的方式打开这个命名管道,如果没有进程以读的方式发开这个命名管道,程序将停在此处;
  3. 如果用读写打开命名管道,则不会阻塞(但是管道是单向)

命名管道和匿名管道区别和联系:

1.区别:匿名管道用int pipe(int pipefd[2]); 创建并打开匿名管道返回描述符
命名管道用mkfifo或者 int mkfifo(const char *pathname, mode_t mode);创建,并没有打开,如果打开需要open;
匿名管道是具有亲缘关系进程间通信的媒介,而命名管道作为同一主机任意进程间通信的媒介;
匿名管道不可见文件系统,命名管道可见于文件系统,是一个特殊类型(管道类型)文件。
2.联系:匿名管道和命名管道都是内核的一块缓冲区,并且都是单向通信;另外当命名管道打开(open)后,所有特性和匿名管道一样(上文匿名管道读写规则与管道特性):两者自带同步(临界资源访问的时序性)与互斥(临界资源同一时间的唯一访问性),管道生命周期随进程退出而结束。

消息队列(System V IPC)

消息队列实际上是操作系统在内核为我们创建的一个队列,通过这个队列的标识符key,每一个进程都可以打开这个队列,每个进程都可以通过这个队列向这个队列中插入一个结点或者获取一个结点来完成不同进程间的通信

  • 如何传输数据:
    用户组织一个带有类型的数据块,添加到队列中,其他的进程从队列中获取数据块,即消息队列发送的是一个带有类型的数据块;消息队列是一个全双工通信,可读可写(可以发送数据,也可以接受数据)

消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在

信号量:(本质:具有一个等待队列的计数器(现在是否有资源可以使用))

  1. 定义: 进程间通信方式之一,用于实现进程间的同步与互斥(进程与线程安全概念)
  • 同步:保证对临界资源访问的时序可控性(保证有序);
  • 互斥:对临界资源同一时间的唯一访问性(保证安全)。

多个进程同时,操作一个临界资源的时候就需要通过同步与互斥机制开实现对临界资源的安全访问;

当信号量没有资源可用(计数器为0)时,这时需要阻塞等待 ;
同步:只有信号量资源计数从0变为1的时候,会通知别人打断阻塞等待,去操作临界资源,也就是释放了资源(计数器+1)之后才能获取资源(计数器-1),然后进行操作;
互斥:信号量
如果想要实现互斥,那么它的计数器只能是0/1(一元信号量)
,我获取的计数器的资源,那么别人就无法获取。

PV操作

  • P操作:获取信号量资源及计数器-1操作,如果计数器为0,需要等待别人释放资源。
  • V操作:释放信号量资源及计数器+1操作;

进程在操作临界资源之前要先获取信号量,判断是否可以对临界资源进行操作,如果信号量没有资源(计数器为0),则需要等待,当别人释放信号量资源后信号量计数变为1,则会唤醒等待的进程去重新获取信号量资源。

信号量计数器大于0,代表信号量有资源,可以操作
信号量计数器等于0,代表信号量没有资源,需要等待

信号量作为进程间通信方式,意味着大家都能访问到信号量,信号量实际也是一个临界资源,当然信号量的这个临界资源的操作是不会出问题的,因为信号量的操作是一个原子操作

共享内存(进程间最快通信

一般数据操作过程把数据从用户态拷贝到内核态,用的时候,再将内核态拷贝到用户态,但共享内存不需要这两步,对虚拟地址空间的操作也就是操作了物理内存,那么另一个虚拟地址空间也可以有这个数据,即不需要拷贝。

因为共享内存直接申请一块物理内存通过页表映射到虚拟地址空间中,操作虚拟地址空间,其实是操作同一块物理内存区域,因此进行数据传输时相较于其他通信方 式,少了两步用户态与内核态数据拷贝的过程,因此共享内存是最快的进程间通信方式。

在这里插入图片描述

3. run()与start()的区别

看下面这段程序,猜一下结果

public class HelloSogou{
     public static  void main(String[] a){
         Thread t=new Thread(){
             public void run(){Sogou();}
     };
     t.run();
     System.out.print("Hello");
     }
     static  void Sogou(){
     System.out.print("Sogou");
    }
}

这时候其实是调用rum()方法,并不是开启一个新线程,所以理所应当的是按顺序执行,结果为:SogouHello。

那我们把t.run()改成t.start()呢,结果应该是什么呢,其实,由于线程的执行是抢占式的,所以应该是Hello和Sougou随机交叉,但是在一般编译器尝试大多数都是hello在前,因为一般都会先执行主线程,这时候可以尝试给主线程加个循环,会发现并不是每次都最后才执行Sogou。

public class test {
    public static  void main(String[] a){
        Thread t=new Thread(){
            public void run(){Sogou();}
        };
        t.start();
        for (int i = 0; i <500 ; i++) {
            System.out.println("Hello");
        }
    }
    static  void Sogou(){
        System.out.print("=========Sogou");
    }
}

再扩展一下,如果两个方法都加了synchronized锁会怎么样呢,
按理论来说,调用main函数(一个线程),main函数开启另一个线程,并启动,但是main函数和Sogou方法是同一个锁,所以main函数执行完毕后才会释放锁,Sogou方法才会执行,这就是为什么,换成start,是HelloSogou,可以尝试如下代码。

public class HelloSogou{
     public static synchronized void main(String[] a){
         Thread t=new Thread(){
             public void run(){Sogou();}
     };
     t.run();
     System.out.print("Hello");
     }
     static synchronized void Sogou(){
     System.out.print("Sogou");
    }
}

多线程的多种可能

题目
假设 a 是一个由线程 1 和线程 2 共享的初始值为 0 的全局变量,则线程 1 和线程 2 同时执行下面的代码,最终 a 的结果不可能是()

boolean isOdd = false;
for(int i=1;i<=2;++i){
if(i%2==1)
	isOdd = true;
else
	 isOdd = false;
a+=i*(isOdd?1:-1);
}
  • 解题思路:线程执行的先后顺序有很大影响,如果线程是同时执行,则会有读取脏数据等问题,要考虑到这些问题。

  • 题解
    在这里插入图片描述

发布了36 篇原创文章 · 获赞 11 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/s_xchenzejian/article/details/100569141
今日推荐