Socket client 인스턴스를 여러개 사용할때..

Plc와 소켓 통신하는 윈폼 프로그램을 개발하고 있습니다.

초기 개발땐 plc가 하나밖에 안되니,
폼 내부에 소켓 클라이언트 만들어서 했고 이때는 문제점을 인지하지 못하고 있었습니다.

그러다, plc device의 갯수가 많아지게 되면서,
해당 코드를 클래스로 생성해서, 비동기로 통신을 하려고 하는데요,

각각의 인스턴스가 별개로 움직이지 않고, 다른 인스턴스의 영향을 받는것 같습니다.

예를들어 plc1에서 프레임을 3-10회 주고 받는 동안
Plc2에서는 나중에 한번 주고 받는 식으로요.

  1. form 에서, 설정 데이터 가져온 후, 해당 디바이스 인스턴스 생성후 start.

  2. 인스턴스 내에서 루프 돌면서 프레임 주고 받기
    이미지
    이미지
    이미지

윈폼 인스턴스(form.cs)내에서 소켓을 여러개 만들어서 하면 문제는 없지만,

이렇게 따로 클래스를 생성해서 주고받으니 이렇게 되네요…

제가 비동기 프로그래밍을 잘못이해하고 있는건지, 아니면 아예 근원적으로 잘못접근하고 있는지를 모르겠습니다.

비전공자기도 하고 어쩌다 보니 개발을 하고 있는데,
도와주십쇼 선생님들!

아래의 내용을 숙지하신 후에는, 이 글 전체의 내용을 지우고 원하는 글을 작성해주세요.

질문 글을 올리기 전 꼭 읽어주세요

질문을 올리실 때는 답글을 달아주시는 분이 최대한 상황을 자세히 알 수 있도록 질문을 올려주세요. 다음의 내용이 들어가면 좋습니다.

  • 무엇을 하고자 하는지
  • 현재 작성한 코드 중 문제가 되는 부분
  • 기대하는 동작

질문글, 답글, 댓글은 한 번 올리면 언제든 누구나 볼 수 있어야 합니다. 질문글에 대한 답글이 올라왔다고해서 글을 지우는 것은 이기적인 행동입니다. 만약 공개적으로 올릴 수 없는 질문이라면 포럼에는 질문을 올리시면 안됩니다.

한 번 올렸던 질문은 다시 올리지 말아주세요. 만약 질문에 대한 답을 받지 못했다면, 혹시 질문이 너무 추상적이지는 않은지, 단순히 무엇을 대신 해달라는 부탁은 아니었는지 생각해보고 고쳐서 다시 올려주세요. 그리고 먼저 올린 질문에 댓글 형태로 새로운 질문을 계속 추가해주세요. 그래야 추가 답변을 쉽게 받으실 수 있고, 나중에 찾아보기도 좋습니다.

답변을 받으면, 감사의 표시로 꼭 답변 채택 버튼을 눌러 마무리해주세요. 나중에 같은 질문을 찾아보시는 다른 분들께도 도움이 됩니다.

마지막으로 당부드릴 말씀이 있습니다. 질문 답변 게시판에서 답글을 달아주시는 분은 본인의 귀한 시간을 쪼개어 지식을 공유해주시는 분입니다. 답글을 달아주시는 분, 그리고 커뮤니티에 참여하는 다른 모든 분들께 기본적인 예의를 지킬 수 있도록 해주세요.

이상의 내용을 숙지하여 모두가 즐겁게 참여할 수 있는 포럼을 만드는 데 힘을 보태어주세요.

2개의 좋아요

네… await를 하면 그 비동기 요청에 대한 반환이 될 때까지 다음 줄이 진행되지 않습니다. await는 비동기 호출을 대기하는 기능이라 그렇습니다.

...
var received = await clientSocket.ReceiveAsync(buffer, SocketFlags...);
...

이 부분입니다.

await를 하지 않고 Task로 받으신 후, Task를 목록으로 만들어서 await Task.WhenAll()를 하신 후 다음으로 넘어가는 것이 좋습니다.

1개의 좋아요

샘플을 만들었습니다.


var feNets = Enumerable.Range(1, 10).Select(x => new FeNet(x)).ToArray();

Console.WriteLine("Test1");
await Test1Async(feNets);

Console.WriteLine("------");

Console.WriteLine("Test2");
await Test2Async(feNets);


// 순차적으로 실행됨
static async Task Test1Async(FeNet[] feNets)
{
    foreach (var feNet in feNets)
    {
        var result = await feNet.ReceiveAsync();
        Console.WriteLine(result);

    }
}

// 한번에 실행됨
static async Task Test2Async(FeNet[] feNets)
{
    var tasks = feNets.Select(x => x.ReceiveAsync()).ToArray();
    await Task.WhenAll(tasks);

    foreach (var task in tasks)
        Console.WriteLine(task.Result);
}

