使用并行调用时需要了解的设计原则

多线程设计原则 (MULTITHREADING DESIGN PRINCIPLES)

目标 (GOAL)

I only consider the case where every action passed to Invoke has no dependency on another action.

我仅考虑传递给Invoke的每个动作都不依赖于另一个动作的情况。

I assume that the reader knows about multithreading, thread synchronization and lock expression. I strongly recommend you to read “C# in a Nutshell”, chapter 14, 22. Also see http://www.albahari.com/threading/

我假设读者了解多线程,线程同步和锁表达式。 我强烈建议您阅读“ C#概述”,第14、22章。另请参见http://www.albahari.com/threading/

使评论可维护 (Make reviews maintainable)

All actions you pass to Parallel.Invoke can also call Parallel.Invoke. Now Imagine that an argument is passed to an action of the top level Invoke, which is passed on to all actions in the following Invoke methods. These can also call Parallel.Invoke and pass the argument and so on. This will create a tree of methods, starting with the action of the top level Invoke, where the argument is used.

您传递给Parallel.Invoke的所有操作也可以调用Parallel.Invoke。 现在,假设将参数传递给顶级Invoke的操作,该操作又传递给以下Invoke方法中的所有操作。 这些还可以调用Parallel.Invoke并传递参数,依此类推。 这将创建一个方法树,从使用该参数的顶级Invoke的动作开始。

To find out if the argument is accessed in a way that a race condition or deadlock could occur one needs to investigate all the places where the argument is used. But not only arguments need to be checked but also access to member variables (instance or static) in every method of the call chains.

为了弄清楚是否以一种可能发生竞争状况或死锁的方式访问该参数,需要调查使用该参数的所有位置。 但是,不仅需要检查参数,还需要在调用链的每个方法中访问成员变量(实例或静态)。

This is time consuming and error-prone. On the other hand, if one knows that the shared data(arguments and member variables) cannot cause a race condition, then there is no need to investigate every usage of it. If the actions only call static methods (see Call only static methods from actions) then, it is also easily visible which data is shared.

这既费时又容易出错。 另一方面,如果知道共享数据(参数和成员变量)不能导致争用条件,则无需研究它的每种用法。 如果操作仅调用静态方法(请参阅仅从操作调用静态方法),则还可以轻松看到共享了哪些数据。

For better illustration here is a “call graph” example. I only left calls to methods AccomplishListHandler and to methods which call Parallel.Invoke:

为了更好地说明,这里是一个“调用图”示例。 我只留下了对方法AccomplishListHandler和对调用Parallel.Invoke的方法的调用:

Image for post
simplified call graph
简化调用图

The highlighted lines represent actions called from Parallel.Invoke. As one can see there are three levels where Parallel.Invoke is called in one call chain.

突出显示的行表示从Parallel.Invoke调用的操作。 可以看到,在一个调用链中调用Parallel.Invoke的过程分为三个级别。

To prevent a race conditions when using the argument, one can apply one of the following:

为了防止在使用参数时出现争用条件,可以应用以下方法之一:

  • Avoid shared state by providing copies

    通过提供副本来避免共享状态
  • Immutable objects

    不变的对象
  • Objects with synchronizes access

    具有同步访问权限的对象

Also by only calling static methods — the actions itself and within the actions, one ensures that there is no chance of sharing members variables.

同样,通过仅调用静态方法(动作本身以及在动作内部),可以确保没有机会共享成员变量。

比赛条件示例 (Example of a race conditions)

A race condition can happen when at least two threads access the same data. For example:

当至少两个线程访问相同的数据时,可能会发生竞争状态。 例如:

Two threads could call Increase() at the same time. Both threads read the same Token value. The token will only be increased by one, although it should be increased by two.

两个线程可以同时调用Growth()。 两个线程读取相同的令牌值。 令牌只会增加1,尽管应该增加2。

防止并发访问共享数据 (Preventing concurrent access to shared data)

为每个线程使用数据副本 (Use copies of data for each thread)

Have a look at this example:

看一下这个例子:

It is undetermined if 1 or 100 is printed. The solution is quite simple:

不确定是打印1还是100。 解决方案非常简单:

It is not so obvious if the variables are modified if the actions pass them to other methods. For example:

如果将动作将变量传递给其他方法,则是否修改变量并不是很明显。 例如:

