작업자 스레드를 통해 ObservableCollection을 어떻게 업데이트합니까?


83

나는있어 ObservableCollection<A> a_collection;컬렉션은 'N'항목이 포함되어 있습니다. 각 항목 A는 다음과 같습니다.

public class A : INotifyPropertyChanged
{

    public ObservableCollection<B> b_subcollection;
    Thread m_worker;
}

기본적으로 WPF 목록보기 + b_subcollection선택한 항목을 별도의 목록보기 (양방향 바인딩, 속성 변경시 업데이트 등)에 표시하는 세부 정보보기 컨트롤에 모두 연결되어 있습니다.

스레딩을 구현하기 시작했을 때 문제가 나타났습니다. 전체 아이디어는 a_collection작업자 스레드를 전체적으로 사용하여 "작업"한 다음 각각을 업데이트하고 b_subcollectionsGUI가 결과를 실시간으로 표시하도록 하는 것이 었 습니다.

시도했을 때 Dispatcher 스레드 만 ObservableCollection을 수정할 수 있으며 작업이 중단되었다는 예외가 발생했습니다.

누구든지 문제를 설명하고 해결 방법을 설명 할 수 있습니까?


모든 스레드에서 작동하고 여러 UI 스레드를 통해 바인딩 할 수있는 스레드로부터 안전한 솔루션을 제공하는 다음 링크를 시도해보십시오. codeproject.com/Articles/64936/…
Anthony

답변:


74

기술적으로 문제는 백그라운드 스레드에서 ObservableCollection을 업데이트하는 것이 아닙니다. 문제는 그렇게 할 때 컬렉션이 변경을 일으킨 동일한 스레드에서 CollectionChanged 이벤트를 발생 시킨다는 것입니다. 즉, 컨트롤이 백그라운드 스레드에서 업데이트되고 있음을 의미합니다.

컨트롤이 바인딩 된 동안 백그라운드 스레드에서 컬렉션을 채우려면이 문제를 해결하기 위해 처음부터 고유 한 컬렉션 형식을 만들어야합니다. 하지만 더 간단한 옵션이 있습니다.

Add 호출을 UI 스레드에 게시합니다.

public static void AddOnUI<T>(this ICollection<T> collection, T item) {
    Action<T> addMethod = collection.Add;
    Application.Current.Dispatcher.BeginInvoke( addMethod, item );
}

...

b_subcollection.AddOnUI(new B());

이 메서드는 즉시 (항목이 실제로 컬렉션에 추가되기 전) 반환 된 다음 UI 스레드에서 항목이 컬렉션에 추가되고 모두가 만족할 것입니다.

그러나 현실은이 솔루션이 모든 크로스 스레드 활동으로 인해 과부하 상태에서 멈출 가능성이 있다는 것입니다. 보다 효율적인 솔루션은 여러 항목을 일괄 처리하고 주기적으로 UI 스레드에 게시하여 각 항목에 대한 스레드를 호출하지 않도록합니다.

BackgroundWorker 구성 클래스가 구현하는 당신은 그것의를 통해 진행보고 할 수있는 패턴 ReportProgress의 백그라운드 작업 중에 방법을. 진행률은 ProgressChanged 이벤트를 통해 UI 스레드에보고됩니다. 이것은 당신을위한 또 다른 옵션 일 수 있습니다.


BackgroundWorker의 runWorkerAsyncCompleted는 어떻습니까? UI 스레드에도 바인딩되어 있습니까?
Maciek

1
예, BackgroundWorker가 설계된 방식은 SynchronizationContext.Current를 사용하여 완료 및 진행 이벤트를 발생시키는 것입니다. DoWork 이벤트는 백그라운드 스레드에서 실행됩니다. 다음은 BackgroundWorker를 설명하는 WPF의 스레딩에 대한 좋은 기사입니다. msdn.microsoft.com/en-us/magazine/cc163328.aspx#S4
Josh

5
이 대답은 단순함이 아름답습니다. 공유 해주셔서 감사합니다!
Beaker

@Michael 대부분의 경우 백그라운드 스레드가 UI를 차단하고 업데이트를 기다리지 않아야합니다. Dispatcher.Invoke를 사용하면 두 스레드가 서로를 기다리는 경우 데드락 위험이 발생하고 기껏해야 코드 성능이 크게 저하됩니다. 특별한 경우에는 이런 식으로해야 할 수도 있지만 대부분의 상황에서 마지막 문장은 정확하지 않습니다.
Josh

@Josh 내 사례가 특별 해 보이기 때문에 내 대답을 삭제했습니다. 내 디자인을 더 자세히 살펴보고 더 잘할 수있는 것이 무엇인지 다시 생각할 것입니다.
마이클

125

.NET 4.5의 새로운 옵션

.NET 4.5부터 컬렉션에 대한 액세스를 자동으로 동기화하고 CollectionChanged이벤트를 UI 스레드로 전달하는 기본 제공 메커니즘이 있습니다. 이 기능을 활성화하려면 UI 스레드 내 에서 호출해야합니다 .BindingOperations.EnableCollectionSynchronization

EnableCollectionSynchronization 두 가지를 수행합니다.

  1. 호출 된 스레드를 기억하고 데이터 바인딩 파이프 라인이 CollectionChanged해당 스레드에서 이벤트 를 마샬링 하도록합니다.
  2. 마샬링 된 이벤트가 처리 될 때까지 컬렉션에 대한 잠금을 획득하여 UI 스레드를 실행하는 이벤트 처리기가 백그라운드 스레드에서 수정되는 동안 컬렉션을 읽지 않도록합니다.

