0%

云计算的三种服务方式

  • IAAS(基础设置即服务):虚拟的服务器、存储、网络
  • PASS(平台即服务):中间件、应用服务器平台等
  • SAAS(软件即服务):行业应用,CRM,ERP、OA等
    阅读全文 »

写在前面,朋友前几天找到我江湖救急,问我怎么合并多张表格,虽然是计算机专业毕业的我但是真的没做过几张表格啊,最后找了好多网站的资料介绍,找到下面可行的方法,回馈给需要的朋友。

在一个excel文件中如果存在几张附表,就可以使用下面的VBA代码合并附表到一个主表中。

方法是先新建一个主表,右键最下面的主表,然后选择查看代码,贴入下面的代码。然后保存运行即可发现已经合并成功。


方案一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Sub main()

For Each sh In Sheets

If sh.Name <> "总表" Then

i = sh.Range("I65536").End(3).Row

k = Range("A65536").End(3).Row

sh.Range("A1:I50" & i).Copy Range("A" & k + 1)

End If

Next

End Sub

方案二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Sub main()
Application.ScreenUpdating = False

For j = 1 To Sheets.Count

If Sheets(j).Name <> ActiveSheet.Name Then

X = Range("A65536").End(xlUp).Row + 1

Sheets(j).UsedRange.Copy Cells(X, 1)

End If

Next

Range("B1").Select

Application.ScreenUpdating = True

MsgBox "当前工作簿下的全部工作表已经合并完毕!", vbInformation, "提示"

End Sub

有些win7、win10用户莫名的遇到了一个问题,就是安装部分软件的时候提示系统资源不足,无法完成请求服务,刚开始以为是系统缺少了某些组件,结果发现是国外杀毒迈克菲(McAfee)在捣鬼。
报错

关闭迈克菲的实时防护或者卸载即可。

在云服务器上安装好系统后:

**1.**先查看系统上有无原有的旧版本的mysql;tomcat;java。

若有则先卸载;卸载命令使用

1
2
yum -y remove mysql*;
yum -y remove mysql*;

**2.**附上能下载的jdk1.7的链接,先下载jdk1.7后tomcat7放在本地目录下;

             #wget http://zhibo100.oss-cn-hangzhou.aliyuncs.com/software/jdk-7u79-linux-x64.tar.gz

             #wget http://zhibo100.oss-cn-hangzhou.aliyuncs.com/software/apache-tomcat-7.0.59.tar.gz

**3.**在/usr目录下建立一个java目录,java下建立jdk和tomcat目录用来存放解压好的文件;

**4.**解压jdk ,tomcat,命令如下;

            # tar -zxvf jdk-7u79-linux-x64.tar.gz -C /usr/java/

            # tar -xzvf apache-tomcat-7.0.59.tar.gz -C /usr/java/tomcat/

**5.**配置环境变量

配置JAVA环境变量并检测

1
vim /etc/profile

在文件末尾加上

#java

    JAVA_HOME=/usr/java/jdk1.7.0_79

    JRE_HOME=${JAVA_HOME}/jre

    CLASS_PATH=${JAVA_HOME}/lib

    PATH=$PATH:${JAVA_HOME}/bin

    export PATH JAVA_HOME CLASS_PATH PATH

保存退出

**6.**刷新环境变量

1
2
3
4
5
6
7
8
9
[root@luo ~]# source /etc/profile

[root@luo ~]# java -version

java version "1.7.0_79"

Java(TM) SE Runtime Environment (build 1.7.0_79-b15)

Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)

如图说明Java安装成功。

**7.**开启tomcat:

1
2
3
cd /usr/java/tomcat/apache-tomcat-7.0.59/bin/

./startup.sh

验证能否访问,在浏览器下输入localhost:8080  能出现tomcat的页面就行了。

设置tomcat自启动,

在进去  vim /etc/rc.d/rc.local 最下面加入:

1
2
3
4
5
export JDK_HOME=/usr/java/jdk1.7.0_79

export JAVA_HOME=/usr/java/jdk1.7.0_79

/usr/java/tomcat/apache-tomcat-7.0.59/bin/startup.sh

然后重启服务器试验。

**8.**安装mysql;因为yum源中默认没有mysql的安装源,所以需要手动添加,按照下面的语句顺序能顺利安装。

1
wget https://dev.mysql.com/get/mysql57-community-release-el6-9.noarch.rpm

b.安装用来配置mysql的yum源的rpm包

1
2
3
rpm -Uvh mysql57-community-release-el6-9.noarch.rpm

yum localinstall -y mysql57-community-release-el6-9.noarch.rpm

安装成功后在/etc/yum.repos.d/下会多出几个mysql的yum源的配置

然后安装mysql

yum install mysql-community-server

image

开启mysql服务

1
service mysqld start

mysql安装成功后创建的超级用户‘root‘@’localhost’的密码会被存储在**/var/log/mysqld.log**,可以使用如下命令查看密码

1
grep 'temporary password' /var/log/mysqld.log

由于mysql修改密码时候如果你输入简单密码会报错,将不能修改密码,使用初始密码更改MySQL检测密码的规则,代码如下:

首先,进入MySQL

然后修改validate_password_policy参数的值(可参考mysql报错博客)

1
2
3
4
5
mysql>setglobal validate_password_policy=0;

setglobal validate_password_length=1;

setglobal validate_password_mixed_case_count=2;

然后通过mysql安全设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
mysql_secure_installation修改密码

# mysql_secure_installation

    Enter current password for root (enter for none): ← 回车 

    Set root password? [Y/n] ← 回车 

    New password: ← 123456 

    Re-enter new password: ← 123456 

    Remove anonymous users? [Y/n] ← 回车(删除匿名用户) 

    Disallow root login remotely? [Y/n] ← 回车(禁止远程root登录) 

    Remove test database and access to it? [Y/n] ← 回车(删除test库) 

    Reload privilege tables now? [Y/n] ← 回车 

    Thanks for using MySQL! 

# mysql -u root -p 

    Enter password:123456 

设置远程登录:

    CREATE USER ‘mysql‘@’%’ IDENTIFIED BY ‘123456’;

    GRANT ALL PRIVILEGES ON . TO ‘root‘@’%’ IDENTIFIED BY ‘123456’ WITH GRANT OPTION;

清除缓存:
mysql> flush privileges;

设置默认编码集

1
2
3
4
5
6
7
# vim /etc/my.cnf  

character-set-server=utf8

# /etc/init.d/mysqld restart  

# mysqld --version

最后把导出的war文件放到服务器中tomcat下的webapps目录下即可。

在eclipse中部署项目web有时候访问jsp会抛出以下类似的异常信息

org.apache.jasper.JasperException: The absolute uri: http://java.sun.com/jsp 

部署web项目,如果抛出此异常。请保证 standard.jar与jstl.jar 两个jar包是否 在部署的项目中存在,如果不存在,请把,这两个包加入 /WEB-INF/lib 中。然后保证jsp中引用的版本地址 和 此版本地址是一致的:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core"prefix="c"%><%@ taglib uri="http://java.sun.com/jsp/jstl/fmt"prefix="f"%><%@ taglib prefix="fn"uri="http://java.sun.com/jsp/jstl/functions"%>

CSDN下载路径:https://download.csdn.net/download/weixin_42216574/10416519

使用管理员权限打开window命令提示符输入如下即可

cmd /k reg add "HKEY_CLASSES_ROOT\Directory\Background\shellex\ContextMenuHandlers\New" /ve /t REG_SZ /d {D969A300-E7FF-11d0-A93B-00A0C90F2719} /f

元编程

元编程的概念来自LISP和smalltalk。

用来生成代码的程序称为元程序metaprogram,编写这种程序就称为元编程metaprogramming。
python主要通过反射来实现元编程。

Python中
所有非object类都继承自Object类
所有类的类型包括type类都是type
type类继承自object类,object类的类型也是type类

type类

type构建类

1
2
3
4
5
6
7
8
9
10
class type(object):
def __init__(cls, what, bases=None, dict=None): # known special case of type.__init__
"""
type(object_or_name, bases, dict)
type(object) -> the object's type
type(name, bases, dict) -> a new type
# (copied from class doc)
"""
pass

构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def __init__(self):
self.x = 1000

def show(self):
return self.__dict__

XClass = type('myclass', (object,), {'a':100, 'b': 'string', 'show':show, '__init__':__init__}) # 字典是类属性 print(XClass)

print(XClass)
print(XClass.__name__)
print(XClass.__dict__)
print(XClass.mro())

XClass().show()

可以借助type构造任何类,用代码生成代码,这就是元编程。

构建元类

一个类可以继承自type类

1
2
3
4
5
class ModelMeta(type):
def __new__(cls, *args):
print(cls)
print(*args)
return super().__new__(cls, *args)

继承自type,ModelMeta就是元类,它可以创建出其他类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class ModelMeta(type):  # 继承自type
def __new__(cls, name, bases, attrs: dict):
print(cls)
print(name)
print(bases)
print(attrs)
print("--------")
return super().__new__(cls, name, bases, attrs)


# 第一种 使用metaclass关键字参数指定元类
class A(metaclass=ModelMeta):
id = 100

def __init__(self):
self.x = 2000


# 第二种 B继承自A后,依然是从ModelMeta的类型
class B(A): # 继承
pass


# 第三种 元类就可以使用下面的方式创建新的类
C = ModelMeta('Class', (), {'y': 200})

print(type(A))
print(type(B))
print(type(C))

从运行结果还可以分析出 __new__(cls, *args) 的参数结构
中间是一个元组 ('A', (), {'__init__': <function A.__init__ at 0x0000000000B6E598>, '__module__':'__main__', '__qualname__': 'A', 'id': 100})
对应 (name, bases, dict)

从运行结果可以看出,只要元类是ModelMeta,创建类对象时,就会调用ModelMeta的__new__方法

元类的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Field:
def __init__(self, fieldname=None, pk=False, nullable=False):
self.fieldname = fieldname
self.pk = pk
self.nullable = nullable

