开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Java实现最简单的RPC框架

发表于 2017-11-14

使用Java原生的序列化、动态代理、反射等实现简单的RPC框架。本示例来源于《分布式服务框架》。

示例

  • EchoService.java
1
2
3
复制代码public interface EchoService {
String echo(String ping);
}
  • EchoServiceImpl.java
1
2
3
4
5
6
复制代码public class EchoServiceImpl implements EchoService {
@Override
public String echo(String ping) {
return ping != null ? ping + "--> I am OK." : " I am OK.";
}
}
  • RpcExporter.java
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
复制代码import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
* Created by lbd on 2017/11/12.
*/
public class RpcExporter {
static Executor executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

public static void exporter(String hostName, int port) throws Exception {
ServerSocket server = new ServerSocket();
server.bind(new InetSocketAddress(hostName, port));
try {
while (true) {
executor.execute(new ExporterTask(server.accept()));
}
} finally {
server.close();
}
}

private static class ExporterTask implements Runnable {
Socket client = null;

public ExporterTask(Socket client) {
this.client = client;
}

@Override
public void run() {
ObjectInputStream input = null;
ObjectOutputStream output = null;

try {
input = new ObjectInputStream(client.getInputStream());
String interfaceName = input.readUTF();
Class<?> service = Class.forName(interfaceName);
String methodName = input.readUTF();
Class<?>[] parameterTypes = (Class<?>[])input.readObject();
Object[] arguments = (Object[])input.readObject();
Method method = service.getMethod(methodName, parameterTypes);
Object result = method.invoke(service.newInstance(), arguments);
output = new ObjectOutputStream(client.getOutputStream());
output.writeObject(result);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (output != null)
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}

if (input != null)
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}

}
}
}
}
  • RpcImporter.java
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
复制代码import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.InetSocketAddress;
import java.net.Socket;

/**
* Created by lbd on 2017/11/12.
*/
public class RpcImporter<S> {
public S importer(final Class<?> serviceClass, final InetSocketAddress addr) {
return (S) Proxy.newProxyInstance(serviceClass.getClassLoader(), new Class<?>[]{serviceClass.getInterfaces()[0]},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Socket socket = null;
ObjectOutputStream output = null;
ObjectInputStream input = null;
try {
socket = new Socket();
socket.connect(addr);
output = new ObjectOutputStream(socket.getOutputStream());
output.writeUTF(serviceClass.getName());
output.writeUTF(method.getName());
output.writeObject(method.getParameterTypes());
output.writeObject(args);
input = new ObjectInputStream(socket.getInputStream());
return input.readObject();
} finally {
if (socket != null)
socket.close();
if (output != null)
output.close();
if (input != null)
input.close();
}
}
});
}
}
  • RpcTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码import java.net.InetSocketAddress;

/**
* Created by lbd on 2017/11/12.
*/
public class RpcTest {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
RpcExporter.exporter("localhost", 7890);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();

RpcImporter<EchoService> proxyImporter = new RpcImporter<>();
EchoService echoService = proxyImporter.importer(EchoServiceImpl.class, new InetSocketAddress("localhost", 7890));
System.out.println(echoService.echo("Are you ok ?"));
}
}

输出

1
复制代码Are you ok ?--> I am OK.

可进入我的博客查看原文

欢迎关注公众号: FullStackPlan 获取更多干货

欢迎关注公众号: FullStackPlan 获取更多干货

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

设计模式-适配器模式

发表于 2017-11-14

定义

适配器模式将一个类的接口,转换成客户端期待的另一个接口。

比如我们想用苹果的充电线给安卓充电。但是安卓的充电接口(type-c)跟苹果(lightning)的不一样,所以就需要一个适配器,将安卓的type-c接口转换成苹果的lightning接口,这样就能用苹果的充电线给安卓充电了。

Adapter.jpg

Adapter.jpg

图中玫瑰金色的就是适配器。

角色

  • 目标(Target):即期望的接口。
  • 适配器(Adapter):用于将源接口转换成目标接口。
  • 被适配者(Adaptee):即源接口。

类图

适配器模式类图.png

适配器模式类图.png

示例

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
复制代码public class AdapterDP {
public static void main(String[] args) {
AppleLightning appleLighting = new AppleLightning();
System.out.println("use lightning to charge");
appleLighting.chargeWithLightning();

System.out.println('\n' + "use type-c to charge");
AndroidTypeC androidTypeC = new AndroidTypeC();
androidTypeC.chargeWithTypeC();

System.out.println('\n' + "use lightning to charge");
Lightning adapter = new Adapter(androidTypeC);
adapter.chargeWithLightning();
}


}

interface Lightning {
void chargeWithLightning();
}

class AppleLightning implements Lightning {
public void chargeWithLightning() {
System.out.println("charging iPhone...");
}
}

class AndroidTypeC {
public void chargeWithTypeC() {
System.out.println("charging android...");
}
}

class Adapter implements Lightning {
public AndroidTypeC androidTypeC;

public Adapter(AndroidTypeC androidTypeC) {
this.androidTypeC = androidTypeC;
}

public void chargeWithLightning() {
androidTypeC.chargeWithTypeC();
}

}

输出

1
2
3
4
5
6
7
8
复制代码use lightning to charge
charging iPhone...

use type-c to charge
charging android...

use lightning to charge
charging android...

注:以上的例子是对象适配器模式,还有另一种适配器模式叫类适配器模式,这里不再赘述。

适配器模式在Hadoop源码中的应用

Hadoop作为广泛应用的大数据组件,其本质是一个分布式系统,在分布式系统中,各个节点之间的通信和交互是必不可少的,为此,Hadoop实现了一套自己的RPC框架,该RPC框架默认使用Protocol Buffer作为序列化工具。

ClientProtocol协议定义了HDFS Client和NameNode交互的所有方法,但是ClientProtocol协议中方法的参数是无法在网络中传输的,需要对参数进行序列化操作,所以HDFS又定义了ClientNamenodeProtocolPB协议,该协议包含了ClientProtocol定义的所有方法,但是参数却是使用protobuf序列化后的格式。

ClientNamenodeProtocolTranslatorPB类作为Client侧的适配器类,实现了ClientProtocol接口,它内部拥有一个实现了ClientNamenodeProtocolPB接口的对象,可以将ClientProtocol调用适配成ClientNamenodeProtocolPB调用。以rename()调用为例,ClientNamenodeProtocolPB将rename(String, String)调用中的两个String参数序列化成一个RenameRequestProto对象,然后调用ClientNamenodeProtocolPB对象的rename(RenameRequestProto)方法,这样就完成了ClientProtocol接口到ClientNamenodeProtocolPB接口的适配。

在该例子中,ClientNamenodeProtocolTranslatorPB类为适配器,ClientProtocol为目标接口(这里的目标是对客户端来说的),ClientNamenodeProtocolPB为源接口。

可进入我的博客查看原文

欢迎关注公众号: FullStackPlan 获取更多干货

欢迎关注公众号: FullStackPlan 获取更多干货

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java常用异常整理 异常类的继承关系 常用异常类 常用异常

发表于 2017-11-14

填坑,整理下Java的常用异常。正确使用异常在实际编码中非常重要,但面试中的意义相对较小,因为对异常的理解和应用很难通过几句话或几行代码考查出来,不过我们至少应答出三点:异常类的继承关系、常用异常类、常用异常类的使用场景,下文将围绕这三点介绍。

异常类的继承关系

image.png

image.png

Java中,所有异常都继承自Throwable类(一个完整可用的类)。整体上分为Error、Exception两个大类,Exception大类又分为UncheckedException(继承于RuntimeException)和CheckedException(继承于Exception,但不继承于RuntimeException)。

为了帮助理解,我在每个类别下都给出了两个常用子类,如Error包括OutOfMemoryError、AssertionError等;UncheckedException包括NullPointerException、IllegalArgumentException;CheckedException包括IOException、InterruptedException。面试画异常类的继承关系时,要求能清楚的说明几个类别并分类别举几个常用的异常类。

常用异常类

下面分类别扩充一下常用的异常类,字典序排序:

类别 常用异常类
Error AssertionError、OutOfMemoryError、StackOverflowError
UncheckedException AlreadyBoundException、ClassCastException、ConcurrentModificationException、IllegalArgumentException、IllegalStateException、IndexOutOfBoundsException、JSONException、NullPointerException、SecurityException、UnsupportedOperationException
CheckedException ClassNotFoundException、CloneNotSupportedException、FileAlreadyExistsException、FileNotFoundException、InterruptedException、IOException、SQLException、TimeoutException、UnknownHostException

需要着重理解的是UncheckedException。

上述异常类都是很常见的,但其中几个异常类设计的不好,需要注意:

  • ConcurrentModificationException:实现“快速失败”的机制,但实际上,“快速失败”机制本身仍然无法保证并发环境下安全性,参考源码|从源码分析非线程安全集合类的不安全迭代器。因此,虽然该异常很常见,不要去依赖它。
  • JSONException:常见于json字符串解析失败的情况,但遮蔽了大量的失败细节,往往很难根据该异常作出处理。如果项目中大量使用json,建议使用第三方的json解析库,如gson等。
  • UnsupportedOperationException:这是一种编码上的恶性妥协,经常在抽象类的成员方法中被用户主动抛出,表示该方法还未实现等,但由于是UncheckedException,运行期才能够发现,完全无益于编码期间的安全性。自己编码时尽量不要使用。
  • SQLException:与JSONException原因相似,但其遮蔽的失败细节范围更广。同时,SQLException还是一个CheckedException,在不能解决问题的情况下,又使代码变的臃肿不堪。建议同。如果做Java Web开发,热门的ORM库都能解决上述问题。

常用异常类的使用场景

常用异常还是有点多,下面分别讲解上述三个类别的使用场景,并在每个类别中选一个例子进行讲解。

Error

