비동기 태스크

 

다중 스레드 프로그래밍을 복잡하게 하는 요인에는 다음과 같은 것들이 있다.

1.     비동기 작업의 완료 여부 감시 : 가급적 폴링이나 대기 이외의 방법을 통한 비동기 작업 완료 여부 판단이 필요하다.

2.     스레드 풀링 : 스레드 풀링을 이용하면 스레드를 시작하거나 제거하는 데 드는 비용의 많은 부분을 줄일 수 있다. 또한 너무 많은 스레드를 만들어서 스레드를 실행하는 것보다 콘텍스트 변경에 더 많은 시간을 낭비하게 되는 상황도 피할 수 있다.

3.     교착 상태 회피 : 서로 다른 두 개의 스레드가 보호되는 데이터에 동시에 접근하려고 할 때 발생할 수 있는 교착 상태를 방지해야 한다.

4.     데이터 엑세스에 대한 동기화와 연속되는 작업들에 대한 원자성 제공 : 작업을 동기화하면 여러 작업들을 단일 작업 단위로 묶고 다른 스레드와 연관된 작업을 적절하게 수행할 수 있다. 잠금 기능을 이용해서 두 스레드가 같은 데이터를 동시에 접근하지 못하도록 할 수 있다.

 

오래 실행되는 메소드는 비동기적으로 호출하도록 다중 스레드 프로그래밍을 이용할 필요가 있다. 이와 같은 시나리오들에 대응하려고 개발자들이 점점 더 많은 다중 스레드 프로그래밍을 하게 되면서 공통된 처리 시나리오 집합과 프로그래밍 패턴들이 속속 등장하고 있다.

C# 5.0에서는 닷넷 4.0 TPL을 이용하는 TAP과 같은 패턴과 이를 지원하기 위한 C# 언어의 새로운 구조로 인해 한층 강력한 프로그래밍이 가능해졌다. 이번 절과 다음 절에 걸쳐 우선 TPL 자체를 살펴 본 다음에 간편하게 TAP 프로그래밍이 가능하게 해주는 async/await 문맥적 키워드를 TPL과 사용하는 방법을 자세히 살펴 볼 것이다. 19장에서는 후반부 절반 정도를 할애해서 TPL C# 5.0을 사용할 수 없는 상황에서 알고 있어야 할 추가적인 다중 스레딩 패턴들을 살펴본다.

 

 

 - Essential C# 5.0 (C#의 기초와 고급을 아우르는 핵심 바이블) - 18장 다중 스레딩

 

스레드 폴링

프로세서에 의존하는 작업에 효과적으로 프로세서 시간을 할당하려면 스레드 풀을 활용한다.

입출력 관련 혹은 장기간 수행되는 작업의 경우, 스레드 풀의 스레드를 할당하지 말고 TPL을 이용하라

 

앞서 성능에 대한 고려사항을 살펴본 것처럼 스레드를 과도하게 사용하면 오히려 성능에 악영향을 미칠 수 있다. 스레드는 비교적 비싼 대가를 요구하는 자원이며 스레드 콘텍스트 변경은 공짜가 아닐 뿐더러 시분할 방식을 이용해서 두 개의 작업을 병렬 처리 형태로 모의하는 것은 한 개씩 실행하는 방식에 비해서 훨씬 느릴 수 있다.

 이와 같은 문제점에 효과적으로 대응하려고 BCL은 스레드 풀을 제공한다. 스레드를 직접 할당하는 대신에 처리하고자 하는 작업이 무엇인지 스레드 풀에 요청할 수 있다. 작업이 끝나면 스레드를 종료하고 제거하는 대신 스레드 풀에 반납하는 방식을 이용하기 때문에 작업이 요청되었을 때 새로운 스레드를 할당하는 데 들어가는 비용을 절약할 수 있다.

훨씬 더 많은 작업들을 비동기 처리하는데 이와 같은 풀링(pooling) 기법을 적용하면 더욱 큰 효과를 얻을 수 있을 것이다. 이런 효율성을 얻을 수 있는 이유는 매번 비동기 호출이 있을 때마다 스레드를 다시 만들지 않고 계속해서 재사용하기 때문이다. 스레드 풀 방식을 사용할 때도 여전히 성능과 동기화 문제를 피할 수 없으므로 세심하게 다뤄야한다.

