Concurnas Logo(真不是C语言)
更好阅读体验请关注公众号:浮世Talk
导读
不是每天都会有一种新的JVM语言诞生,作为新的JVM语言,Concurnas具有现代的语法和功能,是开源的,并且内置了GPU计算,这为机器学习应用提供了可能。让我们一起来看看Concurnas能做什么吧!
Concurnas是什么,有什么特点?
Concurnas是一种全新的通用开源JVM编程语言,它是为构建并发、分布式和并行系统而设计的。Concurnas很容易学习,它提供了令人难以置信的性能和许多用于构建现代企业级计算机软件的功能。Concurnas区别于现有编程语言的地方在于,它提供了一种独特的、简化的方式来执行并发、分布式和并行计算。这些计算形式是现代软件工程中最具挑战性的,但有了Concurnas,它们就变得简单了。
利用Concurnas来构建软件,使开发人员能够轻松、可靠地实现当今多核计算机所提供的全部计算能力,使他们能够编写出更好的软件,提高工作效率。在这篇文章中,我们将通过构建一个交易应用程序的关键组件,来了解一下Concurnas的一些关键特性,这些特性使得Concurnas具有独特的功能。
Concurnas的主要目标
Concurnas的创建有五大目标。
-
提供一个动态类型化语言的语法,同时具有强类型化编译语言的类型安全和性能。具有可选的类型和可选的简明度,并在编译时进行错误检查。
-
为了使并发编程更容易,通过提出一种对非软件工程师来说比传统的线程和锁模型更直观的编程模型,使并发编程更容易。
-
让研究者和实践者都能提高工作效率,使一个想法从理想化到生产的过程中都能使用相同的语言和代码。
-
融合和支持现代软件工程的趋势,包括空安全、特征、模式匹配和一级公民支持依赖注入、分布式计算和GPU计算等现代软件工程的趋势。
-
通过支持特定领域语言的实现,并使其他语言能够嵌入到Concurnas代码中,从而促进未来编程语言的发展。
Concurnas简介
基本语法
首先让我们先说说一些基本的语法,,Concurnas是一种类型推理语言,有可选的类型。
1myInt = 99
2myDouble double = 99.9 //here we choose to be explicit about the type of myDouble
3myString = "hello " + " world!"//inferred as a String
4
5val cannotReassign = 3.2f
6cannotReassign = 7.6 //not ok, compilation error
7
8anArray = [1 2 3 4 5 6 7 8 9 10]
9aList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
10
11aMatrix = [1 2 3 ; 4 5 6 ; 7 8 9]
导入代码
由于Concurnas运行在JVM上,并且与Java兼容,因此我们可以访问现有的大量Java库、JDK库,当然也可以访问任何企业已经用JVM语言创建的软件(如Scala、Kotlin等)。我们可以通过熟悉的机制导入代码。
1from java.util import List
2import java.util.Set
功能
现在我们来介绍一下函数。Concurnas是一种可选择的简约语言,这意味着同一个函数可以实现不同程度的动词,以适应阅读代码的目标受众。因此,以下三种实现在功能上是相同的。
1def plus(a int, b int) int{//the most verbose form
2 return a + b
3}
4
5def plus(a int, b int) {//return type inferred as int
6 a + b//implicit return
7}
8
9def plus(a int, b int) => a + b
10//=> may be used where the function body consists of one line of code
这里有一个简单的功能,我们在后面的文章中会用到。
1def print(fmtString String, args Object...){//args is a vararg
2 System.out.println(String.format(fmtString, args))
3}
Concurnas中的函数参数可以被声明为vararg参数,也就是说,可以传递给它们的参数数量不固定。因此,以下的print函数的调用都是完全有效的。
1print("hello world!") //prints: hello world!
2print("hello world! %s %s %s", 1, 2, 3) //prints: hello world! 1 2 3
并发模型
Concurnas真正突出的地方在于它的并发模型。Concurnas并不向程序员暴露线程,相反,它有类似于线程的 "隔离单元",这些隔离单元是独立的代码,在运行时,通过多路复用到Concurnas运行的机器的底层硬件上,从而并发执行。在创建隔离单元时,我们只受制于所运行的机器的内存量。我们可以通过附加一个代码块或函数调用的叹号("!")操作符来创建一个隔离器:
1m1 String: = {"hello "}! //isolate with explicit returned type String:
2m2 = {"world!"}! //spawned isolate with implicit returned type String:
3msg = m1 + m2
4print(msg) //outputs: hello world!
5
上面,msg只有在创建m1和m2的隔离体完成并发执行并将其结果值写入各自的变量后,才会被计算出来。隔离体之间不允许通过特殊类型 "ref "来共享状态。ref只是一个普通的类型,后面加上一个冒号(":")。例如,上面我们已经看到了生成的隔离体返回String:类型的值。Refs可以被不同的 refs 在非确定性的基础上同时更新。
更多信息请查看:https://jaxenter.com/java-security-2020-169083.html
refs有一个特殊的特点,那就是可以观察到变化,然后我们可以编写代码来对这些变化做出反应,这在Concurnas中是通过onchange和every语句实现的:
1a int: = 10
2b int: = 10
3//^two refs
4
5oc1 := onchange(a, b){
6 plus(a, b)
7}
8
9ev1 := every(a, b){
10 plus(a, b)
11}
12
13oc2 <- plus(a, b)//shorthand for onchange
14ev2 <= plus(a, b)//shorthand for every
15
16//... other code
17
18a = 50//we change the value of a
19
20await(ev2;ev2 == 60)//wait for ev2 to be reactively set to 60
21//carry on with execution...
onchange
语句将在任何一个被监视的参考值被改变时,执行其块中定义的代码。因此,当上面的ref a被更新时,变量oc1、ev1、oc2和ev2将被更新为a和b之和,其中ev1和ev2之前持有a和b的初始值。
动手体验Concurnas
现在我们把基本的东西都弄清楚了,那我们就开始把它们放在一个应用中。假设我们在一个典型的投资银行或对冲基金中的金融交易系统。我们想快速地拼凑出一个反应式的系统,从市场上获取时间戳的资产价格,当价格满足一定条件时,执行一个动作。架构这样的系统最自然的方式就是作为一个反应式系统,它将利用语言中的一些特殊的并发相关特性。
创建第一个函数
首先我们创建一个函数,输出一些可重复性一致的伪随机时间序列数据,用于开发和测试:
1from java.util import Random
2from java.time import LocalDateTime
3
4class TSPoint(-dateTime LocalDateTime, -price double){
5//class with two fields having implicit getter functions automatically defined by prefixing them with -
6 override toString() => String.format("TSPoint(%S, %.2f)", dateTime, price)
7}
8
9def createData(seed = 1337){//seed is an optional parameter with a default value
10 rnd = new Random(seed)
11 startTime = LocalDateTime.\of(2020, 1, 1, 0, 0)//midnight 1st jan 2020
12 price = 100.
13
14 def rnd2dp(x double) => Math.round(x*100)/100. //nested function
15
16 ret = list()
17 for(sOffset in 0 to 60*60*24){//'x to y' - an integer range from 'x' to 'y'
18 time = startTime.plusSeconds(sOffset)
19 ret.add(TSPoint(time, price))
20 price += rnd2dp(rnd.nextGaussian()*0.01)
21 }
22
23 ret
24}
上面我们看到,我们首先定义了一个类TSPoint,其实例对象用来表示与我们的可交易资产相关联的时间序列的各个点。我们来检查一下我们的函数是否输出了一个合理的测试数据范围:
1timeseries = createData()//call our function with default random seed
2prices = t.price for t in timeseries//list comprehension
3
4min = max Double? = null//max and max may be null
5for(price in prices){
6 if(min == null or price < min){ min = price }elif(max == null or price > max){
7 max = price
8 }
9}
10
11print("min: %.2f max: %.2f", min, max)
12//outputs: min: 96.80 max: 101.81
当用默认的random seed
调用我们的函数时,我们可以看到它输出了一个合理的日内数据范围。"最小值:96.80 最大值:101.81"。101.81".
Nullable类型
现在是我们介绍Concurnas对nullable类型的支持的好时机。与现代编程语言的发展趋势一样,Concurnas(就像Kotlin和Swift一样)是一种null safe语言,也就是说,如果一个变量具有null的能力,必须明确声明为null,否则就认为它是非null。不可能给一个非空的类型赋一个空值,而是必须用问号("?")来明确声明该类型为可空的类型:
1aString String
2aString = null //this is a compile time error, aString cannot be null
3
4nullable String?
5nullable = null //this is ok
6
7len = nullable.length()//this is a compile time error as nullable might be null
我们在上面看到,调用nullable.length()会导致编译时出现错误,因为nullable可能是null,这将导致调用length()的函数会抛出可怕的NullPointerException。然而,Concurnas提供了一些操作符来帮助我们,这些操作符可以让我们更安全地处理nullable类型的变量,比如我们的nullable变量。它们如下所示:
1len1 Integer? = nullable?.length() //1. the safe call dot operator
2len2 int = (nullable?: "oops").length() //2. the elvis operator
3len3 int = nullable??.length() //3. the non null assertion operator
这些运算器的作用如下:
-
安全调用点运算符将返回null(因此,如果点的左手边是一个可解析为null的类型,则返回null(因此是一个nullable类型)。
-
elvis操作符与安全调用操作符类似,只是当左边是null时,操作符右边的指定值会被返回,而不是null(上面的例子中的"oops")。
-
非null断言操作符禁用了null保护,如果其左侧解析为null,则会抛出一个异常。
Concurnas还能够推断出可空类型的可空性范围。对于我们已经断言一个nullable变量为非null的区域(例如,在分支if语句中),我们可以将该变量当作不可nullable变量来使用:
1def returnsNullable() String? => null
2
3nullabeVar String? = returnsNullable()
4
5len int = if( nullabeVar <> null ){
6 nullabeVar.length()//ok because nullabeVar cannot be null here!
7}else{
8 -1
9}
10
11print(len)//prints: -1
这种对nullable类型的支持共同帮助我们编写出更可靠、更安全的程序。
触发交易操作
我们现在要继续构建我们的交易系统,我们要在跟踪的资产达到一定的价格时,我们要触发交易操作。当资产的价格高于101.71时,我们可以使用onchange块来触发这个过程:
1lastTick TSPoint://our asset timeseries
2
3onchange(lastTick){
4 if(lastTick.price > 101.71){
5 //perform trade here...
6 return
7 }
8}
注意上面的onchange
块中使用了返回语句,这确保了当交易条件被满足时,相关的交易操作只执行一次,之后onchange
块终止。如果没有返回语句,onchange
块将在交易条件满足时触发,直到最后一个Tick超出范围。
创建一个Ref类型
我们可以很容易地沿着前面的模式进行其他有趣的事情,比如说,我们可以建立一个日内滚动的高/低价位的Ref,低位的高/低价的Ref,如下所示:
1lowhigh (TSPoint, TSPoint)://lowhigh is a tuple type
2
3onchange(lastTick){
4 if(not lowhigh:isSet()){//using : allows us to call methods on refs themselves
5 lowhigh = (lastTick, lastTick)
6 }
7 else{
8 (prevlow, prevHigh) = lowhigh//tuple decomposition
9
10 if(lastTick.price < prevlow.price){ lowhigh = (lastTick, prevHigh) }elif(lastTick.price > prevHigh.price){
11 lowhigh = (prevlow, lastTick)
12 }
13 }
14}
构建一个面向对象的系统
现在,我们已经准备好了交易系统的交易和信息组件,我们准备用它们来建立一个面向对象的系统。为此,我们将利用Concurnas内置的依赖注入(DI)支持。DI是一种现代软件工程技术,它的使用使面向对象软件组件的推理、测试和重用变得更加容易。在Concurnas中,以对象提供者的形式为DI提供了一流的支持,这些对象提供者负责创建类的图,并将依赖注入到所提供的类的实例中。使用方法是可选的,但对于大型项目来说,它的使用会带来收益:
1trait OrderManager{ def doTrade(onTick TSPoint) void }
2trait InfoFeed{ def display(lowhigh (TSPoint, TSPoint):) }
3
4inject class TradingSystem(ordManager OrderManager, infoFeed InfoFeed){
5//'classes' marked as inject may have their dependencies injected
6 def watch(){
7 tickStream TSPoint:
8
9 lowhigh (TSPoint, TSPoint):
10
11 onchange(tickStream){
12 if(not lowhigh:isSet()){
13 lowhigh = (tickStream, tickStream)
14 }
15 else{
16 (prevlow, prevHigh) = lowhigh
17
18 if(tickStream.price < prevlow.price){ lowhigh = (tickStream, prevHigh) }elif(tickStream.price > prevHigh.price){
19 lowhigh = (prevlow, tickStream)
20 }
21 }
22 }
23 infoFeed.display(lowhigh:)//appending : indicates pass-by-ref semantics
24
25 onchange(tickStream){
26 if(tickStream.price > 101.71){
27 ordManager.doTrade(tickStream)
28 return
29 }
30 }
31 tickStream:
32 }
33}
34
35actor TestOrderManager ~ OrderManager{
36 result TSPoint:
37 def doTrade(onTick TSPoint) void {
38 result = onTick
39 }
40
41 def assertResult(expected String){
42 assert result.toString() == expected
43 }
44}
45
46actor TestInfoFeed ~ InfoFeed{
47 result (TSPoint, TSPoint):
48 def display(lowhigh (TSPoint, TSPoint):) void{
49 result := lowhigh//:= assigns the ref itself instead of the refs value
50 }
51
52 def assertResult(expected String){
53 await(result ; (""+result) == expected)
54 }
55}
56
57
58provider TSProviderTests{//this object provider performs dependency injection into instance objects of type `TradingSystem`
59 provide TradingSystem
60 single provide OrderManager => TestOrderManager()
61 single provide InfoFeed => TestInfoFeed()
62}
63
64
65//create our provider and create a TradingSystem instance:
66tsProvi = new TSProviderTests()
67ts = tsProvi.TradingSystem()
68
69//Populate the tickStream with our test data
70tickStream := ts.watch()
71for(tick in createData()){
72 tickStream = tick
73}
74
75//extract tests and check results are as expected...
76testOrdMng = tsProvi.OrderManager() as TestOrderManager
77testInfoFeed = tsProvi.InfoFeed() as TestInfoFeed
78
79//validation:
80testOrdMng.assertResult("TSPoint(2020-01-01T04:06:18, 101.71)")
81testInfoFeed.assertResult("(TSPoint(2020-01-01T19:59:10, 96.80), TSPoint(2020-01-01T10:10:05, 101.81))")
82
83print('All tests passed!')
上面介绍了Concurnas的另外两个有趣的特性:traits和actors。Concurnas中的traits受Scala中的traits启发,但是在这里,我们只是像使用接口一样使用它们(就像在Java等语言中看到的那样),因为它们指定了具体实现类必须提供的方法。在Concurnas中的actors是特殊的类,其实例对象可以在不同的隔离体之间共享,因为actors有自己的并发控制,这样可以避免多个隔离体同时与之交互,对其内部状态的非确定性变化。
更多信息请查看:https://jaxenter.com/openjdk-distro-167736.html
用传统的编程语言从头开始构建一个像上面这样的反应式系统,当然是一件很漫长的事情。从上面的Concurnas可以看出,这是一个直接的操作。
特定领域语言(DSL)
Concurnas的另一个很好的特点是它对域特定语言(DSL)的支持。表达式列表是一个很容易实现DSL的功能。表达式列表基本上可以让我们跳过在方法调用周围写点和括号。这导致了更自然的算法表达方式。我们可以在我们的示例交易系统中使用这个方法。下面是完全有效的Concurnas代码:
1order = buy 10e6 when GT 101.71
这可以通过以下方式创建我们的订单API来实现:
1enum BuySell{BUY, SELL}
2
3def buy(amount double) => Order(BuySell.BUY, amount)
4def sell(amount double) => Order(BuySell.SELL, amount)
5
6open class Trigger(price double)
7class GT(price double) < Trigger(price)
8class LT(price double) < Trigger(price) class Order(direction BuySell, amount Double){ trg Trigger? def when(trg Trigger) => this.trg = trg; this
9}
10
11order = buy 10e6 when GT 101.71
此外,虽然这里没有涉及到,但Concurnas支持运算器重载和扩展功能。
支持GPU计算
现在让我们简单地看一下Concurnas内置的GPU计算的支持。
GPU可以被认为是海量数据并行计算设备,是在大数据集上执行面向数学运算的理想选择。今天,一个典型的高端CPU(如AMD Ryzen Threadripper 3990X)可能有多达64个核心--可提供多达64个并发计算实例,而类似的GPU(如NVIDIA Titan RTX)则有4608个!而现代计算机的所有显卡都有4608个核心。现代计算机中的所有显卡都有一个GPU,实际上我们都可以使用超级计算机。一般来说,在GPU上实现的算法比它们的CPU实现快100倍(甚至更多!)。此外,从硬件和功耗的角度来看,在GPU上进行计算的相对成本远远低于CPU。
但是有一个问题………..GPU算法有一个相对深奥的实现,必须了解底层GPU硬件的细微差别才能获得最佳性能。传统上,C/C++的知识一直是一个要求。但有了Concurnas就不一样了。
Concurnas对GPU计算有一流的公民支持,这意味着支持直接内置到语言本身,使开发者能够利用GPU的力量。因此,我们可以写出Concurnas的代码,并在编译时进行语法和语义检查,大大简化了我们的构建过程,不需要学习C/C++,也不需要依赖代码的运行时检查。
GPU算法的实现入口点被称为gpukernel。我们来看看一个简单的矩阵乘法算法(线性代数的核心部分,在机器学习和金融领域大量使用):
1gpukernel 2 matMult(wA int, wB int, global in matA float[2], global in matB float[2], global out result float[2]) {
2 globalRow = get_global_id(0) // Row ID
3 globalCol = get_global_id(1) // Col ID
4
5 rescell = 0f;
6 for (k = 0; k < wA; ++k) {//matrices are flattened to vectors on the gpu...
7 rescell += matA[globalCol * wA + k] * matB[k * wB + globalRow];
8 }
9 // Write element to output matrix
10 result[globalCol * wA + globalRow] = rescell;
11}
这个GPU内核呈现了一个简洁但又天真的实现。这段代码可以通过优化来显著提高性能,比如说,通过使用本地内存来提高性能。不过目前来看,这已经足够好了。我们可以将其与我们传统的基于CPU的矩阵乘法算法作如下比较:
1def matMultCPU(A float[2], B float[2]) {
2 n = A[0].length
3 m = A.length
4 p = B[0].length
5 result = new float[m][p]
6
7 for(i = 0;i < m;i++){
8 for(j = 0;j < p;j++){
9 for(k = 0;k < n;k++){
10 result[i][j] += A[i][k] * B[k][j]
11 }
12 }
13 }
14 result
15}
核心矩阵乘法算法在GPU和CPU实现中是一样的。但是,有一些不同之处:GPU内核本身是在GPU上并行执行的,这些并行执行中唯一的区别是get_global_id调用返回的值--这些值是用来确定实例应该针对数据集中的哪个数据。此外,返回值需要传递到GPU内核中。
现在我们已经创建了GPU内核,我们就可以在GPU上执行了。这比标准的CPU计算要复杂得多,因为我们要建立一个异步管道,从数据复制到GPU,内核执行,结果复制到GPU,最后清理。幸运的是,Concurnas利用了ref模型的并发性,从而简化了这一过程,让我们可以:让GPU保持忙碌(从而最大限度地提高吞吐量),同时使用多个GPU,并在GPU执行的同时进行其他基于CPU的工作:
1def compareMulti(){
2 //we wish to perform the following on the GPU: matA * matB
3 //matA and matB are both matrices of type float
4 matA = [1f 2 3 ; 4f 5 6; 7f 8 9]
5 matB = [2f 6 6; 3f 5 2; 7f 4 3]
6
7 //use the first gpu available
8 gps = gpus.GPU()
9 deviceGrp = gps.getGPUDevices()[0]
10 device = deviceGrp.devices[0]
11
12 //allocate memory on gpu
13 inGPU1 = device.makeOffHeapArrayIn(float[2].class, 3, 3)
14 inGPU2 = device.makeOffHeapArrayIn(float[2].class, 3, 3)
15 result = device.makeOffHeapArrayOut(float[2].class, 3, 3)
16
17 //asynchronously copy input matrix from RAM to GPU
18 c1 := inGPU1.writeToBuffer(matA)
19 c2 := inGPU2.writeToBuffer(matB)
20
21 //create an executable kernel reference: inst
22 inst = matMult(3, 3, inGPU1, inGPU2, result)
23
24 //asynchronously execute with 3*3 => 9 'threads'
25 //if c1 and c2 have not already completed, wait for them
26 compute := device.exe(inst, [3 3], c1, c2)
27
28 //copy result matrix from GPU to RAM
29 //if compute has not already completed, wait for it
30 ret = result.readFromBuffer(compute)
31
32 //cleanup
33 del inGPU1, inGPU2, result
34 del c1, c2, compute
35 del deviceGrp, device
36 del inst
37
38 //print the result
39 print('result via GPU: ' + ret)
40 print('result via CPU: ' + matMultCPU(matA, matB))
41 //prints:
42 //result via GPU: [29.0 28.0 19.0 ; 65.0 73.0 52.0 ; 101.0 118.0 85.0]
43 //result via CPU: [29.0 28.0 19.0 ; 65.0 73.0 52.0 ; 101.0 118.0 85.0]
44}
结语
我们的文章到此结束,我们已经看了Concurnas的许多方面,这些都是它的独特之处,但还有许多现代程序员感兴趣的特性,比如分布式计算的一等公民支持、时态计算、向量化、语言扩展、非堆内存管理、lambdas和模式匹配等。
- THE END -
Concurnas:https://concurnas.com/
Github:https://github.com/Concurnas/Concurnas
原文:https://jaxenter.com/introducing-new-jvm-lanaguage-concurnas-167915.html