Error通常描述了系统级的错误,并且程序猿无法主动处理——当然,系统级错误也有可能由代码间接导致,这不在我们的讨论范围内。发生系统级错误的时候,系统环境已经不健康了,因此,Error不强制捕获或声明,也就是不强制处理,一般情况下只需要把异常信息记录下来(如果能记下当时的系统快照更好)。

OutOfMemoryError

当可用内存不足时,会由JVM抛出OutOfMemoryError。一般由三种原因导致:

  • 堆设置过小,不满足正常的内存需求
  • 代码中存在内存泄露,占用了大量内存而不能被回收
  • 选择的GC算法与某些极端的应用场景不匹配,内存碎片过多,没有足够大的连续空间分配给对象

JVM抛出OutOfMemoryError前,会尝试进行一次Full GC,如果GC后可用内存还是不足,才会抛出OutOfMemoryError。因此,这时程序猿必然无法主动处理这一问题,只能等程序崩溃后再去查证原因。

查证OutOfMemoryError的技巧足以单开一篇文章了,本文不作深入。

UncheckedException

严格来说,Error也可以被划归UncheckedException,但我们更习惯用UncheckedException描述运行期发生,通常由于代码问题直接引起的程序相关的错误,并且程序猿无法主动处理。注意区分,系统级错误都应该用Error描述。UncheckedException发生的大部分情况是代码写挫了,因此,UncheckedException也不强制捕获或声明,也就是不强制处理,一般情况下记下日志即可。

不同的是,如果可能,要保证UncheckedException是可控的(在异常被动抛出前检查并主动抛出)。

JSONException就是不可控的。

NullPointerException

NullPointerException是最常见的UncheckedException。如果在一个空指针上引用方法或变量等,则运行期会抛出NullPointerException。空指针让程序变的不可控:如果任由空指针在程序运行期随意传递、使用,我们将无法确定程序的行为,也无法确定捕获NullPointerException时程序所处的状态。

解决这一问题的方法很简单:

  • 尽早检查并主动抛出异常
  • 单独、提前处理边界条件
  • 尽量不使用null表示状态,特别是在集合中

前两条原则通用于大部分UncheckedException,可参考String#toLowerCase()的例子。第三条原则需要在代码的健壮与简洁之间做出权衡,优先保证简洁清晰,需要健壮再去健壮。

CheckedException

猴子对CheckedException的理解不到位,如果各位有更好的理解希望能交流一下。以下讲猴子“不到位”的理解。

CheckedException描述了外部环境导致的不太严重的错误,程序猿应该主动处理。注意与系统级错误区分,系统级错误通常是不可恢复的。因此,CheckedException强制捕获或声明,程序猿必须处理。记录日志,包装后再次抛出,在方法签名中声明,是三种最常见的做法。

同UncheckedException一样,CheckedException也要保证是可控的。对CheckedException的可控性要求更高,不仅要主动检查,还要在捕获到异常时,作出合适的处理。

不过,猴子认为大量CheckedException的存在就是个错误。比如FileAlreadyExistsException,更应该由用户主动检查发现,而不应该依赖于异常。对于可以处理的异常,本质上相当于控制流问题,用异常去表达反而让控制流变模糊。不过有时候猴子写小项目,也会为了简化代码,直接将相关异常声明在方法签名中,并一路声明干到main方法。恩,everything is a trade-off。

IOException

产生IOException的原因非常多,但很多时候我们并不关心细节原因,因为文件系统是一个不太可控的因素,这时我们可以以IOException为粒度处理;某些需要关心细节的异常情况,则应使用IOException的子类,以分情况处理。

前面总结的FileAlreadyExistsException、FileNotFoundException、UnknownHostException等,都是IOException的子类。这三种异常恰好都是可以处理的。

挖坑,InterruptedException也相当重要,后面要专门写一篇来整理。

总结

实际的编码工作中,我们应正确的使用异常表达代码设计,并尽可能使用JDK提供的异常类。JDK内置了非常多的异常类,我们只需要掌握一些常用的异常类,然后举一反三。


本文链接:Java常用异常整理
作者:猴子007
出处:monkeysayhi.github.io
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名及链接。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

你真的会阅读Java的异常信息吗? 需要内化的内容 扩展 总

发表于 2017-11-14

给出如下异常信息:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码java.lang.RuntimeException: level 2 exception
at com.msh.demo.exceptionStack.Test.fun2(Test.java:17)
at com.msh.demo.exceptionStack.Test.main(Test.java:24)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
Caused by: java.io.IOException: level 1 exception
at com.msh.demo.exceptionStack.Test.fun1(Test.java:10)
at com.msh.demo.exceptionStack.Test.fun2(Test.java:15)
... 6 more

学这么多年Java,你真的会阅读Java的异常信息吗?你能说清楚异常抛出过程中的事件顺序吗?

需要内化的内容

写一个demo测试

上述异常信息在由一个demo产生:

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
复制代码package com.msh.demo.exceptionStack;

import java.io.IOException;

/**
* Created by monkeysayhi on 2017/10/1.
*/
public class Test {
private void fun1() throws IOException {
throw new IOException("level 1 exception");
}

private void fun2() {
try {
fun1();
} catch (IOException e) {
throw new RuntimeException("level 2 exception", e);
}
}


public static void main(String[] args) {
try {
new Test().fun2();
} catch (Exception e) {
e.printStackTrace();
}
}
}

这次我复制了完整的文件内容,使文章中的代码行号和实际行号一一对应。

根据上述异常信息,异常抛出过程中的事件顺序是:

  1. 在Test.java的第10行,抛出了一个IOExceotion(“level 1 exception”) e1
  2. 异常e1被逐层向外抛出,直到在Test.java的第15行被捕获
  3. 在Test.java的第17行,根据捕获的异常e1,抛出了一个RuntimeException(“level 2 exception”, e1) e2
  4. 异常e2被逐层向外抛出,直到在Test.java的第24行被捕获
  5. 后续没有其他异常信息,经过必要的框架后,由程序自动或用户主动调用了e2.printStackTrace()方法

如何阅读异常信息

那么,如何阅读异常信息呢?有几点你需要认识清楚:

  • 异常栈以FILO的顺序打印,位于打印内容最下方的异常最早被抛出,逐渐导致上方异常被抛出。位于打印内容最上方的异常最晚被抛出,且没有再被捕获。从上到下数,第i+1个异常是第i个异常被抛出的原因cause,以“Caused by”开头。
  • 异常栈中每个异常都由异常名+细节信息+路径组成。异常名从行首开始(或紧随”Caused by”),紧接着是细节信息(为增强可读性,需要提供恰当的细节信息),从下一行开始,跳过一个制表符,就是路径中的一个位置,一行一个位置。
  • 路径以FIFO的顺序打印,位于打印内容最上方的位置最早被该异常经过,逐层向外抛出。最早经过的位置即是异常被抛出的位置,逆向debug时可从此处开始;后续位置一般是方法调用的入口,JVM捕获异常时可以从方法栈中得到。对于cause,其可打印的路径截止到被包装进下一个异常之前,之后打印“… 6 more”,表示cause作为被包装异常,在这之后还逐层向外经过了6个位置,但这些位置与包装异常的路径重复,所以在此处省略,而在包装异常的路径中打印。“… 6 more”的信息不重要,可以忽略。

现在,回过头再去阅读示例的异常信息,是不是相当简单?

为了帮助理解,我尽可能通俗易懂的描述了异常信息的结构和组成元素,可能会引入一些纰漏。阅读异常信息是Java程序猿的基本技能,希望你能内化它,忘掉这些冗长的描述。

如果还不理解,建议你亲自追踪一次异常的创建和打印过程,使用示例代码即可,它很简单但足够。难点在于异常是JVM提供的机制,你需要了解JVM的实现;且底层调用了很多native方法,而追踪native代码没有那么方便。

扩展

为什么有时我在日志中只看到异常名”java.lang.NullPointerException”,却没有异常栈

示例的异常信息中,异常名、细节信息、路径三个元素都有,但是,由于JVM的优化,细节信息和路径可能会被省略。

这经常发生于服务器应用的日志中,由于相同异常已被打印多次,如果继续打印相同异常,JVM会省略掉细节信息和路径队列,向前翻阅即可找到完整的异常信息。

猴哥之前使用Yarn的Timeline Server时遇到过该问题。你能体会那种感觉吗?卧槽,为什么只有异常名没有异常栈?没有异常栈怎么老子怎么知道哪里抛出的异常?线上服务老子又不能停,全靠日志了啊喂!

网上有不少相同的case,比如NullPointerException丢失异常堆栈信息,读者可以参照这个链接实验一下。

如何在异常类中添加成员变量

为了恰当的表达一个异常,我们有时候需要自定义异常,并添加一些成员变量,打印异常栈时,自动补充打印必要的信息。

追踪打印异常栈的代码:

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
复制代码...
public void printStackTrace() {
printStackTrace(System.err);
}
...
public void printStackTrace(PrintStream s) {
printStackTrace(new WrappedPrintStream(s));
}
...
private void printStackTrace(PrintStreamOrWriter s) {
// Guard against malicious overrides of Throwable.equals by
// using a Set with identity equality semantics.
Set<Throwable> dejaVu =
Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
dejaVu.add(this);

synchronized (s.lock()) {
// Print our stack trace
s.println(this);
StackTraceElement[] trace = getOurStackTrace();
for (StackTraceElement traceElement : trace)
s.println("\tat " + traceElement);

// Print suppressed exceptions, if any
for (Throwable se : getSuppressed())
se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);

// Print cause, if any
Throwable ourCause = getCause();
if (ourCause != null)
ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
}
}
...

暂不关心同步问题,可知,打印异常名和细节信息的代码为:

1
复制代码            s.println(this);

JVM在运行期通过动态绑定实现this引用上的多态调用。继续追踪的话,最终会调用this实例的toString()方法。所有异常的最低公共祖先类是Throwable类,它提供了默认的toString()实现,大部分常见的异常类都没有覆写这个实现,我们自定义的异常也可以直接继承这个实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码...
public String toString() {
String s = getClass().getName();
String message = getLocalizedMessage();
return (message != null) ? (s + ": " + message) : s;
}
...
public String getLocalizedMessage() {
return getMessage();
}
...
public String getMessage() {
return detailMessage;
}
...