def __repr__(self):
return "<Field {}>".format(self.fieldname)


class ModelMeta(type): # 继承自type
def __new__(cls, name, bases, attrs: dict):
print(cls)
print(name)
print(bases)
print(attrs, '-------------')
# 使用元类动态注入表名
tblname = '__tablename__'
if tblname not in attrs.keys():
attrs[tblname] = name

primarykeys = []
for k, v in attrs.items():
if isinstance(v, Field):
print(k)
print(v)
print(v.fieldname)
if v.fieldname is None:
v.fieldname = k # 没有名字则使用属性名
if v.pk:
primarykeys.append(v)

attrs['__primarykeys__'] = primarykeys

return super().__new__(cls, name, bases, attrs)


class ModelBase(metaclass=ModelMeta):
pass


class Student(ModelBase):
id = Field(pk=True, nullable=False)
name = Field('username', nullable=False)
age = Field()


print('----------------')
print(Student.__dict__)

元编程的总结

元类是制造类的工厂,是生成类的类。
构造好元类,就可以在类定义时,使用关键字参数metaclass指定元类,可以使用最原始的metatype(name,bases, dict)的方式构造一个类。
元类的 __new__() 方法中,可以获取元类信息、当前类、基类、类属性字典。

元编程一般用于框架开发中。

位运算符有:&(按位与)、|(按位或)、^(按位异或)、~ (按位取反)。

优先级从高到低,依次为~、&、^、|
1. 按位与操作 0&0=0; 0&1=0; 1&0=0; 1&1=1
  • 例子:10&9: 0000 1010 & 0000 1001 = 0000 1000 = 8

负数按补码形式参加按位与运算

  • “与运算”的特殊用途:
    • (1)清零。如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。
    • (2)取一个数中指定位
      方法:找一个数,对应X要取的位,该数的对应位为1,其余位为零,此数与X进行“与运算”可以得到X中的指定位。
      例:设X=10101110,
      取X的低4位,用 X & 0000 1111 = 0000 1110 即可得到;
      还可用来取X的2、4、6位。
2. 按位或运算符(|)
  • 参加运算的两个对象,按二进制位进行“或”运算。

  • 运算规则:0|0=0; 0|1=1; 1|0=1; 1|1=1;

    • 即 :参加运算的两个对象只要有一个为1,其值为1。
  • 例如:3|5 即 0000 0011 | 0000 0101 = 0000 0111 因此,3|5的值得7。 

另,负数按补码形式参加按位或运算。

  • “或运算”特殊作用:

    • (1)常用来对一个数据的某些位置1。

      • 方法:找到一个数,对应X要置1的位,该数的对应位为1,其余位为零。此数与X相或可使X中的某些位置1。

      • 例:将X=10100000的低4位置1 ,用 X | 0000 1111 = 1010 1111即可得到。

3. 异或运算符(^)
  • 参加运算的两个数据,按二进制位进行“异或”运算。

  • 运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0;

    • 即:参加运算的两个对象,如果两个相应位为“异”(值不同),则该位结果为1,否则为0。
  • 例如:10^-9 即 0000 1010 ^ 1111 0111 = 1111 1101(补码) 原码即为1000 0011 即10^-9 = -3

  • “异或运算”的特殊作用:

    • (1)使特定位翻转 找一个数,对应X要翻转的各位,该数的对应位为1,其余位为零,此数与X对应位异或即可。

      • 例:X=10101110,使X低4位翻转,用X ^ 0000 1111 = 1010 0001即可得到。
    • (2)与0相异或,保留原值 ,X ^ 0000 0000 = 1010 1110。

    • 交换a和b

    方法一 方法二
    1.a=a^b 1.a= a-b
    2.b=b^a 2.b= a+b
    3.a=a^b 3.a= b-a
4. 取反运算符(~)
  • 参加运算的一个数据,按二进制位进行“取反”运算。

  • 运算规则:~1=0; ~0=1;

    • 即:对一个二进制数按位取反,即将0变1,1变0。
  • 使一个数的最低位为零,可以表示为:a&~1。

  • 1的值为1111111111111110,再按“与”运算,最低位一定为0。因为“”运算符的优先级比算术运算符、关系运算符、逻辑运算符和其他运算符都高。

5. 左移运算符(<<)
  • 将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。

  • 例:a = a << 2 将a的二进制位左移2位,右补0,

  • 左移1位后a = a * 2;

  • 若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。

6. 右移运算符(>>)
  • 将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。

  • 操作数每右移一位,相当于该数除以2。

  • 例如:a = a >> 2 将a的二进制位右移2位,

  • 左补0 or 补1 得看被移数是正还是负。

  • >> 运算符把 expression1 的所有位向右移 expression2 指定的位数。expression1 的符号位被用来填充右移后左边空出来的位。向右移出的位被丢弃。

  • 例如,下面的代码被求值后,temp 的值是 -4:

    var temp = -14 >> 2

    -14 (即二进制的 11110010)右移两位等于 -4 (即二进制的 11111100)。

  • 无符号右移运算符(>>>)

  • >>> 运算符把 expression1 的各个位向右移 expression2 指定的位数。右移后左边空出的位用零来填充。移出右边的位被丢弃。

