Andy Niu �����ĵ�

Andy Niu

Andy Niu Help  1.0.0.0
计算机原理

变量

 PV操作
 
 理解内存
 
 线程栈空间的大小
 
 理解浮点数精度
 
 理解字符编码
 
 理解unicode和utf8
 
 编程中的契约精神
 

详细描述

变量说明

PV操作
1、首先应弄清PV操作的含义:PV操作由P操作原语和V操作原语组成(原语是不可中断的过程),
    对信号量进行操作,具体定义如下:
    P(S):
        ①将信号量S的值减1,即S=S-1;
        ②如果S>=0,则该进程继续执行;否则该进程置为等待状态,排入等待队列。
    V(S):
        ①将信号量S的值加1,即S=S+1;
        ②该进程继续执行(不阻塞),并且,如果该信号的等待队列中有等待线程,唤醒一等待线程。
2、PV操作的意义:我们用信号量及PV操作来实现进程的同步和互斥。PV操作属于进程的低级通信。
3、什么是信号量?
    信号量(semaphore)的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。
    信号量的值与相应资源的使用情况有关。
    当它的值大于0时,表示当前可用资源的数量。
    当它的值小于0时,其绝对值表示等待使用该资源的进程个数。
    注意,信号量的值仅能由PV操作来改变。
4、P操作,S的值减1。
    如果S>0,说明还有资源,线程不阻塞,继续运行下去。
    如果S=0,说明刚好用完资源,当前线程也获取到了资源,线程不阻塞,继续运行下去。
    如果S<0,说明没有可用资源,当前线程移到等待队列中,等待被唤醒。S的绝对值就是等待线程的个数。
5、V操作,S的值加1。
    如果S>0,说明还有资源,没有线程在等待,不需要做什么。
    如果S=0,说明S加1之前取值是-1,有一个线程在等待,需要做的事情是:唤醒队列中的这个线程。
    如果S<0,说明之前有多个线程在等待,假如当前S取值为-4,说明S加1之前是-5,队列中有5个线程在阻塞。
             这时候需要做的事情是:唤醒队列中第一个线程,还有4个线程等待。
    注意:V操作本身不会被阻塞,只是判断是否需要唤醒阻塞的线程。
6、特别注意:PV操作的P操作会导致线程阻塞,而V操作不会导致线程阻塞,只是判断是否需要唤醒阻塞的线程。
    对于生产者/消费者模式,要表达生产者线程阻塞(不能继续生产),需要使用一个反向的信号量。
参见
理解unicode和utf8
1、首先思考编码的本质,计算机只能认识和存储0和1,要表示一个符号,必须给这个符号指定一个序号,也就是一个数字。
    呈现符号的时候,把这个序号解析出来,找到对应的符号,系统渲染出这个符号。
    存储符号的时候,把符号编码成一个序号,也就是数字,写到内存或者硬盘上。
2、最开始只有英文字符,一个字节可以表示200多个数字,足够给这些符号标识序号,这就是ASCII码
3、但是问题来了,每个国家的符号不一样,个数可能达到几万,因此各个国家都有自己的一套编码规则。
    也就是对自己国家的符号,进行标序号,使用两个字节表示一个符号,就是ANSI,这些编码规则都兼容ASCII码。
    ASCII是单字节编码,ANSI是双字节编码。
    在中国ANSI就是gb2312,2个字节可以表示6万多个符号,足够了,gbk是兼容gb2312的,比gb2312表示的符号更多。
4、这又导致问题,各个国家都有自己的一套编码规则,同一个序号在不同的编码规则中,表示的符号不一样。
    那么,在一个页面中,不能同时表现中文和日文。
    (注意:乱码的本质是用一种编码规则去解析另一种编码规则)
5、怎么解决这个问题?
    需要一种编码规则,对地球上已经存在的每个符号,都指定一个唯一的序号,这就是unicode(universal code)。
    特别注意:unicode只是一个标准,为每个符号分配一个唯一的序号。
6、unicode的实现有utf8,utf16,utf32
    注:这里的utf是universal transformation format
    8、16、32分别表示8bit、16bit、32bit为编码单位。
    其中utf16和utf32是定长编码,而utf8是可变长编码