프로세서를 효과적으로 사용하려고 스레드 풀은 스레드 풀을 이용해서 처리하는 모든 작업이 적절한 시간 내에 끝나고 결과적으로 반환된 스레드를 다른 작업에 재사용할 수 있다는 가정 하에 동작한다. , 스레드 풀은 모든 작업이 비교적 짧은 시간에 끝날 것이라고 가정한다.(밀리초 혹은 초 단위로 끝나는 작업 정도를 의미하며, 몇 시간이나 혹은 며칠씩 처리해야하는 작업은 고려치 않는다.) 이와 같은 가정을 기반으로 성능과 관련한 초급 주제를 살펴 본 것처럼 스레드 풀은 개별 프로세서가 작업에 최대한의 능력을 발휘하고 다양한 작업을 비효율적인 시분할 없이 처리할 수 있다. 스레드 풀은 스레드 생성을 조절해서 프로세서 한 개에 너무 많은 스레드가 할당되지 않도록 함으로써 과도한 시분할이 발생하지 않게 한다. 하지만 이렇게 함으로써 스레드 풀의 스레드가 모두 사용 중인 경우에 대기 중인 작업에 지연이 발생할 수 있다. 장시간 수행하거나 혹은 I/O와 관련된 작업에 스레드 풀의 모든 스레드가 이용되고 있다면 대기 중인 작업에 지연이 있을 수 있다.

 개발자가 직접 조작할 수 있는 개체인 Thread Task와 달리 스레드 풀은 주어진 작업을 처리하는 스레드에 대한 참조를 제공하지 않는다. 그러므로 호출하는 스레드는 앞에서 살펴본 스레드 관리 함수들을 이용해서 동기화나 기타 각종 제어를 작업 스레드에 요구할 수 없다.

 스레드 풀은 오랜 시간이 필요하거나 다른 스레드와 동기화가 필요한 작업에는 적합하지 않다. 진정 우리에게 필요한 것은 스레드와 스레드 풀을 구현으로써 이용할 수 있는 상위 수준의 추상화 계층을 구현하는 것인데, 테스크 병렬 라이브러리(TPL)가 바로 이러한 것을 가능하게 해준다.

 

 

 - Essential C# 5.0 (C#의 기초와 고급을 아우르는 핵심 바이블) - 18장 다중 스레딩

 

프로덕션 코드에서 스레드를 중단시키지 말자

 

프로덕트 코드에서는, 결과를 예측할 수 없고 프로그램을 불안정하게 만드는 스레드 강제 중단의 사용을 피하라.

 

 Thread 개체에서 제공하는 Abort() 메소드는 스레드를 제거하는 기능을 제공한다. 그러려고 Abort()는 런타임을 이용해서 대상 스레드 내에서 ThreadAbortException 예외가 발생하게 한다. 이 예외를 catch할 수 있지만, catch해서 무시하도록 처리한다고 해도 스레드가 실제로 제거되었는지 확인할 때 다시 발생된다. 이렇게 스레드를 강제로 중단하는 것이 바람직하지 않은 수많은 이유 가운데 몇 가지를 들어보면 다음과 같다.

l  이 메소드는 스레드를 중단시키는 시도를 할 뿐이지 그 시도의 성공을 보장하지 않는다. 예를 들어 스레드 내의 제어점이 현재 finally 블록(방해해서는 안 되는 리소스 해제를 위한 중요한 코드를 실행중일 수 있기 때문) 혹은 비관리 코드(비관리 코드 내부에서 중단 시킬 경우  CLR 자체에 문제가 생길 수 있다) 내부에 있는 경우 런타임이 ThreadAbortException을 발생시키지 않는다. 대신 런타임은 제어가 finally 블록을 벗어나거나 관리 코드로 반환되기를 기다렸다가 예외를 발생시킨다. 하지만 반드시 이렇게 처리된다는 보장이 없다. 중단하려는 스레드의 finally 블록에 무한 루프가 있을 수 있고, 역설적으로 애초에 finally 문의 무한 루프 때문에 스레드를 중단하려고 했을 수도 있다.

l  중단된 스레드가 lock 문으로 보호되는 임계 코드 내에 있을 수 있다. finally와 달리 lock 문은 예외를 막지 않는다. 따라서 진행 중인 임계 코드는 예외를 맞아 실행 중에 중단되고 lock 개체는 자동으로 해체되면서 임계 영역에 들어가려던 다른 코드의 진입이 허용된다. 이때 실행되다가 중단된 코드의 상태가 임계 영역에 그대로 남아 있게 되는데, 근본적으로 잠금이라는 개념을 이용하는 이유가 이런 상태(원자성을 제공하지 못하는 상태)를 막기 위한 것이라는 점을 떠올린다면 이것은 분명 잘못된 것이며, 스레드를 중단함으로 인해 스레드에 안전한 코드를 한 순간에 위험한 코드로 만들어 버릴 수 있다.

l  CLR은 스레드의 중단으로부터 내부 데이터 구조가 안전하게 보홈되을 보장하지만 BCL(Base Class Liabrary)의 경우는 그렇지 못하다. 따라서 운 나쁘게 좋지 않은 시점에 예외로 인해 스레드가 중단된다면 개발자가 애써 구축한 데이터 구조나 BCL에서 제공하는 데이터 구조는 망가질 수 있다. 다른 스레드의 코드나 중단되는 스레드의 finally 블록에 있는 코드에서 망가진 데이터를 참조할 수 있기 때문에 오류가 확산될 수 있다.

 

결론적으로 최후 수단이 아니라면 스레드를 강제 중단시켜서는 안 된다. , 전체 어플리케이션 도메인 혹은 프로세스를 비상 종료하는 경우의 일환으로 중단하는 경우에만 사용하는 것이 이상적이다. 다행히 태스크 기반의 비동기 방식은 스레드 강제 종료를 위한 더 단단하고 안전한 취소 패턴을 이용하고 있으며 뒤에서 소개할 비동기 작업 절에서 살펴본다.

 

 - Essential C# 5.0 (C#의 기초와 고급을 아우르는 핵심 바이블) - 18장 다중 스레딩

프로덕션 코드에서 스레드를 유휴 상태로 만들어서는 안 된다

 

프로덕션 코드에서 Thread.Sleep()을 사용하지 마라.

 

