面试官:进程与线程的区别,我:???

一、进程

1、进程与线程的区别

本质区别:
线程是执行的单位,进程是资源的单位。同一进程下的线程之间共享数据。

所以线程和进程在创建、切换、运行之间有下列不同的特性:

  • 创建:线程创建更快。进程创建需要重新开辟空间,线程直接创建,开辟少量资源即可。

  • 切换:线程更快,进程切换需要涉及大量资源的切换,线程共享资源,切换速度更快

  • 运行:进程运行更稳定,一个线程的崩溃,会导致整个进程的崩溃。

  • 通信:进程间通信比较繁琐。

    进程创建的时候需要告诉操作系统开辟一块新的内存空间,速度比较慢,线程创建的时候只需要复制少量的资源。同理切换的速度也是线程比较快。根据这个可以得出下面提到的线程和进程适用的场景。

线程是告诉操作系统执行一条任务代码(线程的创建速度是进程的100倍)

2、进程的创建过程

fork系统调用

3、进程间的通信

1)管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。

(2)命名管道(named pipe):命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。

(3)信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。

(4)消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺

(5)共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。

(6)信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

(7)套接字(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

3、解释孤儿进程,僵死进程,惊群效应

孤儿进程:
如果父进程先退出,子进程还没退出那么子进程将被 托孤给init进程,这是子进程的父进程就是init进程(1号进程).其实还是很好理解的.

僵尸进程:
如果我们了解过linux进程状态及转换关系,我们应该知道进程这么多状态中有一种状态是僵死状态,就是进程终止后进入僵死状态(zombie),等待告知父进程自己终止,后才能完全消失.但是如果一个进程已经终止了,但是其父进程还没有获取其状态,那么这个进程就称之为僵尸进程.僵尸进程还会消耗一定的系统资源,并且还保留一些概要信息供父进程查询子进程的状态可以提供父进程想要的信息.一旦父进程得到想要的信息,僵尸进程就会结束.

惊群效应
惊群简单来说就是多个进程或者线程在等待同一个事件,当事件发生时,所有线程和进程都会被内核唤醒。唤醒后通常只有一个进程获得了该事件并进行处理,其他进程发现获取事件失败后又继续进入了等待状态,在一定程度上降低了系统性能。

https://blog.csdn.net/abhem4170/article/details/101951466

二、线程

2.1 线程的创建

2.2 线程的同步和通信

条件变量是线程同步的一种重要方法,一般和**互斥锁(mutex)**一起使用,那么为什么这么用呢?

理解这个需要先讲一下死锁,死锁产生的四个条件:

  • 互斥条件:一个资源每次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:进程已获得的资源,在未使用完之前,不得强行剥夺
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

同步和通信机制:

  • 互斥量
    互斥量一般和条件变量配合使用。使用资源前上锁,使用结束后通知阻塞的线程

  • 自旋锁
    自旋锁也是广义上的互斥锁, 是互斥锁的实现方式之一, 它不会产生线程的调度, 而是通过"循环"来尝试获取锁, 优点是能很快的获取锁, 缺点是会占用过多的CPU时间, 这被称为忙等待(busy-waiting).

  • 读写锁
    在互斥锁中, 只有两个状态: 加锁和未加锁, 而在一些情况下对于"读"可以并发的进行而不用加锁, 对于读则需要加锁, 比如golang中map的操作.

为了让"读"操作更快的进行(不必加锁), 就诞生了"读写锁"的概念, 它有三个状态: 读模式下加锁状态, 写模式加锁状态和未加锁状态.

规则如下
1、如果有其它线程读数据, 则允许其它线程执行读操作, 但不允许写操作
2、如果有其它线程写数据, 则其它线程都不允许读和写操作

由于这个特性, 读写锁能在读频率更高的情况下有更好的并发性能.

  • barrier(这个不知道怎么翻译)

代码

这几种的区别,欢迎浏览讨论

2.1.1 互斥锁和条件变量

关于条件变量为什么需要加一个互斥锁的问题?
https://www.zhihu.com/question/53631897

多线程中join 和 detach的区别

每个进程都有一个主线程负责执行,一般为main函数里面的线程。

主线程结束,子线程也得结束。

线程环境:

线程存在于进程之中,进程内所有全局资源对于内部每个线程都是可见的。

进程内典型全局资源如下:

1)代码区:这意味着当前进程空间内所有的可见的函数代码,对于每个线程来说,也是可见的