7、现场考虑中文字符 "汉",分别用utf16和utf8表示。
    在unicode的约定中,"汉"的序号是0x6c49,也就是第0x6c49个符号,使用utf16表示,编码就是0x6c49
    使用utf8表示有些复杂,utf8是可变长编码,前缀有几个1,就表示后面使用几个字节表示符号。如下:
    Unicode序号      ║ UTF-8 字节流(二进制) 
    000000 - 00007F ║ 0xxxxxxx 
    000080 - 0007FF ║ 110xxxxx 10xxxxxx 
    000800 - 00FFFF ║ 1110xxxx 10xxxxxx 10xxxxxx 
    010000 - 10FFFF ║ 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 
    0x6c49的二进制是 0110 1100 0100 1001,在范围000800 - 00FFFF,使用三个字节表示,
    按顺序放入1110xxxx 10xxxxxx 10xxxxxx中,0110放入第一个字节的后面4bit,1100 01 放入第二个字节的后面6bit,
    00 1001放入第三个字节的后面6bit,也就是 1110[0110] 10[1100 01] 10[00 1001]中,也就是16进制 E6 B1 89
8、相对于unicode的其它编码实现,utf8使用更广泛,原因是:
    a、完美兼容ascii码,使用一个字节表示。
    b、节省存储空间
9、这里存在一个问题,对于unicode,怎么知道是哪一种编码实现?以及填充方式,也就是字节顺序(Byte Order Mark)。
    (注:字节顺序就是大端和小端,比如有个表示45,大端取值是45【从前往后读】,小端取值是54)
    因此需要在字节流的开头,标识是哪一种的unicode的编码实现和字节顺序,这就是BOM头,如下:
    EF BB BF    UTF-8
    FE FF     UTF-16/UCS-2, little endian
    FF FE     UTF-16/UCS-2, big endian
    FF FE 00 00  UTF-32/UCS-4, little endian
    00 00 FE FF  UTF-32/UCS-4, big-endian
    对于utf16和utf32,有大端和小端的表示方式,但是对于utf8只有一种方式,就是小端。
    因此,对于unicode的编码实现,utf8并不需要字节流的开头标识,思考为什么?
    utf8的编码设计很巧妙,首先没有大端和小端,前缀有几个1,就把后面的几个字节当成一个符号来解析。
10、也就是说utf8不需要包含BOM,包含BOM在linux上面反而会带来一些问题。
    因此,utf8最好不要包含BOM
理解内存
1、32位机器,4G内存,指针使用4个字节表示,可以表示的地址个数是2^32次方,共40多亿个地址,每个地址也就是一个房间。
    也就是说4G内存共有40多亿个房间,可以存放40多亿个整数,可以存放40亿*4个字符。
    但是,每个房间大小是2^32次方,可以表示的整数范围[0,2^32)
    注意:这里有两个概念,多少个房间,每个房间的大小
2、32位机器,1G内存,指针还是使用4个字节表示,可以表示的地址个数是2^30次方,共10多亿个地址,也就是10亿个房间可用。
    可以存放10亿个整数,但是房间大小还是2^32次方,可以表示的整数范围[0,2^32)
3、32位机器,8G内存,指针还是使用4个字节表示,可以表示的地址个数是80亿个,共有80多亿个房间。
    但是每个房间的大小还是2^32次方,可以表示的整数范围是40多亿,而指针本身也是存放在每个房间里,
    也就是说,房间能够表示的指针大小是0到40多亿,编号为50亿的房间是访问不到的。编号40多亿到80多亿的房间,没法访问到,
    是浪费的。
4、怎么解决上面的问题?
    上面问题产生的原因是:房间个数足够多,但是房间大小太小,解决办法是增加房间的大小,使用64位,指针使用8个字节表示,
    房间能够表示的指针大小是0到2^64次方,编号0多亿到80多亿的房间,都能访问到,更大编号的房间也能访问到,最大的编号
    是2^64次方,这个数字非常大。
5、BTY,顺便说一下,IPV4是4个字节表示网络Ip地址,而IPV6不是6个字节,而是16个字节,共128位,IPV6的表示方法是分成8组,
    每一组是2个字节,使用4个16进制的数字。一个16进制范围[0,2^4次方),也就是半个字节,4个16进制数字就是两个字节,
    调试的时候,指针使用8个16机制表示,也就是4个字节。