// 예시를 위한 클래스
class FeNet
{
    public int Id { get; }

    public FeNet(int id)
    {
        Id = id;
    }

    public async Task<int> ReceiveAsync()
    {
        await Task.Delay(Random.Shared.Next(100, 1000));

        return Id;
    }
}

다음은 실행 결과입니다.
Animation

Task1Async()Task2Async()의 동작성을 확인하시면 됩니다.

5개의 좋아요

아… 지금껏 비동기의 개념을,
먼저 실행후 응답 받을때까지 무작정 기다리는게 아닌,
기다리면서 다른 요청을 먼저 받고 실행하는걸로 알고 있었는데, 이게 아니었나봐요;;

정말 감사합니다!!

혹 메일 주소라도 남겨주신다면 기프티콘이라도 보내드릴텐데… 남겨주실수 있다면 부탁드립니다!

3개의 좋아요

기프티콘이라뇨. 감사함의 표현으로 받았습니다.

정성태님의 홈페이지에 가셔서 async 또는 await 키워드로 검색하시면 훌륭한 글들이 검색됩니다.

또 이글을 보면 좋습니다.

C#의 비동기 프로그래밍 | Microsoft Docs

2개의 좋아요

음… 솔직히 이 개념이 머릿속으로 이해가 되진 않아요.

각각의 인스턴스가 각각의 요리사라는 개념인데,
각각의 다른 요리사가(인스턴스) 각각 자신의 방법으로 깉은 요리를 만들어 나아가는데 다른 요리사의 프로세스를 기다린다는게요.