Thread.Sleep() 정적 메소드는 현재 스레드를 유휴 상태로 만드는데, 주어진 시간 동안 운영체제로 하여금 현재 스레드에 시간을 할당하지 않게 된다. 밀리초 단위의 숫자 혹은 TimeSpan 형식의 매개변수를 통해서 실행을 재개하기 전에 운영체제가 대기해야 할 시간을 지정한다. 대기하는 동안에 운영체제는 대기 중인 다른 스레드에 시간을 할당한다. 꽤 그럴듯하게 들리지만 이런 상황에 대처하기 위한 더 나은 설계가 있을 것 같은 나쁜 코드 냄새가 난다.

 스레드는 종종 여러 이벤트와 보조를 맞추려고 유휴 상태에 놓이곤 한다. 하지만 운영체제는 이와 관련된 시점 조절과 관련해 정확성을 일체 보장하지 않는다. , ‘123 밀리초 동안 유휴 상태로 유지해 주세요라고 요청했을 때, 운영체제는 해당 스레드를 적어도 123 밀리초 동안 유휴 상태로 만드는 데 훨씬 더 오래 걸릴 가능성도 있다. 스레드가 유휴 상태로 진입하고 또 벗어나는 데 걸리는 시간은 절대적인 것이 아니며 임의의 시간이 소요될 수 있다. Thread.Sleep()을 고정밀 타이머처럼 사용해서는 안 되는 이유다.

 더 심한 경우는 Thread.Sleep()바보들의 동기화 시스템으로 사용하는 것이다. 예를들어 일정량의 비동기 작업을 수행해야 하고 이 비동기 작업이 끝나기 전에는 현재의 스레드를 더 이상 진행해서는 안 되는 상황을 가정해 보자. 이때 비동기 작업이 끝날 것으로 예상되는 대략적인 시간보다 충분히 더 여유를 잡아서 스레드를 유휴 상태로 만들고 스레드가 유휴 상태에서 깨어났을 때 비동기 작업이 끝났을 것이라는 위험한 가정을 기준으로 개발을 시도할 수 있다. 이것은 정말 위험한 생각이다. 비동기 작업은 여러분이 생각하는 것 보다 더 긴 시간을 요할 수 있다. 이와 같은 상황에 적합한 스레드 동기화 메커니즘을 다음 장에서 살펴본다.

 스레드를 유휴 상태로 만드는 것 자체도 나쁜 프로그래밍 습관이라고 볼 수 있다. 일단, 스레드가 유휴 상태에 들어가면 스레드에서 포함하고 있는 코드는 원해도 실행할 수 없는 무응답 상태가 되기 때문이다. 윈도우 응용 프로그램의 주 스레드를 유휴 상태로 만들면 사용자 인터페이스에서 발생하는 메시지도 처리하지 않게 되기 때문에 결국 프로그램은 응답이 없는 상태에 빠진다.

 더 일반적인 시각에서 보면 스레드와 같은 값비싼 리소스를 할당해 놓고 작업을 하지 않게 하는 것은 역시 바람직한 프로그래밍이 아니다. 아무도 잠만 자는 직원에게 월급을 주지 않으려 할 것 이다. 따라서 수백만이나 수십억에 이르는 프로세서 주기를 허송세월로 흘려보내려고 힘들게 스레드를 할당하지 않는 것이 바람직하다.

 한편, Thread.Sleep()을 적절하게 사용하는 경우도 물론 있다. 유휴 대기 시간을 0으로 설정해 호출하는 방식으로, 운영체제에게 남아있는 시간 할당을 포기할 의사를 전달해서 대기 중인 다른 스레드로 순서를 재빨리 넘기도록 할 수 있다. 이렇게 양보한 스레드는 추가적인 대기 시간 없이 다시 일반적인 스케줄에 따라 작업을 진행한다. 다음으로 테스트 코드에서는 종종 실제로 프로세서에 의미 없는 연산을 시켜 부하를 주는 방법 대신 Thread.Sleep()을 이용해서 일부 장기간의 대기 시간을 요하는 작업을 모의하기도 한다. 다른 이유로 스레드를 유휴 상태로 만들고 있다면 원하는 기능을 구현할 수 있는 더 좋은 방안이 없는지 조심스럽게 검토해 보는 것이 좋다.

 C# 5.0의 태스크 기반 비동기 프로그래밍에서는 Task.Delay() 메소드의 결과에 await 연산을 이용해서 현재 스레드의 진행에 영향을 주지 않고 비동기 지연 처리가 가능하다. 자세한 내용은 다음 장의 타이머 절을 참고하라.

 

 

 - Essential C# 5.0 (C#의 기초와 고급을 아우르는 핵심 바이블) - 18장 다중 스레딩

 

TabControl에 추가 되어 있는 Tab을 드래그하여 빼내면 윈도우가 나오게 하는 기능을 구현하였습니다.

데모 프로그램은 아래와 같습니다.

 

 

 

 

1. DragDrop 탭추가 : Drag&Drop하여 넣고 뺄 수 있는 탭을 추가합니다.

 

 

 

2. DragDrop MDI탭추가 : Drag&Drop하여 넣고 뺄 수 있는 MDI 윈도우를 추가합니다.

 

 

 

3. 일반 탭추가 : 일반탭을 추가하여 탭밖으로 빠지질않는것을 확인합니다.

 

 

