确定同步范围?

发布于 2024-09-26 05:09:35 字数 1940 浏览 0 评论 0原文

在尝试提高我对并发问题的理解时,我正在考虑以下场景(编辑:我已将示例从列表更改为运行时,这更接近我正在尝试的内容)

public class Example {
    private final Object lock = new Object();
    private final Runtime runtime = Runtime.getRuntime();
    public void add(Object o) { 
        synchronized (lock) { runtime.exec(program + " -add "+o); } 
    }
    public Object[] getAll() { 
        synchronized (lock) { return runtime.exec(program + " -list "); }
    }
    public void remove(Object o) { 
        synchronized (lock) { runtime.exec(program + " -remove "+o); } 
    }
}

:代表,每个方法在独立使用时都是线程安全的。现在,我想要弄清楚的是如何处理调用类希望调用的位置:

for (Object o : example.getAll()) {
    // problems if multiple threads perform this operation concurrently
    example.remove(b); 
}

但如上所述,不能保证对 getAll() 的调用和对 remove() 的调用之间的状态保持一致。如果多个线程调用这个,我就会遇到麻烦。所以我的问题是 - 我应该如何使开发人员能够以线程安全的方式执行操作?理想情况下,我希望以一种使开发人员难以避免/错过的方式强制执行线程安全,但同时实现起来并不复杂。到目前为止,我可以想到三个选项:

A:将锁设为“this”,以便调用代码可以访问同步对象,然后调用代码可以包装代码块。缺点:很难在编译时强制执行:

synchronized (example) {
    for (Object o : example.getAll()) {
        example.remove(b);
    }
}

B:将组合代码放入示例类中 - 并从能够优化实现中受益,如本例所示。缺点:添加扩展很痛苦,并且可能混合不相关的逻辑:

public class Example {
   ...
   public void removeAll() { 
       synchronized (lock) { Runtime.exec(program + " -clear"); } 
   }
}

C:提供 Closure 类。缺点:过多的代码,可能过于慷慨的同步块,实际上可能会使死锁更容易:

public interface ExampleClosure {
    public void execute(Example example);
}
public Class Example {
    ...
    public void execute(ExampleClosure closure) { 
        synchronized (this) { closure.execute(this); } 
    }
}

example.execute(new ExampleClosure() {
        public void execute(Example example) { 
            for (Object o : example.getAll()) {
                example.remove(b);
            }
        }
    }
);

我缺少什么吗?应如何确定同步范围以确保代码线程安全?

