面试八股文对校招的用处有多大?系统编程篇

前言

1.本系列面试八股文的题目及答案均来自于网络平台的内容整理,对其进行了归类整理,在格式和内容上或许会存在一定错误,大家自行理解。内容涵盖部分若有侵权部分,请后台联系,及时删除。

2.本系列发布内容分为12篇 分别是:

c/c++语言

数据结构与算法

GDB

设计模式

操作系统

系统编程

网络原理

网络编程

mysql

redis

服务器

RPG

本文为第六篇,后续会陆续更新。 共计200+道八股文。

3.本系列的200+道为整理的八股文系列的一小部分。完整整理完的八股文面试题共计1000+道,100W字左右,体量太大,故此处放至百度云盘链接: https://pan.baidu.com/s/1IOxQs0ifbSPGgxK7Yz7BtQ?pwd=zl1i

提取码:zl1i 需要的同学自取即可。

4.八股文对于面试的同学来说仅作为参考使用,不能作为面试上岸的唯一准备,还是要结合自身的技术能力和项目,同步发育。

六、系统编程

01.除了MQ和websocket之外,你还能想到什么异步通信的办法?

除了MQ和WebSocket之外,异步通信的方法还有以下几种:

  1. HTTP长连接:客户端与服务端建立一次连接后保持长时间的连接,可以实现双向通信。
  2. Comet技术:在客户端发送请求到服务端后,服务端不会立即返回结果,而是等待事件发生或超时后再返回结果。
  3. AJAX轮询:客户端通过定时发送请求来获取最新数据,但这种方式会导致频繁的网络请求和资源浪费。
  4. SignalR:一个基于ASP.NET框架的开源库,支持服务器与客户端之间实时、双向通信。
  5. WebSocket+STOMP协议:使用WebSocket作为底层协议,并结合STOMP协议实现异步通信。 STOMP协议是一种简单的消息传递协议,可以使得客户端与服务器之间进行异步通信。

02.为什么要用多线程。多进程可以吗(webserver的)

》多线程是为了使得多个线程并行的工作以完成多项任务,以提高系统的效率。线程是在同一时间需要完成多项任务的时候被实现的。 使用线程的好处有以下几点: ·使用线程可以把占据长时间的程序中的任务放到后台去处理 ·用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度 ·程序的运行速度可能加快 ·在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较游泳了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。

》在Windows中,进行CPU分配是以线程为单位的,一个进程可能由多个线程组成,这时情况更加复杂,但简单地说,有如下关系: 总线程数<= CPU数量:并行运行 总线程数> CPU数量:并发运行 并行运行的效率显然高于并发运行,所以在多CPU的计算机中,多任务的效率比较高。但是,如果在多CPU计算机中只运行一个进程(线程),就不能发挥多CPU的优势。

多任务操作系统(如Windows)的基本原理是:操作系统将CPU的时间片分配给多个线程,每个线程在操作系统指定的时 间片内完成(注意,这里的多个线程是分属于不同进程的).操作系统不断的从一个线程的执行切换到另一个线程的执行,如此往复,宏观上看来,就好像是多个线 程在一起执行.由于这多个线程分属于不同的进程,因此在我们看来,就好像是多个进程在同时执行,这样就实现了多任务.

架构设计:多进程还是多线程

