Notes3并发/IO(CPU)

Posted 码农编程录

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Notes3并发/IO(CPU)相关的知识,希望对你有一定的参考价值。


1.线程/协程/异步:并发对应硬件资源是cpu,线程是操作系统如何利用cpu资源的一种抽象

线程想提高效率和io密切相关,程序往往都含有io。CPU上下文切换就是先把前一个任务的CPU上下文(也就是CPU寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

线程(操作系统级别概念)是cpu调度的最小单位,cpu并不在意是哪个进程,cpu就是轮换着线程来运行并不需要知道这个线程是属于哪个进程的。左边单核cpu(不是单线程),3个线程(任务都是读取文件)交叉运行完。通过以下两点大大提高了cpu利用率,因而线程想提高效率和io密切相关。
1.DMA过程中cpu一段时间不被线程阻塞。
2.DMA进行数据读取时可复用,因为cpu的总线程具有多条线路,所以DMA可充分利用这些线路,实现并行读取这些文件。

多线程需调用系统底层API才能开辟,在多线程开辟过程中浪费时间,并且在线程运行中上下文切换部分(左边切换多次,右边切换三次)有用户态和内核态转换耗,效率浪费在cpu切换时间点上。所以服务端连接的客户端不活跃多(即io次数少),考虑单线程(io多路复用或nio)协程。上面的1,2,3线程都有io,所以多线程效率高。

协程:编程语言级别的线程,全程处于用户态即协程是用户自定义的线程,协程可大量开辟不用担心用户态和内核态转换,一台机器上几百上千线程可能是极限,但用go语言开辟协程随便几千万,协程有异步(多去多回)的性能,同步(一去一回)的编程方式

2.并发(线程)之可见性:volatile

如下程序一直没结束即另一个线程没结束。一个线程对a写了false,但是对另一个线程并不可见。


如下第一个core为主线程,第二个core为开辟的线程。


如上线程2不能立即读到线程1写后的最新变量值(线程1写,线程2读),多线程不可见性。如何解决多线程不可见性:加volatile关键字使a在主存和localcache间强制刷新一致。

3.并发之原子性(读写原子):AtomicInteger/synchronized

如果线程1和2都进行基于读的变量再对读的变量再进行写,最典型操作i++,T1和T2都进行i++操作。

一开始i=0,经过两个线程两次i++操作结果变成了1,这显然是不对的,并且这种情况下不能用volatile保证这样操作的正确性(两个线程既有读操作,又有基于读操作的写操作,可见性只保证一个线程写另一个线程读是正确的,这里可见性不适用)。

现在想做的是将读操作和写操作合为一步,要么同时发生要么同时不发生(原子性)。在保证原子性同时一定以保证可见性为前提(不是并列关系,AtomicInteger类里本质上就是volatile),本身不可见的话没办法保证原子性。

也可用synchronized同步关键字来保证原子性发生,同步关键字同一时间只有一个线程进入代码段。

volatile可见性关键字最轻量级(保证一个线程写,一个线程读能读到最新的值),AtomicInteger(保证既有读操作又有写操作如i++这种场景下能保证操作的原子性)基于volatile,synchronized最重量级(能保证整个代码块中所有操作都是原子性的)。多线程情况下需要自增请使用Atomicxxx类来实现

4.并发与锁:CPU,协程


内存,cpu(由控制器和运算器组成,通过总线与其他设备连接),io是编程中三个最重要的点。南桥(桥就是连接)连接带宽要求低的设备如是一些鼠标键盘硬盘usb设备等。北桥(集成到了cpu内部)负责带宽比较高的设备如pcie显卡,pcie硬盘,内存RAM需高速访问。如下是cpu常见参数,8核16线程(超线程)。

系统架构指的是处理器指令集,如下常见的6种指令集,X86_64基于X86,ARM不是其他嵌入式类,cortex A系列等。

如下是cpu状态查看。

如何利用cpu资源?外部资源利用都是通过操作系统os提供的接口,os给了我们两种抽象即进程和线程。进程是系统资源分配,调度和管理的最小单位,比如去任务管理器查看使用内存时是看的哪个进程或哪个程序使用了多少内存而不是哪个线程,如果是哪个线程根本不知道是哪个程序里的线程,没法管理。一个进程的内存空间是一套完整的虚拟内存地址空间,这个进程中所有线程都共享这一套地址空间

如下线程的5种状态,只有运行中是占用cpu资源的。阻塞Blocked在java中:有线程sleep就让出cpu资源,还有synchronized关键字对应obj.wait,还有conditation对应await(wait和await会释放锁,sleep不释放锁),还有线程的join等等方法。

线程执行有性能损耗,这些损耗来自线程的创建销毁和切换,线程本质向cpu申请计算资源,用户态转内核态

协程是用户自定义线程但与os的线程不同,协程不进入内核态。自己创建一套API,协程利用线程资源。

4.1 synchronized:mutex重型锁,锁状态

jdk1.6前主要锁的工作方式都是重量级锁(平时理解的锁的方式),它的实现原理就是使用内核中的互斥量mutex,线程1拿了,线程2就拿不到了,这是内核保证的。带来另一个问题就是要进入内核态,用户态内核态切换的开销大。对于重型锁的主要封装都是在C语言的pthread库中,要进入内核态,所以叫重量级锁

如何优化下不进入内核态,或在用户态实现出一把锁?锁状态:0是锁未被使用。如下左边有个线程把state变成0意味着当前线程可拿这把锁了,进入下行把state设为1。解锁时先判断下持有者是不是当前线程,当前线程是持有者才有资格释放锁。

如下左边while(state==1)和下行state=1即比较和赋值这两步操作不是原子性,进行比较时有两个线程同时进行了比较,发现state都=0,都执行了state=1这步操作,两个线程都认为自己拿到了锁,这就是并发问题。如何避免并发问题呢?软件层面无法保证原子性,所以计算机提供了原语cas(compare and swap),两步操作合起来是原子性操作。

while死循环+cas形成的锁就是自旋锁。自旋锁是在用户态实现出一把锁,不进入内核态。cas里有三个参数,比较state是否为0未被使用,是0的话赋值为1并且返回true,无限次尝试拿锁直到拿成功。自旋锁spinlock优点:不进入内核态减少消耗,缺点:无限次尝试拿锁需要消耗cpu,这个缺点在竞争激烈时如很多线程一直拿不到锁,一直在死循环。所以不怎么发生竞争时,自旋锁是比较好的解决方案。

如上两种锁各有优劣,能不能把如上这两种锁结合呢?这种结合也就是jdk1.4之后引入的一种结合,jdk1.4后对重型锁改进引入自旋。比如用synchronized进行拿/加锁时,自旋n次都没能拿到锁,就使用mutex这种重型锁大多场景下如果竞争不激烈的话,自旋n次一定能拿到锁的,这样的话不需要进入内核态,所以效率高。少数情况下竞争激烈,一直自旋导致cpu一直空转,所以自旋一定次数后用mutex进行加锁,mutex如果加锁失败了,线程会被挂起进入阻塞状态让出cpu资源,这样的话算是对资源的合理利用,两者进行了结合。

jdk1.4后自旋n次可通过参数-XX:PreBlockSpin设定,即进入阻塞态之前自旋多少次,没有设置的话默认10次。在jdk1.6后参数不需要我们设置了,而是给了自适应自旋。包括mutex,包括jdk1.4使用的自旋+mutex,包括jdk1.6中使用的自适应自旋+mutex,它们都是指的重量级锁。

jdk1.6提出自适应自旋同时也提出了synchronized关键字应该是四种锁类型:一种叫无锁,一段时间后程序进行有锁并且是偏向锁状态(偏向于某一个线程,如果另一个线程尝试获取锁的话,锁就会升级为轻量级锁),轻量级锁主要应对多线程拿锁,但多线程间没有竞争,如果线程间一旦发生了竞争,轻量级锁会升级为重量级锁,重量级锁里面有自旋和mutex,前面讲的都是重量级锁。

synchronized几种锁状态,不得不提java的对象组成如下:方法区+堆

5.如何应对并发:cdn

1.动静分离,cdn加速资源。2.水平扩展,nginx集群。3.微服务化,多用多分配资源。4.缓存redis减少io寻找。5.队列,秒杀系统采用。

6.IO多路复用:bF用o

硬盘和网卡(IO)。如下A,B。。都是客户端,方框是服务端。首先想到应对并发,写一个多线程程序,每个传上来的请求都是一个线程,现在很多rpc框架用了这种方式,多线程存在弊端:cpu上下文切换,因而多线程不是最好的解决方案,转回单线程。如下while(1)…for…就是单线程。

6.1 select:system是一个C/C++的库函数,select是系统调用函数,

如下虚线上面是准备fds(客户端)和max(max用来卡bitmap多少位)。bitmap是1024位,涵盖了fds中所有信息。上面if(fdx是否有数据)用程序判断在select中是内核来判断,内核判断效率比用户态判断高,因为用户态判断也是询问内核,有个用户态和内核态切换,我们判断每一个每一次都要用户态和内核态切换。

select返回后(如下最后5行)再遍历fd集合中5个fd并判断哪一个fd被set置位了,被set的那个有数据将它数据读出来并puts(处理),和上面用程序判断fd有无数据再处理思路一致。rset被内核set置位了需要每次while回来FD_ZERO赋空值,再FD_SET将fd赋到rset中

以下是select的4个缺点:

6.2 poll:pollfds数组(存5个pollfd结构体struct)替代bitmap

6.3 epoll:epfd是共享内存,不需要用户态切换到内核态

epoll_wait和前面select和poll不一样,有返回值。最后只遍历nfds,不需要轮询,时间复杂度为O(1)。epoll解决select的1,2,3,4。redis,nginx,javaNIO(linux)都用的是epoll,多路io复用借助了硬件上优势DMA。

7.硬盘,网口,套接字:有很多unix底层套接字如mysql本地连接localhost的3306,其实并不走tcp的套接字,而是走底层mysql目录下mysql.sock文件,直接走文件通过unix的底层套接字进行连接,所以本机的数据库访问速度更快

串口硬盘SATA口。

上面是硬盘(块,扇区,文件管理中inode记录的内容),下面是网卡。

8.IO相关的系统调用:编程语言没有魔法,全部依赖操系os的支持,其中最强大的支持莫过于系统调用







如上看出java比C语言系统调用多的多,因为java要启动jvm虚拟机,jvm要读jdk的lib库等很多操作。如上并没有发现open…xml操作,因为java程序主要启动jvm进程,jvm进程可能又起了很多线程去真正运行main函数,所以加-f。

9.Java中BIO:阻塞,多线程

同步:java自己去处理io。异步:java将io交给操作系统去处理,告诉缓存区大小,处理完成回调。阻塞:使用阻塞io时,java调用会一直阻塞到读写完成才返回。非阻塞:使用非阻塞io时,如果不能立马读写,java调用会马上返回,当io事件分发器通知可读写再进行读写,不断循环直到读写完成。

(1) 并发连接数不多时采用,一个连接一个线程,有的客户端没数据,造成服务端线程内存浪费,用线程池解决,但不是根本解决。
(2) 请求注册到多路复用器Selector上,Selector轮询到连接有io数据时才启动一个线程处理。
(4) jdk1.7之后异步非组塞,如数据已经到网卡了,然后操作系统发布一个读事件出来。nio就调用同步read方法进行读取数据了。aio的话,那就是调用异步read方法,等待操作系统真的read完成再回调应用程序。

package com.itheima.com;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
// 服务端两个socket,一个用于listen,一个用于connect
// 客户端一个socket用于connect   // bind,accept,read
public class qqserver {
    static byte[] bytes = new byte[1024];
    public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket();
            serverSocket.bind(new InetSocketAddress(8080));
            while(true) {  // 一直接受客户端
                System.out.println("wait conn");

//111111111111111111111111111111111111111111111111111111111111111111111111111111111111
                Socket socket = serverSocket.accept(); //阻塞           
                // 下面全部代码放thread1线程中,不影响上行主线程accept
                // 1000万线程里只有200万活跃,800万不活跃,线程上下文切换耗资源
                // 所以在服务端不活跃多,考虑单线程。
                System.out.println("connect success");
                System.out.println("wait data");

//111111111111111111111111111111111111111111111111111111111111111111111111111111111111                                  
                socket.getInputStream().read(bytes); //阻塞  read读了多少字节,如果第一个客户端不发消息,则一直停留在这
                System.out.println("data success");                
                String content = new String(bytes); //将bytes字节转为字符串
                System.out.println(content);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
package com.itheima.com;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

public class client {
    public static void main(String[] args) {
        try {
            Socket socket=new Socket("127.0.0.1",8080);
            Scanner scanner = new Scanner(System.in);
            String txt = scanner.next();
            socket.getOutputStream().write(txt.getBytes());
            // socket.getOutputStream().write("111".getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

10.Java中NIO:buffer就是一数组

很少自己去写NIO,但是一直在用NIO,因为netty,tomcat等基础网络框架都在用NIO。在java中封了8种数据类型的buffer,像int buffer等。channel就是一入口,如我们把一个文件生成一个channel意思就是我要读这个文件,把这个文件内容读到buffer里,后面从buffer里拿出数据来就把文件内容读出来了。

下面是文件nio,没用到selector:

下面是网络nio,用到selector:每个channel比作tcp客户端连接,3个channel在整个socketserver创建时会注册到selector中,selector不断扫描下面的channel,一旦channel有数据就会处理,像io多路复用编程方式。

如上是NioTest1.java,下面看下系统调用,发现j2.out里有epoll_create,epoll_wait。

以上是关于Notes3并发/IO(CPU)的主要内容,如果未能解决你的问题,请参考以下文章

复习打卡--0821多线程并发

python 复习—并发编程系统并发线程和进程协程GIL锁CPU/IO密集型计算

并发编程5

并发增加cpu涨幅不大怎么办

异步IO和协程

两种高效的并发模式(半同步/半异步和领导者/追随者)