in trying to improve my understanding on concurrency issues, I am looking at the following scenario (Edit: I've changed the example from List to Runtime, which is closer to what I am trying):

public class Example {
    private final Object lock = new Object();
    private final Runtime runtime = Runtime.getRuntime();
    public void add(Object o) { 
        synchronized (lock) { runtime.exec(program + " -add "+o); } 
    }
    public Object[] getAll() { 
        synchronized (lock) { return runtime.exec(program + " -list "); }
    }
    public void remove(Object o) { 
        synchronized (lock) { runtime.exec(program + " -remove "+o); } 
    }
}

As it stands, each method is by thread safe when used standalone. Now, what I'm trying to figure out is how to handle where the calling class wishes to call:

for (Object o : example.getAll()) {
    // problems if multiple threads perform this operation concurrently
    example.remove(b); 
}

But as noted, there is no guarantee that the state will be consistent between the call to getAll() and the calls to remove(). If multiple threads call this, I'll be in trouble. So my question is - How should I enable the developer to perform the operation in a thread safe manner? Ideally I wish to enforce the thread safety in a way that makes it difficult for the developer to avoid/miss, but at the same time not complicated to achieve. I can think of three options so far:

A: Make the lock 'this', so the synchronization object is accessible to calling code, which can then wrap the code blocks. Drawback: Hard to enforce at compile time:

synchronized (example) {
    for (Object o : example.getAll()) {
        example.remove(b);
    }
}

B: Place the combined code into the Example class - and benefit from being able to optimize the implementation, as in this case. Drawback: Pain to add extensions, and potential mixing unrelated logic:

public class Example {
   ...
   public void removeAll() { 
       synchronized (lock) { Runtime.exec(program + " -clear"); } 
   }
}

C: Provide a Closure class. Drawback: Excess code, potentially too generous of a synchronization block, could in fact make deadlocks easier:

public interface ExampleClosure {
    public void execute(Example example);
}
public Class Example {
    ...
    public void execute(ExampleClosure closure) { 
        synchronized (this) { closure.execute(this); } 
    }
}

example.execute(new ExampleClosure() {
        public void execute(Example example) { 
            for (Object o : example.getAll()) {
                example.remove(b);
            }
        }
    }
);

Is there something I'm missing? How should synchronization be scoped to ensure the code is thread safe?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

月朦胧 2024-10-03 05:09:36

总的来说,这是一个经典的多线程设计问题。通过同步数据结构而不是同步使用数据结构的概念,很难避免这样一个事实:您本质上拥有对没有锁的数据结构的引用。

我建议锁不要离数据结构太近。但这是一个流行的选择。

使这种风格发挥作用的一个潜在技术是使用编辑树遍历器。本质上,您公开了一个对每个元素执行回调的函数。

// pointer to function:
//      - takes Object by reference and can be safely altered 
//      - if returns true, Object will be removed from list

typedef bool (*callback_function)(Object *o);

public void editAll(callback_function func) {  
    synchronized (lock) {
          for each element o { if (callback_function(o)) {remove o} } }  
} 

那么你的循环就变成了:

bool my_function(Object *o) {
 ...
     if (some condition) return true;
}

...
   editAll(my_function);
...

我工作的公司(corensic)有从真实错误中提取的测试用例来验证 Jinx正确发现并发错误。这种没有更高级别同步的低级别数据结构锁定是非常常见的模式。树编辑回调似乎是解决这种竞争条件的流行方法。

In general, this is a classic multithreaded design issue. By synchronizing the data structure rather than synchronizing concepts that use the data structure, it's hard to avoid the fact that you essentially have a reference to the data structure without a lock.

I would recommend that locks not be done so close to the data structure. But it's a popular option.

A potential technique to make this style work is to use an editing tree-walker. Essentially, you expose a function that does a callback on each element.

// pointer to function:
//      - takes Object by reference and can be safely altered 
//      - if returns true, Object will be removed from list

typedef bool (*callback_function)(Object *o);

public void editAll(callback_function func) {  
    synchronized (lock) {
          for each element o { if (callback_function(o)) {remove o} } }  
} 

So then your loop becomes:

bool my_function(Object *o) {
 ...
     if (some condition) return true;
}

...
   editAll(my_function);
...

The company I work for (corensic) has test cases extracted from real bugs to verify that Jinx is finding the concurrency errors properly. This type of low level data structure locking without higher level synchronization is pretty common pattern. The tree editing callback seems to be a popular fix for this race condition.

梦开始←不甜 2024-10-03 05:09:36

我认为每个人都忽略了他真正的问题。当迭代新的对象数组并尝试一次删除一个时,问题在技术上仍然是不安全的(尽管 ArrayList 植入不会爆炸,但它只是不会达到预期的结果)。

即使使用 CopyOnWriteArrayList,当您尝试删除时,也可能在当前列表上读取过时的内容。

您提供的两个建议都很好(A 和 B)。我的一般建议是 B。使集合线程安全是非常困难的。一个好的方法是为客户端提供尽可能少的功能(在合理范围内)。因此,提供removeAll方法并删除getAll方法就足够了。

现在您可以同时说,“好吧,我想保持 API 的原样,让客户端担心额外的线程安全性”。如果是这种情况,请记录线程安全性。记录“查找和修改”操作既非原子又非线程安全的事实。

今天的并发列表实现对于所提供的单个函数来说都是线程安全的(get、remove add 都是线程安全的)。但复合函数并非如此,最好的办法就是记录如何使它们线程安全。

I think everyone is missing his real problem. When iterating over the new array of Object's and trying to remove one at a time the problem is still technically unsafe (though ArrayList implantation would not explode, it just wouldnt have expected results).

Even with CopyOnWriteArrayList there is the possibility that there is an out of date read on the current list to when you are trying to remove.

The two suggestions you offered are fine (A and B). My general suggestion is B. Making a collection thread-safe is very difficult. A good way to do it is to give the client as little functionality as possible (within reason). So offering the removeAll method and removing the getAll method would suffice.

Now you can at the same time say, 'well I want to keep the API the way it is and let the client worry about additional thread-safety'. If thats the case, document thread-safety. Document the fact that a 'lookup and modify' action is both non atomic and non thread-safe.

Today's concurrent list implementations are all thread safe for the single functions that are offered (get, remove add are all thread safe). Compound functions are not though and the best that could be done is documenting how to make them thread safe.

天生の放荡 2024-10-03 05:09:36

我认为 jucCopyOnWriteArrayList 是您试图解决的类似问题的一个很好的例子。

JDK 在列表方面也有类似的问题 - 有多种方法可以在任意方法上进行同步,但在多个调用上没有同步(这是可以理解的)。

因此,CopyOnWriteArrayList 实际上实现了相同的接口,但有一个非常特殊的约定,无论谁调用它,都知道它。

与您的解决方案类似 - 您可能应该实现 List (或任何接口),同时为现有/新方法定义特殊契约。例如,无法保证 getAll 的一致性,并且如果 o 为 null 或不在内部,则对 .remove 的调用不会失败列表等。如果用户想要组合和安全/一致的选项 - 你的这个类将提供一个特殊的方法来完成这个任务(例如safeDeleteAll),让其他方法尽可能接近原始合同。

因此,为了回答你的问题 - 我会选择选项 B,但也会实现你的原始对象正在实现的接口。

I think j.u.c.CopyOnWriteArrayList is a good example of similar problem you're trying to solve.

JDK had a similar problem with Lists - there were various ways to synchronize on arbitrary methods, but no synchronization on multiple invocations (and that's understandable).

So CopyOnWriteArrayList actually implements the same interface but has a very special contract, and whoever calls it, is aware of it.

Similar with your solution - you should probably implement List (or whatever interface this is) and at the same time define special contracts for existing/new methods. For example, getAll's consistency is not guaranteed, and calls to .remove do not fail if o is null, or isn't inside the list, etc. If users want both combined and safe/consistent options - this class of yours would provide a special method that does exactly that (e.g. safeDeleteAll), leaving other methods close to original contract as possible.

So to answer your question - I would pick option B, but would also implement interface your original object is implementing.

心头的小情儿 2024-10-03 05:09:36

来自 List.toArray() 的 Javadoc:

返回的数组将是“安全”的
没有提及它
由该列表维护。 (在其他
换句话说,这个方法必须分配一个新的
数组,即使该列表由以下内容支持
一个数组)。因此,调用者可以自由地
修改返回的数组。

也许我不明白你想要完成什么。您希望 Object[] 数组始终与 List 的当前状态同步吗?为了实现这一点,我认为您必须在 Example 实例本身上同步并保持锁定,直到您的线程完成其方法调用和任何 Object[]当前正在使用的数组。否则,您如何知道原始 List 是否已被另一个线程修改?

From the Javadoc for List.toArray():

The returned array will be "safe" in
that no references to it are
maintained by this list. (In other
words, this method must allocate a new
array even if this list is backed by
an array). The caller is thus free to
modify the returned array.

Maybe I don't understand what you're trying to accomplish. Do you want the Object[] array to always be in-sync with the current state of the List? In order to achieve that, I think you would have to synchronize on the Example instance itself and hold the lock until your thread is done with its method call AND any Object[] array it is currently using. Otherwise, how will you ever know if the original List has been modified by another thread?

芸娘子的小脾气 2024-10-03 05:09:36

选择锁定内容时必须使用适当的粒度。您在示例中抱怨的是粒度级别太低,锁没有涵盖必须一起发生的所有方法。您需要创建将需要在同一个锁中一起发生的所有操作组合起来的方法。

锁是可重入的,因此高级方法可以毫无问题地调用低级同步方法。

You have to use the appropriate granularity when you choose what to lock. What you're complaining about in your example is too low a level of granularity, where the lock doesn't cover all the methods that have to happen together. You need to make methods that combine all the actions that need to happen together within the same lock.

Locks are reentrant so the high-level method can call low-level synchronized methods without a problem.

清君侧 2024-10-03 05:09:35

使用通过 API 公开的 ReentrantReadWriteLock。这样,如果有人需要同步多个 API 调用,他们可以在方法调用之外获取锁。

Use a ReentrantReadWriteLock which is exposed via the API. That way, if someone needs to synchronize several API calls, they can acquire a lock outside of the method calls.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文