4. 일반 폼을 추가 : 일반폼을 추가하여 탭안에 삽입이 안되는 것을 확인합니다.

 

 

프로젝트 파일 : KDYFramework.Winform.Controls.DragDropTabControl.zip

 

 (VS2010 작성)

Visual Studio 2013 서비스팩 업데이트를 진행하고, VS 에디터를 열었더니 에러 메세지와 함께 에디터가 열리지 않는 문제가 생겼습니다.

구글링해본 결과 아래와 같이 진행하시면 해결이 가능합니다.

 

 

 

 

1. VisualStudio2013을 닫습니다.

2. 아래의 경로로 이동합니다. (사용자명은 자신의 PC 이름)

   C:\Users\사용자명\AppData\Local\Microsoft\VisualStudio\12.0\ 

3. ComponentModelCache 폴더의 이름을 아무 이름으로 변경합니다.

   ex) ComponentModelCache_

4. Visual Studio를 다시 실행하면 새로운 ComponentModelCache 폴더가 생성되면서 문제가 해결됩니다.

 

참조: http://stackoverflow.com/questions/23893497/no-editoroptiondefinition-export-found-error

'.Net > 에러' 카테고리의 다른 글

C# 직렬화 (Serializable) Event 포함시 문제해결  (0) 2012.08.10
C# WebClient 사용시 404 에러  (0) 2012.08.09

.Net에서 제공해주는 DataGridView 컨트롤은 멀티헤더 기능을 제공해주지 않습니다.

 

기능이 필요해서 직접 구현했습니다. 필요하신분은 참조하세요~

 

 

 

 

 

 

- 멀티헤더 기능

 

 

-기타 기능

 

 

 

 

-다중 선택 (Ctrl or Shift 누르고 헤더 클릭시)

 

 

- 일괄적용 (선택 영역 데이터 한번에 변경)

 

 

 

 

프로젝트 파일(VS2013 작성) :  

 

DLL.zip

 

Source.zip

 

 

 

 

 

 

1.   CodeDom Lambda 비교

 

 

CodeDom

Lambda

변수 선언

가능                                      

불가 (단 하나의 표현식만 가능)

함수 작성

사용자가 C# 문법을 알아야함

간단하게 식 사용

Math 및 기타

라이브러리 지원

지원

지원(lambda-parser 오픈소스 사용)

컴파일 오류

오류 위치 확인 가능

오류 위치 확인 어려움

컴파일 형태

소스코드->CodeDom 그래프->IL 컴파일 (개발 복잡)

1.     Dll 파일 형태

2.     메모리 형태

소스코드->Expression tree->IL 컴파일

(소스코드 관리 편함)

1.     메모리 형태

호출 속도

1.     Func Delegate로 컴파일시 빠름, 단 함수 형태가 정의 되어있어야함

2.     DynamicInvoke는 느리지만 유연함

 

 

 

 

 

 

 

1.1 변수 선언

Lambda는 단 하나의 표현식만 가능하다. 모든 클래스나 메소드, 또는 선언문은 위해 설계되지 않아서 변수 사용은 불가능하다.

 

- CodeDom

 

-Lambda

 

 

 

 

1.2 Math 및 기타 라이브러리 사용

-CodeDom

 

 

-Lambda

 

 

 

1.3 컴파일 오류

-CodeDom

 

-Lambda

 

1.4 컴파일

CodeDOM CodeDOM 그래프, Lambda는 익스프레션 트리 형태로 변환되어 컴파일 된다. 익스프레션 트리는 CodeDOM과 크게 다를 바가 없지만, 두가지 차이가 있다.

첫째, 익스프레션 트리는 단 하나의 표현식만 가능하다. 모든 클래스나 메소드, 또는 선언문은 위해 설계되지 않았다.

둘째, C#은 언어에서 람다 식과 마찬가지로 직접적으로 익스프레션 트리를 지원한다.

 

-CodeDom

Windows에서는 개별 어셈블리 언로드를 지원하지 않는다. 대신 프로그램 도메인을 언로드하면 어셈블리도 같이 언로드가 된다. 하나의 프로세스에서는 주 프로그램 도메인(1)과 보조 프로그램 도메인(N)을 만들 수 있다. 보조 프로그램 도메인을 하나 생성하여 그 도메인에 어셈블리를 로드하여 사용하고, 재 컴파일시 도메인을 언로드하고 다시 로드하는 방식으로 수행하면 프로그램을 재시작하지 않고 사용자 정의 함수를 호출 할 수 있다.