We don’t know what Compute does with value without looking into it. However, in this case it is not necessary because value is a ValueType. If the parameter is neither ref not out, then a copy of it will be put on the stack before calling the method. That is every Compute method has its own copy of value on the stack. They cannot modify the variable itself.

不了解价值,我们不知道Compute对价值有何作用。 但是,在这种情况下,因为value是ValueType,所以没有必要。 如果该参数都不是ref out,则在调用该方法之前将其副本放入堆栈中。 那就是每个Compute方法在堆栈上都有自己的值副本。 他们不能修改变量本身。

Here is an example using ref parameters:

这是使用ref参数的示例:

It is also obvious that the state of value is indeterminate after Invoke. That is, it can either hold a value assigned in the first Compute or second Compute method. The solution is again:

同样很明显,在调用之后,值的状态是不确定的。 也就是说,它可以保存在第一种Compute方法或第二种Compute方法中分配的值。 解决方案再次是:

If the parameter of Compute is a reference type then one cannot recognize anymore if there is a risk of a race condition. One would have to look into the methods which are invoked from the actions.

如果Compute的参数是一种引用类型,那么如果存在竞争状况的风险,则无法识别。 人们将不得不研究从动作中调用的方法。

If the parameter is ref our out then it is obvious that there is a risk.

如果参数是ref our out,那么很明显存在风险。

Here again one can provide copies. In which case there is no need to look into Compute. Be careful, however, that the complete object is copied and not only the reference.

这里又可以提供副本。 在这种情况下,无需考虑计算。 但是请注意,复制的是完整对象,而不仅仅是引用。

The Array class implements ICloneable, which creates a shallow copy. This works fine if the array contains ValueType. However, with reference types, one needs to make sure to create a “deep copy”.

Array类实现ICloneable,它创建一个浅表副本。 如果数组包含ValueType,则可以正常工作。 但是,对于引用类型,需要确保创建“深层副本”。

In the previous examples, the actions called the same Compute method. Often, they will call different methods. If it is not clear if the variable is modified by looking at Parallel.Invoke and the code around it, then one needs to investigate all these methods.

在前面的示例中,这些操作称为相同的Compute方法。 通常,他们会调用不同的方法。 如果不清楚是否通过查看Parallel.Invoke及其周围的代码来修改变量,则需要研究所有这些方法。

使用不可变的对象 (Using immutable objects)

Imagine one has an instance of a reference type which is used in multiple threads. We could then create deep copies of the object and pass them to the different methods. Another way is to provide an object, which does not allow modifying its members:

想象一下,有一个引用类型的实例在多个线程中使用。 然后,我们可以创建对象的深层副本,并将它们传递给不同的方法。 另一种方法是提供一个对象,该对象不允许修改其成员:

Here both methods, DoSomeThings and DoSomeOtherThings can access the same object. However, ImmutableAddress does not allow that its state can be modified. Therefore there is no risk of a race condition.

在这里,DoSomeThings和DoSomeOtherThings这两种方法都可以访问同一对象。 但是,ImmutableAddress不允许修改其状态。 因此,没有比赛条件的风险。

带锁同步访问 (Synchronize access with lock)

This mechanism will be primarily used to ensure that interface implementations are thread-safe. One must pay attention that all code which accesses the same shared member uses the same lock.

该机制将主要用于确保接口实现是线程安全的。 必须注意,访问同一共享成员的所有代码都使用相同的锁。

Even if there are simple getter and setter one needs to use locks. The reason for this is that locks create memory barriers around the code and prevent reordering and caching of code. See http://www.albahari.com/threading/part4.aspx

即使有简单的getter和setter方法,也需要使用锁。 这样做的原因是,锁在代码周围创建了内存障碍,并阻止了代码的重新排序和缓存。 参见http://www.albahari.com/threading/part4.aspx

The locks prevent race conditions regarding the shared members street and city. The lock in StreatAndCity additionally ensures that once the thread entered the thread the street and city cannot change. However, there is a catch. See ‘Using immutable objects to limit locking’.

锁可以防止有关共享成员街道和城市的比赛条件。 此外,StreatAndCity中的锁定还确保一旦线程进入线程,街道和城市就不会改变。 但是,有一个陷阱。 请参阅“ 使用不可变对象限制锁定 ”。

