为什么编程语言不能自动管理同步/异步问题?


27

我没有找到很多有关此的资源:我想知道是否有可能/以同步方式编写异步代码的好主意。

例如,下面是一些JavaScript代码,该代码检索数据库中存储的用户数(异步操作):

getNbOfUsers(function (nbOfUsers) { console.log(nbOfUsers) });

能写这样的东西会很好:

const nbOfUsers = getNbOfUsers();
console.log(getNbOfUsers);

因此,编译器将自动处理等待响应的内容,然后执行 console.log。在必须将结果用于其他任何地方之前,它将始终等待异步操作完成。我们将减少回调承诺,异步/等待或其他方法的使用,并且不必担心操作结果是否立即可用。

nbOfUsers使用try / catch或类似Swift中的可选参数仍然可以管理错误(得到整数还是错误?)语言。

可能吗?这可能是一个可怕的主意/乌托邦……我不知道。


58
我不太了解您的问题。如果“总是等待异步操作”,则它不是异步操作,而是同步操作。你能澄清一下吗?也许给出您正在寻找的行为类型的规范?另外,“您对此有何想法”在Software Engineering上是没主题的。您需要在一个具体问题的背景下提出您的问题,该问题具有单个,明确,规范,客观的正确答案。
约尔格W¯¯米塔格

4
@JörgWMittag我想象一个假设的C#隐含awaitSA Task<T>将其转换为T
Caleth

6
您提出的建议是行不通的。由编译器决定是否要等待结果还是触发并忘记。或在后台运行,然后等待。为什么要限制自己呢?
畸形的

5
是的,这是一个糟糕的主意。只需使用async/即可await,这使执行的异步部分变得明确。
贝尔吉

5
当您说两件事同时发生时,就是说这些事情以任何顺序发生都是可以的。如果您的代码无法明确说明哪些重新排序不会违反您的代码期望,那么就无法使它们并发。
罗布

Answers:


65

异步/等待正是您建议的自动化管理,尽管有两个额外的关键字。他们为什么重要?除了向后兼容之外?

  • 如果没有明确的地方可以暂停和恢复协程,我们将需要一个类型系统来检测必须等待的值。许多编程语言没有这种类型系统。

  • 通过显式指定等待值,我们还可以将等待值作为第一类对象:promise。在编写高阶代码时,这可能非常有用。

  • 异步代码对语言的执行模型具有非常深远的影响,类似于该语言中是否存在异常。特别是,异步功能只能等待异步功能。这会影响所有调用功能!但是,如果我们在此依赖关系链的末尾将功能从非异步更改为异步怎么办?这将是一个向后不兼容的更改…除非所有函数都异步并且默认情况下等待每个函数调用。

    这是非常不希望的,因为它对性能的影响非常差。您将无法简单地返回便宜的价值。每个函数调用将变得更加昂贵。

异步很棒,但是某种隐式异步实际上是行不通的。

像Haskell这样的纯函数式语言有些逃脱,因为执行顺序在很大程度上是不确定的和不可观察的。或用不同的措辞表达:必须明确编码任何特定的操作顺序。对于现实世界的程序而言,这可能非常麻烦,尤其是那些非常适合异步代码的I / O繁重的程序。


2
您不一定需要类型系统。无需tyoe系统或语言支持,即可轻松实现ECMAScript,Smalltalk,Self,Newspeak,Io,Ioke,Seph等透明期货。在Smalltalk及其子代中,对象可以透明地更改其标识,在ECMAScript中,它可以透明地更改其形状。这就是使Futures透明化所需的全部,不需要异步的语言支持。
Mittag

6
@JörgWMittag我知道您在说什么以及它如何工作,但是没有类型系统的透明期货使得同时拥有一流期货变得相当困难,不是吗?我需要某种方式来选择是否要向未来发送消息或向未来的价值发送消息,最好是更好的选择,someValue ifItIsAFuture [self| self messageIWantToSend]因为这比与通用代码集成要困难得多。
amon

8
@amon“我可以将我的异步代码编写为promise和promise是monads。” 这里实际上并不需要单子。暴徒本质上只是诺言。由于Haskell中的几乎所有值均已装箱,因此Haskell中的几乎所有值均已实现。这就是为什么您可以par在纯Haskell代码中的任何地方扔很多东西并免费获得并行攻击。
DarthFennec