using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace CalcDynamicCompile {     class AssemblyHelper :IDisposable     {         AppDomain newAppDomain;         AssemblyLoader proxy;                  internal void LoadAssembly(string assemName)         {             AppDomainSetup setup = new AppDomainSetup();             setup.ShadowCopyFiles = "true";             newAppDomain = AppDomain.CreateDomain("newDomain"null, setup);             proxy = newAppDomain.CreateInstanceAndUnwrap(typeof(AssemblyLoader).Assembly.FullName, 

typeof(AssemblyLoader).FullName) as AssemblyLoader;             proxy.LoadAssembly(assemName);         }         internal string LoadCode()         {             return proxy.LoadCode();         }                  internal void CreateInstance()         {             this.proxy.CreateInstance();                      }         internal void CreateDelegate(string methodName)         {             this.proxy.CreateDelegate(methodName);         }

        void IDisposable.Dispose()         {             proxy = null;             AppDomain.Unload(newAppDomain);             newAppDomain = null;         }         internal void Test(object[] param)         {             this.proxy.Test(param);         }     } }

 

 문제점은 도메인간에는 데이터가 공유되지 않기 때문에, MarshalByRefObject(응용 프로그램 도메인 간 경계를 넘어 개체에 엑세스할 수 있는 Abstract Class) Class를 상속 받은 개체에서만 사용자 정의 함수를 호출 할 수 있다. AppDomain으로 메소드 호출시에 Type이나 Assembly 등의 개체가 전달되거나 반환되면 상대방 AppDomain 측에서 해당 어셈블리의 로드가 자동으로 일어난다. 따라서 어셈블리의 동적 로드와 언로드의 목적 달성을 위해서는 이런 종류의 호출은 피해야 한다.

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Reflection; using System.Diagnostics; namespace CalcDynamicCompile {     public class AssemblyLoader : MarshalByRefObject     {         Assembly assem;         object instance;

Delegate dele;         Func<floatfloatfloat> func;         public void LoadAssembly(string filename)         {             assem = Assembly.LoadFile(filename);         }         public override object InitializeLifetimeService()         {             // 이 객체가 원격 도메인 유휴 시간에 의해 자동으로 삭제되지 않게 함             return null;         }         internal string LoadCode()         {             string code = string.Empty;             string[] list = assem.GetManifestResourceNames();             if (list.Length > 0)             {                 using (System.Resources.ResourceReader sr = new System.Resources.ResourceReader(assem.GetManifestResourceStream(list[0])))                 {                     string resourceType;                     byte[] resourceData;                     sr.GetResourceData("code.txt"out resourceType, out resourceData);                     code = Encoding.ASCII.GetString(resourceData).Remove(0, 4);                 }             }             return code;         }         internal void CreateInstance()         {             Type type = assem.GetExportedTypes()[0];             instance = Activator.CreateInstance(type);         }         internal void CreateDelegate(string methodName)         {             Type type = assem.GetExportedTypes()[0];             MethodInfo info = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); ;             List<Type> tArgs = new List<Type>();             foreach (var param in info.GetParameters())                 tArgs.Add(param.ParameterType);             tArgs.Add(info.ReturnType);             Type delDecltype = System.Linq.Expressions.Expression.GetDelegateType(tArgs.ToArray());             this.dele = Delegate.CreateDelegate(delDecltype, this.instance, info);             this.func = (Func<floatfloatfloat>)this.dele;         }         internal void Test(object[] param)         {             Stopwatch sw = Stopwatch.StartNew();             float res = 0;             for (int i = 0; i < 100000; i++)             {                 if (param == null)                 {                     res = (float)this.dele.DynamicInvoke(null);                 }                 else                 {                     res = (float)this.dele.DynamicInvoke(param);                 }             }             sw.Stop();             Console.WriteLine(string.Format("DynamicInvoke : {0}", sw.Elapsed.ToString()), res.ToString());             sw = Stopwatch.StartNew();             for (int i = 0; i < 100000; i++)             {                 res = this.func((float)param[0], (float)param[1]);             }             sw.Stop();             Console.WriteLine(string.Format("FuncInvoke : {0}", sw.Elapsed.ToString()), res.ToString());             sw = Stopwatch.StartNew();             for (int i = 0; i < 100000; i++)             {                 res = ((float)param[0] * (float)param[1]) / 2 + 3 - 2 * 321;             }             sw.Stop();             Console.WriteLine(string.Format("Call : {0}\n", sw.Elapsed.ToString()), res.ToString());         }         float Test(float a, float b)         {             return (a * b) / 2 + 3 - 2 * 321;         }     }

 

 Code 저장 : 사용자가 작성한 Code를 저장하기 위해선 다른 Text파일로 저장 및 관리 해두는 방법과 Dll 파일의 EmbeddedResource 기능을 이용하는 방법이 있다.

컴파일시 ResourceWriter 개체를 이용해 Resource를 만들어서 사용자가 작성한 코드를 Resource 파일로 작성 후 Recource 파일을 어셈블리에 추가시키면 된다.

System.CodeDom.Compiler.CompilerParameters parameters = new CompilerParameters();
parameters.GenerateExecutable = false;
parameters.IncludeDebugInformation = false;
parameters.GenerateInMemory = true;
 
System.Resources.ResourceWriter writer = new System.Resources.ResourceWriter("Resources.resx");
writer.AddResource("code.txt"Encoding.ASCII.GetBytes(code));
writer.Generate();
writer.Close();
parameters.EmbeddedResources.Add("Resources.resx");
 
 
parameters.ReferencedAssemblies.Add("System.dll");
parameters.ReferencedAssemblies.Add("System.Core.dll");
parameters.ReferencedAssemblies.Add("System.Data.dll");
parameters.ReferencedAssemblies.Add("System.Drawing.dll");
parameters.ReferencedAssemblies.Add("System.Windows.Forms.dll");
 
parameters.OutputAssembly = DllName;
 
CompilerResults r = CodeDomProvider.CreateProvider("CSharp").CompileAssemblyFromSource(parameters, code);
if (r.Errors.HasErrors == true)
{
    CompilerError err = r.Errors[0];
    MessageBox.Show(string.Format("Line:{0}, Column:{1}, ErrorNumber{2}\r\nMessage:{3}"
       err.Line.ToString(), err.Column.ToString(), err.ErrorNumber.ToString(), err.ErrorText));
}

로드시에는 역으로 어셈블리에서 Resource를 검색 후 ResourceReader 개체를 이용해 Resource를 읽는다.

string code = string.Empty;
 
if (File.Exists(DllName))
{
    using (AssemblyHelper assem = new AssemblyHelper())
    {
        assem.LoadAssembly(Application.StartupPath + "\\" + DllName);
        code = assem.LoadCode();
    }
}
 
return code;

 

-Lambda

코드 형태의 문자열을 Lambda식으로 변경하는 기능은 .Net에서 제공해주지 않아서 직접구현하거나 오픈 소스를 이용해야한다. http://code.google.com/p/lambda-parser/ 오픈소스를 이용하면 응용 프로그램에 로드된 라이브러리는 모두 사용 할 수 있다. Parse 메서드는 String 형태의 Code와 파라미터의 타입 정보, 사용할 클래스의 NameSpace를 넘겨주면 Expression을 생성한다. 그 이후 Compile 메서드를 통해 Delegate를 생성한다.

 

string code = this.tbCode.Text.Trim().Split('=')[1];
if (string.IsNullOrEmpty(code))
    return;
code = this.tbCode.Text;
 
 
LambdaExpression ex = ExpressionParser.Parse(code, nullthis.parameters.ToArray(), 
           "System""System.Collections.Generic");
 
this.dele = ex.Compile();
 
this.func = (Func<floatfloatfloat>)dele;
 
MessageBox.Show("Success");

 

 

 

1.5 CodeDom, Lambda 테스트

테스트 결과

  컴파일 모드 : Release

테스트한 식 : (a * b) / 2 + 3 - 2 * 321

테스트 값 : float a = 2, float b = 3

호출 테스트 수 : 100,000

총 테스트 수: 6

 

CodeDom, Lambda 테스트 코드

Stopwatch sw = Stopwatch.StartNew();
 
float res = 0;
 
for (int i = 0; i < 100000; i++)
{
    if (param == null)
    {
        res = (float)this.dele.DynamicInvoke(null);
    }
    else
    {
        res = (float)this.dele.DynamicInvoke(param);
    }
}
sw.Stop();
 
Console.WriteLine(string.Format("DynamicInvoke : {0}", sw.Elapsed.ToString()), res.ToString());
 
 
sw = Stopwatch.StartNew();
 
for (int i = 0; i < 100000; i++)
{
    res = this.func((float)param[0], (float)param[1]);
}
 
sw.Stop();
 
Console.WriteLine(string.Format("FuncInvoke : {0}", sw.Elapsed.ToString()), res.ToString());
 
sw = Stopwatch.StartNew();
 
for (int i = 0; i < 100000; i++)
{
    res = ((float)param[0] * (float)param[1]) / 2 + 3 - 2 * 321;
}
 
sw.Stop();
 
Console.WriteLine(string.Format("Call : {0}\n", sw.Elapsed.ToString()), res.ToString());

 

 

CodeDom 결과 :

Dynamic

Func

CSharp

테스트1

0.0648687

0.0008836

0.0004886

테스트2

0.0615519

0.0008796

0.0004813

테스트3

0.0621032

0.0008879

0.0004833

테스트4

0.0618416

0.0008455

0.0004631

테스트5

0.0614814

0.0008925

0.0004909

테스트6

0.0619039

0.0008714

0.0004929

평균

0.062291783

0.00087675

0.00048335

Call 기준(배수)

128.8751078

1.813902969

1

1 CodeDom 테스트 결과

 

Lambda 결과 :

Dynamic

Func

CSharp

테스트1

0.0962379

0.0009402

0.0004741

테스트2

0.0890706

0.000969

0.0005181

테스트3

0.07660805

0.0009647

0.0005141

테스트4

0.065423

0.0010326

0.0004853

테스트5

0.0898079

0.0009634

0.0004621

테스트6

0.0904247

0.00097

0.000487

평균

0.084595358

0.000973317

0.000490117

CSharp 기준(배수)

172.6024926

1.985887714

1

2 Lambda 테스트 결과

1.6  결론

지금까지 CodeDom Lambda를 테스트 해보았다. 속도 테스트 결과 CodeDom이 약간 우세하였다. 내가 생각하는 장단점은 아래와 같다. 둘 중 하나를 선택하라면 Lambda식이 적합한 것 같다.

 

CodeDom의 장점은 변수의 선언 및 복잡한 식의 계산을 쉽게 할 수 있다는 것이다. 단점은 사용자가 C# 언어를 학습 해야 하고, 함수의 네이밍, 메모리 관리 등 개발이 복잡하다는 것이다.

Lambda의 장점은 간편한 식을 작성 할 수 있고, 사용자에게 친숙하고, 함수의 관리가 편하다. 단점은 변수의 선언 및 복잡한 식은 설정하기 어렵다.

 

프로젝트 : CalcDynamicCompile.zip 

 

 

1.   Math Expression Parser 비교

 

1.1 DataTable

.Net에서 제공하는 DataTable Class Compute 메서드를 이용해 식을 계산한 코드이다. 먼저 a, b 문자를 값으로 치환한 후 계산을 진행한다. Compute 메서드의 반환값은 object로써 type에 맞게 캐스팅 해주어야하는 단점이 있다. 테스트는 아래와 같이 진행하였다.

object res = null;
DataTable dt = new DataTable();
 
 
string exp = expression;
foreach (KeyValuePair<stringstring> item in values)
{
    exp = exp.Replace(item.Key, item.Value);
}
 
Stopwatch st = Stopwatch.StartNew();
for (int i = 0; i < 100000; i++)
{
    res = dt.Compute(exp, null);
    Type type = res.GetType();
 
    if (type == typeof(int))
    {
        int a = (int)res;
    }
    else
    {
        double b = (double)res;
    }
}
st.Stop();
Console.WriteLine(string.Format("DataTable = {0} : {1}", st.Elapsed.ToString(), res.ToString()));

 

1.2 Ncalc

CodePlex에 등록되어 있는 오픈소스로써 System.Math 클래스의 메서들과 사용자 정의 함수를 지원한다. 사용자 정의 함수는 이벤트 방식으로 함수명으로 비교해서 개발자가 직접 처리를 해주어야 한다.

Debug.Assert(0 == new Expression("Sin(0)").Evaluate());

  Debug.Assert(2 == new Expression("Sqrt(4)").Evaluate());

Debug.Assert(0 == new Expression("Tan(0)").Evaluate());

 

//함수 처리

Expression e = new Expression("SecretOperation(3, 6)");

  e.EvaluateFunction += delegate(string name, FunctionArgs args)

      {

          if (name == "SecretOperation")

              args.Result= (int)args.Parameters[0].Evaluate() + (int)args.Parameters[1].Evaluate();

      };

  Debug.Assert(9 == e.Evaluate());

 

테스트는 아래와 같이 진행하였다.

object res = null;
NCalc.Expression ex = new NCalc.Expression(expression);
 
ex.Parameters.Clear();
 
Dictionary<stringobject> v = new Dictionary<stringobject>();
 
foreach (KeyValuePair<stringstring> item in values)
{
    ex.Parameters.Add(item.Key, Convert.ToSingle(item.Value));
}
 
Stopwatch st = Stopwatch.StartNew();
 
 
for (int i = 0; i < 100000; i++)
{
    res = ex.Evaluate();
}
 
st.Stop();
Console.WriteLine(string.Format("NCalc = {0} : {1}", st.Elapsed.ToString(), res.ToString())); 

 

1.3 Flee

CodePlex에 등록되어 있는 오픈 소스로써 Fast Lightweight Expression Evaluator라는 이름과 같이 테스트한 오픈소스 중에서는 가장 빠르다. System.Math Class 및 타 Class를 등록해서 사용 할 수있고, 사용자 정의 함수 기능을 제공한다. 사용자 정의 함수는 문자열로 정의 할 수 있지만, 매개변수가 바뀔시 Recalculate라는 메서드를 호출하여 엔진 내부를 수정 해야 하는 문제가 있다.

 

// 함수 테스트
CalculationEngine engine = new CalculationEngine();
ExpressionContext context = new ExpressionContext();
 
 
context.Variables.Add("a", 2);
context.Variables.Add("b", 2);
 
engine.Add("Func1""(a*b) + 2.0", context);
 
 
Stopwatch st = Stopwatch.StartNew();
double res = 0;
for (int i = 0; i < 800; i++)
{
    res = engine.GetResult<double>("Func1");
    context.Variables["a"] = 3;
    engine.Recalculate("Func1");
}
st.Stop();
Console.WriteLine(string.Format("FleeFunc = {0} : {1}", st.Elapsed.ToString(), res.ToString()));

 

테스트는 아래와 같이 진행하였다.

ExpressionContext context = new ExpressionContext();
 
double res = double.NaN;
 
context.Variables.Clear();
            
foreach (KeyValuePair<stringstring> item in values)
{
    context.Variables[item.Key] = Convert.ToDouble(item.Value);
}
 
IGenericExpression<double> exp = context.CompileGeneric<double>(expression);
 
Stopwatch st = Stopwatch.StartNew();
for (int i = 0; i < 100000; i++)
{
    res = exp.Evaluate();
}
st.Stop();
Console.WriteLine(string.Format("Flee = {0} : {1}", st.Elapsed.ToString(), res.ToString()));

 

1.4 MathParseNet

CodeProject에 등록되어 있는 오픈 소스로써 sin, cos, tg, ctg 등 기본적인 수학 함수를 제공한다. 식의 해석과 동시에 값을 반환하는 형식이여서 오픈 소스 중에 가장 느렸고, 반환 값의 타입은 int, double 두개의 타입만 존재한다.

MathParserNet.Parser parser = new MathParserNet.Parser();
 
double res = double.NaN;
parser.RemoveAllVariables();
 
foreach (KeyValuePair<stringstring> item in values)
{
    parser.AddVariable(item.Key, item.Value);
}
 
Stopwatch st = Stopwatch.StartNew();
 
for (int i = 0; i < 100000; i++)
{
    MathParserNet.SimplificationReturnValue ret = parser.Simplify(expression);
    if (ret.ReturnType == MathParserNet.SimplificationReturnValue.ReturnTypes.Integer)
    {
        res = ret.IntValue;
    }
    else
    {
        res = ret.DoubleValue;
    }
}
st.Stop();
Console.WriteLine(string.Format("MathParseNet = {0} : {1}", st.Elapsed.ToString(),res.ToString()));

 

 

 

1.5  테스트

  컴파일 모드 : Release

테스트한 식 : (a * b) / 2 + 3 - 2 * 321

테스트 값 : float a = 2, float b = 3

호출 테스트 수 : 100,000

 

테스트 결과:

CSharp

Lambda

DataTable

Ncalc

Flee

MathParseNet

Test1

0.000352

0.000498

0.433453

0.205334

0.017759

14.45983

Test2

0.000259

0.0005

0.350152

0.204596

0.017735

14.61664

Test3

0.00028

0.000492

0.34059

0.154331

0.017455

14.54161

Test4

0.000243

0.000489

0.339338

0.154261

0.017464

14.52951

Test5

0.000251

0.000552

0.3525

0.206181

0.018056

14.5986

평균

0.000277

0.000506

0.363207

0.184941

0.017694

14.54924

CSharp 기준(배수)

1

1.827653

1311.215

667.656

63.87588

52524.33

3 Parser 테스트 결과

 

1.6 결론

Flee 오픈소스가 가장 빠르지만, CSharp으로 하드코딩한 연산에 비해 63배나 느렸다. Parser들은 속도에 민간한 프로젝트에 적용하기는 힘들 것 같다.

 

 

프로젝트 :AboutCalculParser.zip

 

 

C# Excel 2007 추가기능(AddIn) 만들기

 

엑셀 리본 컨트롤에 새로운 탭을 만들어서, 사용자가 선택한 셀들의 합과 평균을 메시지 창에 보여주는 작업을 진행하겠습니다.

 

 

 

우선 프로젝트를 하나 생성하겠습니다. 설치된 테블릿에서 Office를 선택후 Excel 2007 추가 기능을 선택 후 솔루션 이름을 “AboutExcelAddIn”으로 생성합니다.

 

 

 

생성 후 프로젝트에서 오른쪽 마우스를 클릭하여 새 항목 추가를 클릭합니다. 리본(비주얼 디자이너)를 선택하여 생성합니다.

 

 

 

리본을 생성 후에 솔루션 탐색기를 확인해보시면 아래와 같습니다.

 

 

 

 

먼저 Ribbon1.cs를 클릭합니다. 아래의 이미지와 같이 ButtonGroup Sum, Avg Button을 생성합니다.

 

 

Sum Name btnSum, Avg Name btnAvg로 지정합니다. 더블 클릭하여 버튼이 클릭이 되었을 때 이벤트들을 등록합니다.

 

ThisAddIn 코드를 보시면 기본적으로 아래와 같은 코드가 자동으로 추가되어 있는 것을 확인하실수 있습니다.

 

namespace AbooutExcelAddIn
{
    public partial class ThisAddIn
    {
        private void ThisAddIn_Startup(object sender, System.EventArgs e)
        {
            
        }
 
        private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
        {
           
        }
 
        #region VSTO generated code
 
        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InternalStartup()
        {
            this.Startup += new System.EventHandler(ThisAddIn_Startup);
            this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
        }
        
        #endregion
    }
}

 

 

Ribbon 개체를 생성 후 Sum Avg 버튼에 대한 Click 이벤트를 등록하였습니다. Application 개체를 통해서 Excel에 액세스 할 수 있습니다. 아래의 코드에서는 사용자가 선택한 Cell들을 가져오는 작업을 진행하였습니다. dynamic이기 때문에 Intelligence를 확인 하실 수 없습니다. 구글링을 통해 API를 검색하시거나 디버깅해서 어떤 값들이 있는지 확인하셔야 합니다.

 

 

using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Office.Tools.Ribbon; using Excel = Microsoft.Office.Interop.Excel; using Microsoft.Office.Interop.Excel; namespace AbooutExcelAddIn {     public partial class Ribbon1     {         void btnAvg_Click(object sender, RibbonControlEventArgs e)         {             double sum = 0;             int count = 0;             Application app = Globals.ThisAddIn.Application;             foreach (var item in app.Selection.Cells)             {                 string text = item.FormulaLocal;                 double value = 0;                 if (double.TryParse(text, out value))                 {                     sum += value;                     count++;                 }             }             double avg = sum / count;             System.Windows.Forms.MessageBox.Show(avg.ToString());         }         void btnSum_Click(object sender, RibbonControlEventArgs e)         {             double sum = 0;             Application app = Globals.ThisAddIn.Application;             foreach (var item in app.Selection.Cells)             {                 string text = item.FormulaLocal;                 double value = 0;                 if (double.TryParse(text, out value))                     sum += value;             }             System.Windows.Forms.MessageBox.Show(sum.ToString());         }     } } 

Ribbon을 추가하는 작업을 하지 않았는데, 어떻게 추가되었는지 궁금하신분들은, Ribbon Designer Class를 확인해보시면 답이나옵니다.

 

 

완료되었습니다. 컴파일을 진행 하신후 실행하게 되면 아래와 같이 동작하는 Excel 추가기능을 작성하신겁니다.

 

 

 

Excel 옵션->추가 기능에 가보시면 작성한 추가 기능이 등록된 것을 확인 할 수 있습니다.

 

 

 

프로젝트 파일 :

AbooutExcelAddIn.zip

 

+ Recent posts