((우습지만, ‘아 이럴때 나눠서 병렬로 나눠야 하는건가?” 라는 생각까지 했었습니다;;:wink:

1개의 좋아요

저는 말씀하신

아… 지금껏 비동기의 개념을,
먼저 실행후 응답 받을때까지 무작정 기다리는게 아닌,
기다리면서 다른 요청을 먼저 받고 실행하는걸로 알고 있었는데, 이게 아니었나봐요;;

여기에 공감합니다.

다만 비동기여도 다른 작업에 영향을 미칠 수 있기 때문입니다.

콘솔 프로그램이면 별로 상관없을 수 있을 듯한데 대표적으로 UI가 있는 프로그램에선 UI thread 라는 게 있기 때문에 '차단’이 아닌 '대기’는 유용한 개념입니다.

Windows라면 차단이 걸릴 시 Windows Message의 흐름이 멈추게 되면 UI가 멈추고, 사용자는 렉걸렸다, 프로그램이 죽었다 라고 생각할 테니까요.

대표적으로 UI 프로그램에서 다른 프로그램 돌린다고 ManualResetEventSlim 같은 API나 AutoResetEvent 같은 API를 쓰면 UI가 차단되지만 async await를 통해 작업하면 UI가 멈추지 않고 (Hang 또는 Freeze) 다른 작업을 이어나갈 수 있게 됩니다.

5개의 좋아요

음… 사실

이 말이 맞아요. 정확하게 이해하신 겁니다.

다만 async / await 라는 키워드가 이 부분을 숨기는 역할을 하고 있어서 혼란을 주는 겁니다.

정확히는 await 한 이후의 코드들은 해당 Task 가 완료될 때까지 기다리는 게 아니라
await 이전 호출로 제어권을 넘기게 되는 거예요. 그래서 실제로는 기다리는 것처럼 보이게 만드는 효과가 있는 겁니다. @vidaconcerveza 님의 이해가 정확한 거예요.

이게 무슨 얘기냐면, await 이후의 코드들은 비동기 호출 이후 콜백으로 연결해서 실행하는 것과 같은 거예요.

이걸 @dimohy 님의 샘플로 표현해보자면

Console.WriteLine("Test1");
Test1Async(feNets).ContinueWith(t => 
{
  Console.WriteLine("------");

  Console.WriteLine("Test2");
  Test2Async(feNets);
}); 

뭐 요런 식으로 표현할 수가 있는 겁니다.
사실 이건 명확하죠?
Test1Async(feNets) 를 비동기로 실행하고 그 실행이 끝나면 Continuewith() 에 할당된 델리게이트를 실행한다.
이거거든요.

async/await 가 나오기 전, Task 를 이용한 비동기 구현이 딱 이 방식이었죠.
그런데 이 방식에는 단점이 있슴다. 바로 복잡성.

비동기 이후 동작들이 복잡해지고, 또 그 콜백에서 또 비동기를 구현하게 되면
콜백을 중첩으로 쌓아서 표현해야하는 콜백 지옥이 펼쳐집니다.
(콜백 지옥을 검색해보시면 이게 어떤 의미인지 이해가 확실히 될 겁니다.)

사람이 이해하기 어려운 중첩 콜백이 반복적으로 작성되고, 이게 유지 보수에 어려움을 가져다주지요.

그래서 이 중첩된 콜백들을 동기 표현처럼 만들어주는 async/await 가 나온 겁니다.
(.NET4.5 C#5.0 에 추가되었지요. 이후 이 async/await 은 일종의 패러다임으로 자리잡게 됩니다.)

근데 저는 여기서 await 이라는 키워드 이름에 문제가 있다고 느낄 때가 꽤 있어요.
await 이라는 단어는 task 가 반환될 때까지 기다린다… 라는 느낌이 있거든요.

그래서 사람들이 기다린다 라는 표현을 자주 사용하는데
근데 실제로 기다리는 건 아니예요. 제어권을 넘겨주고 호출한 스레드는 다른 일을 하러 가는 겁니다.

다만 이게, 코드에서 동기처럼 표현되어 있으니
그런 차원에서 await의 의미가 기다린다 에 가깝다 라는 점을 이해해야하는 거죠.

정리하자면

Task(비동기) 호출 → 이후 실행해야하는 부분을 await 키워드로 표현.

정도로 생각하시면 됩니다.

10개의 좋아요

위에 다른분들이 좋은 방법을 말씀해주셔서 저는 제가 이해했던 방법을 한번 말해볼게요.

저는 아래와 같은 코드를 작성해보고 이해했었습니다.

void async FooAsync()
{
    Task.Run(() => { while(true){ } });
    Task.Run(() => { while(true){ } });
    Task.Run(() => { while(true){ } });
}

void async Foo2Async()
{
    await Task.Run(() => { while(true){ } });
    Task.Run(() => { while(true){ } });
    Task.Run(() => { while(true){ } });
}

위 코드는 어떻게 동작할까요? 첫번째 Foo는 3줄이 모두 동작 되고 Foo2는 await에서 해당 Task가 완료 되기를 기다립니다. 이러면 아래에 있는 Task가 동작하지 않습니다.

두 foo 모두 UI Thread를 사용하지 않기 때문에 UI가 멈추지는 않습니다. 여기서 await이 어떻게 동작하는지 이해했었습니다. 더 자세한건 구글링하면서 공부하면 금방 이해하실 수 있습니다.


이제 다른 이야기를 해보죠. 먼저 작성자분의 문제를 저도 고민해본 입장에서 해답은 아닐지라도 저는 이렇게 생각했었고 어떻게 해결했는지 적어볼게요.

가장 중요한 키워드는 이것이 아닐까요?

기존) 1:1 PLC 통신
변경) 1:N PLC 통신

자 여기서 1:1 PLC 통신은 그냥 하면 되었습니다.

그러다보니 1:1 통신을 여러개 만들어서 그냥 Thread에 넣고 돌리면 되는거아니야? 라는 생각을 하게 됩니다. 그래서 찾아보니 Task/await을 사용해서 async를 구현 하면 되겠네? 라는 생각을하고, 그래서 찾아보니 Socket에서 Async 기능이 있네? 이걸로 구현하면 되겠다. 라는 생각까지 도달합니다.

근데 여기가 잘못된 접근 방법이죠. 1:N 통신은 device들의 스케쥴링이 필요한것이지 통신 그 자체의 비동기가 필요한것이 아닙니다.

즉, socket의 async 기능이 필요 한것이 아니라 Object를 N개를 만들고 이것들을 Task/await으로 관리하는 해결법이 필요하게 됩니다.

저는 device 관리 하는 방법으로 이 문제를 해결 했었고 여기서 마이크로서비스 아키텍처를 참고하고 적용하려고 노력했던것 같습니다.

7개의 좋아요

아… 이제 좀 이해가 되네요.
큰 도움 되었습니다!

MS홈페이지 내에서도 비동기 소켓 코드로 소개 되어 있어서, 단순하게 생각했었는데,

1:N의 상황이 되면서 접근 자체를 잘못하고 있었네요.

Form.cs 내에 여러개의 소켓을 만들어 하는 경우 이러한 문제가 없었기때문에 될거라고 생각하기도 했구요.

도움을 주신 분들, 정말 감사합니다!

4개의 좋아요

저도 한참 고민을 했던 내용인데 ㅋ 클라이언트가 N개 일때 수신부는 하나로 운영이 어렵다는 결론에 도달하게 되더군요 ㅎㅎ 수신부는 1개인데 3개에서 데이터가 들어오니 수신 처리가 지연이 되면 전부다 원할한 통신이 안되더군요 클라이언트 별로 수신부를 만들고 처리 로직에 동시접근을 막을 싱크를 거니 안정적으로 수신이 되더군요 ㅋ