参见
理解字符编码
1、可以这样理解,字符编码就是对于字符集合,每个字符设置一个id,第几个表示某个字符。
2、举例来说,有两套编码,编码A只有三个字符 甲a1,编码B只有六个字符 abcd12,他们的编码分别是: 
    甲[00] a[01] 1[10];a[000] b[001] c[010] d[011] 1[100] 2[101]
3、对于不同字符编码,同一个id对应不同的字符,如甲[00] 和 a[000],同一个字符对应的Id不同,如a[01] 和 a[000]
4、这里要区分,当成某种编码解释和转化为某种编码。
5、当成某种编码解释,也就是只看Id,存在两种情况:
    一是编码A有意义的字符,当成编码B来看,是一堆没有意义的字符,也就是乱码。
    二是,当成编码B来看,编码B中可能没有对应的Id,错误。
6、转化为某种编码,先看编码A的Id,找到对应的字符,再看这个字符在编码B中对应哪个Id,将前一个Id换成编码B中对应的Id。
    比如:编码A [0110]-->对应a1-->从编码B中找到a1对应的Id-->Id替换,变成[000100]。
    这里存在一个问题,假如编码A中的一个字符,比如甲,在编码B中没有对应的字符,这个时候出错。
理解浮点数精度
1、为什么叫浮点数?
   相对于浮点数,就是固点数,小数点固定在最右边,也就是整数。浮点数的小数点,根据指数的取值,左右移动。
2、考虑二进制整数,假设只有2个bit,可以表示00,01,10,11,共四个整数,表示范围是[0,3],可以表示这个范围内的所有整数。
3、考虑二进制小数,假设只有2个bit,可以表示多少个小数?   
    答案也是四个。假设小数点在最左边,分别为00,01,10,11,表示的值分别为0.0,0.25,0.50,0.75。表示范围[0.0,0.75],
    特别注意:不同于整数,整数可以表示范围内的每一个整数,如[0,3]。而小数只能表示[0.0, 0.75]范围内的四个小数。
    我们知道从0.0到0.75有无数个小数,两位二进制只能表示其中的4个。
4、十进制0.0到0.9,有几个可以使用二进制表示?
   只有两个,0.0和0.5。考虑十进制,小数第一位是1/10,小数第二位是1/100,小数第三位是1/1000,那么二进制呢?
   小数第一位是1/2,小数第二位是1/4,小数第三位是1/8,那么0.1 可以使用下面的方式表示吗?
   a1*1/2 + a2*1/4 + a3*1/8 + ....
   存在这样的a1,a2,a3吗?
   答案是不存在。
5、0.1+0.2 为什么不等于0.3?而是0.30000000000000004
   在计算的时候,计算机要把0.1和0.2转化为二进制表示,由上面分析,我们知道计算机无法准确表示0.1和0.2,
   只能是无限接近地表示,那么无限接近0.1和0.2的两个值相加,当然不能保证是0.3,但是可以保证的是,结果无限接近0.3。
6、那么怎么解释 0.2+03 会等于0.5呢?
    举个例子,计算 1.6+1.8,现在假设不能准确表示1.6和1.8,只能准确表示整数,先转化为最接近的整数,也就是2+2=4,
    这与1.6+1.8=3.4,再转化为最接近的3,相差为1。
    那么是不是,所有的计算结果都不准确呢?
    不是这样,考虑1.2+1.8 =3,转化为处理是 1+2 =3,计算的结果是准确的。
    也就是说,转化过程中精度缺失,如果两个加数都多了一点,其和多了一点加一点。
    如果一个加数多了一点,一个加数少了一点,刚好相互抵消,其和刚好非常准确,一点不差。
7、二进制与十进制的转化,
   二进制整数转为十进制整数,二进制小数转为十进制小数都简单。
   十进制整数转为二进制小数,除2取余,倒序排列。
   十进制小数转为二进制小数,乘2取整,顺序排列。用这种方法,可以知道0.1永远不能得到0
