在一个公司,尝试很难得到的东西以正确的方式完成工作,它只是悲哀地有时会发现一些在这篇文章中我们的代码库共享代码。 我们实际上是在尝试纠正以下一些问题,因此我从并发开始,为新人们( 总是有新人们 )分享这一点。 即使这些示例都是基于Java的,但完全相同的想法也适用于您选择的编程语言。
以下代码示例仅是为了说明问题,我们的生产代码也有类似的一般问题。
假设我们要从URL下载内容。 我们可以简单地使用以下功能。 此代码同步运行。
下面的函数可以用具有同步执行功能的任何其他东西替代,例如访问数据库或将某些内容保存到文件系统。 基于此,可悲的是,在我们的代码库中发现了以下内容。
如我们所见,这确实是下载内容的一种非常糟糕的方式。 它绝对不会异步执行任何操作。 当我们查看函数getContenAsyncBadWay
的签名时,甚至更糟,因为所有人都表示它将异步运行。
我们可以通过执行以下代码来验证它是否正在串行运行。
输出将是。
hello
...
done
waiting
正如我们所观察到的,即使我们不认为,所有事情都是线性发生的。
我们正在尝试训练我们的一些资源(人员)不要这样做,而写以下内容。
我们正在努力避免这些愚蠢的错误。
注意,我们将.getContent
调用包装在supplyAsync
。 我们可以通过执行与之前相似的代码来再次对其进行测试。
哪个可以正确输出我们的期望。
hello
waiting
...
done
请注意输出中的顺序,它确实表明一切正常。
早期阻止和链接
我们在代码库中发现的另一个问题是,我们阻塞了异步操作以访问其结果,在大多数情况下,这是完全不必要的。
以下示例显示了我们遇到的相同问题。
这是非常糟糕的,因为您可以认为操作是异步发生的,但这并不比 以同步方式 调用 .getContent
更好 ,因为我们在调用之后立即阻塞。
其结果将是。
hello
!=293
"=25
#=406
$=33
&=5
'=79
(=21
)=117
*=117
...
waiting
同样,此代码被CompletableStage
污染,即使使用正确的实现.getContentAsyncBetterWay
,它也仍然非常同步。
相反,我们应该执行以下操作。
注意我们如何将计算阶段链接到.countWords
,然后打印结果。
运行此版本将为我们提供以下内容。
hello
waiting
!=293
"=25
#=406
$=33
&=5
'=79
(=21
)=117
*=117
+=1
,=24
...
再一次注意输出顺序,该顺序指示事物的运行方式。
由于某些原因,有些人很难理解这些概念。
有时您绝对需要结果
为了继续我们的示例,让我们添加以下支持功能。
现在,我们可以运行先前示例的修改版本。
在这里,尽管我们.join
ING,我们正在推迟,直到快结束的时候,所以我们允许其他操作执行, AsyncOps.sumAsync
在这种情况下。 该程序的输出将是。
hello
waiting
Summing -1974445143 + -1585876417
!=293
"=25
#=406
$=33
&=5
'=79
(=21
)=117
*=117
...
但是,使用同一程序的以下版本,我们仍然可以做得更好。
在这种情况下, .forEach
将在计算值之后发生, .join
将等待它。 看起来似乎相同,但要考虑很大的计算值。 在第一个版本中,只有在打印(或处理)之前,什么都不会执行。 在此版本中,对计算值的处理实际上是异步运行的。
多个异步操作正在运行
在许多情况下,我们需要同时运行两个或多个异步操作,并且在许多情况下它们彼此独立。 由于我们的代码库受到.join
污染,因此我们无法在应有的情况下处理多处理问题,因此编写了类似以下内容的代码。
重要的是不要使用.join
因为在大多数情况下,我们希望在此之后运行其他操作。 如果这些操作是同步的,则使用CompletionStage
提供的单子操作。 如果操作是不同的,独立的计算,则对第一个进行阻塞将阻止第二个运行,这会大大降低性能。
尝试使用Async / Await编写更好的并发代码
这是C#和.NET多年来使用的范例,由于大多数代码看起来非常串行,因此减少了考虑并发的时间。 但是,该模型在大多数情况下提高了速度,同时增加了所执行操作的清晰度。
让我们看一个示例,它可以帮助我们了解其工作原理。
假设我们要运行执行以下操作的异步操作:
- 从URL下载内容
- 打印内容
- 计算一个字符出现在下载内容中的次数。
- 打印柜台
- 归还柜台
首先,让我们看看如何使用经典的Java Async API来完成此操作。
我们可能会争辩说这已经足够好了,但是还有另一种使用异步等待的方法胜过这种方法。
很少注意的事情。 首先,此功能是完全异步的,内部没有任何阻塞。 其次,对await
的函数调用将控制流返回给函数getContentTotalSizeFor
的调用者,直到完成等待函数为止。 每次此函数等待其依赖项工作完成时,这将允许执行更多工作。 而且,该代码非常易于理解和编写,并且不需要考虑它。
我们可以使用以下方法进行测试。
输出将是。
hello
waiting
...
59
请注意,即使在getContentTotalSizeFor
内部await
,它也不会阻塞。 第一次单击await
,控件返回到调用方(在本例中为main),因此它将继续执行工作( System.out.println("waiting");
)。 一旦函数等待结束, getContentTotalSizeFor
将从原处恢复执行。
让我们回过头来,在代码上放一些数字以跟踪执行情况。
请注意,每次await
都完成后,控制将从该点开始恢复。 如果找到一个新的await
,控件将返回到调用方。 同时,所有这些都是异步的,并且不会阻止调用方函数的工作。
在编译时, async/await
代码将转换为类似的字节代码,就像我们在第一个示例中编写常规CompletableFuture
代码一样。
如果您现在还不确信,那么让我们看一些更有趣的例子。
要么
下面是一个更复杂的示例。
或者我们可以将其编写如下。
注意, squares
和cubes
的计算是两个独立的计算,它们是同时异步运行的。
结论
首先,我们需要充分理解代码库中当前存在的问题,以便我们与编写该代码的人员一起工作并解决问题。 这会影响不同级别的性能。
另外,对于那些不熟悉并发的人,请尽量不要阻塞异步操作,在大多数情况下,阻塞是不必要的,会降低性能,并且可能有一种完全不阻塞的方法。
最后,通过使用更高的抽象来管理并发性,正如我们通过使用async/await
所看到的那样,我们可以编写更好的并发代码而没有太多麻烦。 并发自然很难,但是有一些方法可以使并发变得更简单,充分利用并使用它们。
如果您喜欢这个故事并认为其他人可以从中受益,请单击“喜欢”按钮,其他人也可以看到它。
From: https://hackernoon.com/a-sad-story-about-concurrency-346990a9a3fe