2
Async / await让我想起了monad的延续。
les

3
实际上,异常和异步/等待都是代数效应的实例。
Alex Reinking

21

您缺少的是异步操作的目的:它们使您可以利用等待时间!

如果您通过隐式并立即等待答复将异步操作(例如从服务器请求一些资源)转变为同步操作,则线程在等待时间上无法执行任何其他操作。如果服务器需要10毫秒来响应,则浪费了大约3000万个CPU周期。响应的等待时间成为请求的执行时间。

程序员发明异步操作的唯一原因是将固有长期运行的任务的延迟隐藏在其他有用的计算之后。如果您可以用有用的工作填补等待时间,则可以节省CPU时间。如果不能,那么异步操作就不会丢失任何内容。

因此,我建议您接受您的语言提供给您的异步操作。他们在那里是为了节省您的时间。


我想到的是一种功能语言,其中的操作不会阻塞,因此即使它具有同步语法,长时间运行的计算也不会阻塞线程
Cinn

6
@Cinn在问题中我没有发现,问题中的示例是Javascript,它没有此功能。然而,通常这是相当难的编译器找到并行有意义的机会,你描述:这样的功能的有意义的开发需要程序员明确去想什么,他们很长的潜伏期调用后摆正。如果使运行时足够智能,从而避免程序员遇到此要求,则您的运行时可能会吞噬性能,因为它需要在各个函数调用之间进行积极的并行化。
cmaster

2
所有计算机都以相同的速度等待。
鲍勃·贾维斯

2
@BobJarvis是的。但是他们在等待时间内可以完成多少工作……
cmaster

13

有的。

它们还不是主流(因为),因为异步是一个相对较新的功能,我们直到现在才对它感到满意,如果它甚至是一个很好的功能,或者如何以友好/可用/友好的方式向程序员展示表现力等 现有的异步功能大部分都固定在现有语言上,这需要一些不同的设计方法。

也就是说,在任何地方做这显然不是一个好主意。一个常见的失败是在循环中进行异步调用,从而有效地串行化其执行。异步调用是隐式的,可能会掩盖这种错误。另外,如果您支持从Task<T>(或与您的语言等效的)到的隐式强制转换T,这可能会增加类型检查器的复杂性/成本,并在不清楚程序员真正想要的是哪两个时会报告错误。

但是,这些不是不可克服的问题。如果您想支持这种行为,尽管会有所取舍,但几乎可以肯定。


1
我认为一个想法可能是将所有内容包装在异步函数中,同步任务将立即解决,我们得到一种统一的处理方式(编辑:@amon解释了为什么这是一个坏主意...)
Cinn

8
您能举几个“ 一些做 ”的例子吗?
贝尔吉

2
异步编程并不是什么新鲜事物,只是当今人们不得不更频繁地处理它。
立方

1
@Cubic-据我所知,这是一种语言功能。在此之前(笨拙的)用户级功能。
Telastyn的

12

有一些语言可以做到这一点。但是,实际上并不需要太多,因为可以使用现有语言功能轻松实现。

只要您有某种表达异步的方法,就可以将FuturesPromises纯粹作为一种库功能来实现,则不需要任何特殊的语言功能。只要您有一些表示透明代理,就可以将这两个功能放在一起,就可以使用透明期货

例如,在Smalltalk及其子孙中,一个对象可以更改其标识,它可以从字面上“成为”另一个对象(实际上,执行此操作的方法称为Object>>become:)。

想象一个长时间运行的计算返回一个Future<Int>。这Future<Int>具有所有的相同方法Int,除了具有不同的实现。Future<Int>+方法不会添加另一个数字并返回结果,而是返回一个Future<Int>包装计算的新值。等等等等。无法明智地通过返回a来实现的方法Future<Int>,将自动返回await结果,然后调用self become: result.,这将使当前正在执行的对象(self即,Future<Int>)从字面上变为result对象,即从现在开始,以前是a的对象引用Future<Int>是现在Int无处不在,对客户完全透明。

不需要与异步相关的特殊语言功能。


好的,但是如果两者都共享一些通用接口,那将出现问题Future<T>T而我使用该接口中的功能。是否应该become得到结果,然后再使用该功能?我在考虑类似相等运算符或字符串调试表示的事情。
阿蒙

