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