显然,默认实现的打印格式就是示例的异常信息格式:异常名(全限定名)+细节信息。detailMessage由用户创建异常时设置,因此,如果有自定义的成员变量,我们通常在toString()方法中插入这个变量。参考com.sun.javaws.exceptions包中的BadFieldException,看看它如何插入自定义的成员变量field和value:

1
2
3
复制代码  public String toString() {
return this.getValue().equals("https")?"BadFieldException[ " + this.getRealMessage() + "]":"BadFieldException[ " + this.getField() + "," + this.getValue() + "]";
}

严格的说,BadFieldException的toString中并没有直接插入field成员变量。不过这不影响我们理解,感兴趣的读者可自行翻阅源码。

总结

根据异常信息debug是程序员的基本技能,这里围绕异常信息的阅读和打印过程作了初步探索,后续还会整理一下常用的异常类,结合程序猿应该记住的几条基本规则,更好的理解如何用异常帮助我们写出clean code。

Java相当完备的异常处理机制是一把双刃剑,用好它能增强代码的可读性和鲁棒性,用不好则会让代码变的更加不可控。例如,在空指针上调用成员方法,运行期会抛出异常,这是很自然的——但是,是不可控的等待它在某个时刻某个位置抛出异常(实际上还是“确定”的,但对于debug来说是“不确定”的),还是可控的在进入方法伊始就检查并主动抛出异常呢?进一步的,哪些异常应该被即刻处理,哪些应该继续抛到外层呢?抛往外层时,何时需要封装异常呢?看看String#toLowerCase(),看看ProcessBuilder#start(),体会一下。


本文链接:你真的会阅读Java的异常信息吗?
作者:猴子007
出处:monkeysayhi.github.io
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名及链接。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

用经验主义方法从用户的角度预测用户观看视频时的体验质量

发表于 2017-11-14

临时链接已失效
微信公众平台运营中心

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

shedlock源码解析

发表于 2017-11-14

序

本文主要解析一下shedlock的实现。

LockProvider

shedlock-core-0.16.1-sources.jar!/net/javacrumbs/shedlock/core/LockProvider.java

1
2
3
4
5
6
7
8
复制代码public interface LockProvider {

/**
* @return If empty optional has been returned, lock could not be acquired. The lock
* has to be released by the callee.
*/
Optional<SimpleLock> lock(LockConfiguration lockConfiguration);
}

LockProvider入参是lockConfiguration,返回SimpleLock。

StorageBasedLockProvider

shedlock-core-0.16.1-sources.jar!/net/javacrumbs/shedlock/support/StorageBasedLockProvider.java

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
50
51
52
复制代码public class StorageBasedLockProvider implements LockProvider {
private final StorageAccessor storageAccessor;
private final LockRecordRegistry lockRecordRegistry = new LockRecordRegistry();

protected StorageBasedLockProvider(StorageAccessor storageAccessor) {
this.storageAccessor = storageAccessor;
}

@Override
public Optional<SimpleLock> lock(LockConfiguration lockConfiguration) {
boolean lockObtained = doLock(lockConfiguration);
if (lockObtained) {
return Optional.of(new StorageLock(lockConfiguration, storageAccessor));
} else {
return Optional.empty();
}
}

/**
* Sets lockUntil according to LockConfiguration if current lockUntil &lt;= now
*/
protected boolean doLock(LockConfiguration lockConfiguration) {
String name = lockConfiguration.getName();

if (!lockRecordRegistry.lockRecordRecentlyCreated(name)) {
// create document in case it does not exist yet
if (storageAccessor.insertRecord(lockConfiguration)) {
lockRecordRegistry.addLockRecord(name);
return true;
}
lockRecordRegistry.addLockRecord(name);
}

return storageAccessor.updateRecord(lockConfiguration);
}

private static class StorageLock implements SimpleLock {
private final LockConfiguration lockConfiguration;
private final StorageAccessor storageAccessor;

StorageLock(LockConfiguration lockConfiguration, StorageAccessor storageAccessor) {
this.lockConfiguration = lockConfiguration;
this.storageAccessor = storageAccessor;
}

@Override
public void unlock() {
storageAccessor.unlock(lockConfiguration);
}
}

}

使用StorageAccessor来实现加锁

LockManager

shedlock-core-0.16.1-sources.jar!/net/javacrumbs/shedlock/core/LockManager.java

1
2
3
4
5
6
复制代码/**
* Executes task if not locked.
*/
public interface LockManager {
void executeWithLock(Runnable task);
}

默认实现
shedlock-core-0.16.1-sources.jar!/net/javacrumbs/shedlock/core/DefaultLockManager.java

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
复制代码public class DefaultLockManager implements LockManager {
private static final Logger logger = LoggerFactory.getLogger(DefaultLockManager.class);

private final LockingTaskExecutor lockingTaskExecutor;
private final LockConfigurationExtractor lockConfigurationExtractor;

public DefaultLockManager(LockProvider lockProvider, LockConfigurationExtractor lockConfigurationExtractor) {
this(new DefaultLockingTaskExecutor(lockProvider), lockConfigurationExtractor);
}

public DefaultLockManager(LockingTaskExecutor lockingTaskExecutor, LockConfigurationExtractor lockConfigurationExtractor) {
this.lockingTaskExecutor = requireNonNull(lockingTaskExecutor);
this.lockConfigurationExtractor = requireNonNull(lockConfigurationExtractor);
}

@Override
public void executeWithLock(Runnable task) {
Optional<LockConfiguration> lockConfigOptional = lockConfigurationExtractor.getLockConfiguration(task);
if (!lockConfigOptional.isPresent()) {
logger.debug("No lock configuration for {}. Executing without lock.", task);
task.run();
} else {
lockingTaskExecutor.executeWithLock(task, lockConfigOptional.get());
}
}
}

委托给lockingTaskExecutor来加锁
shedlock-core-0.16.1-sources.jar!/net/javacrumbs/shedlock/core/DefaultLockingTaskExecutor.java

1
2
3
> public class DefaultLockingTaskExecutor implements LockingTaskExecutor {  
> private static final Logger logger = LoggerFactory.getLogger(DefaultLockingTaskExecutor.class);
> private final LockProvider lockProvider;

复制代码public DefaultLockingTaskExecutor(LockProvider lockProvider) {
this.lockProvider = requireNonNull(lockProvider);
}

@Override
public void executeWithLock(Runnable task, LockConfiguration lockConfig) {
Optional lock = lockProvider.lock(lockConfig);
if (lock.isPresent()) {
try {
logger.debug(“Locked {}.”, lockConfig.getName());
task.run();
} finally {
lock.get().unlock();
logger.debug(“Unlocked {}.”, lockConfig.getName());
}
} else {
logger.debug(“Not executing {}. It’s locked.”, lockConfig.getName());
}
}

1
}

复制代码>这里跟lockProvider衔接上

SpringLockableTaskSchedulerFactoryBean(偷梁换柱)

shedlock-spring-0.16.1-sources.jar!/net/javacrumbs/shedlock/spring/SpringLockableTaskSchedulerFactoryBean.java

1
2


复制代码@Override
public Class<?> getObjectType() {
return LockableTaskScheduler.class;
}

@Override
protected LockableTaskScheduler createInstance() throws Exception {
return new LockableTaskScheduler(
taskScheduler,
new DefaultLockManager(lockProvider, new SpringLockConfigurationExtractor(defaultLockAtMostFor, defaultLockAtLeastFor, embeddedValueResolver))
);
}

1
2


复制代码>主要是LockableTaskScheduler的工厂方法

LockableTaskScheduler(task scheduler lock wrapper)

shedlock-spring-0.16.1-sources.jar!/net/javacrumbs/shedlock/spring/LockableTaskScheduler.java

1
2
3
public class LockableTaskScheduler implements TaskScheduler, DisposableBean {  
private final TaskScheduler taskScheduler;
private final LockManager lockManager;

复制代码public LockableTaskScheduler(TaskScheduler taskScheduler, LockManager lockManager) {
this.taskScheduler = requireNonNull(taskScheduler);
this.lockManager = requireNonNull(lockManager);
}

@Override
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
return taskScheduler.schedule(wrap(task), trigger);
}

@Override
public ScheduledFuture<?> schedule(Runnable task, Date startTime) {
return taskScheduler.schedule(wrap(task), startTime);
}

@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Date startTime, long period) {
return taskScheduler.scheduleAtFixedRate(wrap(task), startTime, period);
}

@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long period) {
return taskScheduler.scheduleAtFixedRate(wrap(task), period);
}

@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Date startTime, long delay) {
return taskScheduler.scheduleWithFixedDelay(wrap(task), startTime, delay);
}

@Override
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long delay) {
return taskScheduler.scheduleWithFixedDelay(wrap(task), delay);
}

private Runnable wrap(Runnable task) {
return new LockableRunnable(task, lockManager);
}

@Override
public void destroy() throws Exception {
if (taskScheduler instanceof DisposableBean) {
((DisposableBean) taskScheduler).destroy();
}
}

1
}

复制代码>对task scheduler包装了一层,织入了lock的逻辑

问题

上面将了半天,讲了lockProvider以及lockManager,还有LockableTaskScheduler是如何给task scheduler加上锁的,还有LockableTaskScheduler的工厂方法SpringLockableTaskSchedulerFactoryBean。那么问题来了,spring的schedule凭什么就使用你配置的LockableTaskScheduler呢?

1
2
3
4
5
6
7
8
@Bean  
public ScheduledLockConfiguration scheduledLockConfiguration(LockProvider lockProvider) {
return ScheduledLockConfigurationBuilder
.withLockProvider(lockProvider)
.withPoolSize(10)
.withDefaultLockAtMostFor(Duration.ofMinutes(10))
.build();
}