我知道它没有添加任何功能,关键是我们有不同的语法来立即编写解析计算和长时间运行的计算,然后将结果用于其他目的使用相同的方式。我想知道我们是否可以使用一种语法来透明地处理这两种语法,使其更具可读性,因此程序员不必处理它。像do 一样a + b,这两个整数,无论​​a和b是否立即可用或以后都可用,我们只写a + b(可以做Int + Future<Int>
Cinn

@Cinn:是的,您可以使用透明期货来做到这一点,并且您不需要任何特殊的语言功能。您可以使用Smalltalk,Self,Newspeak,Us,Korz,Io,Ioke,Seph,ECMAScript中的已有功能来实现它,并且正如我刚刚阅读的Python一样。
约尔格W¯¯米塔格

3
@amon:透明期货的想法是,您不知道这是未来。从您的角度来看,两者之间没有公共接口Future<T>T因为从您的角度来看,没有Future<T>,只有一个T。现在,关于如何提高效率,在操作上应该阻塞还是不阻塞等方面,当然存在许多工程上的挑战,但这实际上与您是以语言还是库功能无关。透明度是OP在问题中提出的要求,我不会认为这很困难而且可能没有道理。
约尔格W¯¯米塔格

3
@Jörg似乎除了函数式语言外,其他任何问题都会出现,因为您无法知道何时在该模型中实际执行代码。通常说来,Haskell可以正常工作,但是我看不出它在更多的过程语言中如何工作(甚至在Haskell中,如果您关心性能,有时还必须强制执行并理解底层模型)。但是,这是一个有趣的想法。
Voo

7

他们做到了(嗯,其中大多数)。您要查找的功能称为线程

但是线程有其自身的问题:

  1. 因为代码可以在被暂停的任何一点,你不能永远假定事物不会改变“自己”。使用线程进行编程时,您会浪费大量时间来思考程序应如何处理变化。

    想象一个游戏服务器正在处理一个玩家对另一个玩家的攻击。像这样:

    if (playerInMeleeRange(attacker, victim)) {
        const damage = calculateAttackDamage(attacker, victim);
        if (victim.health <= damage) {
    
            // attacker gets whatever the victim was carrying as loot
            const loot = victim.getInventoryItems();
            attacker.addInventoryItems(loot);
            victim.removeInventoryItems(loot);
    
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon} and you die!");
            victim.setDead();
        } else {
            victim.health -= damage;
            victim.sendMessage("${attacker} hits you with a ${attacker.currentWeapon}!");
        }
        attacker.markAsKiller();
    }
    

    三个月后,一个玩家发现自己被杀死并准确地注销attacker.addInventoryItems,然后victim.removeInventoryItems失败了,他可以保留自己的物品,而攻击者也可以获得他的物品的副本。他这样做了几次,凭空创造了一百万吨的黄金,使游戏的经济崩溃。

    或者,攻击者可以在游戏向受害者发送消息时注销,并且他不会在头顶上看到“谋杀者”标签,因此他的下一个受害者不会逃脱。

  2. 因为代码可以在被暂停的任何一点,你需要到处使用锁操纵数据结构时。我在上面给出了一个示例,该示例在游戏中具有明显的后果,但它可能更微妙。考虑将项目添加到链接列表的开头:

    newItem.nextItem = list.firstItem;
    list.firstItem = newItem;
    

    如果您说线程只能在执行I / O时挂起,而在任何时候都不能挂起,那么这不是问题。但是,我敢肯定,您可以想象会有一个I / O操作的情况-例如日志记录:

    for (player = playerList.firstItem; player != null; player = item.nextPlayer) {
        debugLog("${item.name} is online, they get a gold star");
        // Oops! The player might've logged out while the log message was being written to disk, and now this will throw an exception and the remaining players won't get their gold stars.
        // Or the list might've been rearranged and some players might get two and some players might get none.
        player.addInventoryItem(InventoryItems.GoldStar);
    }
    
  3. 由于代码可以在被暂停的任何点,那么可能有很多状态的保存。系统通过为每个线程提供一个完全独立的堆栈来处理此问题。但是堆栈非常大,因此一个32位程序中最多只能有2000个线程。或者,您可以减小堆栈大小,但有使其变得过小的风险。


3