7. 复合赋值运算符
  • 位运算符与赋值运算符结合,组成新的复合赋值运算符,它们是:
位运算符 例子 相当于
&= 例:a &= b 相当于a=a & b
|= 例:a |= b 相当于a=a | b
>>= 例:a >>= b 相当于a=a >> b
<<= 例:a <<= b 相当于a=a << b
^= 例:a ^= b 相当于a=a ^ b
  • 运算规则:和前面讲的复合赋值运算符的运算规则相似。

不同长度的数据进行位运算

如果两个不同长度的数据进行位运算时,系统会将二者按右端对齐,然后进行位运算。

以“与”运算为例说明如下:我们知道在C语言中long型占4个字节,int型占2个字节,如果一个long型数据与一个int型数据进行“与”运算,右端对齐后,左边不足的位依下面三种情况补足,

(1)如果整型数据为正数,左边补16个0。
(2)如果整型数据为负数,左边补16个1。

(3)如果整形数据为无符号数,左边也补16个0。

如:long a=123;int b=1;计算a & b。
a 0000 0000 0111 1011 &
b 0000 0000 0000 0001
= 0000 0000 0000 0001
如:long a=123;int b=-1;计算a & b。
a 0000 0000 0111 1011 &
b 1111 1111 1111 1111
= 0000 0000 0111 1011
如:long a=123;unsigned int b=1;计算a & b。
a 0000 0000 0111 1011
b 0000 0000 0000 0001
= 0000 0000 0000 0001

8. 原码、反码、补码,负数表示法
  • 原码:5 => 0b101,1 => 0b1 ,-1 => -0b1, bin(-1)
  • 反码:
    • 正数的反码与原码相同;负数的反码符号位不变其余按位取反
  • 补码:
    • 正数的补码与原码相同;负数的补码符号位不变其余按位取反后+1

日志分析

一般采集流程

日志产出 -> 采集(Logstash、Flume、Scribe) -> 存储 -> 分析 -> 存储(数据库、NoSQL) -> 可视化

开源实时日志分析ELK平台

Logstash收集日志,并存放到ElasticSearch集群中,Kibana则从ES集群中查询数据生成图表,返回浏览器端

数据提取

数据
非结构化数据

一眼看不出结构的数据。(二进制的,无法用文本理解)

半结构化数据

日志是半结构化数据,是有组织的,有格式的数据。可以分割成行和列,就可以当做表理解和处理了,当然也可以分析里面的数据。

结构化数据

数据库内的数据(能够像是行和列一样很好的组织起来)

文本分析

日志是文本文件,需要依赖文件IO、字符串操作、正则表达式等技术。
通过这些技术就能够把日志中需要的数据提取出来。4

这是最常见的日志,nginx、tomcat等WEB Server都会产生这样的日志。 这里面每一段有效的数据对后期的分析都是必须的。

1
2
183.60.212.153 - - [19/Feb/2013:10:23:29 +0800] "GET /o2o/media.html?menu=3 HTTP/1.1" 200 16691 
"-" "Mozilla/5.0 (compatible; EasouSpider; +http://www.easou.com/search/spider.html)"

思路:如果用空格切割,数据并没有按照业务分割好,比如时间就被分开了,URL相关的也被分开了,User Agent的空格最多,被分割了。 所以,定义的时候不选用这种在filed中出现的字符就可以省很多事,例如使用’\x01’、‘\0x02’这个不可见的ASCII。

1
2
for field in line.split("\x02"):
print(field)

类型转换

fields中的数据是有类型的,例如时间、状态码等。对不同的field要做不同的类型转换,甚至是自定义的转换

时间转换
19/Feb/2013:10:23:29 +0800 对应格式是
%d/%b/%Y:%H:%M:%S %z
使用的函数是datetime类的strptime方法

1
2
3
4
5
6
import datetime

def convert_time(timestr):
return datetime.datetime.strptime(timestr, '%d/%b/%Y:%H:%M:%S %z')
#lambda timestr: datetime.datetime.strptime(timestr, '%d/%b/%Y:%H:%M:%S %z'

请求信息的解析

1
2
3
4
5
6
#GET /o2o/media.html?menu=3 HTTP/1.1 
#method url protocol 三部分都非常重要
def get_request(request:str):
return dict(zip(['method','url','protocol'],request.split()))

#lambda request: dict(zip(['method','url','protocol'],request.split()))

映射
对每一个字段命名,然后与值和类型转换的方法对应。解析每一行是有顺序的

映射
对每一个字段命名,然后与值和类型转换的方法对应。解析每一行是有顺序的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import datetime


line = '''183.60.212.153 - - [19/Feb/2013:10:23:29 +0800] \
"GET /o2o/media.html?menu=3 HTTP/1.1" 200 16691 "-" \
"Mozilla/5.0 (compatible; EasouSpider; +http://www.easou.com/search/spider.html)"'''

CHARS = set(' \'"[]')
print(CHARS)