进程间通讯(以下简称IPC)问题

  既然不可能把整个系统放入一个进程,那就必然会碰到IPC的问题。下面就来说一下该如何选择IPC。

  各种操作系统里面,有很多稀奇古怪的IPC类型。由于要考虑跨平台,首先砍掉一批(关于IPC的跨平台问题,我在“跨平台开发”系列中会提 到)。剩下的IPC类型中,能够进行数据传输的IPC就不多了,主要有如下几种:套接字(以下简称Socket)、共享内存、管道、文件。

  其中Socket是我强烈推荐的IPC方式,理由如下:使用Socket可以天然地支持分布式部署;使用Socket可以比较容易地实现多种编 程语言的混合(比如C++、Java、Python、Flex都支持Socket);使用Socket还可以省掉了一大坨“锁操作”的代码。

  列位看官中,或许有人在担心Socket的性能问题,其实大可不必多虑。当两个进程在本机上进行Socket通讯时,由于可以使用 localhost环回地址,数据不用经过物理网卡,操作系统内核还可以进行某些优化。这种情况下,Socket相对其它几种IPC机制,不会有太大的性 能偏差。

  最后再补充一下,Socket方式也可以有效防止扯皮问题。举个例子:张三写了一个进程A,李四写了一个进程B,进程A通过Socket方式发 数据给进程B。突然有一天,两个进程的通讯出故障了。然后张三就说是李四接收数据出错;李四就说张三发送数据出错。这时候怎么办捏?很简单,随便找个 Sniffer软件当场抓一下数据包并Dump出来看,问题就水落石出了。

   为啥还要线程?

  上面说了这么多进程的好处,有同学要问了:“那线程有什么用捏?”总的来说,使用线程出于两方面的考虑:性能因素和编码方便。

  1、性能因素

  由于某些操作系统(比如Windows)中的进程比较重型,如果频繁创建进程或者创建大量进程,会导致操作系统的负载过高。举例如下:

  假设你要开发一个类似Web Server的应用。你针对每一个客户端请求创建一个对应的进程用于进行数据交互(是不是想起了古老的CGI :-)。一旦这个系统扩容,用户的并发连接数一增加,你的应用立马死翘翘。

  上面的例子表明,跨平台软件系统的进程数要保持相对稳定。如果你的进程数会随着某些环境因素呈线性增长,那就相当不妙了(顺带说一下,如果线程数会随着环境因素呈线性增长,也相当不妙)。而根据业务逻辑的单元划分进程,顺便能达到“进程数的相对稳定”的效果。

  2、编码方面

  由于业务逻辑内部的数据耦合比较紧密。如果业务逻辑内部的并发也用进程来实现,可能会导致大量的IPC编码(任意两个进程之间只要有数据交互,就得写一坨IPC代码)。这或许会让相关的编程人员怨声载道。

  当然,编码方面的问题也不是绝对的。假如你的系统有很成熟且方便易用的IPC库,可以比较透明地封装IPC相关操作,那这方面的问题也就不存在了。

鱼还是熊掌:浅谈多进程多线程的选择

关于多进程和多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,线程是CPU调度的最小单位”,这句话应付考试基本上够了,但如果在工作中遇到类似的选择问题,那就没有这么简单了,选的不好,会让你深受其害。 经常在网络上看到有的XDJM问“多进程好还是多线程好?”、“Linux下用多进程还是多线程?”等等期望一劳永逸的问题,我只能说:没有最好,只有更好。根据实际情况来判断,哪个更加合适就是哪个好。 我们按照多个不同的维度,来看看多线程和多进程的对比(注:因为是感性的比较,因此都是相对的,不是说一个好得不得了,另外一个差的无法忍受)。 img

》多进程和多线程:没有绝对的好与坏,只有哪个更加合适的问题。我们来看实际应用中究竟如何判断更加合适。

1)需要频繁创建销毁的优先用线程 原因请看上面的对比。 这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的 2)需要进行大量计算的优先使用线程 所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。 这种原则最常见的是图像处理、算法处理。 3)强相关的处理用线程,弱相关的处理用进程 什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。 一 般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。 当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。 4)可能要扩展到多机分布的用进程,多核分布的用线程 原因请看上面对比。 5)都满足需求的情况下,用你最熟悉、最拿手的方式 至于“数据共享、同步”、“编程、调试”、“可靠性”这几个维度的所谓的“复杂、简单”应该怎么取舍,我只能说:没有明确的选择方法。但我可以告诉你一个选择原则:如果多进程和多线程都能够满足要求,那么选择你最熟悉、最拿手的那个。 需要提醒的是:虽然我给了这么多的选择原则,但实际应用中基本上都是“进程+线程”的结合方式,千万不要真的陷入一种非此即彼的误区。

03.为什么要用线程池,线程池中的线程是怎么运作的?

1、为什么使用线程池?

线程池是运用场景最多的并发框架,几乎所有需要一步或者并发执行任务的程序都可以使用线程池。使用线程池一般有以下三个好处:

①降低资源的消耗,通过重复利用已经创建的线程降低线程创建和销毁造成的消耗。

②提高相应速度,当任务到达的时候,任务可以不需要等到线程创建就能立刻执行。

③提高线程的可管理性,线程是稀缺资源,使用线程池可以统一的分配、调优和监控。