使用不可变对象限制锁定 (Using immutable objcets to limit locking)

Let’s extend on the previous example. Although access is synchronized within Address, the instance can still have an invalid state. Let’s assume that there are two valid addresses:

让我们继续前面的示例。 尽管访问是在地址内同步的,但实例仍可以具有无效状态。 假设有两个有效地址:

  • Saint-Catherine Street, Montreal

    蒙特利尔圣凯瑟琳街
  • King street, Toronto

    多伦多国王街

Initially the address instance has the value ‘Saint-Catherine, 1’. Thread one starts to set the new address and first sets Street and then City. Thread two reads StreatAndCity. Now it can happen that thread two obtains ‘King Street, Toronto’, which is not a valid address.

最初,该地址实例的值为'Saint-Catherine,1'。 线程一开始设置新地址,首先设置街道,然后设置城市。 线程二读取StreatAndCity。 现在可能发生了,第二个线程获得了“多伦多国王街”,这不是一个有效的地址。

To prevent this both threads need to use the same lock for setting and reading the values:

为了防止这种情况,两个线程都需要使用相同的锁来设置和读取值:

The problem here is that one needs to use the same lock. In this case it is only in two methods but there could be more. The implementer needs to know which lock object to use in all the different places. The type itself does not indicate which object to use. One the other hand the Address type could offer a sync object:

这里的问题是,一个人需要使用相同的锁。 在这种情况下,只有两种方法,但可能还会更多。 实现者需要知道在所有不同位置使用哪个锁对象。 类型本身并不指示要使用哪个对象。 另一方面,“地址”类型可以提供一个同步对象:

However, nothing prevents other code from using IAddress.SynchObject as well. The more often the same lock object is used the higher is the risk of running into a deadlock and slowing down execution.

但是,没有什么可以阻止其他代码也使用IAddress.SynchObject。 使用同一锁定对象的次数越多,陷入死锁并减慢执行速度的风险就越高。

To avoid this additional locking one could remove Address.StreetAndCity and leave it to the client of this object to construct a string containing the street and city. However, one could also ensure that street and city are only set together. In the following example this is done using an immutable object. The access to that object must be locked as it is shared. One benefit shows in StreetAndCity. The lock is used to assign the immutable object to a local variable. Afterwards one can process the data without holding the lock. If somebody called SetStreetCity then this would not affect the local copy.

为了避免这种额外的锁定,可以删除Address.StreetAndCity并将其留给此对象的客户端,以构造一个包含街道和城市的字符串。 但是,还可以确保仅将街道和城市设置在一起。 在下面的示例中,这是使用不可变对象完成的。 由于该对象是共享的,因此必须对其进行锁定。 StreetAndCity有一项好处。 该锁用于将不可变对象分配给局部变量。 之后,无需持有锁就可以处理数据。 如果有人叫SetStreetCity,那么这不会影响本地副本。

仅从动作中调用静态方法 (Call only static methods from actions)

In the previous examples the methods called from the actions have parameters. Even without parameters there is a risk that data is shared by threads:

在前面的示例中,从操作调用的方法具有参数。 即使没有参数,也存在线程共享数据的风险:

If both methods are not static then they have access to all instance fields. Every field can potentially be shared. Now imagine a relative large class with 20 fields. Also imagine that the methods call a couple of other non-static methods from the same class, which in turn could also call instance methods. For example, DoSomeThings calls directly or indirectly 10 methods — let’s call them DoSomeThings methods. DoSomeOtherThings calls directly or indirectly 10 methods — let’s call them DoSomeOtherThings methods. All DoSomeThings methods need to be investigated if they use instance fields which are also used by DoSomeOtherThings methods. Additionally any other instance methods which are not used in Parallel.Invoke needed to be checked as well, because the object which invokes Parallel.Invoke could be accessed my multiple threads.

如果这两种方法都不是静态的,则它们可以访问所有实例字段。 每个字段都可以共享。 现在想象一下一个有20个字段的相对较大的类。 还要想象一下,这些方法调用了同一类中的其他两个非静态方法,这些非静态方法又可以调用实例方法。 例如,DoSomeThings直接或间接调用10个方法-我们称它们为DoSomeThings方法。 DoSomeOtherThings直接或间接调用10个方法-我们称它们为DoSomeOtherThings方法。 如果所有DoSomeThings方法都使用实例字段(DoSomeOtherThings方法也使用这些实例字段),则需要进行调查。 另外,还需要检查Parallel.Invoke中未使用的任何其他实例方法,因为调用Parallel.Invoke的对象可以在我的多个线程中访问。