这种配置仅仅当spring工厂里头没有配置taskScheduler的时候,起作用。如果项目已经显示指定taskScheduler的时候,那么就不会使用LockableTaskScheduler。不过可以通过实现SchedulingConfigurer接口强制指定使用LockableTaskScheduler。

doc

  • 使用shedlock将spring schedule上锁

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Docker】Docker三剑客实践之部署集群

发表于 2017-11-14

作者:不洗碗工作室 - Marklux
出处:marklux.cn/blog/55
版权归作者所有,转载请注明出处

前言

DOCKER技术在推出后掀起了一阵容器化技术的热潮,容器化使得服务的部署变得极其简易,这为微服务和分布式计算提供了很大的便利。

为了把容器化技术的优点发挥到极致,docker公司先后推出了三大技术:docker-machine,docker-compose,docker-swarm,可以说是几乎实现了容器化技术中所有可能需要的底层技术手段。

在使用go语言实现了判题引擎并打包好docker镜像后,就需要进行分布式判题的编写,这次就让我们手动实践,尝试使用docker的三大杀器来部署一个多机器构成的判题服务集群。

三剑客简介

docker-machine

docker技术是基于Linux内核的cgroup技术实现的,那么问题来了,在非Linux平台上是否就不能使用docker技术了呢?答案是可以的,不过显然需要借助虚拟机去模拟出Linux环境来。

docker-machine就是docker公司官方提出的,用于在各种平台上快速创建具有docker服务的虚拟机的技术,甚至可以通过指定driver来定制虚拟机的实现原理(一般是virtualbox)。

docker-compose

docker镜像在创建之后,往往需要自己手动pull来获取镜像,然后执行run命令来运行。当服务需要用到多种容器,容器之间又产生了各种依赖和连接的时候,部署一个服务的手动操作是令人感到十分厌烦的。

dcoker-compose技术,就是通过一个.yml配置文件,将所有的容器的部署方法、文件映射、容器连接等等一系列的配置写在一个配置文件里,最后只需要执行docker-compose up命令就会像执行脚本一样的去一个个安装容器并自动部署他们,极大的便利了复杂服务的部署。

docker-swarm

swarm是基于docker平台实现的集群技术,他可以通过几条简单的指令快速的创建一个docker集群,接着在集群的共享网络上部署应用,最终实现分布式的服务。

相比起zookeeper等集群管理框架来说,swarm显得十分轻量,作为一个工具,它把节点的加入、管理、发现等复杂的操作都浓缩为几句简单的命令,并且具有自动发现节点和调度的算法,还支持自定制。虽然swarm技术现在还不是非常成熟,但其威力已经可见一般。

浅谈docker服务架构和远程API

在正式使用docker技术部署集群应用时,我们应该先来了解一下docker工作的一些底层原理,和docker远程调用的API,这样才能大体了解集群究竟是如何运作的。

daemon

之前的docker入门文章中讲过,docker的基础服务,比如容器的创建、查看、停止、镜像的管理,其实都是由docker的守护进程(daemon)来实现的。

每次执行的docker指令其实都是通过向daemon发送请求来实现的。

daemon的运作(通信模式)主要有两种,一种是通过unix套接字(默认,但只能在本地访问到,比较安全),一种是通过监听tcp协议地址和端口来实现(这个可以实现在远程调用到docker服务)。

远程API

除了通过远程tcp协议访问远程主机上的docker服务外,docker还提供了一套基于HTTP的API,可以使用curl来实现操作远程主机上的docker服务,这为开发基于WEB的docker服务提供了便利。

远程docker使用示例

最终实现集群的时候实际是使用docker的远程调用来将不同的docker主机连接成一个整体的(通过tcp协议)。

我们不妨先来手动模拟尝试一下docker服务的远程调用吧。

首先需要在提供服务的主机上将docker的运行方式改为tcp,具体方法为修改/etc/default/docker中的DOCKER_OPTS为如下内容

1
复制代码-H tcp://127.0.0.1:4243 -H unix:///var/run/docker.sock

-H 后的参数是自己定义的要绑定的tcp地址和端口,成功绑定后重启docker服务就可以在该端口访问到docker的daemon服务。

不幸的是:

  • 在OSX平台上,并没有找到docker的daemon配置文件
  • 在OSX平台上,使用docker -H tcp://0.0.0.0:4243 -H unix:///var/run/docker.sock -d这样的命令来尝试以tcp的方式启动docker daemon也是失败的,并没有任何作用
  • 目前推测除了Linux平台上,其他平台上的配置方法都不太一样,但是在网络上暂时没有找到解决方案,所以后面的操作我只能通过在本地创建多个docker-machine的方式来模拟实现远程调用。

假设我们在192.168.1.123这台主机上开启了docker服务,监听了2375端口,那么我们就可以在同一网段的其他主机上(比如192.168.1.233)通过docker -H tcp://192.168.1.123:2345 <command>的方式调用到该主机上的docker服务。

比如

1
2
3
复制代码docker -H tcp://192.168.1.123:2345 ps
docker -H tcp://192.168.1.123:2345 images
docker -H tcp://192.168.1.123:2345 run ...

最终swarm构建集群的时候,就是通过这样的远程服务调用来调度各个节点的。

集群和分布式运算

在正式开始实践集群之前,我们有必要了解究竟什么是集群,什么是分布式计算。

首先,这两者有一个共同点,就是他们都是使用了多个服务节点的,通俗的说,就是要用到多台服务器协同工作(不一定是实体,也可能是虚拟机)。

而两者的区别在于:

  • 集群是多台机器执行同一个业务,每次根据调度算法寻找最合适的节点来执行该业务
  • 分布式计算是将一个业务拆分成多个独立的部分,由多台机器共同协作完成

集群的优点在于,当业务的需要的资源比较大时,可以避免由一个服务器去独自承担压力,而且即便有一个节点宕机了,业务仍然可以继续正常运行。这有点类似于负载均衡。

分布式的优点则是在计算上,可以协同多台机器发挥计算的威力,进行需要超高性能的运算。

构建集群

说现在我们正式开始构建集群。

使用docker-machine创建节点

由于实体机器的缺乏以及在osx上无法正常开启tcp的docker服务,我们基于docker-machine来创建多个虚拟机,作为集群中的节点。

执行下面的命令就可以创建一个新的docker-machine虚拟机manager1

1
复制代码docker-machine create --driver virtualbox manager1

在创建了虚拟机后,可以使用docker-machine env manager1来查看虚拟机manager1的相关信息,包括IP地址等

现在我们继续执行命令创建worker1和worker2两个节点,使用docker-machine ls命令可以查看到所有正在工作的虚拟机:

1
2
3
4
5
复制代码docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
manager1 - virtualbox Running tcp://192.168.99.100:2376 v17.06.1-ce
worker1 - virtualbox Running tcp://192.168.99.101:2376 v17.06.1-ce
worker2 - virtualbox Running tcp://192.168.99.102:2376 v17.06.1-ce

创建docker machine后,可以通过docker-machine ssh manager1 <command>的方式来访问虚拟机,执行指令。

创建swarm集群

初始化一个swarm集群的命令为:

1
复制代码docker swarm init --listen-addr <MANAGER-IP>:<PORT> --advertise-addr <IP>

--listen-addr参数是管理者节点的docker服务所在的IP:PORT,也就是说,可以通过这个组合访问到该节点的docker服务。

--advertise-addr是广播地址,也就是其他节点加入该swarm集群时,需要访问的IP

现在我们在manager1节点里创建swarm网络,执行

1
复制代码docker-machine ssh manager1 docker swarm init --listen-addr 192.168.99.100:2377 --advertise-addr 192.168.99.100

返回响应:

1
2
3
4
5
6
7
8
9
复制代码Swarm initialized: current node (23lkbq7uovqsg550qfzup59t6) is now a manager.

To add a worker to this swarm, run the following command:

docker swarm join \
--token SWMTKN-1-3z5rzoey0u6onkvvm58f7vgkser5d7z8sfshlu7s4oz2gztlvj-c036gwrakjejql06klrfc585r \
192.168.99.100:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

这样便创建了一个swarm集群,并且manager1节点目前是以管理者的身份加入在节点中的。

现在我们把worker1和worker2两个节点加入到swarm集群中去,分别在两个节点的虚拟机中执行docker swarm join --token ..即可:

1
2
3
4
复制代码docker-machine ssh worker1 docker swarm join --token \
SWMTKN-1-3z5rzoey0u6onkvvm58f7vgkser5d7z8sfshlu7s4oz2gztlvj-c036gwrakjejql06klrfc585r \
192.168.99.100:2377
This node joined a swarm as a worker.
1
2
3
4
复制代码docker-machine ssh worker2 docker swarm join --token \
SWMTKN-1-3z5rzoey0u6onkvvm58f7vgkser5d7z8sfshlu7s4oz2gztlvj-c036gwrakjejql06klrfc585r \
192.168.99.100:2377
This node joined a swarm as a worker.

在任何一个节点上执行docker node ls都可以查看到当前整个集群中的所有节点:

1
2
3
4
5
复制代码docker-machine ssh manager1 docker node ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
manager1 - virtualbox Running tcp://192.168.99.100:2376 v1.12.3
worker1 - virtualbox Running tcp://192.168.99.101:2376 v1.12.3
worker2 - virtualbox Running tcp://192.168.99.102:2376 v1.12.3

创建跨主机网络

集群建立完毕之后我们要做的就是在集群上部署我们的服务。但是首先应该让所有的节点处在一个共享的网络中,这样当我们把服务部署在这个共享网络中,就相当于部署在整个集群中了。

使用docker network ls可以查看到当前主机所参与的所有网络:

1
2
3
4
5
6
复制代码docker-machine ssh manager1 docker network ls
NETWORK ID NAME DRIVER SCOPE
764ff31881e5 bridge bridge local
fbd9a977aa03 host host local
6p6xlousvsy2 ingress overlay swarm
e81af24d643d none null local

其中SCOPE为swarm,DRIVER为overlay的即为集群节点中的共享网络。集群建立后会有一个默认的ingress共享网络,现在我们来再创建一个:

1
复制代码docker-machine ssh manager1 docker network create --driver overlay swarm_test

在跨主机网络上部署服务

在集群上部署应用,就是在共享网络上部署服务(service)。

但首先要保证每个节点上都已经有所需的镜像和环境了,这点便可以通过将同一份docker-compose配置文件共享到每个主机上,使用docker-compose在每个节点上下载镜像和搭建环境的工作。

由于judge_server的服务架构很简单,就一个镜像,所以我在这里直接在每台主机上把它pull下来就好了:

1
2
3
复制代码docker-machine ssh manager1 docker pull registry.cn-qingdao.aliyuncs.com/marklux/judge_server:1.0
docker-machine ssh worker1 docker pull registry.cn-qingdao.aliyuncs.com/marklux/judge_server:1.0
docker-machine ssh worker2 docker pull registry.cn-qingdao.aliyuncs.com/marklux/judge_server:1.0

接下来便是重头戏,我们使用manager1节点,在共享网络上启动我们的服务

1
复制代码docker service create --replicas 3 --name judge_swarm -p 8090:8090 --network=swarm_test registry.cn-qingdao.aliyuncs.com/marklux/judge_server:1.0

这个命令看起来是不是很像docker run?没错,swarm最终的目的就是把操作集群变得像操作单一的docker服务端一样简单!

–replicas 用于指定服务需要的节点数量,也就是集群的规模,这个值是弹性的,你可以在后续动态的更改它。

当服务中某个节点挂掉时,swarm将会搜寻集群中剩余的可用节点,顶替上去。也就是说,swarm会动态的调度,总是保持服务是由3个节点运行着的。

-p 用于暴露端口到宿主机,这样我们就能访问到了。

–network用于指定部署service的网络是哪一个

现在在manager1节点中使用docker service ls来查看集群中的服务:

1
2
3
复制代码docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
kofcno637cmq judge_swarm replicated 3/3 registry.cn-qingdao.aliyuncs.com/marklux/judge_server:1.0 *:8090->8090/tcp

现在我们尝试在本地访问192.168.99.100:8090/ping,就可以得到响应了,事实上,现在无论将ip换为worker1或者worker2的,响应的结果都是一样,因为此时所有节点已经处在一个共同的集群网络下了

经过大量的访问测试,可以看到hostname是在变化着的,这说明每次请求,都由swarm动态的调度,选择了不同的节点来进行处理。

遗留问题

至此集群的部署已经完成,但是我们还遗留了几个问题没有解决:

  • 集群节点的动态添加删除不是很方便,这导致在web端管理判题服务机有一定的难度,当然可以通过docker的REMOTE API来实现,不过复杂度比较高
  • 集群节点间的文件同步不太好实现,可能需要自己写脚本同步或是使用rsync之类的服务来实现
  • swarm非常适合快速构建大量集群来实现业务的处理,不过对于只有几台机器的情况而言,有些”杀鸡用牛刀”的感觉

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Linux】Linux系统编程入门 线程

发表于 2017-11-14

作者:不洗碗工作室 - Marklux
出处:marklux.cn/blog/56
版权归作者所有,转载请注明出处

文件和文件系统

文件是linux系统中最重要的抽象,大多数情况下你可以把linux系统中的任何东西都理解为文件,很多的交互操作其实都是通过文件的读写来实现的。

文件描述符

在linux内核中,文件是用一个整数来表示的,称为 文件描述符,通俗的来说,你可以理解它是文件的id(唯一标识符)

普通文件

  • 普通文件就是字节流组织的数据。
  • 文件并不是通过和文件名关联来实现的,而是通过关联索引节点来实现的,文件节点拥有文件系统为普通文件分配的唯一整数值(ino),并且存放着一些文件的相关元数据。

目录与链接

  • 正常情况下文件是通过文件名来打开的。
  • 目录是可读名称到索引编号之间的映射,名称和索引节点之间的配对称为链接。
  • 可以把目录看做普通文件,只是它包含着文件名称到索引节点的映射(链接)

进程

进程是仅次于文件的抽象概念,简单的理解,进程就是正在执行的目标代码,活动的,正在运行的程序。不过在复杂情况下,进程还会包含着各种各样的数据,资源,状态甚至虚拟计算机。

你可以这么理解进程:它是竞争计算机资源的基本单位。

进程、程序与线程

  1. 程序

程序,简单的来说就是存在磁盘上的二进制文件,是可以内核所执行的代码
2. 进程

当一个用户启动一个程序,将会在内存中开启一块空间,这就创造了一个进程,一个进程包含一个独一无二的PID,和执行者的权限属性参数,以及程序所需代码与相关的资料。

进程是系统分配资源的基本单位。

一个进程可以衍生出其他的子进程,子进程的相关权限将会沿用父进程的相关权限。
3. 线程

每个进程包含一个或多个线程,线程是进程内的活动单元,是负责执行代码和管理进程运行状态的抽象。

线程是独立运行和调度的基本单位。

进程的层次结构(父进程与子进程)

在进程执行的过程中可能会衍生出其他的进程,称之为子进程,子进程拥有一个指明其父进程PID的PPID。子进程可以继承父进程的环境变量和权限参数。

于是,linux系统中就诞生了进程的层次结构——进程树。

进程树的根是第一个进程(init进程)。

过程调用的流程: fork & exec

一个进程生成子进程的过程是,系统首先复制(fork)一份父进程,生成一个暂存进程,这个暂存进程和父进程的区别是pid不一样,而且拥有一个ppid,这时候系统再去执行(exec)这个暂存进程,让他加载实际要运行的程序,最终成为一个子进程的存在。

进程的结束

当一个进程终止时,并不会立即从系统中删除,内核将在内存中保存该进程的部分内容,允许父进程查询其状态(这个被称为等待终止进程)。

当父进程确定子进程已经终止,该子进程将会被彻底删除。

但是如果一个子进程已经终止,但父进程却不知道它的状态,这个进程将会成为 僵尸进程

服务与进程

简单的说服务(daemon)就是常驻内存的进程,通常服务会在开机时通过init.d中的一段脚本被启动。

进程通信

进程通信的几种基本方式:管道,信号量,消息队列,共享内存,快速用户控件互斥。

程序,进程和线程

现在我们再次详细的讨论这三个概念

程序(program)

程序是指编译过的、可执行的二进制代码,保存在储存介质上,不运行。

进程(process)

进程是指正在运行的程序。

进程包括了很多资源,拥有自己独立的内存空间。

线程

线程是进程内的活动单元。

包括自己的虚拟储存器,如栈、进程状态如寄存器,以及指令指针。

  • 在单线程的进程中,线程即进程。而在多线程的进程中,多个线程将会共享同一个内存地址空间
  • 参考阅读

PID

可以参考之前的基础概念部分。

在C语言中,PID是由数据类型pid_t来表示的。

运行一个进程

创建一个进程,在unix系统中被分为了两个流程。

  1. 把程序载入内存并执行程序映像的操作:exec
  2. 创建一个新进程:fork

exec

最简单的exec系统调用函数:execl()

  • 函数原型:
1
复制代码int execl(const char * path,const chr * arg,...)

execl()调用将会把path所指的路径的映像载入内存,替换当前进程的映像。

参数arg是以第一个参数,参数内容是可变的,但最后必须以NULL结尾。

  • 举例:
1
2
3
4
5
6
7
复制代码int ret;

ret = execl("/bin/vi","vi",NULL);

if (ret == -1) {
perror("execl");
}

上面的代码将会通过/bin/vi替换当前运行的程序

注意这里的第一个参数vi,是unix系统的默认惯例,当创建、执行进程时,shell会把路径中的最后部分放入新进程的第一个参数,这样可以使得进程解析出二进制映像文件的名字。

1
2
3
4
5
6
7
复制代码int ret;

ret = execl("/bin/vi","vi","/home/mark/a.txt",NULL);

if (ret == -1) {
perror("execl");
}

上面的代码是一个非常有代表性的操作,这相当于你在终端执行以下命令:

1
复制代码vi /home/mark/a.txt
  • 返回值:

正常情况下其实execl()不会返回,调用成功后会跳转到新的程序入口点。

成功的execl()调用,将改变地址空间和进程映像,还改变了很多进程的其他属性。

不过进程的PID,PPID,优先级等参数将会被保留下来,甚至会保留下所打开的文件描述符(这就意味着它可以访问所有这些原本进程打开的文件)。

失败后将会返回-1,并更新errno。

其他exec系函数

略,使用时查找

fork

通过fork()系统调用,可以创建一个和当前进程映像一模一样的子进程。

  • 函数原型
1
复制代码pid_t fork(void)

调用成功后,会创建一个新的进程(子进程),这两个进程都会继续运行。

  • 返回值

如果调用成功,
父进程中,fork()会返回子进程的pid,在子进程中返回0;
如果失败,返回-1,并更新errno,不会创建子进程。

  • 举例

我们看下面这段代码

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
复制代码#include <unistd.h>
#include <stdio.h>
int main ()
{
pid_t fpid; //fpid表示fork函数返回的值
int count=0;

printf("this is a process\n");

fpid=fork();

if (fpid < 0)
printf("error in fork!");
else if (fpid == 0) {
printf("i am the child process, my process id is %d\n",getpid());
printf("我是爹的儿子\n");
count++;
}
else {
printf("i am the parent process, my process id is %d\n",getpid());
printf("我是孩子他爹\n");
count++;
}
printf("统计结果是: %d\n",count);
return 0;
}

这段代码的运行结果比较神奇,是这样的:

1
2
3
4
5
6
7
复制代码this is a process
i am the parent process, my process id is 21448
我是孩子他爹
统计结果是: 1
i am the child process, my process id is 21449
我是爹的儿子
统计结果是: 1

在执行了fork()之后,这个程序就拥有了两个进程,父进程和子进程分别往下继续执行代码,进入了不同的if分支。

如何理解pid在父子进程中不同?

其实就相当于链表,进程形成了链表,父进程的pid指向了子进程的pid,因为子进程没有子进程,所以pid为0。