2)静态存储区:全局变量,静态空间

3)动态存储区:堆空间

线程内典型的局部资源:

1)本地栈空间:存放本线程的函数调用栈,函数内部的局部变量等

2)部分寄存器变量:线程下一步要执行代码的指针偏移量

一个进程发起后,会首先生成一个缺省的线程,通常称这个线程为主线程,C/C++程序中,主线程就是通过main函数进入的线程,由主线程衍生的线程成为从线程,从线程也可以有自己的入口函数,相当于主线程的main函数,这个函数由用户指定。通过thread构造函数中传入函数指针实现,在指定线程入口函数时,也可以指定入口函数的参数。就像main函数有固定的格式要求一样,线程的入口函数也可以有固定的格式要求,参数通常都是void类型,返回类型根据协议的不同也不同,pthread中是void,winapi中是unsigned int,而且都是全局函数。

最常见的线程模型中,除主线程较为特殊之外,其他线程一旦被创建,相互之间就是对等关系,不存在隐含的层次关系。每个进程可创建的最大线程数由具体实现决定。

无论在windows中还是Posix中,主线程和子线程的默认关系是:无论子线程执行完毕与否,一旦主线程执行完毕退出,所有子线程执行都会终止。这时整个进程结束或僵死,部分线程保持一种终止执行但还未销毁的状态,而进程必须在其所有线程销毁后销毁,这时进程处于僵死状态。线程函数执行完毕退出,或以其他非常方式终止,线程进入终止态,但是为线程分配的系统资源不一定释放,可能在系统重启之前,一直都不能释放,终止态的线程,仍旧作为一个线程实体存在于操作系统中,什么时候销毁,取决于线程属性。在这种情况下,主线程和子线程通常定义以下两种关系:

1、可会合(joinable):这种关系下,主线程需要明确执行等待操作,在子线程结束后,主线程的等待操作执行完毕,子线程和主线程会合,这时主线程继续执行等待操作之后的下一步操作。主线程必须会合可会合的子线程。在主线程的线程函数内部调用子线程对象的wait函数实现,即使子线程能够在主线程之前执行完毕,进入终止态,也必须执行会合操作,否则,系统永远不会主动销毁线程,分配给该线程的系统资源也永远不会释放。

2、相分离(detached):表示子线程无需和主线程会合,也就是相分离的,这种情况下,子线程一旦进入终止状态,这种方式常用在线程数较多的情况下,有时让主线程逐个等待子线程结束,或者让主线程安排每个子线程结束的等待顺序,是很困难或不可能的,所以在并发子线程较多的情况下,这种方式也会经常使用。

在任何一个时间点上,线程是可结合(joinable)或者是可分离的(detached),一个可结合的线程能够被其他线程回收资源和杀死,在被其他线程回收之前,它的存储器资源如栈,是不释放的,相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。

线程的分离状态决定一个线程以什么样的方式来终止自己,在默认的情况下,线程是非分离状态的,这种情况下,原有的线程等待创建的线程结束,只有当pthread_join函数返回时,创建的线程才算终止,释放自己占用的系统紫云啊,而分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。

三、一些比较典型的题目

2.1 什么时候用多线程?什么时候用多进程?

线程和进程的区别如下:
1、需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的。

2、线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应

3、因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程;

4、并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求;

5、需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。

通过1\2可以得出如下结论:
I/O密集型用多线程,因为I/O会涉及频繁的切换,用多线程开销比较小
计算密集型用多进程,因为进程是资源的单位,有多核CPU的话,搞多个进程,比较快

2.2、常见C++多线程面试题


#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
using namespace std;
mutex m;
condition_variable cond;
int flag=10;
void fun(int num){
    for(int i=0;i<50;i++){
        unique_lock<mutex> lk(m);//A unique lock is an object that manages a mutex object with unique ownership in both states: locked and unlocked.
        while(flag!=num)
            cond.wait(lk);//在调用wait时会执行lk.unlock()
        for(int j=0;j<num;j++)
            cout<<j<<" ";
        cout<<endl;
        flag=(num==10)?100:10;
        cond.notify_one();//被阻塞的线程唤醒后lk.lock()恢复在调用wait前的状态
    }
}

int main(){
    thread child(fun,10);
    fun(100);
    child.join();
    return 0;
}

参考文献

[1] https://www.cnblogs.com/fah936861121/articles/8043187.html

发布了92 篇原创文章 · 获赞 20 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/qq_16761099/article/details/97400176