这里的许多答案都具有误导性,因为尽管问题实际上是在询问异步编程而不是非阻塞IO,但在这种特殊情况下,我认为我们不能在不讨论另一方的情况下进行讨论。

尽管异步编程本质上是异步的,但异步编程的存在主要是为了避免阻塞内核线程。Node.js通过回调或Promises 使用异步,以允许从事件循环中分派阻塞操作,而Java中的Netty通过回调或CompletableFutures 使用异步,以执行类似的操作。

但是,非阻塞代码不需要异步性。这取决于您的编程语言和运行时愿意为您做多少。

Go,Erlang和Haskell / GHC可以为您解决这个问题。您可以编写类似的内容,var response = http.get('example.com/test')并在等待响应的同时让它在后台释放内核线程。这是通过goroutines,Erlang进程完成的,或者forkIO在阻塞时放开了后台的内核线程,以使其在等待响应时可以做其他事情。

的确,语言无法真正为您处理异步性,但是某些抽象使您比其他人走得更远,例如无界延续或不对称协程。但是,异步代码的主要原因(阻止系统调用)绝对可以从开发人员那里抽象出来。

Node.js和Java支持异步非阻塞代码,而Go和Erlang支持同步非阻塞代码。它们都是具有不同权衡的有效方法。

我相当主观的论点是,那些反对代表开发人员管理非阻塞运行时的争论就像在上世纪90年代反对垃圾收集的争论一样。是的,这会产生成本(在这种情况下,主要是更多的内存),但它使开发和调试更加容易,并使代码库更健壮。

我个人认为,异步非阻塞代码应在将来保留给系统编程,而更多现代技术堆栈应迁移到同步非阻塞运行时以进行应用程序开发。


1
这是一个非常有趣的答案!但是我不确定我是否理解您在“同步”和“异步”非阻塞代码之间的区别。对我来说,同步非阻塞代码意味着像C函数这样的东西waitpid(..., WNOHANG),如果必须阻塞,它将失败。还是“同步”在这里意味着“没有程序员可见的回调/状态机/事件循环”?但是对于您的Go示例,我仍然必须通过从通道读取来显式等待goroutine的结果,不是吗?与JS / C#/ Python中的async / await相比,async的异步程度如何?
amon

1
我使用“异步”和“同步”来讨论向开发人员公开的编程模型,并使用“阻塞”和“非阻塞”来讨论对内核线程的阻塞,在此期间内核线程无法做任何有用的事情,即使有其他需要执行的计算,并且可以使用备用逻辑处理器。好的,goroutine可以等待结果而不会阻塞底层线程,但是如果需要,另一个goroutine可以通过通道与之通信。但是,goroutine不必直接使用通道来等待无阻塞套接字的读取。
路易·杰克曼

好的,我现在了解您的区别。尽管我更关心在协程之间管理数据和控制流,但您更关心从不阻塞主内核线程。我不确定Go或Haskell在这方面是否比C ++或Java有优势,因为它们也可以启动后台线程,这样做只需要一点点代码即可。
amon

@LouisJackman可以在您上一次关于异步非阻塞系统编程的声明中详细说明。异步非阻塞方法的优点是什么?
sunprophit

@sunprophit异步非阻塞只是编译器的转换(通常是异步/等待),而同步非阻塞则需要运行时支持,例如复杂堆栈操作的某种组合,在函数调用上插入屈服点(可能与内联冲突),跟踪“减少”(需要像BEAM这样的VM)等。像垃圾回收一样,它在减少运行时复杂性上进行权衡以简化易用性和健壮性。像C,C ++和Rust这样的系统语言由于它们的目标域而避免了像这样的较大的运行时功能,因此异步非阻塞在这里更有意义。
Louis Jackman

2

如果我没看错,您是在要求同步编程模型,但要有高性能的实现。如果这是正确的,那么我们已经以绿色线程或进程(例如Erlang或Haskell)的形式对我们可用。是的,这是个好主意,但是对现有语言的改造并不总是那么顺利。


2

