본문 바로가기

유니티(Unity)

다중 스레드에서 딕셔너리 동시 접근시 에러

InvalidOperationException: Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct.

 

InvalidOperationException: 비동시 컬렉션을 변경하는 작업에는 독점 액세스 권한이 있어야 합니다. 이 컬렉션에 대한 동시 업데이트가 수행되어 컬렉션의 상태가 손상되었습니다. 컬렉션의 상태가 더 이상 올바르지 않습니다.

 

서로 다른 스레드에서 딕셔너리에 동시 접근하면 해당 에러가 발생한다.

해결 시도 : 
다른 스레드에서 작동하는 코드를 다음과 같이 작성하였다.

try { connectDict[port] = (tcp, stream); }
catch (InvalidOperationException)
{
    while (!connectDict.ContainsKey(port))
    {
        Debug.Log(port);
        connectDict[port] = (tcp, stream);
    }
}

그리고 테스트 버튼을 만들어 딕셔너리를 확인 해보았다.

foreach (KeyValuePair<int, (TcpClient, NetworkStream)> pair in connectDict)
    Debug.Log($"{pair.Key} : {pair.Value.Item1 == null}, {pair.Value.Item2 == null}");

하지만 안타깝게도 하나의 pair를 제외한 나머지는 0 : false, false 가 나왔다. 딕셔너리가 손상되어 버린것.

특이하게도 catch문의 while문이 작동을 안함. 손상전에 try-catch문이 작동하는 것 같음.

 

해결법 1:  멤버변수를 이용하여 순차적으로 처리해줌.

나의 경우 멤버변수인 _port를 이용하여 해결

private void ServerConnection(string ip, int port, Action<TcpClient, NetworkStream> serverConnection)
{
    TcpClient tcp = null;
    NetworkStream stream = null;
    tcp = new TcpClient(ip, port);

    stream = tcp.GetStream();
    while (!(port - _port == connectDict.Count)) Debug.Log("Wait");
    connectDict[port] = (tcp, stream);
    serverConnection(tcp, stream);
}

하지만 이렇게 되면 기존의 코드의 규칙이 바뀌면 이걸 또 바꿔줘야함

 

해결법 2:

기존의 딕셔너리보다는 속도가 떨어지지만 다중 스레드에서도 안정성 보장된다.

ConcurrentDictionary<TKey,TValue>

https://learn.microsoft.com/ko-kr/dotnet/api/system.collections.concurrent.concurrentdictionary-2?view=net-8.0

 

ConcurrentDictionary<TKey,TValue> 클래스 (System.Collections.Concurrent)

여러 개의 스레드에서 동시에 액세스할 수 있는 키/값 쌍의 스레드로부터 안전한 컬렉션을 나타냅니다.

learn.microsoft.com

 

 

InvalidOperationException: Collection was modified; enumeration operation may not execute.

이 에러는 딕셔너리와 같은 컬렉션을 foreach문을 통해 작업 중 딕셔너리가 수정되면 발생되는 에러이다.

이것또한 보통 다른 스레드에서 동시작업할 때 발생되는 에러이다.

나의 경우, while문에서 foreach문을 통해 딕셔너리를 읽고 있는 중에, 다른 스레드에서 단발적인 수정이 이루어지므로 try-catch문을 통해 해결하였다.

try
{
    foreach (int num in dict.Keys)
    {
        if (list.Contains(num)) continue;
        if (myClientNum == num) continue;
        Transform newPlayer = Instantiate(playerPrefab, playGround);
        newPlayer.name = num.ToString();
        playersDict[num] = newPlayer.GetChild(0);
        list.Add(num);
    }
    //나간 유저 있는지
    foreach (int ls in list)
    {
        if (dict.ContainsKey(ls)) continue;
        foreach (Transform player in playGround)
        {
            if (player.name != ls.ToString()) continue;
            Destroy(player.gameObject);
            playersDict.Remove(ls);
            list.Remove(ls);
        }
    }
}
catch (InvalidOperationException e) { Debug.LogError(e); }