ps:本笔记不保证正确性,属个人总结
不定时更新
笔记:
乐观锁和悲观锁:在进行多线程操作的时候,刚刚会出现多个线程同时对一个数据进行读写,这时候为了保证数据一致性,需要对数据进行加锁。悲观锁指的是常见的锁,比如unix中的pthread_mutex_lock()。他的思想是,我不管你对数据做了什么操作,我都要锁起来,保证数据一致。乐观锁对数据不进行加锁,他只在数据进行写操作时进行判断,利用一条原子指令,c++中有一条CAS指令叫做__sync_bool_compare_and_swap(type * number, type oldNumber, type newNumber),number指针指向我们要保护的数据,他的操作是判断oldNumber和number指向数据进行比较,如果相同,那么number指向的数据变成newNumber的值。注意,这条指令是原子操作,由操作系统(或者是CPU指令)提供。对比下可以发现,悲观锁在阻塞等待占用的资源比较多,不适用读操作多的情况,乐观锁不加锁,所以适合读操作多的情况,但是缺陷是需要反复轮询。具体性能差异可以自行尝试。
(悲观)锁的种类:互斥量mutex,读写锁rwlock,条件变量cond,自旋锁spin,屏障barrier。互斥量是最简单的锁,直接lock()锁上,或者阻塞等待unlock()。读写锁分为读锁和写锁,加了读锁后写操作会被阻塞,而加了写锁后读写操作都会被阻塞。条件变量是先在一个地方设置wait()阻塞住,然后在其他地方启动signal,发送信号给阻塞的线程。要与互斥量配合使用,原因是使用条件变量的时候,往往带一个判断条件,比如if (a == 0) wait();那么把a先锁起来。调用wait的时候,会传入一个锁作为参数,执行的时候会先解锁,再阻塞住,接受到信号再锁上,是一个原子操作。自旋锁是和互斥量相似,但是互斥量是利用休眠对线程进行阻塞,而自旋锁是利用忙等进行阻塞,类似于我们写的while(lock_num == 0);适用于锁占用时间不长的情况,和一些不能休眠的线程,比如中断处理程序,现在一些互斥量会采取自旋一段时间,然后再休眠的做法提高效率。屏障,顾名思义,可以在某个地方设置一个屏障,然后设置一个count,代表等到多少个线程到达这里后取消阻塞。如果没有到达指定线程数,其他先到达的线程都会被阻塞住。
(悲观)锁的属性:进程共享/私有,超时时间,递归/非递归(可重入/不可重入),阻塞/非阻塞(lock/trylock)。进程共享/私有:可以设置这个锁是不是进程共享,底层实现是将锁所在空间映射到自己进程的独立空间上。超时时间:可以设置阻塞多久,如果超过超时时间,取消阻塞,并返回一个错误码ETIMEDOUT。递归/非递归(可重入/不可重入):同一个线程对同一个锁可以同时加锁多次,但是同样需要记数。其他线程想要加锁必须等待拥有锁的线程释放完该锁。作用是一定程度上避免死锁。考虑一种情况,同一个线程连续进行两次lock,两次unlock。如果采用非递归锁,会导致死锁,如果采用非递归锁就不会。他的思想是,同一个线程可以同时拥有一个资源多次,并不会产生冲突,因为你是一个线程,不可能同时读写多次造成数据不一致。
原子性操作:一般是指某个操作(可能包含一个或者多个指令)要么成功要么失败,而且中途不会被打断,像执行一条指令那样。
可重入函数:指一个函数可以同时被调用多次,而且不会有影响,一般是因为该函数拥有自己的独立空间。不可重入函数一般是因为写了全局变量或者静态变量之类的变量,而且没有加以保护。注意可重入不保证原子性,可重入可以被中断,但是不影响结果,而原子性是不允许被中断的。
死锁情况:锁的情况很多。其一:A代码段锁了locka,现在需要对lockb进行加锁;同时,B代码段锁了lockb,现在需要对locka加锁。可以看到两边都要彼此的资源,但是都获取不到,获取不到的话,就会卡在那里,不会释放自己有的资源,导致死锁。其二:执行了类似lock(); lock(); unlock(); unlock();的代码。(待补充)
死锁避免:对于第一种情况,可以在一开始就提前分配好资源,也就是在定义的时候就把锁给加上,但是这样会导致其他没拥有锁的线程等待太久。或者采用死锁检测算法,先检测资源数是否足够,再决定要不要执行该线程,这样也有弊端,可能一直执行不了。对于第二种情况,可以使用递归锁解决,我们知道同一个线程可以重复对递归锁加锁,也就是说同一个线程不会卡在这种地方。当然最好的方法就是不要写这种写法。(待补充)
网络字节序:接触这东西是在写websocket解析的时候。先讲下大小端,大端是指数据的最高字节在地址最低位,小端是指数据的最低字节在地址的最低位。比如一个int有4字节,比如1,按小端方式存储,1作为第一个字节会被放到最低位,而大端存储会把1放到最高位。所以为了避免数据不一致,需要进行转换。tcp规定使用大端,所以websocket也是使用大端。注意如果传输的是char(一个字节)数组,是不需要转换的,只有传输多个字节为一个整体,比如int,long等由多个字节合成一个整体的才需要转换。
read()返回值:这个其实挺重要的,但是往往被忽视。当句柄设置为阻塞式时,read()返回值分三种情况,当返回数值大于零,说明成功读取,返回读取的字节数;当返回值为0,说明读到文件结尾了;当读到-1时,说明读取出现错误。非阻塞式read也有三种返回值,大于零同样是返回成功读取的字节数,0说明文件被关闭,-1分为没读取到,或者错误返回,当read没读取到任何字节时,会设置errno为EWOULDBLOCK,如果errno是其他标志,说明是错误返回。
write()返回值:和read()差不多,阻塞式write()返回大于零的值,是为成功写入的字节数,0说明缓冲区满了,-1代表错误结束。非阻塞式write()返回大于零的值,是为成功写入的字节数,0说明文件被关闭,-1可能是缓冲区满了,此时errno为EWOULDBLOCK或者EAGAIN或者EINTR,否则为异常结束。
socket阻塞/非阻塞:创建socket时可以选择阻塞还是非阻塞,阻塞式socket会在accept或者read等地方被阻塞住,非阻塞式则不会。阻塞式socket谨慎写while(read(fd, buffer, buffer_size) > 0);之类的代码,因为他只会等到你socket关闭或者异常结束时才会返回小于等于0的数,也就是说你socket读取全程会卡在这里。使用非阻塞式socket要注意可能来不及读取。正常设置socket为阻塞式,可以选择sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);加入SOCK_NONBLOCK标志设置socket为非阻塞式。不过就算你设置了阻塞式socket,也可以选择用accept4()函数执行非阻塞式accept,accept4比accept多了一个第四个参数flag,可以设置SOCK_NONBLOCK标志设置为非阻塞。同样,可以使用recv()代替read()实现非阻塞读,recv()多了第四个参数flag,设置为MSG_DONTWAIT标志即可。注意socket连接时,你accept返回一个socket时,这个socket是否阻塞是由发起人决定的,与你的监听socket是否阻塞无关。
epoll之ET、LT:用过epoll的知道,调用epoll_wait()可以接受到触发的事件。ET为边缘触发,每个事件触发后,epoll_wait()只会返回他一次,这意味着如果你没有处理完毕,你下次再执行epoll_wait()他不会被返回。LT为水平触发,他会一直返回那些未被处理完的事件。一般来说ET效率高,但是需要特殊处理。这里需要注意的是,你用的是非阻塞式socket还是阻塞式socket,ET模式下只可以使用非阻塞式socket,原因可以看上一条,阻塞式IO会一直等到socket关闭或者异常结束,也就是说,如果你用了阻塞式socket,读取一次后,下次再有读取事件epoll也不会提醒你了。使用ET模式时,写操作不会重复提示,除非缓冲区为空,同理读操作只有缓冲区满了才会再次提示。
问题:
1、为什么epoll需要写事件提示?
写事件很多时,防止等待太久,将事件加入epoll,等缓冲区可写/为空时epoll再提示你去写,节省资源
2、epoll LT模式下应该如何配置?
3、epoll ET模式下应该如何配置?
ET模式下,应采用非阻塞socket,因为用了非阻塞式socket,所以accept也是非阻塞的,你可以采用轮询方式询问accept,但是资源消耗大,可以将accept当作一个读事件(in)注册在epoll上,让epoll提醒你。其他读写任务同理。
4、json传输,解析比较费时,如何解决?
自己定义协议,或者使用protobuf
5、使用cpp websocket读取为什么有时候只读了一半?
如果你是自己解析的协议,注意数据是可能带'\0'的,处理时要做好相应处理。