写时复制

传统的fork机制是,调用fork时,内核会复制所有的内部数据结构,复制进程的页表项,然后把父进程的地址空间按页复制给子进程(非常耗时)。

现代的fork机制采用了一种惰性算法的优化策略。

为了避免复制时系统开销,就尽可能的减少“复制”操作,当多个进程需要读取他们自己那部分资源的副本时,并不复制多个副本出来,而是为每个进程设定一个文件指针,让它们读取同一个实际文件。

显然这样的方式会在写入时产生冲突(类似并发),于是当某个进程想要修改自己的那个副本时,再去复制该资源,(只有写入时才复制,所以叫写时复制)这样就减少了复制的频率。

联合实例

在程序中创建一个子进程,打开另一个应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码pid_t pid;

pid = fork();

if (pid == -1)
perror("fork");

//子进程
if (!pid) {
const char * args[] = {"windlass",NULL};

int ret;

// 参数以数组方式传入
ret = execv("/bin/windlass",args);

if (ret == -1) {
perror("execv");
exit(EXIT_FAILURE);
}
}

上面的程序创建了一个子进程,并且使子进程运行了/bin/windlas程序。

终止进程

exit()

  • 函数原型
1
复制代码void exit (int status)

该函数用于终止当前的进程,参数status只用于标识进程的退出状态,这个值将会被传送给当前进程的父进程用于判断。

还有一些其他的终止调用函数,在此不赘述。

等待子进程终止

如何通知父进程子进程终止?可以通过信号机制来实现这一点。但是在很多情况下,父进程需要知道有关子进程的更详细的信息(比如返回值),这时候简单的信号通知就显得无能为力了。

如果终止时,子进程已经完全被销毁,父进程就无法获取关于子进程的任何信息。

于是unix最初做了这样的设计,如果一个子进程在父进程之前结束,内核就把这个子进程设定成一种特殊的运行状态,这种状态下的进程被称为僵尸进程,它只保留最小的概要信息,等待父进程获取到了这些信息之后,才会被销毁。

wait()

  • 函数原型
1
复制代码pid_t wait(int * status);

这个函数可以用于获取已经终止的子进程的信息。

调用成功时,会返回已终止的子进程的pid,出错时返回-1。如果没有子进程终止会导致调用的阻塞直到有一个子进程终止。

waitpid()

  • 函数原型
1
复制代码pid_t waitpid(pid_t pid,int * status,int options);

waitpid()是一个更为强大的系统调用,支持更细粒度的管控。

一些其他可能会遇到的等待函数

  • wait3()
  • wait4()

简单的说,wait3等待任意一个子进程的终止,wait4等待一个指定子进程的终止。

创建并等待新进程

很多时候我们会遇到下面这种情景:

你创建了一个新进程,你想等待它调用完之后再继续运行你自己的进程,也就是说,创建一个新进程并立即开始等待它的终止。

一个合适的选择是system():

1
复制代码int system(const char * command);

system()函数将会调用command提供的命令,一般用于运行简单的工具和shell脚本。

成功时,返回的是执行command命令所得到的返回状态。

你可以使用fork(),exec(),waitpid()来实现一个system()。

下面给出一个简单的实现:

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
复制代码int my_system(const char * cmd)
{
int status;
pid_t pid;

pid = fork();

if (pid == -1) {
return -1;
}

else if (pid == 0) {
const char * argv[4];

argv[0] = "sh";
argv[1] = "-c";
argv[2] = cmd;
argv[3] = NULL;

execv("bin/sh",argv);
// 这传参调用好像有类型转换问题

exit(-1);

}//子进程

//父进程
if (waitpid(pid,&status,0) == -1)
return -1;
else if (WIFEXITED(status))
return WEXITSTATUS(status);

return -1;
}

幽灵进程

上面我们谈论到僵尸进程,但是如果父进程没有等待子进程的操作,那么它所有的子进程都将成为幽灵进程,幽灵进程将会一直存在(因为等不到父进程调用,就一直不终止),导致系统运行速度的拖慢。

正常情况下我们不该让这种情况发生,然而如果父进程在子进程结束之前就结束了,或者父进程还没有机会等待其僵尸进程的子进程,就先结束了,这样就不可避免的产生了幽灵进程。

linux内核有一个机制来避免这样的情况发生。

无论何时,只要有进程结束,内核就会遍历它的所有子进程,并且把他们的父进程重新设置为init,而init会周期性的等待所有的子进程,以确保没有长时间存在的幽灵进程。

进程与权限

略,待补充

会话和进程组

进程组

每个进程都属于某个进程组,进程组就是由一个或者多个为了实现作业控制而相互关联的进程组成的。

一个进程组的id是进程组首进程的pid(如果一个进程组只有一个进程,那进程组和进程其实没啥区别)。

进程组的意义在于,信号可以发送给进程组中的所有进程。这样可以实现对多个进程的同时操作。

会话

会话是一个或者多个进程组的集合。

一般来说,会话(session)和shell没有什么本质上的区别。

我们通常使用用户登录一个终端进行一系列操作这样的例子来描述一次会话。

  • 举例
1
复制代码$cat ship-inventory.txt | grep booty|sort

上面就是在某次会话中的一个shell命令,它会产生一个由3个进程组成的进程组。

守护进程(服务)

守护进程(daemon)运行在后台,不与任何控制终端相关联。通常在系统启动时通过init脚本被调用而开始运行。

在linux系统中,守护进程和服务没有什么区别。

对于一个守护进程,有两个基本的要求:其一:必须作为init进程的子进程运行,其二:不与任何控制终端交互。

产生一个守护进程的流程

  1. 调用fork()来创建一个子进程(它即将成为守护进程)
  2. 在该进程的父进程中调用exit(),这保证了父进程的父进程在其子进程结束时会退出,保证了守护进程的父进程不再继续运行,而且守护进程不是首进程。(它继承了父进程的进程组id,而且一定不是leader)
  3. 调用setsid(),给守护进程创建一个新的进程组和新的会话,并作为两者的首进程。这可以保证不存在和守护进程相关联的控制终端。
  4. 调用chdir(),将当前工作目录改为根目录。这是为了避免守护进程运行在原来fork的父进程打开的随机目录下,便于管理。
  5. 关闭所有的文件描述符。
  6. 打开文件描述符0,1,2(stdin,stdout,err),并把它们重定向到/dev/null。

daemon()

用于实现上面的操作来产生一个守护进程

  • 函数原型
1
复制代码int daemon(int nochdir,int noclose);

如果参数nochdir是非0值,就不会将工作目录定向到根目录。
如果参数noclose是非0值,就不会关闭所有打开的文件描述符。

成功时返回0,失败返回-1。

注意调用这个函数生成的函数是父进程的副本(fork),所以最终生成的守护进程的样子就是父进程的样子,一般来说,就是在父进程中写好要运行在后台的功能代码,然后调用daemon()来把这些功能包装成一个守护进程。

这样子看上去好像是把当前执行的进程包装成了一个守护进程,但其实包装的是它派生出的一个副本。

线程

基础概念

线程是进程内的执行单元(比进程更低一层的概念),具体包括 虚拟处理器,堆栈,程序状态等。

可以认为 线程是操作系统调度的最小执行单元。

现代操作系统对用户空间做两个基础抽象:虚拟内存和虚拟处理器。这使得进程内部“感觉”自己独占机器资源。

虚拟内存

系统会为每个进程分配独立的内存空间,这会让进程以为自己独享全部的RAM。

但是同一个进程内的所有线程共享该进程的内存空间。

虚拟处理器

这是一个针对线程的概念,它让每个线程都“感觉”自己独享CPU。实际上对于进程也是一样的。

多线程

多线程的好处

  • 编程抽象

模块化的设计模式

  • 并发

在多核处理器上可以实现真正的并发,提高系统吞吐量

  • 提高响应能力

防止串行运算僵死

  • 防止i/o阻塞

避免单线程下,i/o操作导致整个进程阻塞的情况。此外也可以通过异步i/o和非阻塞i/o解决。

  • 减少上下文切换

多线程的切换消耗的性能远比进程间的上下文切换小的多

  • 内存共享

因为同一进程内的线程可以共享内存,在某些场景下可以利用这些特性,用多线程取代多进程。

多线程的代价

调试难度极大。

在同一个内存空间内并发性的读写操作会引发多种问题(如脏数据),对多进程情景下的资源同步变得困难,而且多个独立运行的线程其时间和顺序具有不可预测性,会导致各种各样奇怪的问题。

这一点可以参考并发带来的问题。

线程模型

线程的概念同时存在于内核和用户空间中。

内核级线程模型

每个内核线程直接转换成用户空间的线程。即内核线程:用户空间线程=1:1

用户级线程模型

这种模型下,一个保护了n个线程的用户进程只会映射到一个内核进程。即n:1。

可以减少上下文切换的成本,但在linux下没什么意义,因为linux下进程间的上下文切换本身就没什么消耗,所以很少使用。

混合式线程模型

上述两种模型的混合,即n:m型。

很难实现。

*协同程序

‌提供了比线程更轻量级的执行单位。

线程模式

每个连接对应一个线程

也就是阻塞式的I/O,实际就是单线程模式

线程以串行的方式运行,一个线程遇到I/O时线程必须被挂起等待直到操作完成后,才能再继续执行。

事件驱动的线程模式

单线程的操作模型中,大部分的系统负荷在于等待(尤其是I/O操作),因此在事件驱动的模式下,把这些等待操作从线程的执行过程中剥离掉,通过发送异步I/O请求或者是I/O多路复用,引入事件循环和回调来处理线程和I/O之间的关系。

有关I/O的几种模式,参考这里

简要概括一下,分为四种:

  • 阻塞IO:串行处理,单线程,同步等待
  • 非阻塞IO:线程发起IO请求后将立即得到结果而不是等待,如果IO没有处理完将返回ERROR,需要线程自己主动去向Kernel不断请求来判断IO是否完成
  • 异步IO:线程发起IO请求后,立即得到结果,Kernel执行完IO后会主动发送SIGNAL去通知线程
  • 事件驱动IO:属于非阻塞IO的一个升级,主要用于连接较多的情况,让Kernel去监视多个socket(每个socket都是非阻塞式的IO),哪个socket有结果了就继续执行哪个socket。