This simply is not maintainable.

这根本无法维持。

By making the methods static, one prevents that they can access instance fields. There might still be shared static fields. But in general there are a lot less static fields than instance fields. Access to static fields can be synchronized using locks or immutable objects (see Using immutable objects to limit locking).

通过使这些方法静态,可以防止它们可以访问实例字段。 可能仍然存在共享的静态字段。 但是通常而言,静态字段比实例字段少得多。 可以使用锁或不可变对象来同步对静态字段的访问 (请参阅使用不可变对象来限制锁定)。

The effort for reviewing the code decreases significantly, because one only needs to check the use of static fields.

审查代码的工作量大大减少,因为只需要检查静态字段的使用即可。

Another aspect is that by using static methods, one must create parameters for all instance fields which are otherwise directly used if the methods were not static. Consider the following example:

另一个方面是,通过使用静态方法,必须为所有实例字段创建参数,如果方法不是静态的,则可以直接使用这些参数。 考虑以下示例:

Because the methods are static, the arguments for BBB and CCC must be passed from one method to the next. That is AAA receives the arguments which are used in AAA, BBB and CCC. If we use a static method in Parallel.Invoke, then we see at a glance all the values which are used. We also see if one argument from one static method is used in another:

因为方法是静态的,所以BBB和CCC的参数必须从一种方法传递到另一种方法。 即AAA接收在AAA,BBB和CCC中使用的参数。 如果我们在Parallel.Invoke中使用静态方法,那么我们一眼就能看到所有使用的值。 我们还查看了另一种静态方法是否使用了一个静态方法的一个参数:

Here we see that age is used in AAA an EEE and could cause a race condition. However, also name and male could be unsafe, if they shared, for example if they are instance or static fields:

在这里,我们看到AAA EEE中使用了年龄,并可能导致比赛条件。 但是,如果name和male共享(例如,它们是实例或静态字段),则它们也可能是不安全的:

If Person is accessed by more than one thread then FullName, Male and Age must be thread-safe. The reason is that while Parallel.Invoke is running A in a new thread, another thread could modify the properties of Person. This can be prevented by applying the principles described in ‘4 Preventing concurrent access to shared data’.

如果通过多个线程访问“人员”,则“全名”,“男性”和“年龄”必须是线程安全的。 原因是,当Parallel.Invoke在新线程中运行A时,另一个线程可能会修改Person的属性。 可以通过应用“ 4防止并发访问共享数据”中描述的原理来防止这种情况。

If all the arguments, which are passed to the static methods, are thread-safe then one does not need to investigate the methods (including the complete call chain), with respect to race conditions regarding these arguments.

如果传递给静态方法的所有参数都是线程安全的,则无需考虑这些参数的竞争条件(包括完整的调用链)。

For the sake of maintainability, ensure that these arguments are thread-safe.

为了可维护性,请确保这些参数是线程安全的

接口实现 (Interface implementations)

Document thread-safety

文件线程安全

In order to make code revision easier, a thread-save implementation needs to be documented as such:

为了使代码修订更容易,需要将线程保存实现记录如下:

Now, when one wants to revise the parallel code, one only needs to navigate to the types of the interface arguments. For example, the following code is after refactoring the parallel code to use static methods:

现在,当要修改并行代码时,只需导航到接口参数的类型即可。 例如,以下代码在重构并行代码以使用静态方法之后:

Many of the parameters are interface types and used in both methods. The implementations are not known in the containing class, because these interfaces are injected in the constructor. When doing the revision it is sufficient to locate the type definitions and verify that they are documented to be thread-safe.

许多参数是接口类型,并且在两种方法中都使用。 这些实现在包含类中是未知的,因为这些接口已注入构造函数中。 进行修订时,只需找到类型定义并验证它们是否具有线程安全性就足够了。

Creating copies of interface implementation

创建接口实现的副本

To make interface implementations thread-safe one needs to synchronize access to shared variables as explained in ‘Synchronize access with lock’ and ‘Using immutable objects to limit locking’.