8、思考一下,十进制小数0.1不能用二进制准确表示,那么是不是所有的二进制小数,都可以使用十进制准确表示呢?
    可以。二进制小数,转为十进制,就是a1/2+a2/4+a3/8....,a1,a2,a3取值为0或者1,
    那问题就转化为,1除2的n次方,是不是都能除尽。类推一下,0.5,0.25,0.125,每次末位都是5,除2结尾是25,永远都能除尽。
9、从数学的角度分析,对于小数,2进制只能表示1/2,  3进制只能表示1/3, 2/3, 3进制没法表示1/2, 也就是一半,
    你会说1.5/3 就是一半呀,这就是一个递归的问题,那1.5怎么用3进制表示?没法表示。
    那么10进制,只能表示1/10, ...9/10,  而恰巧5/10就是1/2, 因此10进制能够表示2进制的任何小数。
10、如果我想让0.1+0.2 等于0.3,怎么办?
    从上面分析知道,二进制可以表示可表示范围内的任意一个整数。
    我们把0.1和0.2根据小数点分成两部分,同时记住小数点的位置。分别变成整数相加,再合并进位,
    在字符串中添加小数点的位置即可。需要注意的是:小数点左边右对齐,小数点右边左对齐。
    比如:12.46+5.5400,分别为12+5,4600+5400。
    代码如下:
    string NzbUtils::GetRightDouble(string a, string b)
    {
        vector<string> aVec;
        StringSplit(a,".",aVec,true);
        vector<string> bVec;
        StringSplit(b,".",bVec,true);
        int c1 =  atoi(aVec[0].c_str()) + atoi(bVec[0].c_str());
        int maxLen = aVec[1].size() > bVec[1].size() ? aVec[1].size():bVec[1].size();
        if(maxLen > aVec[1].size())
        {
            while(maxLen > aVec[1].size())
            {
                aVec[1]+="0";
            }
        }
        if(maxLen > bVec[1].size())
        {
            while(maxLen > bVec[1].size())
            {
                bVec[1]+="0";
            }
        }
        int c2 =  atoi(aVec[1].c_str()) + atoi(bVec[1].c_str());
        char ch[64] = {0};
        sprintf(ch,"%d",c2);
        if(strlen(ch) > aVec[1].size())
        {
            c2 = atoi(ch+1);
            c1 = c1+1;
        }
        char ret[64] = {0};
        sprintf(ret,"%d.%d",c1,c2);
        return ret;
    }
线程栈空间的大小
1、Linux下面线程栈默认是10M,单位是K,设置和查询如下:
    [root@localhost IBP]# ulimit -s
    10240
    [root@localhost IBP]# ulimit -s 5120
    [root@localhost IBP]# ulimit -s
    5120
    因此特别注意:Linux线程开得太多,会导致内存使用特别多,解决办法是:
    减少线程的个数,同时线程栈的大小设置小一点。
    因为线程栈只是用于记录程序的调用过程和栈上自动变量的内存分配,一般2M就够用了。
2、Windows下线程栈默认只有1M,一般情况下也是够用的。
编程中的契约精神
1、问题来源:
    软件开发中有一个本质性的难题,那就是错误处理。错误处理是程序员面临的最大困难之一,C++之父说过:
    探察错误的一方(接口提供者)不知道如何处理错误,而知道如何处理错误的一方(接口使用者)往往没有能力探察错误。
    使用防御式代码来解决,往往会破坏程序的正常结构,而带来更多的错误。
2、这个问题的解决办法是:异常机制。
3、但是,这就要搞清楚什么是真正的异常,只有在真正异常的情况下使用异常。
    以往,我们认为异常就是:接口没有完成它所声称可以完成的任务。但是这句话是不完整的。
    还要加上一句,在满足约束条件的情况下,接口没有完成它所声称可以完成的任务,才是异常。
4、这也就是软件开发中的契约精神,接口提供者和接口使用者是平等的,我提供正确的服务,你要保证约束条件,
    我没有义务"排除万难"地去完成任务。接口提供者声称我能完成什么任务,但同时也要指定约束条件。
5、如果没有契约精神,会导致责权不清的问题:我说你给的条件不满足,你说我没有做防御式编码,来回踢皮球。
Copyright (c) 2015~2016, Andy Niu @All rights reserved. By Andy Niu Edit.