def makekey(line:str):
start = 0
flag = False
stopchar = ''
for i, c in enumerate(line): # [a]
if c in CHARS:
if c == '[':
flag = True
start = i + 1
if c == ']':
flag = False
if c == '"':
flag = not flag
if flag:
start = i + 1
if flag:
continue
if start == i:
start = i + 1
continue
yield line[start:i]
start = i + 1
if start < len(line):
yield line[start:]
names = ('remote', '', '', 'datetime', 'request', 'status', 'length', '', 'useragent')
ops = (None, None, None, lambda timestr: datetime.datetime.strptime(timestr, '%d/%b/%Y:%H:%M:%S
lambda request: dict(zip(['method', 'url', 'protocol'], request.split())),
int, int, None, None
def extract(line:str):
return dict(map(lambda item: (item[0], item[2](item[1]) if item[2] is not None else
item[1]),zip(names, makekey(line), ops)))
print(extract(line))

正则表达式的提取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import datetime
import re


line = '''183.60.212.153 - - [19/Feb/2013:10:23:29 +0800] \
"GET /o2o/media.html?menu=3 HTTP/1.1" 200 16691 "-" \
"Mozilla/5.0 (compatible; EasouSpider; +http://www.easou.com/search/spider.html)"'''

ops = {
'datetime': lambda timestr: datetime.datetime.strptime(timestr, '%d/%b/%Y:%H:%M:%S %z'),
'status': int,
'length': int
}

pattern = '''(?P<remote>[\d.]{7,}) - - \[(?P<datetime>[/\w +:]+)\] \
"(?P<method>\w+) (?P<url>\S+) (?P<protocol>[\w/\d.]+)" \
(?P<status>\d+) (?P<length>\d+) .+ "(?P<useragent>.+)"'''

regex = re.compile(pattern)

def extract(line:str) -> dict:
matcher = regex.match(line)
return {k:ops.get(k, lambda x:x)(v) for k,v in matcher.groupdict().items()}

print(extract(line))

异常处理
日志中不免会出现一些不匹配的行,需要处理。

这里使用re.match方法,有可能匹配不上。所以要增加一个判断
采用抛出异常的方式,让调用者获得异常并自行处理。

1
2
3
4
5
6
7
8
def extract(logline:str) -> dict:
"""返回字段的字典,如果返回None说明匹配失败"""
matcher = regex.match(line)
if matcher:
return {k:ops.get(k, lambda x:x)(v) for k,v in matcher.groupdict().items()}
else:
return None # 或输出日志记录

数据载入

1
2
3
4
5
6
7
8
9
def load(path):
"""装载日志文件"""
with open(path) as f:
for line in f:
fields = extract(line)
if fields:
yield fields
else:
continue # TODO 解析失败就抛弃,或者打印日志

并发

并发和并行的区别

并行、parallel

​ 同时做某些事情,可以互不干扰的同一时刻做几件事

并发、concurrency

​ 一个时段内有事情要处理。


并发的解决

1、队列、缓冲区

使用队列,先进先出,解决了资源使用的问题。排成的队列,其实就是一个缓冲地带,就是缓冲区。(队列的作用:解耦,缓冲)

2、争抢

通过争抢,当一个抢到时就会触发一种类似锁机制,抢到资源就上锁,排他性的锁。这也是一种高并发解决方案,但是这样就有可能会有些会很长时间都抢不到。

3、预处理

一种提前加载用户需要的数据的思路,预处理思想,缓存常用。(要考虑到冷、热数据的问题,经常访问的数据可以先预加载)

4、并行

一般日常可以通过购买服务器,或多开进程、线程实现并行处理,来解决并发问题。注意:这些都是水平拓展的思想。

注:
如果线程在单CPU上处理,就不是并行了。
但是多数服务器都是多CPU的,服务的部署往往是多机的、分布式的,这都是并行处理。

5、提速

提高单个CPU性能,或单个服务器安装更多的CPU。
这是一种垂直扩展思想。

6、消息中间件

一般就是在程序之前实现的技术。

常见的消息中间件有RabbitMQ、ActiveMQ(Apache)、RocketMQ(阿里Apache)、kafka(Apache)等

总结:一般来说不同的并发场景使用不同的策略,而策略可能是多种方式的优化组合。


进程和线程

在实现了线程的操作系统中,线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个程序的执行实例就是一个进程。
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

进程和程序的关系
程序是源代码编译后的文件,而这些文件存放在磁盘上。当程序被操作系统加载到内存中,就是进程,进程中存放着指令和数据(资源),它也是线程的容器。

Linux进程有父进程、子进程,Windows的进程是平等关系。

线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。
一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。
在许多系统中,创建一个线程比创建一个进程快10-100倍。

进程、线程的理解
现代操作系统提出进程的概念,每一个进程都认为自己独占所有的计算机硬件资源。
进程就是独立的王国,进程间不可以随便的共享数据。
线程就是省份,同一个进程内的线程可以共享进程的资源,每一个线程拥有自己独立的堆栈

线程的状态

状态 含义
就绪(Ready) 线程能够运行,但在等待被调度。可能线程刚刚创建启动,或刚刚从阻塞中恢复,或者被其他线程抢占
运行(Running) 线程正在运行
阻塞(Blocked) 线程等待外部事件发生而无法运行,如I/O操作
终止(Terminated) 线程完成,或退出,或被取消

Python中的进程和线程


进程会启动一个解释器进程,线程共享一个解释器进程

Python的线程开发


Python的线程开发使用标准库threading

Thread类

1
2
3
#签名
def __init__(self, group=None, target=None, name=None,
args=(), kwargs=None, *, daemon=None):
参数名 含义
target 线程调用的对象,就是目标函数
name 为线程起的名字
args 为目标函数传递实参,元组
kwargs 为目标函数关键字传参,字典

线程启动

1
2
3
4
5
6
7
8
9
import threading

# 最简单的线程程序
def worker():
print("I'm working")
print('Fineshed')

t = threading.Thread(target=worker, name='worker') # 线程对象
t.start() # 启动

通过threading.Thread创建一个线程对象,target是目标函数,name可以指定名称。
但是线程没有启动,需要调用start方法。
线程之所以执行函数,是因为线程中就是执行代码的,而最简单的封装就是函数,所以其实还是函数调用,当函数执行完,线程就退出,当主线程结束后,程序也就执行完毕。

线程退出

Python没有提供线程退出的方法,线程在下面情况时退出。

1、线程函数内语句执行完毕
2、线程函数中抛出未处理的异常

Python的线程没有优先级,没有线程组的概念,也不能被销毁、停止、挂起、那也没有恢复、中断了。

threading的属性和方法

名称 含义
current_thread() 返回当前线程对象
main_thread() 返回主线程对象
active_count() 当前处于alive状态的线程个数
enumerate() 返回所有活着的线程的列表,不包括已经终止的线程和未开始的线程
get_ident() 返回当前线程的ID,非0整数

active_count、enumerate方法返回的值还包括主线程。

Thread实例的属性和方法

名称 含义
name 只是一个名字,只是一个标识,名称可以重名。getName(),setName(),获取设置这个名词
ident 线程ID,它是非0整数。线程启动后才会有ID,否则为None。线程退出,此ID依旧可以访问。此ID可以重复使用
is_alive() 返回线程是否活着

注意:线程的name这是一个名称,可以重复;ID必须唯一,但可以在线程退出后再利用。

start方法和run方法

名称 含义
start() 启动线程。每一个线程必须且只能执行该方法一次
run() 运行线程函数

虽然说,start()方法会调用run()方法,而run()方法可以运行函数。
但是在使用start()方法启动线程,是启动了一个新的线程,而使用run方法只是在主线程中调用了一个普通的函数而已,并没有启动新的线程。


多线程

一个进程中如果有多个线程,就是多线程,实现一种并发。

当使用start方法启动线程后,进程内有多个活动的线程并行的工作,就是多线程。

一个进程中至少有一个线程,并作为程序的入口,这个线程就是主线程。一个进程至少有一个主线程。
其他线程称为工作线程。


线程安全

线程安全,就是线程执行一段代码,不会产生不确定的结果,那这段代码就是线程安全的。

当在线程中使用print函数的时候,可以让它不打印换行,这样就可以避免print函数线程不安全;还可以使用标准库中的logging模块,日志处理模块,线程安全的。

1
2
3
4
5
6
7
8
9
10
11
import threading
import logging

def worker():
for x in range(20):
logging.warning("{} is running".format(threading.current_thread().name))

for x in range(1, 5):
name = "worker-{}".format(x)
t = threading.Thread(target=worker, name=name)
t.start()

daemon线程和non-daemon线程

进程靠线程执行代码,至少有一个主线程,其它线程是工作线程。

主线程是第一个启动的线程。

父线程:如果线程A中启动了一个线程B,A就是B的父线程。

子线程:B就是A的子线程。

python中,构造线程的时候,可以设置daemon属性,这个属性必须在start方法前设置好。

1
2
3
4
5
6
# 源码Thread的__init__方法中
if daemon is not None:
self._daemonic = daemon # 用户设定bool值
else:
self._daemonic = current_thread().daemon
self._ident = None

线程daemon属性,如果设定就是用户的设置,否则就取当前线程的daemon值。
主线程是non-daemon线程,即daemon = False。

名称 含义
daemon属性 表示线程是否是daemon线程,这个值必须在start()之前设置,否则引发RuntimeError异常
isDaemon() 是否是daemon线程
setDaemon 设置为daemon线程,必须在start方法之前设置

总结:线程具有daemon属性,可以显示设置为True或False,也可以不设置,则取默认值None
如果不设置daemon,就取当前线程的daemon来设置它。

主线程是non-daemon线程,即daemon = False。
从主线程创建的所有线程的不设置daemon属性,则默认都是daemon = False,也就是non-daemon线程。

Python程序在没有活着的non-daemon线程运行时退出,也就是剩下的只能是daemon线程,主线程才能退出,否则主线程就只能等待。

如果有non-daemon线程的时候,主线程退出时,也不会杀掉所有daemon线程,直到所有non-daemon线程全部结束,如果还有daemon线程,主线程需要退出,会结束所有daemon线程,退出。


join方法

join(timeout=None),是线程的标准方法之一。
一个线程中调用另一个线程的join方法,调用者将被阻塞,直到被调用线程终止。
一个线程可以被join多次。
timeout参数指定调用者等待多久,没有设置超时,就一直等到被调用线程结束。
一个线程调用谁的join方法,就是join谁,就要等谁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time
import threading


def foo(n):
for i in range(n):
print(i)
time.sleep(0.3)

t1 = threading.Thread(target=foo, args=(10,), daemon=True)
t1.start()
t1.join()

print("Main Thread Exiting")

使用了join方法后,daemon线程执行完了,主线程才退出。


daemon线程的应用场景

这个概念唯一的作用就是,当你把一个线程设置为 daemon,它会随主线程的退出而退出。

主要应用场景有:

1、后台任务。如发送心跳包、监控,这种场景最多。

2、主线程工作才有用的线程。如主线程中维护这公共的资源,主线程已经清理了,准备退出,而工作线程使用这些资源工作也没有意义了,一起退出最合适。

3、随时可以被终止的线程

如果主线程退出,想所有其它工作线程一起退出,就使用daemon=True来创建工作线程。

比如,开启一个线程定时判断WEB服务是否正常工作,主线程退出,工作线程也没有必须存在了,应该随着主线程退出一起退出。这种daemon线程一旦创建,就可以忘记它了,只用关心主线程什么时候退出就行了。
daemon线程,简化了程序员手动关闭线程的工作。

如果在non-daemon线程A中,对另一个daemon线程B使用了join方法,这个线程B设置成daemon就没有什么意义了,因为non-daemon线程A总是要等待B。

如果在一个daemon线程C中,对另一个daemon线程D使用了join方法,只能说明C要等待D,主线程退出,C和D不管是否结束,也不管它们谁等谁,都要被杀掉。


threading.local类

1
2
3
4
5
6
7
8
9
10
11
12
import threading
import time
# 局部变量实现
def worker():
x = 0
for i in range(100):
time.sleep(0.0001)
x += 1
print(threading.current_thread(), x)

for i in range(10):
threading.Thread(target=worker).start()

当想保证线程安全,可以使用局部变量来进行运算,避免错误。

当然如果想使用全局变量,那就可以使用threading下的local类,将这个类实例化得到一个全局变量,但是不同的线程使用这个对象存储的数据,其他线程看不见。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import threading
import time

global_date = threading.local()


def worker():
global_date.x = 0
for i in range(100):
time.sleep(0.001)
global_date.x += 1
print(threading.current_thread(), global_date.x)


for i in range(5):
threading.Thread(target=worker).start()

#<Thread(Thread-3, started 3648)> 100
#<Thread(Thread-4, started 9404)> 100
#<Thread(Thread-1, started 7472)> 100
#<Thread(Thread-5, started 10876)> 100
#<Thread(Thread-2, started 12424)> 100

threading.local类构建了一个大字典,存放所有线程相关的字典,定义如下:
{ id(thread) -> (ref(thread), thread-local dict) }
每一线程实例的id为key,元组为value。value中两部分为线程对象引用,每个线程自己的字典。

本质
运行时,threading.local实例处在不同的线程中,就从大字典中找到当前线程相关键值对中的字典,覆盖threading.local实例的 __dict__ 。
这样就可以在不同的线程中,安全地使用线程独有的数据,做到了线程间数据隔离,如同本地变量一样安全

定时器Timer/延时执行


threading.Timer继承自Thread,这个类用来定义延迟多久后执行一个函数。

class.threading.Timer(interval, function, args=None, kwargs=None)
start方法执行之后,Timer对象会处于等待状态,等待了interval秒之后,然后开始执行function函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import threading
import time
import logging

FORMAT = "%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(level=logging.INFO, format=FORMAT)


def worker():
logging.info("in worker")
time.sleep(2)


t = threading.Timer(4, worker)
t.setName("timer")

t.start()

while True:
print(threading.enumerate())
time.sleep(1)
if len(threading.enumerate()) == 1:
break


#打印
[<_MainThread(MainThread, started 10416)>, <Timer(timer, started 2768)>]
[<_MainThread(MainThread, started 10416)>, <Timer(timer, started 2768)>]
[<_MainThread(MainThread, started 10416)>, <Timer(timer, started 2768)>]
[<_MainThread(MainThread, started 10416)>, <Timer(timer, started 2768)>]
2018-10-12 16:24:44,066 timer 2768 in worker
[<_MainThread(MainThread, started 10416)>, <Timer(timer, started 2768)>]
[<_MainThread(MainThread, started 10416)>, <Timer(timer, started 2768)>]

Timer提供了cancel方法,用来取消一个未执行的函数,如果上面例子中worker函数已经开始执行,cancel就没有任何效果了。

总结
Timer是线程Thread的子类,就是线程类,具有线程的能力和特征。
它的实例是能够延时执行目标函数的线程,在真正执行目标函数之前,都可以cancel它。
cancel方法本质使用Event类实现。这并不是说,线程提供了取消的方法。

线程同步

概念:线程同步,线程间协同,通过某种技术,让一个线程访问某些数据时,其他线程不能访问这些数据,直到该线程完成对数据的操作。

不同操作系统实现技术有所不同,有临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphore)、事件Event等

Event事件


Event事件,是线程间通信机制中最简单的实现,使用一个内部的标记flag,通过flag的True或False的变化来进行操作。

名称 含义
set() 标记设置为True
clear() 标记设置为False
is_set() 标记是否为True
wait(timeout=None) 设置等待标记为True的时长,None为无限等待。等到返回True,未等到超时了返回False

需求:

老板雇佣了一个工人,让他生产杯子,老板一直等着这个工人,直到生产了10个杯子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import logging
import time
from threading import Event, Thread

FORMAT = "%(asctime)s %(threadName)s %(thread)s %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)


def boss(event: Event):
logging.info("I'm boss, waitting for U.")
event.wait() #等待
logging.info("Good Job.")


def worker(event: Event, count=10):
logging.info("i'm working for U.")
cups = []
while True:
logging.info("make 1")
time.sleep(0.5)
cups.append(1)
if len(cups) >= count:
event.set() #通知
break
logging.info("I'm finish my job, cups = {}".format(cups))


event = Event()
t1 = Thread(target=boss, name="bose", args=(event,)).start()
t2 = Thread(target=worker, name="worker", args=(event,)).start()

总结
使用同一个Event对象的标记flag。
谁wait就是等到flag变为True,或等到超时返回False。不限制等待的个数。

wait的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from threading import Event, Thread
import logging
logging.basicConfig(level=logging.INFO)

def do(event:Event, interval:int):
while not event.wait(interval): # 条件中使用,返回True或者False
logging.info('do sth.')

e = Event()
Thread(target=do, args=(e, 3)).start()

e.wait(10) # 也可以使用time.sleep(10)
e.set()
print('main exit')


#输出

INFO:root:do sth.
INFO:root:do sth.
INFO:root:do sth.
main exit

Process finished with exit code 0

Event的wait优于time.sleep,它会更快的切换到其它线程,提高并发效率。


Lock

锁,凡是存在共享资源争抢的地方都可以使用锁,从而保证只有一个使用者可以使用这个资源。

锁,一旦线程获得锁,其它试图获取锁的线程将被阻塞

名称 含义
acquire(blocking=True,timeout=-1) 默认阻塞,阻塞可以设置超时时间。非阻塞时,timeout禁止设置。成功获取锁,返回True,否则返回False
release() 释放锁。可以从任何线程调用释放。已上锁的锁,会被重置为unlocked,未上锁的锁上调用,抛RuntimeError异常。

加锁和解锁


一般来说,加锁就需要解锁,但是加锁后解锁前,还要有一些代码执行,就有可能抛异常,一旦出现异常,锁是无法释放,但是当前线程可能因为这个异常被终止了,这就产生了死锁。

加锁、解锁常用语句:
1、使用try…finally语句保证锁的释放

1
2
3
4
5
try:            
self.lock.acquire()
finally:
self.lock.release()

2、with上下文管理,锁对象支持上下文管理

1
2
3
4
self.__lock = threading.Lock() 
with self.__lock:
******

锁的应用场景


锁适用于访问和修改同一个共享资源的时候,即读写同一个资源的时候。

如果全部都是读取同一个共享资源需要锁吗?
不需要。因为这时可以认为共享资源是不可变的,每一次读取它都是一样的值,所以不用加锁

使用锁的注意事项:

  • 少用锁,必要时用锁。使用了锁,多线程访问被锁的资源时,就成了串行,要么排队执行,要么争抢执行
    • 举例,高速公路上车并行跑,可是到了省界只开放了一个收费口,过了这个口,车辆依然可以在多车道上一起跑。过收费口的时候,如果排队一辆辆过,加不加锁一样效率相当,但是一旦出现争抢,就必须加锁,一辆辆过。
  • 加锁时间越短越好,不需要就立即释放锁
  • 一定要避免死锁

可重入锁RLOCK


可重入锁,是线程相关的锁。
线程A获得可重复锁,并可以在同一线程中多次成功获取,不会阻塞。最后要在线程A中做和acquire次数相同的release。release多了会报错。有个count在计数。属主owner会记录当前是谁在使用锁。

当锁未释放完,其他线程获得锁就会阻塞,直到当前持有锁的线程释放完锁。

Condition


构造方法Condition(lock=None),可以传入一个Lock或Rlock对象,默认是Rlock。

名称 含义
acquire(*args) 获取锁
wait(self,timeout = None) 等待或超时
notify(n =1) 唤醒至多指定指定个数的等待的线程,没有等待的线程就没有任何操作
notify_all() 唤醒所有等待的线程

Condition用于生产者、消费者模型,为了解决生产者消费者速度匹配的问题。