2、线程池的实现原理

当线程池提交一个任务到线程池后,执行流程如下:

线程池先判断核心线程池里面的线程是否都在执行任务。如果不是都在执行任务,则创建一个新的工作线程来执行任务。如果核心线程池中的线程都在执行任务,则判断工作队列是否已满。如果工作队列没有满,则将新提交的任务存储到这个工作队列中,如果工作队列满了,线程池则判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理 ,也就是拒接策略。

img

具体:

(1)如果当前线程少于corePoolSize,就创建新的线程来执行任务,但是这一步会获取全局锁。

(2)如果当前运行的线程大于等于corePoolSize,则将任务加入BlockingQueue。

(3)如果无法将任务加入队列中, 队列已满的话,则创建新的线程处理任务。这一步需要全局锁。

(4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution()方法。

执行execute()方法时,尽可能 地避免获取全局锁。

img

线程池参数介绍:

1)corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线 程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任 务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法, 线程池会提前创建并启动所有基本线程。

2)runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几 个阻塞队列。

ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原 则对元素进行排序。

LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通 常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。

SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用 移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工 厂方法Executors.newCachedThreadPool使用了这个队列。

PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

3)maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并 且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如 果使用了无界的任务队列这个参数就没什么效果。

4)ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设 置更有意义的名字。使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线 程设置有意义的名字。

5)RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状 态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法 处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。

AbortPolicy:直接抛出异常。

CallerRunsPolicy:只用调用者所在线程来运行任务。

DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

DiscardPolicy:不处理,丢弃掉。

keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以, 如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。 ·TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟 (MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒 (NANOSECONDS,千分之一微秒)

04.生产者消费者,信号量的使用

生产者消费者问题是同步问题。有一个固定大小的缓冲区,生产者生产商品并将其输入缓冲区。消费者从缓冲区中删除项目并消费它们。

当消费者从缓冲区中消费商品时,生产者不应将商品生产到缓冲区中,反之亦然。因此,缓冲区只能一次由生产者或使用者访问。

生产者消费者问题可以使用信号量解决。生产者和消费者过程的代码如下:

1.生产者过程

下面给出了定义生产者流程的代码-

do {
   .
   . PRODUCE ITEM
   .
   wait(empty);
   wait(mutex);
   .
   . PUT ITEM IN BUFFER
   .
   signal(mutex);
   signal(full);

} while(1);

在上面的代码中,互斥,空和满是信号量。此处互斥锁初始化为1,empty初始化为n(缓冲区的最大大小),full初始化为0。

互斥信号量确保相互排斥。空和满信号量计算缓冲区中的空和满空间数量。

物料生产后,对空物料进行等待操作。这表明缓冲区中的空白空间已减少了1。然后对互斥量执行等待操作,以使使用者进程不会受到干扰。

将项目放入缓冲区后,对互斥锁和满载进行信号操作。前者指示使用者进程现在可以执行,而后者指示缓冲区已满1。

2.消费者流程

下面给出了定义使用者流程的代码:

do {

   wait(full);
   wait(mutex);
   . .
   . REMOVE ITEM FROM BUFFER
   .
   signal(mutex);
   signal(empty);
   .
   . CONSUME ITEM
   .
} while(1);

等待操作完全执行。这表明缓冲区中的项减少了1。然后对互斥量执行等待操作,以使生产者过程不会干扰。

然后将该项目从缓冲区中删除。之后,对互斥锁和空寄存器执行信号操作。前者指示使用者进程现在可以起作用,而后者指示缓冲区中的空白空间增加了1。

05.队列空时,消费者和生产者会发生什么?线程池请求队列是用什么实现的?(链表)

我们都了解线程池的作用,这里不多做赘述。

首先来看线程池的构造函数:

	public ThreadExecutor(int corePoolSize, //线程池中的线程数
		    int maximumPoolSize, //线程池中的最大线程数
		    long keepAliveTime, //线程数超过指定数值后,多余的空闲线程的存活时间
		    TimeUnit unit, //线程池维护线程所允许的空闲时间的单位
		    BlockingQueue<Runnable> workQueue, //被提交但未被执行的任务等待队列
		    ThreadFactory threadFactory, //线程工厂,用于创建线程
            RejectedExecutionHandler handler)  //拒绝策略,当提交的任务太多,不够线程处理后,如何拒绝任务

我们看到线程池构造函数有7个参数,参数的作用如上图注释。

线程池中维护的任务队列有许多不同功能的实现,今天我们学习以下四种:

1.直接提交队列:使用的是SynchronousQueue实现的队列。提交到这个队列的任务不是真的保存在队列中,而是立即将任务提交给线程执行,如果线程池中没有线程,则立即创建线程执行,如果线程池中线程数大于最大线程数,则会执行拒绝策略,任务拒绝执行。

2.有界的任务队列: 使用的是ArrayBlockingQueue实现的队列。按照先进先出的算法处理任务。使用它时必须设定一个最大容量参数,当有任务提交到线程池中,首先判断线程池中线程数如果小于核心线程数,则立即创建新线程执行任务,若大于核心线程数则将任务提交到任务队列。如果任务队列满了,再创建新的线程执行任务。直到线程数达到最大线程数。

3.无界任务队列:使用的是LinkedBlockingQueue实现的队列。按照先进先出的算法处理任务。使用时和有界队列正好相反,任务队列没有固定容量,如果线程池中线程大于核心线程数,会将任务一直提交到任务队列,直到内存耗尽。

4.无界优先任务队列:PriorityBlockingQueue实现的队列。不是按照先进先出的算法执行,而是按照任务的优先级进行执行。

如何实现不同功能的线程池的呢?

1.newFixedThreadPool 固定线程数线程池

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

我们看到,ThreadPoolExecutor的构造函数,核心线程数和最大线程数被设置成大小一样,第三个参数,线程存活时间设置成0L,意思是当线程池中有线程空闲时,立刻被停止。第四个参数,使用无界队列作为任务队列,意思是当线程数大于核心线程数时,提交到任务队列,直到内存耗尽。

2.newSingleThreadExecutor 单线程线程池

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

ThreadPoolExecutor的构造函数,核心线程数和最大线程数被设置成1,线程存活时间0L,无界任务队列。保证只有一个线程工作。

3.newCachedThreadExecutor 可缓存线程池

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

可以看到核心线程数为0,最大线程数无穷大,存活时间60L,任务队列是直接提交队列。如果线程池中没有空闲线程,将任务提交到任务队列,直接创建线程去执行,如果空闲60s,由于核心线程数为0,则会被回收。

06.C++多线程并发问题(场景千万级数量级怎么处理)

C++11标准在标准库中为多线程提供了组件,这意味着使用C++编写与平台无关的多线程程序成为可能,而C++程序的可移植性也得到了有力的保证。另外,并发编程可提高应用的性能,这对对性能锱铢必较的C++程序员来说是值得关注的。

1. 何为并发

并发指的是两个或多个独立的活动在同一时段内发生。生活中并发的例子并不少,例如在跑步的时候你可能同时在听音乐;在看电脑显示器的同时你的手指在敲击键盘。这时我们称我们大脑并发地处理这些事件,只不过我们大脑的处理是有次重点的:有时候你会更关注你呼吸的频率,而有时候你更多地被美妙的音乐旋律所吸引。这时我们可以说大脑是一种并发设计的结构。这种次重点在计算机程序设计中,体现为某一个时刻只能处理一个操作。

与并发相近的另一个概念是并行。它们两者存在很大的差别。并行就是同时执行,计算机在同一时刻,在某个时间点上处理两个或以上的操作。判断一个程序是否并行执行,只需要看某个时刻上是否多两个或以上的工作单位在运行。一个程序如果是单线程的,那么它无法并行地运行。利用多线程与多进程可以使得计算机并行地处理程序(当然 ,前提是该计算机有多个处理核心)。

  • 并发:同一时间段内可以交替处理多个操作:

img

图中整个安检系统是一个并发设计的结构。两个安检队列队首的人竞争这一个安检窗口,两个队列可能约定交替着进行安检,也可能是大家同时竞争安检窗口(通信)。后一种方式可能引起冲突:因为无法同时进行两个安检操作。在逻辑上看来,这个安检窗口是同时处理这两个队列。

  • 并行:同一时刻内同时处理多个操作:

img

图中整个安检系统是一个并行的系统。在这里,每个队列都有自己的安检窗口,两个队列中间没有竞争关系,队列中的某个排队者只需等待队列前面的人安检完成,然后再轮到自己安检。在物理上,安检窗口同时处理这两个队列。

并发的程序设计,提供了一种方式让我们能够设计出一种方案将问题(非必须地)并行地解决。如果我们将程序的结构设计为可以并发执行的,那么在支持并行的机器上,我们可以将程序并行地执行。因此,并发重点指的是程序的设计结构,而并行指的是程序运行的状态。并发编程,是一种将一个程序分解成小片段独立执行的程序设计方法。

2.并发的基本方式途径

多线程与多进程是并发的两种途径。 想象两个场景:

  • 场景一:你和小伙伴要开发一个项目,但小伙伴们放寒假都回家了,你们只能通过QQ聊天、手机通话、发送思维导图等方式来进行交流,总之你们无法很方便地进行沟通。好处是你们各自工作时可以互不打扰。
  • 场景二:你和小伙伴放假都呆在学校实验室中开发项目,你们可以聚在一起使用头脑风暴,可以使用白板进行观点的阐述,总之你们沟通变得更方便有效了。有点遗憾的是你在思考时可能有小伙伴过来问你问题,你受到了打扰。

这两个场景描绘了并发的两种基本途径。每个小伙伴代表一个线程,工作地点代表一个处理器。场景一中每个小伙伴是一个单线程的进程,他们拥有独立的处理器,多个进程同时执行;场景二中只有一个处理器,所有小伙伴都是属于同一进程的线程。

2.1 多进程并发

多个进程独立地运行,它们之间通过进程间常规的通信渠道传递讯息(信号,套接字,文件,管道等),这种进程间通信不是设置复杂就是速度慢,这是因为为了避免一个进程去修改另一个进程,操作系统在进程间提供了一定的保护措施,当然,这也使得编写安全的并发代码更容易。 运行多个进程也需要固定的开销:进程的启动时间,进程管理的资源消耗。

2.2 多线程并发

在当个进程中运行多个线程也可以并发。线程就像轻量级的进程,每个线程相互独立运行,但它们共享地址空间,所有线程访问到的大部分数据如指针、对象引用或其他数据可以在线程之间进行传递,它们都可以访问全局变量。进程之间通常共享内存,但这种共享通常难以建立且难以管理,缺少线程间数据的保护。因此,在多线程编程中,我们必须确保每个线程锁访问到的数据是一致的。

3. C++中的并发与多线程

C++标准并没有提供对多进程并发的原生支持,所以C++的多进程并发要靠其他API——这需要依赖相关平台。 C++11 标准提供了一个新的线程库,内容包括了管理线程、保护共享数据、线程间的同步操作、低级原子操作等各种类。标准极大地提高了程序的可移植性,以前的多线程依赖于具体的平台,而现在有了统一的接口进行实现。

C++11 新标准中引入了几个头文件来支持多线程编程:(所以我们可以不再使用 CreateThread 来创建线程,简简单单地使用 std::thread 即可。)

  • < thread > :包含std::thread类以及std::this_thread命名空间。管理线程的函数和类在 中声明.
  • < atomic > :包含std::atomic和std::atomic_flag类,以及一套C风格的原子类型和与C兼容的原子操作的函数。
  • < mutex > :包含了与互斥量相关的类以及其他类型和函数
  • < future > :包含两个Provider类(std::promise和std::package_task)和两个Future类(std::future和std::shared_future)以及相关的类型和函数。
  • < condition_variable > :包含与条件变量相关的类,包括std::condition_variable和std::condition_variable_any。

3.1 初试多线程

 1、主进程等待子线程

  

#include <iostream>``#include <thread>``#include <Windows.h>`` ` `using` `namespace` `std;`` ` `void` `thread01()``{``  ``for` `(``int` `i = 0; i < 5; i++)``  ``{``    ``cout << ``"Thread 01 is working !"` `<< endl;``    ``Sleep(100);``  ``}``}``void` `thread02()``{``  ``for` `(``int` `i = 0; i < 5; i++)``  ``{``    ``cout << ``"Thread 02 is working !"` `<< endl;``    ``Sleep(200);``  ``}``}`` ` `int` `main()``{``  ``thread` `task01(thread01);``  ``thread` `task02(thread02);``  ``task01.join();``  ``task02.join();`` ` `  ``for` `(``int` `i = 0; i < 5; i++)``  ``{``    ``cout << ``"Main thread is working !"` `<< endl;``    ``Sleep(200);``  ``}``  ``system``(``"pause"``);``}

  2.主进程和子进程互不干扰

  

#include <iostream>``#include <thread>``#include <Windows.h>`` ` `using` `namespace` `std;`` ` `void` `thread01()``{``  ``for` `(``int` `i = 0; i < 5; i++)``  ``{``    ``cout << ``"Thread 01 is working !"` `<< endl;``    ``Sleep(100);``  ``}``}``void` `thread02()``{``  ``for` `(``int` `i = 0; i < 5; i++)``  ``{``    ``cout << ``"Thread 02 is working !"` `<< endl;``    ``Sleep(200);``  ``}``}`` ` `int` `main()``{``  ``thread` `task01(thread01);``  ``thread` `task02(thread02);``  ``task01.detach();``  ``task02.detach();`` ` `  ``for` `(``int` `i = 0; i < 5; i++)``  ``{``    ``cout << ``"Main thread is working !"` `<< endl;``    ``Sleep(200);``  ``}``  ``system``(``"pause"``);``}

  3.带参数的子线程

#include <iostream>``#include <thread>``#include <Windows.h>`` ` `using` `namespace` `std;`` ` `//定义带参数子线程``void` `thread01(``int` `num)``{``  ``for` `(``int` `i = 0; i < num; i++)``  ``{``    ``cout << ``"Thread 01 is working !"` `<< endl;``    ``Sleep(100);``  ``}``}``void` `thread02(``int` `num)``{``  ``for` `(``int` `i = 0; i < num; i++)``  ``{``    ``cout << ``"Thread 02 is working !"` `<< endl;``    ``Sleep(200);``  ``}``}`` ` `int` `main()``{``  ``thread` `task01(thread01, 5); ``//带参数子线程``  ``thread` `task02(thread02, 5);``  ``task01.detach();``  ``task02.detach();`` ` `  ``for` `(``int` `i = 0; i < 5; i++)``  ``{``    ``cout << ``"Main thread is working !"` `<< endl;``    ``Sleep(200);``  ``}``  ``system``(``"pause"``);``}

  4.多线程竞争的情况

有两个问题,一是有很多变量被重复输出了,而有的变量没有被输出;二是正常情况下每个线程输出的数据后应该紧跟一个换行符,但这里大部分却是另一个线程的输出。

这是由于第一个线程对变量操作的过程中,第二个线程也对同一个变量进行各操作,导致第一个线程处理完后的输出有可能是线程二操作的结果。针对这种数据竞争的情况,可以使用线程互斥对象mutex保持数据同步。

mutex类的使用需要包含头文件mutex:

#include <iostream>``#include <thread>``#include <Windows.h>``#include <mutex>`` ` `using` `namespace` `std;`` ` `mutex mu; ``//线程互斥对象`` ` `int` `totalNum = 100;`` ` `void` `thread01()``{``  ``while` `(totalNum > 0)``  ``{``    ``mu.lock(); ``//同步数据锁``    ``cout << totalNum << endl;``    ``totalNum--;``    ``Sleep(100);``    ``mu.unlock(); ``//解除锁定``  ``}``}``void` `thread02()``{``  ``while` `(totalNum > 0)``  ``{``    ``mu.lock();``    ``cout << totalNum << endl;``    ``totalNum--;``    ``Sleep(100);``    ``mu.unlock();``  ``}``}`` ` `int` `main()``{``  ``thread` `task01(thread01);``  ``thread` `task02(thread02);``  ``task01.detach();``  ``task02.detach();``  ``system``(``"pause"``);``}

  

3.2 在类中使用子线程的一个问题

  当我们再类中使用子线程我们会发现,我们不能把初始函数设置为类的成员函数,必须要把成员函数设置成static类型的才可以,但是这有设计到一个问题,就是static的类成员函数不能调用非static的变量成员,下面是一个两全其美的方法:

thread` `sendtask(bind(&client::sendata, ``this``));``//其中client是类的名字

  这样就可以解决我们的问题。

07.哪几种常见的 signal? SIGSEGV... -> 正常终止程序的信号?-> kill 进程,几号信号?

常见的信号有很多种,以下列举几种常见的:

  1. SIGSEGV:表示程序访问了无效的内存地址。
  2. SIGABRT:表示程序自己调用了abort()函数来终止程序。
  3. SIGINT:表示程序收到了中断信号(通常是由用户按下Ctrl+C发送的)。
  4. SIGTERM:表示程序收到了终止信号(通常是由kill命令发送的,默认为15号信号)。
  5. SIGKILL:表示强制杀死进程(无法被阻塞、处理或忽略),一般用于紧急情况。它会立即终止进程,不管进程是否正在执行任务。
  6. SIGPIPE:表示向已关闭的管道或Socket写数据时,系统会发送这个信号给进程,以提示进程该操作已失败。

正常终止程序可以通过exit()函数来实现,也可以使用return语句从main函数返回0来实现。kill进程使用kill命令,在Linux系统中默认发送15号信号(SIGTERM)。

08.什么情况下会使用静态变量

静态变量是指在程序运行期间只分配一次内存,并且在整个程序运行期间都存在的变量。通常情况下,我们会根据以下几种情况来使用静态变量:

  1. 需要在多个函数中共享同一个变量:由于局部变量的作用域只在当前函数中有效,因此如果需要在多个函数中共享同一个变量,则需要定义为全局变量或者静态变量。
  2. 需要保护变量不被修改:如果定义了一个全局变量,那么任何一个函数都可以修改它的值,这可能会导致出现难以调试的问题。而将该全局变量定义为static类型后,就可以保证该变量只能被定义所在的文件访问到,从而避免出现这种问题。
  3. 需要统计某些数据:有时候我们需要对某些数据进行统计(例如记录某个函数被调用的次数),则可以使用静态局部变量,在每次调用该函数时累加其值,从而达到统计目的。
  4. 为了提高效率:由于静态变量只会分配一次内存空间,并且不需要动态地分配和释放内存,因此相比于动态分配内存的方式,使用静态变量可以提高程序的效率和响应速度。

总之,静态变量在程序中有着广泛的应用场景,能够帮助我们更好地管理和使用变量。

09.多线程读写同一个静态变量你是怎么解决的

在多线程环境下,对于同一个静态变量的读写操作可能会产生数据竞争,因此需要采取相应的措施来解决这个问题。以下是一些可能的解决方案:

  1. 使用互斥锁:可以使用互斥锁来保护静态变量,在每次访问该变量时先获得锁,并在完成操作后释放锁。这样可以确保同时只有一个线程能够访问该变量,避免出现数据竞争问题。
  2. 使用原子操作:某些编程语言提供了原子操作(atomic operation)来保证对于某个共享变量的读写操作是不可分割的。在使用原子操作时,即使多个线程同时访问该变量也不会发生数据竞争问题。
  3. 避免共享状态:如果可能的话,尽量避免多个线程直接对同一个静态变量进行读写操作,而是通过其他方式实现各自之间的通信和协作。例如可以使用消息队列、管道等机制来传递数据。

总之,在处理多线程环境下的静态变量时需要格外小心,必须采取相应的措施来避免数据竞争和其他潜在的并发问题。

10.用过无锁编程吗,知道原子量吗

无锁编程是一种并发编程技术,通过避免使用传统的锁机制来实现线程安全。在无锁编程中,每个线程都可以独立地执行自己的操作,而不需要等待其他线程释放锁资源。这种方式可以提高程序的并发性能和可扩展性。

无锁编程通常涉及对共享数据进行原子化操作。原子化操作是指当多个线程同时访问同一个变量时,仅有一个线程能够修改该变量,并且在该线程修改期间其它线程不能够同时修改该变量。这样就保证了数据的一致性和正确性。

无锁编程常用于高并发场景下,例如网络服务器、数据库等需要处理大量请求的系统。与传统加锁机制相比,无锁编程可以减少竞争情况下所需等待的时间,从而提高程序的响应速度和吞吐率。

在实践中,无锁编程需要考虑一些细节问题以确保程序正确性和效率优化。例如,在选择适当的原子化操作时需要注意其复杂度和成本;在处理共享内存时需要注意内存屏障等问题;在使用原子化类型时要小心对其进行正确、有效的初始化等。

原子量是一种并发编程中的数据类型,能够确保多个线程同时访问同一个变量时,仅有一个线程能够修改该变量,并且在该线程修改期间其它线程不能够同时修改该变量。这样可以保证数据的一致性和正确性。

原子量通常是由硬件或操作系统提供支持的,能够实现基本的原子化操作,例如读、写、加、减等。在C++11标准中引入了std::atomic模板类来实现原子化操作,包括std::atomic, std::atomic, std::atomic等类型。

使用原子量进行并发编程时需要注意以下问题:

  1. 原子化操作应该尽可能地简单和快速,以避免因为过度竞争而导致程序效率下降;
  2. 在使用原子量时要小心处理内存屏障等问题;
  3. 原子化操作只能解决单个变量的问题,如果需要对多个变量进行原子化操作,则需要使用更高级别的同步机制;
  4. 在使用自定义类型进行原子化操作时,需要保证其正确、有效地初始化。
全部评论

相关推荐

1.&nbsp;什么是C++中的模板特化?2.&nbsp;什么是多态?如何在C++中实现?3.&nbsp;什么是虚函数?它的作用是什么?4.&nbsp;C++中的构造函数和析构函数有什么区别?5.&nbsp;什么是抽象类和接口?如何定义它们?6.&nbsp;C++中的类型转换有哪些方式?7.&nbsp;什么是命名空间的嵌套?如何使用?8.&nbsp;如何使用std::mutex进行线程同步?9.&nbsp;C++中的智能指针如何实现自定义删除器?10.&nbsp;什么是RAII的优点?11.&nbsp;C++中的const修饰符有什么作用?12.&nbsp;如何实现一个简单的单例模式?13.&nbsp;什么是C++中的内存对齐?14.&nbsp;如何使用std::atomic实现原子操作?15.&nbsp;C++11引入的std::initializer_list是什么?16.&nbsp;如何使用std::tuple?它的使用场景是什么?17.&nbsp;C++中的decltype有什么作用?18.&nbsp;什么是函数对象(Functors)?19.&nbsp;如何实现一个简单的事件机制?20.&nbsp;C++中的std::move和std::forward有什么区别?21.&nbsp;什么是Lambda捕获?如何使用?22.&nbsp;C++中的std::promise和std::future是什么?23.&nbsp;如何使用std::chrono进行时间测量?24.&nbsp;C++中的std::array如何与std::vector进行比较?25.&nbsp;什么是std::string_view?它的优点是什么?26.&nbsp;C++中的using和typedef有什么区别?27.&nbsp;如何实现一个简单的观察者模式?28.&nbsp;C++中的std::any是什么?如何使用?29.&nbsp;什么是C++中的异常安全保证?30.&nbsp;如何使用std::filesystem处理文件系统操作?31.&nbsp;C++中的constexpr函数有什么限制?32.&nbsp;什么是std::optional的使用场景?33.&nbsp;如何在C++中实现一个简单的线程池?34.&nbsp;C++中的std::string如何支持拼接和查找?35.&nbsp;什么是C++中的static_assert?有什么用?36.&nbsp;如何使用std::copy和std::move进行拷贝和移动?37.&nbsp;C++中的std::vector如何动态调整大小?38.&nbsp;如何在C++中实现一个简单的计时器?39.&nbsp;C++中的std::list和std::vector的内存使用有什么不同?40.&nbsp;什么是C++中的命名空间别名?如何使用?41.&nbsp;如何使用std::variant进行类型安全的联合体?42.&nbsp;C++中的std::shared_mutex如何实现读写锁?43.&nbsp;如何在C++中实现一个简单的状态机?44.&nbsp;C++中的std::function如何与回调函数结合使用?45.&nbsp;什么是C++中的虚表(vtable)?46.&nbsp;如何使用std::for_each和Lambda表达式遍历容器?47.&nbsp;C++中的std::unordered_map是如何实现的?48.&nbsp;如何使用std::map和std::set进行排序?49.&nbsp;C++中的std::unique_lock和std::lock_guard有什么区别?50.&nbsp;如何在C++中实现一个简单的命令模式?问题答案已经整理到专栏中了,关注我分享更多知识。&nbsp;&nbsp;c++/嵌入式面经专栏-牛客网 https://www.nowcoder.com/creation/manager/columnDetail/MJNwoM
点赞 评论 收藏
分享
点赞 3 评论
分享
牛客网
牛客企业服务