为了使接口实现线程安全,需要同步对共享变量的访问,如“与锁同步访问”和“使用不可变对象以限制锁定”中所述。

On the other hand one could also create copies.

另一方面,也可以创建副本。

Usually interface implementations are injected into objects in their constructors. That is, the actual implementation types are not known. If one has access to the IUnityContainer then one could create a new instance:

通常,接口实现被注入其构造函数中的对象中。 即,实际的实现类型未知。 如果可以访问IUnityContainer,则可以创建一个新实例:

However, this will only create a new instance if the interface was registered using the TransientLifetimeManager, which is the default when no other life time manager is specified. One must also take into account that the interface may also receive injected interfaces when it is resolved. These must as well be registered with the TransientLivetimeManager. Starting with the interface which is to be resolved one needs to create a dependency tree of interfaces and investigate each and every interface. This is error-prone and time consuming.

但是,只有在接口使用TransientLifetimeManager注册的情况下,这才会创建一个新实例,这是未指定其他生存时间管理器时的默认值。 还必须考虑到,接口在解析后也可能会接收注入的接口。 这些也必须在TransientLivetimeManager中注册。 从要解决的接口开始,需要创建接口的依赖关系树并调查每个接口。 这是容易出错且耗时的。

Therefore it is better to ensure that interface implementations are thread-safe and document it accordingly (Document thread-safety).

因此,最好确保接口实现是线程安全的,并相应地对其进行记录(记录线程安全)。

有关使用锁的提示 (Tips regarding usage of lock)

私人锁对象 (Private lock object)

Use a private lock object.

使用私有锁对象。

Avoid using lock(this), or lock(typeof(AnyType)).

避免使用lock(this)或lock(typeof(AnyType))。

  • This reduces the risk for deadlocks (other code cannot use the same object for locking)

    这减少了死锁的风险(其他代码无法使用同一对象进行锁定)
  • Prevents excessive locking. The more often the same lock is used, the higher is the probability that other thread, using the same thread must wait.

    防止过度锁定。 使用同一锁的频率越高,使用同一线程的其他线程必须等待的可能性就越高。

避免在锁定范围内调用方法 (Avoid calling methods from locked scope)

DoSomeOtherThings might need to wait on another lock, which is held by a different thread. If that thread then calls DoSomeThings, then we have a deadlock.

DoSomeOtherThings可能需要等待另一个锁,该锁由另一个线程持有。 如果该线程随后调用DoSomeThings,那么我们将陷入僵局。

保持锁定范围小 (Keep the locked scope small)

If the scope is large then the possibility is high that other methods are called or other synchronized members are accessed. Both increase the risk of deadlocks.

如果范围很大,则调用其他方法或访问其他同步成员的可能性很高。 两者都增加了死锁的风险。

使用共享数据的副本最大程度地减少锁定 (Use copies of shared data to minimize locking)

Here is part of code from Using immutable objects to limit locking:

这是使用不可变对象限制锁定的部分代码:

The lock is only used to assign the immutable object. Afterwards one can process the data without the risk that it can be modified by a different thread.

该锁仅用于分配不可变对象。 之后,人们可以处理数据,而不必担心会被其他线程修改。

易挥发的 (Volatile)

Do not use volatile. There is a misleading documentation about it. Better use lock to be on the safe side.

不要使用挥发物。 关于它有一个误导性文档。 为了安全起见,最好使用锁。

See: http://www.albahari.com/threading/part4.aspx

请参阅: http//www.albahari.com/threading/part4.aspx

异常处理 (Exception Handling)

Parallel.Invoke takes care of exceptions thrown by the actions. One only needs to catch the AggregateException:

Parallel.Invoke负责处理操作引发的异常。 一个只需捕获AggregateException即可:

Resources for the curious
--------------------------
http://www.albahari.com/threading/http://joeduffyblog.com/2006/10/26/concurrency-and-the-impact-on-reusable-libraries/http://joeduffyblog.com/2008/03/28/concurrencyoriented-code-reviews/http://www.monitis.com/blog/improving-net-application-performance-part-8-locking-and-synchronization/
Image for post

翻译自: https://medium.com/@vladimirtasic/design-principles-you-need-to-know-when-using-parallel-invoke-4cdccc8f87c9

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值