我很欣赏这个问题,并且发现大多数答案都只是为了捍卫现状。从低级到高级语言,我们已经陷入困境了一段时间。显然,更高的语言将是一种语言,它不再关注语法(需要诸如await和async这样的显式关键字),而更多地涉及意图。(对查尔斯·西蒙尼(Charles Simonyi)表示谢意,但考虑了2019年和未来。

如果我告诉程序员,写一些代码来简单地从数据库中获取值,那么您可以放心地假设我的意思是,“而且,顺便说一句,不要挂起UI”和“不要引入其他难以掩盖错误的注意事项” ”。拥有下一代语言和工具的未来程序员,肯定会能够编写仅从一行代码中获取值然后从那里获取代码的代码。

最高级的语言是英语,并依靠任务执行者的能力来知道您真正想要做的事情。(考虑一下《星际迷航》中的计算机,或询问Alexa。)我们离这还很遥远,但距离越来越近,我的期望是语言/编译器可以更多地生成健壮的,有意的代码,而无需付出太多努力。需要人工智能。

一方面,像Scratch这样的较新的视觉语言可以做到这一点,并且不会被所有语法技术所困扰。当然,正在进行许多幕后工作,因此程序员不必担心。就是说,我不是用Scratch编写业务类软件,因此,像您一样,我也希望成熟的编程语言可以自动管理同步/异步问题。


1

您描述的问题有两个方面。

  • 从外部看,您正在编写的程序应该整体上表现为异步
  • 函数调用是否可能放弃控制在调用站点上应该可见。

有两种方法可以实现此目的,但基本上可以归结为

  1. 具有多个线程(处于某种抽象级别)
  2. 在语言级别具有多种功能,所有这些功能都这样命名foo(4, 7, bar, quux)

对于(1),我将分叉并运行多个进程,产生多个内核线程,以及将语言运行时级别线程调度到内核线程上的绿色线程实现。从问题的角度来看,它们是相同的。在这个世界上,从其线程的角度来看没有函数会放弃或失去控制。在本身线程有时不具有控制,有时甚至没有运行,但你不放弃在这个世界上你自己的线程控制。符合此模型的系统可能具有或不具有产生新线程或加入现有线程的能力。适合该模型的系统可能具有复制 Unix之类的线程的能力,也可能没有能力fork

(2)很有趣。为了做到公正,我们需要谈论介绍和消除形式。

我将展示为什么await不能以向后兼容的方式将隐式添加到Javascript之类的语言中。基本思想是,通过向用户公开承诺并在同步和异步上下文之间进行区分,Javascript泄漏了实现细节,从而阻止了统一处理同步和异步功能。还有一个事实,就是您不能await在异步函数主体之外进行承诺。这些设计选择与“使调用者看不到异步性”不兼容。

您可以使用lambda引入同步函数,并通过函数调用消除它。

同步功能介绍:

((x) => {return x + x;})

同步功能消除:

f(4)

((x) => {return x + x;})(4)

您可以将其与异步函数的引入和消除进行对比。

异步功能介绍

(async (x) => {return x + x;})

消除异步函数(注意:仅在async函数内部有效)

await (async (x) => {return x + x;})(4)

这里的根本问题是异步函数也是产生promise对象的同步函数

这是在node.js repl中同步调用异步函数的示例。

> (async (x) => {return x + x;})(4)
Promise { 8 }

假设您可以使用一种语言,甚至是一种动态类型的语言,其中异步和同步函数调用之间的差异在调用站点上是不可见的,在定义站点上也可能不可见。

采用这样的语言并将其简化为Javascript是可能的,您只需要有效地使所有函数异步即可。


1

使用Go语言goroutine和Go语言运行时,您可以像编写同步代码一样编写所有代码。如果某个操作在一个goroutine中阻塞,则在其他goroutine中继续执行。通过通道,您可以在Goroutine之间轻松地进行通信。这通常比Java语言中的回调或其他语言中的异步/等待更容易。有关一些示例和说明,请参见https://tour.golang.org/concurrency/1

此外,我对此没有经验,但是我听说Erlang具有类似的功能。

因此,是的,有诸如Go和Erlang这样的编程语言可以解决同步/异步问题,但是不幸的是,它们还不是很流行。随着这些语言的流行,也许它们提供的功能也将以其他语言实现。


我几乎从未使用过Go语言,但似乎您已明确声明go ...,所以它看起来与否类似await ...
Cinn

1
@Cinn实际上,不。您可以使用将任何调用作为goroutine置于其自己的光纤/绿线中go。而且几乎所有可能阻止的调用都由运行时异步完成,运行时仅在此期间切换到其他goroutine(协作式多任务)。您通过等待消息来等待。
Deduplicator

2
虽然Goroutines是一种并发性,但我不会将它们与async / await放在同一个存储桶中:不是协同协程,而是自动(并抢先!)调度的绿色线程。但这也不会使等待自动进行:Go等效于await从channel读取<- ch
amon

@amon据我所知,goroutine是在运行时通过本机线程(通常足以使真正的硬件并行度最大化)进行协作调度的,而这些则是由OS抢先调度的。
Deduplicator

OP要求“能够以同步方式编写异步代码”。如前所述,使用goroutine和go运行时,您可以完全做到这一点。您不必担心线程的细节,只需编写阻塞的读写操作,就好像代码是同步的一样,其他goroutine(如果有)将继续运行。您甚至不必“等待”或阅读频道即可获得此好处。因此,我认为Go是最符合OP需求的编程语言。

1

有一个非常重要的方面尚未提出:重新进入。如果您在异步调用期间运行了其他任何代码(即:事件循环)(如果不需要,那么为什么甚至还需要异步?),那么这些代码会影响程序状态。您无法隐藏来自调用者的异步调用,因为调用者可能依赖于程序状态的某些部分以在其函数调用期间保持不受影响。例:

function foo( obj ) {
    obj.x = 2;
    bar();
    log( "obj.x equals 2: " + obj.x );
}

如果bar()是异步函数,则可能obj.x在执行过程中对其进行更改。如果没有任何暗示bar是异步的并且可能产生效果的提示,这将是非常意外的。唯一的选择是怀疑每个可能的函数/方法都是异步的,并在每次函数调用后重新获取并重新检查任何非本地状态。这很容易产生细微的错误,如果通过函数获取某些非本地状态,则甚至根本不可能。因此,程序员需要意识到哪些功能有可能以意想不到的方式改变程序状态:

async function foo( obj ) {
    obj.x = 2;
    await bar();
    log( "obj.x equals 2: " + obj.x );
}

现在可以清楚地看到,它bar()是一个异步函数,处理它的正确方法是重新检查之后的期望值obj.x并处理可能发生的任何更改。

正如其他答案所指出的那样,像Haskell这样的纯函数式语言可以完全避免任何共享/全局状态,从而完全摆脱了这种影响。我对函数式语言没有太多的经验,所以我可能会对函数式语言有偏见,但是我不认为在编写较大的应用程序时缺少全局状态是没有优势的。


0

对于您在问题中使用的Java脚本,有一点要注意:Java脚本是单线程的,并且只要没有异步调用,就可以保证执行顺序。

因此,如果您有一个像您这样的序列:

const nbOfUsers = getNbOfUsers();

您保证在此期间不会执行其他任何操作。无需锁或任何类似物品。

但是,如果getNbOfUsers是异步的,则:

const nbOfUsers = await getNbOfUsers();

意味着在getNbOfUsers运行时,执行收益,以及其他代码可能在两者之间运行。反过来,这可能需要进行一些锁定,具体取决于您正在执行的操作。

因此,最好知道呼叫何时是异步的,以及何时不是异步的,因为在某些情况下,如果呼叫是同步的,则将不需要采取其他预防措施。


没错,问题中的第二个代码无效,好像getNbOfUsers()返回了Promise。但这正是我的问题的重点,为什么我们需要显式地将其编写为异步的,编译器可以检测到它并以不同的方式自动处理它。
Cinn

@Cinn这不是我的意思。我的观点是,在异步调用执行期间,执行流可能会到达代码的其他部分,而对于同步调用则是不可能的。就像有多个线程正在运行,但不知道它。这可能会导致大问题(通常很难检测和重现)。
jcaron

-4

std::async从C ++ 11开始,这在C ++中可用。

模板函数async异步运行函数f(可能在单独的线程中,该线程可能是线程池的一部分),并返回std :: future,该变量最终将保存该函数调用的结果。

对于C ++ 20协程,可以使用:


5
这似乎无法回答问题。根据您的链接:“协程TS为我们提供了什么?三个新的语言关键字:co_await,co_yield和co_return” ...但是问题是为什么我们首先需要一个await(或co_await在这种情况下)关键字?
阿图罗·托雷斯·桑切斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.