并发,并行,竞争!

并发和并行

并发,是指同一时间周期内需要运行(处理)多个线程。

并行,是指同一时刻有多个线程在运行。

本质上,并发是一种编程概念,而并行是一种硬件属性,并发可以通过并行的方式实现,也可以不通过并行的方式实现(单cpu)。

竞争

并发编程带来的最大挑战就是竞争,这主要是因为多个线程同时执行时,执行结果的顺序存在不可预料性

  • 一个最简单的示范,可以参考java并发编程中的基本例子。

请看下面这行代码:

1
复制代码  x++;

假设x的初始值为5,我们使用两个线程同时执行这行代码,会出现很多不一样的结果,即运行完成后,x的值可能为6,也可能为7。(这个是并发最基本的示范,自己理解一下很容易明白。)

原因简要描述为下:

一个线程执行x++的过程大概分为3步:

1. 把x加载到寄存器
2. 把寄存器的值+1
3. 把寄存器的值写回到x中


当两个线程出现竞争的时候,就是这3步执行的过程在时间上出现了不可预料性,假设线程1,2将x加载到寄存器的时候x都是5,但当线程1写回x时,x成为6,线程2写回x时,x还是6,这就与初衷相悖。


如果有更多的线程结果将变得更加难以预料。

解决竞争的手段:同步

简要的说,就是在会发生竞争的资源上,取消并发,而是采用同步的方式访问和操作。

锁

最常见的,处理并发的机制,就是锁机制了,当然系统层面的锁比DBMS等其他一些复杂系统的锁要简单一些(不存在共享锁,排他锁等一些较为复杂的概念)。

但是锁会带来两个问题:死锁和饿死。

解决这两个问题需要一些机制以及设计理念。具体有关锁的部分可以参考DBMS的并发笔记。

关于锁,有一点要记住。

锁住的是资源,而不是代码

编写代码时应该切记这个原则。

系统线程实现:PThreads

原始的linux系统调用中,没有像C++11或者是Java那样完整的线程库。

整体看来pthread的api比较冗余和复杂,但是基本操作也主要是 创建、退出等。

需要留意的一点是linux机制下,线程存在一个被称为joinable的状态。下面简要了解一下:

Join和Detach

这块的概念,非常类似于之前父子进程那部分,等待子进程退出的内容(一系列的wait函数)。

linux机制下,线程存在两种不同的状态:joinable和unjoinable。

如果一个线程被标记为joinable时,即便它的线程函数执行完了,或者使用了pthread_exit()结束了该线程,它所占用的堆栈资源和进程描述符都不会被释放(类似僵尸进程),这种情况应该由线程的创建者调用pthread_join()来等待线程的结束并回收其资源(类似wait系函数)。默认情况下创建的线程都是这种状态。

如果一个线程被标记成unjoinable,称它被分离(detach)了,这时候如果该线程结束,所有它的资源都会被自动回收。省去了给它擦屁股的麻烦。

因为创建的线程默认都是joinable的,所以要么在父线程调用pthread_detach(thread_id)将其分离,要么在线程内部,调用pthread_detach(pthread_self())来把自己标记成分离的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Just for fun——PHP框架之简单的模板引擎 原理

发表于 2017-11-14

原理

使用模板引擎的好处是数据和视图分离。一个简单的PHP模板引擎原理是

  1. extract数组($data),使key对应的变量可以在此作用域起效
  2. 打开输出控制缓冲(ob_start)
  3. include模板文件,include遇到html的内容会输出,但是因为打开了缓冲,内容输出到了缓冲中
  4. ob_get_contents()读取缓冲中内容,然后关闭缓冲ob_end_clean()

实现

封装一个Template类

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
复制代码<?php

class Template {
private $templatePath;
private $data;

public function setTemplatePath($path) {
$this->templatePath = $path;
}

/**
* 设置模板变量
* @param $key string | array
* @param $value
*/
public function assign($key, $value) {
if(is_array($key)) {
$this->data = array_merge($this->data, $key);
} elseif(is_string($key)) {
$this->data[$key] = $value;
}
}


/**
* 渲染模板
* @param $template
* @return string
*/
public function display($template) {
extract($this->data);
ob_start();
include ($this->templatePath . $template);
$res = ob_get_contents();
ob_end_clean();
return $res;
}

}

测试

test.php

1
2
3
4
5
6
7
8
9
10
11
复制代码<?php

include_once './template.php';

$template = new Template();
$template->setTemplatePath(__DIR__ . '/template/');
$template->assign('name', 'salamander');
$res = $template->display('index.html');


echo $res;

template目录下index.html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>模板测试</title>
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
h1 {
text-align: center;
padding-top: 20px;
}
</style>
</head>
<body>
<h1><?=$name?></h1>
</body>
</html>

Tip

为什么display要返回一个字符串呢?原因是为了更好的控制,嵌入到控制器类中。
对于循环语句怎么办呢?这个的话,请看流程控制的替代语法

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

tensorflow-梯度下降,有这一篇就足够了 前言 梯度

发表于 2017-11-14

前言

最近机器学习越来越火了,前段时间斯丹福大学副教授吴恩达都亲自录制了关于Deep Learning Specialization的教程,在国内掀起了巨大的学习热潮。本着不被时代抛弃的念头,自己也开始研究有关机器学习的知识。都说机器学习的学习难度非常大,但不亲自尝试一下又怎么会知道其中的奥妙与乐趣呢?只有不断的尝试才能找到最适合自己的道路。

请容忍我上述的自我煽情,下面进入主题。这篇文章主要对机器学习中所遇到的GradientDescent(梯度下降)进行全面分析,相信你看了这篇文章之后,对GradientDescent将彻底弄明白其中的原理。

梯度下降的概念

梯度下降法是一个一阶最优化算法,通常也称为最速下降法。要使用梯度下降法找到一个函数的局部极小值,必须向函数上当前点对于梯度(或者是近似梯度)的反方向的规定步长距离点进行迭代搜索。所以梯度下降法可以帮助我们求解某个函数的极小值或者最小值。对于n维问题就最优解,梯度下降法是最常用的方法之一。下面通过梯度下降法的前生今世来进行详细推导说明。

梯度下降法的前世

首先从简单的开始,看下面的一维函数:

1
apache复制代码f(x) = x^3 + 2 * x - 3

在数学中如果我们要求f(x) = 0处的解,我们可以通过如下误差等式来求得:

1
subunit复制代码error = (f(x) - 0)^2

当error趋近于最小值时,也就是f(x) = 0处x的解,我们也可以通过图来观察:

通过这函数图,我们可以非常直观的发现,要想求得该函数的最小值,只要将x指定为函数图的最低谷。这在高中我们就已经掌握了该函数的最小值解法。我们可以通过对该函数进行求导(即斜率):

1
apache复制代码derivative(x) = 6 * x^5 + 16 * x^3 - 18 * x^2 + 8 * x - 12

如果要得到最小值,只需令derivative(x) = 0,即x = 1。同时我们结合图与导函数可以知道:

  • 当x < 1时,derivative < 0,斜率为负的;
  • 当x > 1时,derivative > 0,斜率为正的;
  • 当x 无限接近 1时,derivative也就无限=0,斜率为零。

通过上面的结论,我们可以使用如下表达式来代替x在函数中的移动

1
abnf复制代码x = x - reate * derivative

当斜率为负的时候,x增大,当斜率为正的时候,x减小;因此x总是会向着低谷移动,使得error最小,从而求得 f(x) = 0处的解。其中的rate代表x逆着导数方向移动的距离,rate越大,x每次就移动的越多。反之移动的越少。

这是针对简单的函数,我们可以非常直观的求得它的导函数。为了应对复杂的函数,我们可以通过使用求导函数的定义来表达导函数:若函数f(x)在点x0处可导,那么有如下定义:

上面是都是公式推导,下面通过代码来实现,下面的代码都是使用python进行实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
repl复制代码>>> def f(x):
... return x**3 + 2 * x - 3
...
>>> def error(x):
... return (f(x) - 0)**2
...
>>> def gradient_descent(x):
... delta = 0.00000001
... derivative = (error(x + delta) - error(x)) / delta
... rate = 0.01
... return x - rate * derivative
...
>>> x = 0.8
>>> for i in range(50):
... x = gradient_descent(x)
... print('x = {:6f}, f(x) = {:6f}'.format(x, f(x)))
...

执行上面程序,我们就能得到如下结果:

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
apache复制代码x = 0.869619, f(x) = -0.603123
x = 0.921110, f(x) = -0.376268
x = 0.955316, f(x) = -0.217521
x = 0.975927, f(x) = -0.118638
x = 0.987453, f(x) = -0.062266
x = 0.993586, f(x) = -0.031946
x = 0.996756, f(x) = -0.016187
x = 0.998369, f(x) = -0.008149
x = 0.999182, f(x) = -0.004088
x = 0.999590, f(x) = -0.002048
x = 0.999795, f(x) = -0.001025
x = 0.999897, f(x) = -0.000513
x = 0.999949, f(x) = -0.000256
x = 0.999974, f(x) = -0.000128
x = 0.999987, f(x) = -0.000064
x = 0.999994, f(x) = -0.000032
x = 0.999997, f(x) = -0.000016
x = 0.999998, f(x) = -0.000008
x = 0.999999, f(x) = -0.000004
x = 1.000000, f(x) = -0.000002
x = 1.000000, f(x) = -0.000001
x = 1.000000, f(x) = -0.000001
x = 1.000000, f(x) = -0.000000
x = 1.000000, f(x) = -0.000000
x = 1.000000, f(x) = -0.000000

通过上面的结果,也验证了我们最初的结论。x = 1时,f(x) = 0。
所以通过该方法,只要步数足够多,就能得到非常精确的值。