매우 중요한 것은 이것이 모든 것을 처리하지는 않는다는 것입니다 . 본질적으로 스레드로부터 안전하지 않은 컬렉션에 대한 스레드로부터 안전한 액세스를 보장 하려면 컬렉션이 수정 되려고 할 때 백그라운드 스레드에서 동일한 잠금을 획득하여 프레임 워크와 협력 해야합니다.

따라서 올바른 작동에 필요한 단계는 다음과 같습니다.

1. 사용할 잠금 유형을 결정합니다.

이것은 어떤 과부하를 EnableCollectionSynchronization사용 해야하는지 결정 합니다. 대부분의 경우 간단한 lock명령문으로 충분하므로이 오버로드 가 표준 선택이지만 멋진 동기화 메커니즘을 사용하는 경우 사용자 지정 잠금지원됩니다 .

2. 컬렉션 생성 및 동기화 활성화

선택한 잠금 메커니즘 에 따라 UI 스레드 에서 적절한 오버로드 호출합니다 . 표준 lock문을 사용하는 경우 잠금 개체를 인수로 제공해야합니다. 사용자 지정 동기화를 사용하는 경우 CollectionSynchronizationCallback대리자와 컨텍스트 개체 (일 수 있음 null) 를 제공해야합니다 . 호출 될 때이 대리자는 사용자 지정 잠금을 획득하고 Action전달 된 잠금을 호출하고 반환하기 전에 잠금을 해제해야합니다.

3. 컬렉션을 수정하기 전에 잠금하여 협력

컬렉션을 직접 수정하려는 경우에도 동일한 메커니즘을 사용하여 컬렉션을 잠 가야합니다. 이 작업을 수행 lock()에 전달 된 동일한 잠금 개체에 대한 EnableCollectionSynchronization간단한 시나리오에서, 또는 사용자 정의 시나리오에서 동일한 사용자 정의 동기화 메커니즘.


2
이로 인해 UI 스레드가 처리 할 때까지 컬렉션 업데이트가 차단됩니까? 불변 개체의 단방향 데이터 바인딩 컬렉션 (비교적 일반적인 시나리오)과 관련된 시나리오에서 각 개체의 "마지막 표시된 버전"과 변경 대기열을 유지하는 컬렉션 클래스를 가질 수있는 것처럼 보입니다. , BeginInvokeUI 스레드에서 모든 적절한 변경을 수행하는 메소드를 실행하는 데 사용 합니다. [ BeginInvoke주어진 시간 에 최대 하나만 보류됩니다.
supercat 2013

16
작은 예가이 답변을 훨씬 더 유용하게 만듭니다. 아마도 올바른 해결책이라고 생각하지만 구현 방법을 모르겠습니다.
RubberDuck

3
@Kohanz UI 스레드 디스패처를 호출하면 여러 가지 단점이 있습니다. 가장 큰 것은 UI 스레드가 실제로 디스패치를 ​​처리 할 때까지 컬렉션이 업데이트되지 않고 응답 성 문제를 일으킬 수있는 UI 스레드에서 실행된다는 것입니다. 반면에 잠금 방법을 사용하면 컬렉션을 즉시 업데이트하고 UI 스레드에 의존하지 않고 백그라운드 스레드에서 처리를 계속할 수 있습니다. UI 스레드는 필요에 따라 다음 렌더링주기의 변경 사항을 따라 잡을 것입니다.
Mike Marynowski

2
4.5에서 컬렉션 동기화를 한 달 동안 살펴 봤지만이 답변 중 일부가 정확하지 않다고 생각합니다. 대답은 enable 호출이 UI 스레드에서 발생해야하고 콜백이 UI 스레드에서 발생한다고 말합니다. 이들 중 어느 것도 사실이 아닌 것 같습니다. 백그라운드 스레드에서 컬렉션 동기화를 활성화하고이 메커니즘을 계속 활용할 수 있습니다. 또한, 프레임 워크의 깊은 호출 (. ViewManager.AccessCollection CF 어떤 마샬링하지 않는다 referencesource.microsoft.com/#PresentationFramework/src/... )
레지 널드 블루

2
EnableCollectionSynchronization에 대한이 스레드에 대한 답변에서 더 많은 통찰력을 얻을 수 있습니다. stackoverflow.com/a/16511740/2887274
Matthew S

22

.NET 4.0에서는 다음 한 줄을 사용할 수 있습니다.

.Add

Application.Current.Dispatcher.BeginInvoke(new Action(() => this.MyObservableCollection.Add(myItem)));

.Remove

Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));

11

후손을위한 컬렉션 동기화 코드입니다. 이것은 단순한 잠금 메커니즘을 사용하여 컬렉션 동기화를 활성화합니다. UI 스레드에서 컬렉션 동기화를 활성화해야합니다.

public class MainVm
{
    private ObservableCollection<MiniVm> _collectionOfObjects;
    private readonly object _collectionOfObjectsSync = new object();

    public MainVm()
    {

        _collectionOfObjects = new ObservableCollection<MiniVm>();
        // Collection Sync should be enabled from the UI thread. Rest of the collection access can be done on any thread
        Application.Current.Dispatcher.BeginInvoke(new Action(() => 
        { BindingOperations.EnableCollectionSynchronization(_collectionOfObjects, _collectionOfObjectsSync); }));
    }

    /// <summary>
    /// A different thread can access the collection through this method
    /// </summary>
    /// <param name="newMiniVm">The new mini vm to add to observable collection</param>
    private void AddMiniVm(MiniVm newMiniVm)
    {
        lock (_collectionOfObjectsSync)
        {
            _collectionOfObjects.Insert(0, newMiniVm);
        }
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.