梯度下降法的今生

上面是对一维函数进行求解,那么对于多维函数又要如何求呢?我们接着看下面的函数,你会发现对于多维函数也是那么的简单。

1
apache复制代码f(x) = x[0] + 2 * x[1] + 4

同样的如果我们要求f(x) = 0处,x[0]与x[1]的值,也可以通过求error函数的最小值来间接求f(x)的解。跟一维函数唯一不同的是,要分别对x[0]与x[1]进行求导。在数学上叫做偏导数:

  • 保持x[1]不变,对x[0]进行求导,即f(x)对x[0]的偏导数
  • 保持x[0]不变,对x[1]进行求导,即f(x)对x[1]的偏导数

有了上面的理解基础,我们定义的gradient_descent如下:

1
2
3
4
5
6
7
8
9
repl复制代码>>> def gradient_descent(x):
... delta = 0.00000001
... derivative_x0 = (error([x[0] + delta, x[1]]) - error([x[0], x[1]])) / delta
... derivative_x1 = (error([x[0], x[1] + delta]) - error([x[0], x[1]])) / delta
... rate = 0.01
... x[0] = x[0] - rate * derivative_x0
... x[1] = x[1] - rate * derivative_x1
... return [x[0], x[1]]
...

rate的作用不变,唯一的区别就是分别获取最新的x[0]与x[1]。下面是整个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
repl复制代码>>> def f(x):
... return x[0] + 2 * x[1] + 4
...
>>> def error(x):
... return (f(x) - 0)**2
...
>>> def gradient_descent(x):
... delta = 0.00000001
... derivative_x0 = (error([x[0] + delta, x[1]]) - error([x[0], x[1]])) / delta
... derivative_x1 = (error([x[0], x[1] + delta]) - error([x[0], x[1]])) / delta
... rate = 0.02
... x[0] = x[0] - rate * derivative_x0
... x[1] = x[1] - rate * derivative_x1
... return [x[0], x[1]]
...
>>> x = [-0.5, -1.0]
>>> for i in range(100):
... x = gradient_descent(x)
... print('x = {:6f},{:6f}, f(x) = {:6f}'.format(x[0],x[1],f(x)))
...

输出结果为:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
apache复制代码x = -0.560000,-1.120000, f(x) = 1.200000
x = -0.608000,-1.216000, f(x) = 0.960000
x = -0.646400,-1.292800, f(x) = 0.768000
x = -0.677120,-1.354240, f(x) = 0.614400
x = -0.701696,-1.403392, f(x) = 0.491520
x = -0.721357,-1.442714, f(x) = 0.393216
x = -0.737085,-1.474171, f(x) = 0.314573
x = -0.749668,-1.499337, f(x) = 0.251658
x = -0.759735,-1.519469, f(x) = 0.201327
x = -0.767788,-1.535575, f(x) = 0.161061
x = -0.774230,-1.548460, f(x) = 0.128849
x = -0.779384,-1.558768, f(x) = 0.103079
x = -0.783507,-1.567015, f(x) = 0.082463
x = -0.786806,-1.573612, f(x) = 0.065971
x = -0.789445,-1.578889, f(x) = 0.052777
x = -0.791556,-1.583112, f(x) = 0.042221
x = -0.793245,-1.586489, f(x) = 0.033777
x = -0.794596,-1.589191, f(x) = 0.027022
x = -0.795677,-1.591353, f(x) = 0.021617
x = -0.796541,-1.593082, f(x) = 0.017294
x = -0.797233,-1.594466, f(x) = 0.013835
x = -0.797786,-1.595573, f(x) = 0.011068
x = -0.798229,-1.596458, f(x) = 0.008854
x = -0.798583,-1.597167, f(x) = 0.007084
x = -0.798867,-1.597733, f(x) = 0.005667
x = -0.799093,-1.598187, f(x) = 0.004533
x = -0.799275,-1.598549, f(x) = 0.003627
x = -0.799420,-1.598839, f(x) = 0.002901
x = -0.799536,-1.599072, f(x) = 0.002321
x = -0.799629,-1.599257, f(x) = 0.001857
x = -0.799703,-1.599406, f(x) = 0.001486
x = -0.799762,-1.599525, f(x) = 0.001188
x = -0.799810,-1.599620, f(x) = 0.000951
x = -0.799848,-1.599696, f(x) = 0.000761
x = -0.799878,-1.599757, f(x) = 0.000608
x = -0.799903,-1.599805, f(x) = 0.000487
x = -0.799922,-1.599844, f(x) = 0.000389
x = -0.799938,-1.599875, f(x) = 0.000312
x = -0.799950,-1.599900, f(x) = 0.000249
x = -0.799960,-1.599920, f(x) = 0.000199
x = -0.799968,-1.599936, f(x) = 0.000159
x = -0.799974,-1.599949, f(x) = 0.000128
x = -0.799980,-1.599959, f(x) = 0.000102
x = -0.799984,-1.599967, f(x) = 0.000082
x = -0.799987,-1.599974, f(x) = 0.000065
x = -0.799990,-1.599979, f(x) = 0.000052
x = -0.799992,-1.599983, f(x) = 0.000042
x = -0.799993,-1.599987, f(x) = 0.000033
x = -0.799995,-1.599989, f(x) = 0.000027
x = -0.799996,-1.599991, f(x) = 0.000021
x = -0.799997,-1.599993, f(x) = 0.000017
x = -0.799997,-1.599995, f(x) = 0.000014
x = -0.799998,-1.599996, f(x) = 0.000011
x = -0.799998,-1.599997, f(x) = 0.000009
x = -0.799999,-1.599997, f(x) = 0.000007
x = -0.799999,-1.599998, f(x) = 0.000006
x = -0.799999,-1.599998, f(x) = 0.000004
x = -0.799999,-1.599999, f(x) = 0.000004
x = -0.799999,-1.599999, f(x) = 0.000003
x = -0.800000,-1.599999, f(x) = 0.000002
x = -0.800000,-1.599999, f(x) = 0.000002
x = -0.800000,-1.599999, f(x) = 0.000001
x = -0.800000,-1.600000, f(x) = 0.000001
x = -0.800000,-1.600000, f(x) = 0.000001
x = -0.800000,-1.600000, f(x) = 0.000001
x = -0.800000,-1.600000, f(x) = 0.000001
x = -0.800000,-1.600000, f(x) = 0.000000

细心的你可能会发现,f(x) = 0不止这一个解还可以是x = -2, -1。这是因为梯度下降法只是对当前所处的凹谷进行梯度下降求解,对于error函数并不代表只有一个f(x) = 0的凹谷。所以梯度下降法只能求得局部解,但不一定能求得全部的解。当然如果对于非常复杂的函数,能够求得局部解也是非常不错的。

tensorflow中的运用

通过上面的示例,相信对梯度下降也有了一个基本的认识。现在我们回到最开始的地方,在tensorflow中使用gradientDescent。

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
makefile复制代码import tensorflow as tf

# Model parameters
W = tf.Variable([.3], dtype=tf.float32)
b = tf.Variable([-.3], dtype=tf.float32)
# Model input and output
x = tf.placeholder(tf.float32)
linear_model = W*x + b
y = tf.placeholder(tf.float32)

# loss
loss = tf.reduce_sum(tf.square(linear_model - y)) # sum of the squares
# optimizer
optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)

# training data
x_train = [1, 2, 3, 4]
y_train = [0, -1, -2, -3]
# training loop
init = tf.global_variables_initializer()
sess = tf.Session()
sess.run(init) # reset values to wrong
for i in range(1000):
sess.run(train, {x: x_train, y: y_train})

# evaluate training accuracy
curr_W, curr_b, curr_loss = sess.run([W, b, loss], {x: x_train, y: y_train})
print("W: %s b: %s loss: %s"%(curr_W, curr_b, curr_loss))

上面的是tensorflow的官网示例,上面代码定义了函数linear_model = W * x + b,其中的error函数为linear_model - y。目的是对一组x_train与y_train进行简单的训练求解W与b。为了求得这一组数据的最优解,将每一组的error相加从而得到loss,最后再对loss进行梯度下降求解最优值。

1
2
abnf复制代码optimizer = tf.train.GradientDescentOptimizer(0.01)
train = optimizer.minimize(loss)

在这里rate为0.01,因为这个示例也是多维函数,所以也要用到偏导数来进行逐步向最优解靠近。

1
2
css复制代码for i in range(1000):
sess.run(train, {x: x_train, y: y_train})

最后使用梯度下降进行循环推导,下面给出一些推导过程中的相关结果

1
2
3
4
5
6
7
8
9
10
11
12
13
stylus复制代码W: [-0.21999997] b: [-0.456] loss: 4.01814
W: [-0.39679998] b: [-0.49552] loss: 1.81987
W: [-0.45961601] b: [-0.4965184] loss: 1.54482
W: [-0.48454273] b: [-0.48487374] loss: 1.48251
W: [-0.49684232] b: [-0.46917531] loss: 1.4444
W: [-0.50490189] b: [-0.45227283] loss: 1.4097
W: [-0.5115062] b: [-0.43511063] loss: 1.3761
....
....
....
W: [-0.99999678] b: [ 0.99999058] loss: 5.84635e-11
W: [-0.99999684] b: [ 0.9999907] loss: 5.77707e-11
W: [-0.9999969] b: [ 0.99999082] loss: 5.69997e-11

这里就不推理验证了,如果看了上面的梯度下降的前世今生,相信能够自主的推导出来。那么我们直接看最后的结果,可以估算为W = -1.0与b = 1.0,将他们带入上面的loss得到的结果为0.0,即误差损失值最小,所以W = -1.0与b = 1.0就是x_train与y_train这组数据的最优解。

好了,关于梯度下降的内容就到这了,希望能够帮助到你;如有不足之处欢迎来讨论,如果感觉这篇文章不错的话,可以关注我的博客,查看我的其它文章。

博客地址

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…391392393…399

开发者博客

3990 日志
1304 标签
RSS
© 2024 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%