BCL 문법 정리
목차
시간
using System.Diagnostics; //타이머를 쓰려면 선언해야 한다.
namespace ConsoleApplication1
{
class TimeEx
{
public static void Main()
{
DateTime now = DateTime.Now;
Console.WriteLine(now); //현재 시간 출력
Console.WriteLine(now.Ticks); //기준시간 1년 1월 1일. 유닉스나 자바에선 1970년 1월 1일
DateTime utcNow = DateTime.UtcNow;
Console.WriteLine(utcNow); //협정 세계시 출력 .Local로 하면 지역시간 반영
DateTime myBirthday = new DateTime(now.Year, 1, 24);
Console.WriteLine(myBirthday.Kind); //Local이나 Utc인지를 리턴하는데, 지정하지 않았으므로 Unspecified 출력
DateTime endOfYear = new DateTime(DateTime.Now.Year, 12, 31);
TimeSpan dayGaps = endOfYear - now; //DateTime의 빼기연산해서 나온 결과값은 TimeSpan으로 나온다.
Console.WriteLine(dayGaps.Days); //Day외에도 시간, 분, 초 , 밀리초 단위도 출력 가능
DateTime before = DateTime.Now; //Sum() 메서드의 수행 시간을 측정하는 부분
Sum();
DateTime after = DateTime.Now;
long gap = after.Ticks - before.Ticks; //Ticks의 기준은 100ns.
Console.WriteLine(gap);
Stopwatch st = new Stopwatch(); //스탑워치 기능도 쓸 수 있다.
st.Start();
Sum();
st.Stop();
Console.WriteLine("total Ticks : " + st.ElapsedTicks);
}
private static int Sum()
{
int sum = 0;
for (int i = 0; i < 1000000; i++)
{
sum += i;
}
return sum;
}
}
}
문자열 처리
String
class StringEx
{
public static void Main()
{
string txt = "Hello World!";
string[] stringArr;
Console.WriteLine(txt.Contains("Hello")); //true
Console.WriteLine(txt.Contains("WORLD")); //false
Console.WriteLine(txt.EndsWith("rld!")); //true
Console.WriteLine(txt.EndsWith("Hello")); //false
Console.WriteLine(txt.GetHashCode()); //해시값 반환
Console.WriteLine("Hello World!".GetHashCode()); //이렇게 해도 똑같은 값 반환.
Console.WriteLine(txt.IndexOf("World!")); //해당 문자열 포함 시 그 문자열의 인덱스 리턴, 아니면 -1
Console.WriteLine(txt.IndexOf("Dog")); // -1
Console.WriteLine(txt.Replace("World", "c#")); //앞 문자열을 뒷 문자열로 대체 후 전체 리턴 Hello c#! 출력
stringArr = txt.Split('o'); //'o'를 기준으로 나눈 문자열들을 배열로 리턴
OutputArrayString(stringArr);// "Hell", " W", "rld!" 출력
Console.WriteLine(txt.StartsWith("Hel")); //true
Console.WriteLine(txt.StartsWith("hel")); //false
Console.WriteLine(txt.Substring(4)); //4번째부터 끝까지 출력. "o World!" 출력
Console.WriteLine(txt.Substring(4, 2)); //4번째부터 길이 2만큼 "o " 출력
//Console.WriteLine(txt.Substring(4, 50)); //ArgumentOutOfRangeException 발생
Console.WriteLine(txt.ToLower()); //소문자 변환 후 리턴
Console.WriteLine(txt.ToUpper()); //대문자 변환 후 리턴
Console.WriteLine(txt); //변환 내용이 유지되는 것은 아니다. 그대로 Hello World! 출력
Console.WriteLine(txt.Trim('!', 'H')); //문자열의 앞뒤에 해당 문자가 있으면 삭제
//매개변수 입력하지 않으면 앞뒤에 있는 공백 삭제
Console.WriteLine(txt.Length); //문자열의 길이 리턴, 12
Console.WriteLine(txt != "Hello World!"); //false
Console.WriteLine(txt == "Hello World!"); //true;
Console.WriteLine(txt[4]); //해당 위치에 있는 문자 반환
int a = 100;
int b = 50;
int formatInt = 123456;
double formatDouble = 123546.789;
string formatTxt = String.Format("A의 값은 {0}입니다. B의 값은 {1}이구요, 이 둘을 더한 값은 {2}입니다.", a, b, a + b);
Console.WriteLine(formatTxt);
formatTxt = String.Format("A의 값은 {0}입니다. B의 값은 {1}이고,, 이 둘을 더한 값은 {2}입니다. 뺀 값은 {3}입니다", a, b, a + b);
//매개변수 개수보다 인덱스가 더 많으면 예외 발생.
//인덱스[, 정렬][:형식] 순으로 지정한다. //정렬은 양수면 우측, 음수면 좌측 정렬을 한다.
Console.WriteLine("정수 형식 : {0,20:d10}", formatInt); //정수 형식. 123456에 10자리를 맞추기 위해 0000123456이 출력된다.
Console.WriteLine("숫자 형식 : {0,20:n10}", formatDouble); //숫자 형식. 123456.789에 .789를 포함해서 10자리 소수점이 맞춰진다.
Console.WriteLine("퍼센트 형식 : {0,-20:p3}", a); //백분율 형식.100으로 곱하고 백분율 기호와 함께 표시된다.
// p뒤의 숫자는 소수점 자리수를 의미한다.
Console.WriteLine("16진수 형식 : {0,-20:x10}", formatInt); //16진수 형식. 10자리를 맞추기 위해 앞에 0이 표시된다.
Console.WriteLine("날짜/시간 형식 : {0,-20:F}", DateTime.Now);
}
private static void OutputArrayString(string[] arr)
{
foreach (string txt in arr) Console.WriteLine(txt);
}
}
StringBuilder
class StringBuilderEx
{
public static void Main()
{
string txt = "Hello World!";
string txt2 = "Hello World!";
Stopwatch st = new Stopwatch();
st.Start();
for (int i = 0; i < 300000; i++)
{
txt = txt + "1"; //매번 늘어난 크기의 공간을 새롭게 할당하기 때문에, 30만번의 메모리 할당과 복사 때문에 많은 시간이 걸린다.
}
st.Stop();
Console.WriteLine("String으로 작업하는데 걸린 시간 : " + st.Elapsed); //18초 정도 걸린다.
StringBuilder sb = new StringBuilder(txt2);
//StringBuilder 내에 일정한 메모리를 할당하고, txt2를 복사한다. 공간이 부족하면 공간을 2배로 할당한다.
st.Start();
for( int i = 0; i < 300000; i++)
{
sb.Append("1");
}
st.Stop();
Console.WriteLine("StringBuilder로 작업하는데 걸린 시간 : " + st.Elapsed); //2밀리초 정도 걸린다.
}
}
Text Encoding
Text.Encoding에선 인코딩 타입을 제공해준다. ASCII, Default, Unicode(UTF16), UTF32, UTF8 등이 주로 쓰인다.
class TextEncodingEx
{
public static void Main()
{
const string fileName = "UTF8Test.txt";
byte[] dataArray = null;
using (System.IO.FileStream
fileStream = new FileStream(fileName, FileMode.Open))
{
dataArray = new byte[fileStream.Length];
fileStream.Read(dataArray, 0, dataArray.Length);
string data = Encoding.UTF8.GetString(dataArray);
//UTF8로 생성된 txt파일을 정상적으로 읽어온다. BOM문자도 읽어와서 ?로 뜬다.
Console.WriteLine(data);
}
}
}
정규 표현식
정규 표현식 표기법은 여기 를 참고한다.
class RegularExpressionEx //정규 표현식을 위한 클래스이다.
{
public static void Main()
{
string email = "black개dog14@gmail.com";
string email2 = "blackdog14@★gmail.com";
Console.WriteLine(IsEmail(email)); //true
Console.WriteLine(IsEmail(email2)); //false
Console.WriteLine(isEmail2(email)); //true;
Console.WriteLine(isEmail2(email2)); //false;
string txt = "Hello World! Welcome to my world!";
Regex regex = new Regex("world", RegexOptions.IgnoreCase); //ignoreCase옵션을 넣어주면 대소문자 구분 없이 비교한다.
string result = regex.Replace(txt, "Universe"); //입력한 정규식 패턴과 일치하면 대체해준다.
Console.WriteLine(txt); //원래 문자열 출력
Console.WriteLine(result); //world가 Universe로 대체된 문자열 출력
}
//내가 생각한 이메일 규칙은 1. @문자를 반드시 한 번 포함한다.
//2. @ 이전의 문자열은 문자와 숫자만 허용한다.
//3. @ 이후의 문자열은 문자와 숫자만 허용하고 반드시 1번의 .문자를 포함한다.
public static bool IsEmail(string txt)
{
string[] parts = txt.Split('@');
if (parts.Length != 2) //@ 문자를 0번 포함하거나 2번 이상 포함하면 false 리턴
{
return false;
}
if (!isAlphaNumeric(parts[0])) //@ 문자 앞의 문자열이 문자나 숫자가 아니면 false 리턴
{
return false;
}
parts = parts[1].Split('.');
if (parts.Length != 2) //. 문자를 0번 포함하거나 2번 이상 포함하면 false 리턴
{
return false;
}
if( isAlphaNumeric(parts[0]) && isAlphaNumeric(parts[1])) //.문자 앞 뒤의 문자열들이 문자나 숫자가 아니면 false 리턴
{
return true;
}
else
{
return false;
}
}
public static bool isAlphaNumeric(string txt) //문자나 숫자인지 판별
{
foreach(char ch in txt)
{
if( !char.IsLetterOrDigit(ch) )
{
return false;
}
}
return true;
}
public static bool isEmail2(string txt) //정규식으로 표현
{
Regex regex = new Regex(@"([0-9a-zA-Z]+)@([0-9a-zA-Z]+)(\.[0-9a-zA-Z]+)$");
return regex.IsMatch(txt);
}
}
직렬화
BitConverter
문자열은 인코딩 방식에 따라 바이트 배열로의 전환이 달라질 수 있지만, 기본 자료형은 변환 방법이 고정되어 있다. BitConverter 타입의 GetByte 메서드를 이용하면 기본 자료형을 바이트 배열로 변환할 수 있다. 그리고 바이트 배열의 16진수 값을 Tostring 메서드를 통해 제공한다.
class SerialEx
{
public static void Main()
{
//기본 타입을 바이트 배열로 변환
byte[] boolBytes = BitConverter.GetBytes(true);
byte[] shortBytes = BitConverter.GetBytes((short)32000);
byte[] intBytes = BitConverter.GetBytes((int)16661151);
//바이트 배열을 다시 기본 타입으로 복원
bool boolResult = BitConverter.ToBoolean(boolBytes, 0);
short shortResult = BitConverter.ToInt16(shortBytes, 0);
int IntResult = BitConverter.ToInt32(intBytes, 0);
//복원 확인
Console.WriteLine(boolResult.ToString()); //true 출력
Console.WriteLine(shortResult.ToString()); //32000 출력
Console.WriteLine(IntResult.ToString()); // 16661151 출력
//바이트 배열을 16진수 문자열로 표현
//BitConverer 후, 출력 값이 엔디안 방식에 따라 cpu별로 다르게 표시 될 수 있다.
Console.WriteLine(BitConverter.ToString(boolBytes));
Console.WriteLine(BitConverter.ToString(shortBytes));
Console.WriteLine(BitConverter.ToString(intBytes));
int n = 1652300;
string text = n.ToString(); // int를 string으로 직렬화.
int result = int.Parse(text); // string을 int로 역직렬화.
Console.WriteLine(result); // 이 경우에는 직렬화 수단이 바이트 배열이 아닌 문자열이 된 것.
}
}
MemoryStream
메모리에 바이트를 순서대로 읽고 쓰는 작업을 수행하는 클래스다.
class MemoryStreamEx
{
public static void Main()
{
//직렬화
byte[] shortByte = BitConverter.GetBytes((short)32000); //short 32000를 바이트 배열로 직렬화
byte[] intByte = BitConverter.GetBytes(16522300); //int 16522300을 바이트 배열로 직렬화
MemoryStream ms = new MemoryStream(); //바이트 배열을 읽고 쓰기 위해서 MemoryStream 객체 생성
ms.Write(shortByte, 0, shortByte.Length); //ms 객체에 shortByte 기록 (2바이트)
ms.Write(intByte, 0, intByte.Length); //ms 객체에 intByte 기록 (4바이트)
ms.Position = 0; //ms에 순서대로 저장된 바이트 배열들을 다시 읽어오기 위해서 포지션을 0으로 설정
//역직렬화
byte[] outByte = new byte[2];
ms.Read(outByte, 0, 2); //outByte에는 ms의 0~1 사이에 있는 값들을 읽어온다. position은 2로 이동
short shortResult = BitConverter.ToInt16(outByte, 0); //바이트 배열을 short로 변환
Console.WriteLine(shortResult); //32000 출력
outByte = new byte[4];
ms.Read(outByte, 0, 4); //outByte에는 ms의 2~5 사이에 있는 값들을 읽어온다. position은 5로 이동
int intResult = BitConverter.ToInt32(outByte, 0); //바이트 배열을 int로 변환
Console.WriteLine(intResult); //16522300 출력
byte[] arr = ms.ToArray(); //ms를 바로 바이트 배열로 변환 가능.
short shortResult2 = BitConverter.ToInt16(arr, 0); //arr의 0번부터 변환
Console.WriteLine(shortResult2); //32000 출력
int intResult2 = BitConverter.ToInt32(arr, 2); //arr의 2번부터 변환
Console.WriteLine(intResult2); //16522300 출력
//Byte 배열에는 Position 기능이 없으므로 변환하려는 바이트의 위치를 직접 지정해줘야 한다.
}
}
StreamWriter/Reader
class StreamWREx
{
public static void Main()
{
//Stream에 문자열을 쓰려면 Encoding을 해서 바이트 배열로 변환해야 한다.
MemoryStream ms = new MemoryStream();
byte[] buf = Encoding.UTF8.GetBytes("Hello World!");
ms.Write(buf, 0, buf.Length);
//StreamWriter는 문자열 인코딩 방식을 생성자에서 받는다. 그 이후에는 바로 해당 인코딩 방식에 따라 자동으로 변환한다.
StreamWriter sw = new StreamWriter(ms, Encoding.UTF8);
StreamReader sr = new StreamReader(ms, Encoding.UTF8);
//읽을때는 저장 인코딩 형식을 맞춰야한다. 다른 인코딩 형식으로 읽으면 깨짐.
sw.WriteLine("안녕 세상아!");
//MemoryStream에 넣을 때와는 달리 직접 인코딩을 해주지 않아도 설정해둔 방식으로 들어간다.
Console.WriteLine(sr.ReadToEnd()); //아무것도 출력되지 않는다. 아직 내부버퍼에만 있는 상태여서.
sw.Flush(); //내부 버퍼에 보관하던 문자열들을 모두 stream에 쓴다. (기본 버퍼 사이즈 = 1024바이트)
Console.WriteLine(sr.ReadToEnd());
//아무것도 출력되지 않는다. 내부버퍼에서 stream으로 보냈지만, 포지션이 뒤쪽에 있어서 아무것도 없는곳을 읽는다.
ms.Position = 0;
Console.WriteLine(sr.ReadToEnd());
//내부버퍼에서 stream으로도 보냈고, 포지션도 0으로 초기화해서 stream안의 모든 문자열들이 출력된다.
sw.WriteLine("Blackdog");
sw.WriteLine(3000); //Tostring 메서드를 통해서 "3000"이 들어간다.
sw.Flush(); //내부 버퍼에 보관하던 문자열들을 모두 stream에 쓴다.
ms.Position = 0;
string txt = sr.ReadToEnd(); //안의 모든 내용을 읽기 위해서 포지션 0으로 초기화.
Console.WriteLine(txt);
}
}
BinaryWriter/Reader
class BinaryRWEx //기본형 타입을 2진 데이터로 바꿔서 쓴다. 문자열은 UTF-8로 바꾼다.
{
public static void Main()
{
MemoryStream ms = new MemoryStream();
BinaryWriter bw = new BinaryWriter(ms);
bw.Write("Hello World!" + Environment.NewLine); //문자열은 무조건 UTF-8인코딩
bw.Write("Blackdog" + Environment.NewLine);
bw.Write(1048575); //1111(15) + 1111 1111 1111 1111(65535) = 1048575
bw.Flush(); //내부 버퍼의 내용을 강제로 스트림에 쓴다. (내부 버퍼의 크기는 16바이트)
ms.Position = 0;
BinaryReader br = new BinaryReader(ms);
string first = br.ReadString();
string second = br.ReadString();
//int test = br.ReadInt32(); //int로 받아오면 1048575로 결과값이 출력된다.
ushort third = br.ReadUInt16(); //억지로 4바이트를 2바이트로 쪼개면 65535출력
ushort fourth= br.ReadUInt16(); //15출력. 순서가 뒤바뀐 이유는 리틀 엔디안이기 때문에.
Console.WriteLine("{0}{1}{2}/{3}", first, second, third, fourth); //Hello World!, Blackdog, 65535/15 출력.
byte[] arr = ms.ToArray();
Util.PrintArr(arr); //데이터의 의미 단위마다 그 길이를 알려주는 1바이트가 있고 그 뒤에 데이터가 바이트화 되서 출력된다.
//14 hello world! 개행, 10 blackdog 개행 식으로...
}
}
BinaryFormatter
BinaryFormatter는 2진 데이터로 직렬화하기 때문에 다른 사용자 정의 클래스 직렬화 방식에 비해서 속도가 빠르고 직렬화 결과의 용량도 작다. 그리고 닷넷 응용 프로그램끼리만 직렬화해서 데이터 교환이 가능하다.
[Serializable] //Serializable 특성을 지정하면 사용자 정의 클래스 직렬화를 간편하게 할 수 있다.
class Person
{
[NonSerialized] //Serializable 특성은 기본적으로 클래스 내 모든 프로퍼티를 대상으로 직렬화를 수행한다.
public int age; //직렬화를 원치 않는 프로퍼티는 NonSerialized 특성을 지정하면 된다.
//BinaryFormatter 에서는 private으로 설정한 프로퍼티도 직렬화가 된다.
public string name;
public Person(int age, string name)
{
this.age = age;
this.name = name;
}
public override string ToString()
{
return string.Format("{0} {1}", this.age, this.name);
}
}
class BinaryFormatterEx
{
public static void Main()
{
Person person = new Person(25, "Doh");
BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
bf.Serialize(ms, person);
ms.Position = 0;
//마찬가지로 포지션을 맨 처음으로 해줘야한다. 아니면 SerializationException 출력.
Person clone = bf.Deserialize(ms) as Person;
Console.WriteLine(clone); //age에 NonSerialized를 지정하면 0으로 출력, 지정하지 않으면 입력한 그대로 25 출력.
byte[] arr = ms.ToArray(); //이 바이트 배열을 네트워크를 통해 다른 컴퓨터로 전송 후 다시 역직렬화 가능.
//다만 직렬화 방식이 닷넷 내부에 고유하게 정의되어서 다른 플랫폼에서는 역직렬화 불가.
Util.PrintArr(arr);
}
}
XmlSerializer
XmlSerializer는 클래스의 내용을 문자열로 직렬화한다. 그리고 [Serializable] 특성을 지정하지 않아도 사용할 수 있지만, 다음과 같은 제약사항이 있다.
- public 접근 제한자의 클래스여야 한다.
- 기본 생성자를 포함하고 있어야 한다.
- public 접근 제한자가 적용된 필드만 직렬화/역직렬화한다.
public class Person //public 접근 제한자의 클래스여야 XmlSerializer를 쓸 수 있다.
{
public int age; //public 으로 선언된 프로퍼티만 직렬화/역직렬화가 가능하다.
public string name;
private int gender;
public Person() //기본 생성자를 포함하고 있어야 한다.
{
}
public Person(int age, string name, int gender)
{
this.age = age;
this.name = name;
this.gender = gender;
}
public override string ToString()
{
return string.Format("{0} {1}", this.age, this.name);
}
}
class XmlSerializerEx
{
public static void Main()
{
MemoryStream ms = new MemoryStream();
XmlSerializer xs = new XmlSerializer(typeof(Person)); //직렬화하려는 타입을 설정한다.
Person person = new Person(25, "Doh", 1);
//MemoryStream에 Person을 문자열로 직렬화.
xs.Serialize(ms, person); //public으로 선언한 age와 name만 직렬화된다.
ms.Position = 0;
Person clone = xs.Deserialize(ms) as Person;
Console.WriteLine(clone); //private으로 설정한 프로퍼티는 직렬화 되지 않아서 출력되지 않음.
byte[] buf = ms.ToArray();
Console.WriteLine(Encoding.UTF8.GetString(buf));
//Xml형식으로 된 문자열을 출력한다. 기본적으로 객체를 직렬화 할 때 UTF-8 인코딩 문자열로 직렬화한다.
//XmlSerializer는 다른 플랫폼 사이에서 상호 운용성이 높다.
//하지만 실제 전송되는 데이터에 비해 형식상 붙는 데이터가 많아서 크기가 커진다.
}
}
DataContractJsonSerializer
DataContractJsonSerializer는 클래스의 내용을 Json으로 직렬화한다. XmlSerializer보다 더 적은 크기로 변환이 가능하다. 또 닷넷 이외의 여러 플랫폼 사이에서도 변환이 가능하다.
[DataContract] //직렬화하려는 클래스에 표시. public으로 선언되어 있으면 꼭 표시해주지 않아도 된다.
class Person //직렬화 대상 클래스의 접근 제한에 영향을 받는다. 다른 클래스의 nested 클래스가되서 private되면 예외가 발생한다.
{ //nested 클래스가 됐어도 [DataContract],[DataMember] 특성을 정의해주면 직렬화가 가능하다.
//그 외 접근이 가능한 상황에서는 명시적으로 선언하지 않아도 작동한다.
[DataMember]
public int age;
[DataMember] //[DataContract]를 선언했을 때, [DataMember]를 선언하지 않으면 직렬화되지 않는다.
public string name;
[DataMember]
private int gender; //private은 직렬화되지 않는다.
public Person()
{
}
public Person(int age, string name, int gender)
{
this.age = age;
this.name = name;
this.gender = gender;
}
public override string ToString()
{
return string.Format("{0} {1}", this.age, this.name);
}
}
public class JsonSerializerEx
{
public static void Main()
{
DataContractJsonSerializer dcjs = new DataContractJsonSerializer(typeof(Person));
//참조관리자를 통해서 System.Runtime.Serialization 추가해야 사용가능.
MemoryStream ms = new MemoryStream();
Person person = new Person(25, "Doh", 1);
dcjs.WriteObject(ms, person);
ms.Position = 0;
Person clone = dcjs.ReadObject(ms) as Person;
Console.WriteLine(clone);
byte[] arr = ms.ToArray();
Console.WriteLine(Encoding.UTF8.GetString(arr));
//{"age":25,"gender":1,"name":"Doh"}를 출력한다. 기본적으로 UTF-8로 인코딩하기 때문에
//읽기 위해서 UTF-8로 인코딩한다.
}
}
컬렉션
배열은 크기가 고정되어 있다. 변수 자체에서는 재할당을 통해서 크기를 바꿀 수 있지만 크기를 바꾸기 전에 할당한 값은 보존되지 않는다. 컬렉션을 이용하면 크기가 바뀌면서 값이 보존되는 배열같은 기능을 사용할 수 있다.
ArrayList
class ArrayListEx
{
public static void Main()
{
ArrayList al = new ArrayList();
al.Add("Hello"); //박싱이 발생한다.
al.Add(6); //값 형식을 담으면 박싱이 발생하기 때문에 값 형식을 담기에는 적절치 않다. 대신 List<T>가 권장된다.
al.Add("World!");
al.Add(true);
int num = (int)al[1]; //언박싱
Console.WriteLine(al.Contains(6)); //6의 포함여부 검사. true.
Console.WriteLine(al.Contains("hello")); //대소문자 불일치 때문에 false.
al.Remove("World!"); //삭제하면 [3]에 있던 True가 자동으로 [2]로 온다.
Console.WriteLine(al[2]); //true.
al[2] = false;
Console.WriteLine(al[2]); //false;
Console.WriteLine("ArrayList내 모든 요소 출력");
foreach (object obj in al)
{
Console.WriteLine(obj);
}
}
}
IComparer 인터페이스를 이용한 정렬 구현 예제이다.
class BackNumberComparer : IComparer //IComparer를 구현하는 등번호 내림차순 정렬하는 클래스를 만든다.
{
public int Compare(object x, object y)
{
Person person1 = x as Person;
Person person2 = y as Person;
if (person1.BackNumber > person2.BackNumber) return -1;
else if (person1.BackNumber == person2.BackNumber) return 0;
else return 1;
}
}
class Person //Person의 속성은 등번호와 이름이다.
{
int backNumber;
string name;
public Person()
{
this.backNumber = 0;
this.name = "무명";
}
public Person(int backNumber, string name)
{
this.backNumber = backNumber;
this.name = name;
}
public int BackNumber { get => backNumber; set => backNumber = value; }
public string Name { get => name; set => name = value; }
}
class CollectionComparerEx
{
public static void Main()
{
ArrayList al = new ArrayList(); //ArrayList에 5개의 데이터를 입력한다.
al.Add(new Person(10, "강백호"));
al.Add(new Person(11, "서태웅"));
al.Add(new Person(4, "채치수"));
al.Add(new Person(14, "정대만"));
al.Add(new Person(7, "송태섭"));
al.Sort(new BackNumberComparer()); //BackNumberComparer를 인자로 전달한다.
foreach (Person person in al)
{
Console.WriteLine(person.BackNumber + "번 " + person.Name); //등번호 내림차순으로 정렬되어 나온다.
}
}
}
IComparable 인터페이스를 이용한 정렬 구현 예제이다.
class Person : IComparable //Person의 속성은 등번호와 이름이다.
{
int backNumber;
string name;
public Person()
{
this.backNumber = 0;
this.name = "무명";
}
public Person(int backNumber, string name)
{
this.backNumber = backNumber;
this.name = name;
}
public int BackNumber { get => backNumber; set => backNumber = value; }
public string Name { get => name; set => name = value; }
public int CompareTo(object obj) //CompareTo가 자동으로 호출되면서 비교작업을 수행한다.
{
Person person = obj as Person;
if (this.BackNumber > person.BackNumber) return -1;
else if (this.BackNumber == person.BackNumber) return 0;
else return 1;
}
public override string ToString()
{
return string.Format("{0}번 {1}", BackNumber, Name);
}
}
class CollectionIComparableEx
{
public static void Main()
{
ArrayList al = new ArrayList(); //ArrayList에 5개의 데이터를 입력한다.
al.Add(new Person(10, "강백호"));
al.Add(new Person(11, "서태웅"));
al.Add(new Person(4, "채치수"));
al.Add(new Person(14, "정대만"));
al.Add(new Person(7, "송태섭"));
al.Sort(); //안의 요소들이 IComparable을 구현하기 때문에 해당 요소의 CompareTo 메서드를 자동으로 호출해서 비교작업을 수행한다.
foreach (Person person in al)
{
Console.WriteLine(person); //등번호 내림차순으로 정렬되어 나온다.
}
}
}
Hashtable
Key값을 통해서 Value를 찾는다. ArrayList 같은 경우에 요소를 검색할 때 순차적으로 접근하지만, Hashtable은 Key값의 HashCode값을 통해서 바로 접근한다. 때문에 컬렉션의 크기가 클수록 Hashtable을 쓰는게 유리하다.
class HashtableEx
{
public static void Main()
{
Hashtable ht = new Hashtable();
ht.Add("서태웅", 11); //Hashtable에 값 추가, key, value모두 object 타입으로 다뤄지기 때문에 박싱 발생.
ht.Add("강백호", 10);
ht.Add("정대만", 14);
ht.Add("채치수", 4);
ht.Add("송태섭", 7);
//ht.Add("송태섭", 3); //key값이 겹치는 경우 ArgumentException 발생
Console.WriteLine(ht["정대만"]);
ht.Remove("채치수"); //"채치수"의 value 삭제
ht["강백호"] = 100; //"강백호" 의 value 변경
foreach (object key in ht.Keys)
{
Console.WriteLine("{0} => {1}", key, ht[key]);
}
}
}
SortedList
SortedList는 Hashtable과 마찬가지로 key, value로 저장하지만 key자체가 정렬되어 저장된다. key값이 정렬 순서에 영향을 미친다.
class SortedListEx
{
public static void Main()
{
SortedList sl = new SortedList();
sl.Add(10, "강백호"); //값을 넣을 때 마다 key값 기준으로 정렬해서 저장된다. 숫자 오름차순.
sl.Add(11, "서태웅");
sl.Add(7, "송태섭");
sl.Add(14, "정대만");
sl.Add(4, "채치수");
foreach( object key in sl.Keys)
{
Console.WriteLine("{0}번 {1}", key, sl[key]); //숫자 오름차순으로 결과를 출력한다.
}
}
}
Stack
class StackEx
{
public static void Main()
{
Stack st = new Stack();
st.Push(1); //인자를 object로 다루기 때문에 박싱 발생
st.Push(2);
st.Push(3);
st.Push("강백호");
st.Push(7);
int last = (int)st.Pop(); //7제거하면서 last로 저장. 언박싱
st.Push(5);
last = (int)st.Peek(); //5 제거하지 않으면서 last로 저장. 언박싱
object[] arr = st.ToArray();
Util.PrintObjectArr(arr); //5, 강백호, 3, 2, 1 순으로 출력.
st.Clear();
object[] arr2 = st.ToArray();
Util.PrintObjectArr(arr2); //clear() 해서 아무것도 없기 때문에 출력되는게 없다.
}
}
Queue
class QueueEx
{
public static void Main()
{
Queue q = new Queue();
q.Enqueue(1); //매개변수를 object로 받기 때문에 박싱 발생.
q.Enqueue(2);
q.Enqueue(3);
q.Enqueue(4);
int last = (int)q.Dequeue(); //언박싱
Console.WriteLine(last); //1 출력.
q.Enqueue(5);
Console.WriteLine();
int[] copyArr = new int[5];
q.CopyTo(copyArr, 0); //2,3,4,5 복사.
Util.PrintIntArr(copyArr); //2,3,4,5,0 출력됨.
Console.WriteLine(q.Contains("강백호")); //false 출력
last = (int)q.Peek(); //맨 앞인 2를 삭제하지 않고 last에 저장.
Console.WriteLine(last); //2 출력.
Console.WriteLine();
object[] arr = q.ToArray();
Util.PrintObjectArr(arr); //2,3,4,5 출력
q.Clear();
Console.WriteLine();
arr = q.ToArray();
Util.PrintObjectArr(arr); //clear되었기 때문에 아무것도 출력하지 않는다.
}
}
제네릭
ArrayList의 박싱/언박싱 문제를 해결하기 위해서는 ArrayList가 다루는 데이터 타입을 고정시켜야 한다. 하지만 타입 별로 코드를 각각 다르게 구현해야 하는 문제점이 발생하는데, 이를 해결해주는 것이 제네릭이다.
컴파일 시 Type Parameter 정보를 갖는 IL과 메타데이터를 생성하고. IL을 JIT 컴파일 할 때 Type Parameter를 실제 타입으로 대체한다.
기본 사용법은 다음과 같다.
int n = 5;
List<int> list = new List<int>();
list.Add(n); //박싱 과정 없이 바로 넣는다.
제네릭 예제 코드
public class GenericStack<T> //제네릭 클래스, 형식 매개변수 이름은 임의지정 가능.
{
T[] objList;
int pos;
public GenericStack(int size)
{
objList = new T[size];
}
public void Push(T newValue)
{
objList[pos] = newValue;
pos++;
}
public T Pop()
{
pos--;
return objList[pos];
}
}
public class TwoGeneric<K, V> //2개 이상도 가능하다.
{
K key;
V value;
public void Set(K key, V value)
{
this.key = key;
this.value = value;
}
}
class GenericEx1
{
public static void Main()
{
GenericStack<int> intStack = new GenericStack<int>(10);
//CLR이 T에 대응되는 타입으로 대체해서 확장한다.
intStack.Push(1);
intStack.Push(2);
//intStack.Push("ㅁㅇㄹ"); 타입 에러가 난다.
intStack.Push(3);
Console.WriteLine(intStack.Pop());
Console.WriteLine(intStack.Pop());
Console.WriteLine(intStack.Pop());
WriteLog<int>(10);
WriteLog<bool>(true);
WriteLog<double>(20.158);
WriteLog<string>("logTest");
}
public static void WriteLog<T>(T item) //제네릭 메서드.
{
string output = string.Format("{0}: {1}", DateTime.Now, item);
Console.WriteLine(output);
}
//where 예약어를 이용해서 형식 매개변수의 제약조건을 정할 수 있다.
//T타입으로 지정된 item1,item2가 IComparable을 상속받은 타입이라고 가정하고 CompareTo를 호출 가능하게 한다.
//형식 매개변수의 수만큼 where 조건을 걸 수 있다.
public static T Max<T>(T item1, T item2) where T : IComparable
{
if (item1.CompareTo(item2) >= 0)
{
return item1;
}
else
{
return item2;
}
}
public class MyType<T> where T : ICollection, IConvertible //매개변수 2개에 제약조건을 건 경우
{
}
public class Dict<K, V> where K: ICollection
where V: IComparable //이렇게 매개변수 각각에 제약조건을 걸 수 있다.
{
}
//Where T: struct : T 형식 매개변수는 반드시 값 형식만 가능하다.
//Where T: class : T 형식 매개변수는 반드시 참조 형식만 가능하다.
//Where T: new() : T 형식 매개변수의 타입에는 반드시 기본 생성자가 정의돼 있어야 한다.
//Where T: U : T 형식 매개변수는 반드시 U 형식 인수(사용자가 지정한 다른 형식 매개변수)에 해당하는 타입이거나, 상속을 받은 클래스만 가능하다.
}
BCL에 적용된 제네릭
닷넷 2.0의 BCL에 추가된 컬렉션이다. 하위 호환성을 지키기 위해서 기존 컬렉션 타입은 그대로 유지하고, 각각에 대응되는 제네릭 타입을 새롭게 만들어서 System.Collections.Generic 네임스페이스에 추가했다.
기존 컬렉션 | 대응되는 제네릭 버전 컬렉션 |
---|---|
ArrayList | List<T> |
Hashtable | Dictionary<TKey, TValue> |
SortedList | SortedDictionary<TKey,TValue> |
Stack | Stack<T> |
Queue | Queue<T> |
기존 인터페이스에서도 박싱/언박싱 문제가 발생하는 경우 새롭게 제네릭 버전이 제공된다.
기존 인터페이스 | 대응되는 제네릭 버전 인터페이스 |
---|---|
IComparable | IComparable<T> |
IComparer | IComparer<T> |
IEnumerable | IEnumerable<T> |
IEnumerator | IEnumerator<T> |
ICollection | ICollection<T> |
파일
FileStream
FileMode
열거형 값 | 설명 |
---|---|
CreateNew | 파일을 항상 새로 만든다. 같은 이름의 파일이 존재하면 IOException 예외가 발생 |
Create | 파일을 생성한다. 같은 이름의 파일이 존재하면 기존 데이터를 지우고 만든다. |
Open | 이미 있는 파일을 연다. 만약 열려는 파일이 존재하지 않으면 FileNotFoundException 발생 |
OepnOrCreate | 같은 이름의 파일이 있다면 열고, 아니면 새로 만든다. |
Truncate | 이미 있는 파일을 열고, 기존 데이터는 모두 삭제한다. 만약 열려는 파일이 존재하지 않으면 FileNotFoundException 발생 |
Append | 파일을 무조건 열어서 기존 데이터 내용 뒤로 덧붙인다. 파일이 존재하지 않으면 새로 만든다. |
FileAccess
열거형 값 | 설명 |
---|---|
Read | 파일을 읽기 목적으로 연다. |
Write | 파일을 쓰기 목적으로 연다. |
ReadWrite | 파일을 읽기 및 쓰기 목적으로 연다. FileAccess.Read l FileAccess.Write 로도 쓸 수 있다. |
FileShare
열거형 값 | 설명 |
---|---|
None | 같은 파일을 두 번 이상 열면 실패한다. 맨 처음 파일을 열고 있는 FileStream만 사용 가능케 하는 설정이다. |
Read | 같은 파일에 대해 Read로 여는 것만 허용한다. 맨 처음 파일을 열고 있는 FileStream만 모든 동작이 가능하고 그 다음부터 적용한다. |
Write | 같은 파일에 대해 Write로 여는 것만 허용한다. 맨 처음 파일을 열고 있는 FileStream만 모든 동작이 가능하고 그 다음부터 적용한다. |
ReadWrite | 같은 파일에 대해 Read, Write로 여는 것 모두 허용한다. 같은 파일에 대해 서로 다른 FileStream에서 읽고 쓰는 것이 가능하다. |
class FileStreamEx
{
public static void Main()
{
using (FileStream fs = new FileStream("test.log", FileMode.OpenOrCreate))//test.log라는 파일을 없으면 생성하고 있으면 연다.
{
StreamWriter sw = new StreamWriter(fs, Encoding.UTF8);
sw.WriteLine("Hello World!");
sw.WriteLine("Blackdog!");
sw.WriteLine(32000);
sw.Flush();
}
using (FileStream fs = new FileStream("test2.log", FileMode.Append, FileAccess.Write ,FileShare.None))
//test2.log라는 파일을 없으면 만들고 있으면 기존 내용에 덧붙인다.
//실행 횟수만큼 반복해서 뒤에 덧붙여진다.
//단어의 길이를 나타내는 바이트가 앞에 덧붙여지기 때문에 메모장으로 열면 그 바이트도 문자로 인식되어 메모장에서 보인다.
{
BinaryWriter bw = new BinaryWriter(fs, Encoding.UTF8);
bw.Write("Hello World!" + Environment.NewLine);
bw.Write("Blackdog" + Environment.NewLine);
bw.Write(32000); //32000도 2진 데이터로 변환되었다가 문자열로 취급당해서 } 로 출력된다.
bw.Flush();
//using (FileStream fs2 = new FileStream("test2.log", FileMode.Append, FileAccess.Write))
////FileShare.None으로 설정된 블록 안에서 다시 그 파일을 접근하려 하면
////파일은 다른 프로세스에서 사용 중이므로 프로세스에서 액세스할 수 없습니다. 라는 System.IOException 예외 출력.
//{
//}
Console.ReadLine();
//프로세스 중에 윈도우에 열어보기 위해 ReadLine으로 잠시 프로그램이 종료되는 것을 막는다.
//윈도우에서 test2.log 접근 시, FileAccess가 None이기 때문에 ,
//다른 프로세스가 파일을 사용 중이기 때문에 프로세스가 액세스 할 수 없습니다. 라는 메시지가 출력된다.
//메모장에서 여는 건 Read이기 때문에 막힌다. FileShare.Write로 설정해도 역시 막힌다.
//FileShare.Read로 설정하고 메모장으로 열면 잘 열린다.
}
using (FileStream fs2 = new FileStream("test2.log", FileMode.Open, FileAccess.Write))
{
BinaryWriter bw = new BinaryWriter(fs2, Encoding.UTF8);
bw.Write("adf adfadf!" + Environment.NewLine); //open으로 했으므로 이 내용이 메모장 제일 앞에 덮어서 기록됨.
bw.Flush();
}
//using (FileStream fs2 = new FileStream("test2.log", FileMode.Append, FileAccess.ReadWrite))
////Append는 쓰기 전용 모드에서만 사용 가능.
////Argument Exception 발생
//{
//}
}
}
class Program //다른 프로젝트에서 접근했을 때의 코드
{
static void Main(string[] args)
{
Console.WriteLine(Environment.CurrentDirectory);
Environment.CurrentDirectory = @"C:\Users\DH\Documents\Visual Studio 2015\Projects\ConsoleApplication1\ConsoleApplication1\bin\Debug";
Console.WriteLine(Environment.CurrentDirectory);
//존재하지 않는 디렉토리가 경로상에 있으면 DirectoryNotFoundException 예외 발생
using (FileStream fs3 = new FileStream(Environment.CurrentDirectory + "\\test2.log", FileMode.Open, FileAccess.ReadWrite))
{
BinaryWriter bw = new BinaryWriter(fs3, Encoding.UTF8);
bw.Write("??" + Environment.NewLine);//프로세스 중에는 다른 프로세스에서 열리지 않는다.
bw.Flush();
}
}
}
File/FileInfo
class FileEx
{
public static void Main()
{
File.Copy("복사전.txt", "복사후.txt"); //같은 폴더에 있는 복사전.txt를 복사후.txt로 복사한다.
//File.Copy("복사전.txt", "복사후.txt", true); //같은 폴더의 복사전.txt를 복사후.txt로 덮어쓴다.
//복사후.txt 파일이 있으면 System.IO.IOException: '복사후.txt' 파일이 이미 있습니다. 예외 출력
Console.WriteLine(File.Exists("복사전.txt")); //폴더 내에 복사전.txt가 존재하므르 true 없으면 false.
File.Move("복사후.txt", "..\\이동후.txt"); //상위 폴더에 이동후.txt라는 이름으로 바뀌어서 이동한다.
File.Move("복사후.txt", "이동후.txt"); //폴더가 동일하면 파일명 변경.
string target = "타겟.txt"; //Move 메서드는 덮어쓰기 같은 옵션이 없으므로 다음과 같이 덮어쓰기를 구현 가능. target은 경로를 포함한다.
if (File.Exists(target) == true)
{
File.Delete(target);
}
File.Move("전타겟.txt", target);
//이미 그 경로에 있으면, System.IO.IOException: 파일이 이미 있으므로 만들 수 없습니다. 예외 출력.
byte[] arr = File.ReadAllBytes("복사전.txt");
Util.PrintByteArr(arr); //복사전.txt를 읽어서 byte 배열로 출력.
Console.WriteLine(Encoding.UTF8.GetString(arr)); //test라는 내용 출력.
string[] txtArr = File.ReadAllLines("ReadAllLinesTest.txt", Encoding.Default); //줄마다 string 하나씩으로 배열에 들어간다.
foreach (string txtLine in txtArr) Console.WriteLine(txtLine); //모두 출력.
string txt = File.ReadAllText("ReadAllLinesTest.txt", Encoding.Default); //모든 텍스트가 string 하나에 들어간다.
Console.WriteLine(txt);
File.WriteAllBytes("바이트배열복사후.txt", arr); //그대로 text로 복사됨.
File.WriteAllLines("WriteAllLineTest.txt", txtArr); //1줄 2줄 3줄 4줄 5줄 내용 모두 복사 됨.
File.WriteAllText("WriteAllTextTest.txt", txt); //1줄 2줄 3줄 4줄 5줄 내용 모두 복사 됨.
string c20Text = new string('c', 20); //c x 20개의 문자열 생성
File.WriteAllText("씨이십개.txt", c20Text);
string clone = File.ReadAllText("씨이십개.txt");
Console.WriteLine(clone);
//FileInfo 타입은 File 타입의 기능을 인스턴스 멤버로 일부 구현함.용법은 같다.
//FileInfo source = new FileInfo("소스.txt"); 로 소스 설정하고 사용하면 된다.
}
}
Directory/DirectoryInfo
Directory와 DirectoryInfo의 관계도 File과 FileInfo의 관계와 동일하다.
class DirectoryEx
{
public static void Main()
{
Directory.CreateDirectory("만들어볼까!");
//이미 경로에 똑같은 이름의 디렉토리가 존재하면 아무것도 안한다.
//Directory.CreateDirectory("만들어볼까?");
//'?' 때문에 System.ArgumentException: 경로에 잘못된 문자가 있습니다. 예외 출력.
//Directory.Delete("지워볼까!");
//경로에 존재하지 않는 디렉터리를 삭제할 경우 DirectoryNotFoundException 예외 출력.
Console.WriteLine(Directory.Exists("진짜있어!")); //true 출력.
string[] paths = Directory.GetDirectories("/");
//경로의 형식이 잘못된 경우 System.ArgumentException 예외 출력.
foreach (string path in paths) Console.WriteLine(path); //경로 내의 디렉터리 목록을 출력한다.
Console.WriteLine();
string[] files = Directory.GetFiles("/");
foreach (string file in files) Console.WriteLine(file); //경로 내의 파일 목록을 출력한다.
foreach (string txt in Directory.GetLogicalDrives()) Console.WriteLine(txt); //시스템에 설치된 디스크 드라이브의 문자 목록 출력.
//특정 폴더와 그 하위 폴더를 검색해서 파일 찾기
string targetPath = @"C:\Users\DH\Documents\Visual Studio 2015\Projects\ConsoleApplication1\ConsoleApplication1\bin\Debug";
foreach (string txt in Directory.GetFiles(targetPath, "*.???", SearchOption.AllDirectories)) Console.WriteLine(txt);
//검색하려는 파일명, 확장자와 맨 위 디렉토리만 검색할지, 그 하위도 검색할지 설정가능.
}
}
Path
파일 경로와 관련된 정적 메서드를 제공한다.
class PathEx
{
public static void Main()
{
string samplePath = @"C:\Users\DH\Documents\Visual Studio 2015\Projects\ConsoleApplication1\ConsoleApplication1\bin\Debug\testProj.exe";
Console.WriteLine(Path.ChangeExtension(samplePath, ".dll")); //testProj.exe를 .dll로 바꾼 문자열 리턴.
Console.WriteLine(Path.GetDirectoryName(samplePath));
//문자열에서 파일의 이름이 포함되어있으면 부모 디렉터리 리턴, 디렉터리만 포함되어있으면 그 부모 디렉터리 리턴.
Console.WriteLine(Path.GetFullPath("testProj.exe")); //해당 파일의 모든 경로 문자열 리턴.
Console.WriteLine(Path.GetFileName(samplePath)); //문자열에서 파일명 리턴.
Console.WriteLine(Path.GetFileNameWithoutExtension(samplePath));//문자열에서 확장자를 뺀 파일명 리턴.
Console.WriteLine(Path.GetExtension(samplePath)); //확장자만 리턴.
Console.WriteLine(Path.GetPathRoot(samplePath)); //루트 드라이브만 리턴
string filePath = Path.Combine(@"C:\temp", "test", "myfile.dat"); //각 디렉터리 명을 조합한 파일 경로 문자열을 리턴한다.
Console.WriteLine(filePath);
string newDirName = "my?new"; //폴더명에 ? 들어갈 수 없음.
int include = newDirName.IndexOfAny(Path.GetInvalidFileNameChars());
//GetInvalidFileNameChars는 파일명에 포함되면 안되는 문자들의 배열
//그 배열이 문자들이 새로 만들 디렉토리명에 포함되지 않으면 IndexOfAny() 메서드가 -1를 리턴. 그 외 숫자가 리턴되면 포함되있다는 뜻
if (include != -1)
{
Console.WriteLine("폴더명에 적합하지 않은 문자가 있음");
}
string createdTempFilePath = Path.GetTempFileName(); //임시 폴더에 임시 파일을 생성하고 그 경로 반환
Console.WriteLine(createdTempFilePath);
string tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
//임시 폴더 경로와, 랜덤으로 임시 파일을 생성한 그 파일명과 combine해서 리턴한다.
Console.WriteLine(tempFilePath);
}
}
스레딩
Thread
기본동작
class TreadEx
{
public static void Main()
{
Thread thread = Thread.CurrentThread;
Console.WriteLine(thread.ThreadState); //현재 스레드의 상태를 리턴한다.
Console.WriteLine(DateTime.Now);
Thread.Sleep(1000); //1초간 멈춤
Console.WriteLine(DateTime.Now); //1초 차이가 나게 출력된다.
//스레드 생성
Thread t = new Thread(ThreadFunc); //스레드에 실행될 메서드 이름(대리자)를 집어넣는다.
//t라는 스레드는 시작되면 ThreadFunc를 실행한다.
t.IsBackground = true; //배경스레드로 설정.
//t를 배경스레드로 설정하면 ThreadFunc로 넘어가기 전에(엄밀히 말하면 넘어가는 동안) Main() 함수가 끝나고 프로세스가 종료된다.
//t 스레드가 운영체제의 스케쥴러에 선택되기 전에 주 스레드가 종료되었기 때문에 이런 현상이 발생한다.
//배경 스레드는 실행종료에 영향을 미치지 않는다. 그렇기 때문에 주 스레드가 종료되면 프로세스가 종료된다.
//실행 종료에 영향을 미치는 것은 전경 스레드(foreground thread)이다.
t.Start();
//더이상 실행할 명령어가 없으므로 주 스레드 종료.
//t.Join(); //t t스레드의 실행이 끝날 때 까지 주 스레드는 기다리게 된다.
//Console.WriteLine("주 스레드 종료!");
}
static void ThreadFunc()
{
Console.WriteLine("ThreadFunc가 동작합니다!");
//이 명령어가 끝나면 t 스레드 종료. 실행중인 스레드가 있으면 프로세스는 종료되지 않는다.
}
}
스레드에 2개 이상의 값을 전달하여 실행하는 경우
class ThreadParam
{
public int x;
public int y;
}
class ThreadEx2
{
public static void Main()
{
Thread t = new Thread(ThreadFunc);
ThreadParam tp = new ThreadParam();
tp.x = 1;
tp.y = 2;
t.Start(tp); //스레드에 2개 이상의 값을 매개변수로 넣고 실행하려면,
//전달할 값의 수 만큼 필드를 포함한 객체를 넣어준다.
}
static void ThreadFunc(object initialValue)
{
ThreadParam value = (ThreadParam)initialValue;
Console.WriteLine("넘어온 x값:{0}, 넘어온 y값:{1}", value.x, value.y);
}
}
스레드를 이용한 소수 개수 구하는 코드
class ThreadEx3
{
public static void Main()
{
Console.WriteLine("입력한 숫자까지의 소수 개수 출력 (종료: 'x' + Enter)");
while(true)
{
Console.WriteLine("숫자를 입력하세요.");
string userNumber = Console.ReadLine();
if(userNumber.Equals("x", StringComparison.OrdinalIgnoreCase) == true) //대소문자 관계없이 x입력 시 종료.
{
Console.WriteLine("프로그램 종료");
break;
}
Thread t = new Thread(CountPrimeNumbers); //소수 계산을 다른 쓰레드로 넘기면 중간에도 프로그램 종료 가능.
t.IsBackground = true; //t 쓰레드를 배경 쓰레드로 바꿔야 주스레드에서 프로세스 종료 가능.
t.Start(userNumber);
}
}
static void CountPrimeNumbers(object initialNumbers) //소수 개수 더해주는 메서드.
{
int primeCandidate = int.Parse((string)initialNumbers);
int totalPrimes = 0;
for (int i = 2; i < primeCandidate; i++)
{
if (Util.IsPrime(i) == true) totalPrimes++;
}
Console.WriteLine("숫자 {0}까지의 소수 개수? {1}", primeCandidate, totalPrimes);
}
}
Monitor
class MonitorEx1
{
int number = 0;
public static void Main()
{
MonitorEx1 me = new MonitorEx1();
Thread t1 = new Thread(ThreadFunc);
Thread t2 = new Thread(ThreadFunc);
t1.Start(me);
t2.Start(me);
t1.Join();
t2.Join();
Console.WriteLine(me.number);
}
static void ThreadFunc(object inst)
{
MonitorEx1 me = inst as MonitorEx1;
for (int i = 0; i < 1000000; i++) //동기화 처리가 되지 않았을 때
{
//반복 횟수가 커질수록 예측할 수 없는 값이 나온다.
//공유 리소스인 number에 대한 동기화 처리를 하지 않았기 때문이다.
me.number++;
}
for (int i = 0; i < 100000; i++) //모니터를 사용하여 동기화 처리를 했을 때
{
Monitor.Enter(me);
//모니터 Enter와 Exit 사이의 코드는 한 순간에 스레드 하나에만 진입해서 실행된다.
//Enter와 Exit 메서드로 전달하는 인자는 참조형 타입의 인스턴스여야 한다.
//me객체의 잠금이 한 스레드에 점유되고 있으면 다른 스레드는 me 객체에 대한 작업을 하지 못하고 대기 상태로 들어간다.
//값 타입을 매개변수로 넣으면 System.Threading.SynchronizationLockException: 예외 출력.
try
{
me.number++;
}
finally
{
Monitor.Exit(me);
}
}
for (int i = 0; i < 100000; i++) //lock을 사용하여 동기화 처리를 했을 때
{
lock (me)
{
me.number++;
}
}
}
}
안전하지 않은 메서드
class MyData
{
int number = 0;
//public object _numberLock = new object(); //임의의 object 객체를 lock을 위해 만들어준다.
public int Number { get => number; }
public void Increment()
{
//이 메서드에 아무런 동기화 기능도 제공하지 않았다.이를 스레드에 안전하지 않은 메서드라고 한다.
number++;
//lock(_numberLock) //메서드 내부에서는 동기화 처리를 이렇게 할 수 있다.
//{
// number++;
//}
}
}
class MonitorEx2
{
public static void Main()
{
MyData data = new MyData();
Thread t1 = new Thread(ThreadFunc);
Thread t2 = new Thread(ThreadFunc);
t1.Start(data);
t2.Start(data);
t1.Join();
t2.Join();
Console.WriteLine(data.Number);
}
static void ThreadFunc(object inst)
{
MyData data = inst as MyData;
for ( int i = 0; i < 100000; i++)
{
//lock(data) { data.Increment(); } 안전하지 않은 메서드에 대한 외부 동기화 처리
data.Increment();
}
}
}
대부분의 BCL 정적 멤버는 다중 스레드 접근에 안전하다. 하지만 인스턴스 멤버는 다중 스레드로 접근했을 때 안전하지 않다. 모든 멤버에 lock을 걸면 다중 스레드 접근에는 안전해지지만, 성능 상의 문제가 발생할 수 있다. 대부분의 경우는 단일 스레드에서만 접근하기 때문에 lock 보호 장치를 걸지 않았고, 동기화가 필요한 경우에는 직접 처리해야 한다.
lock 보호 장치 유무에 따른 속도 비교
class MonitorEx3
{
public static void Main()
{
MyData data = new MyData();
Thread t1 = new Thread(ThreadFunc);
Thread t2 = new Thread(ThreadFunc);
t1.Start(data);
t2.Start(data);
t1.Join();
t2.Join();
Console.WriteLine(data.Number);
}
class MyData
{
int number = 0;
public object _numberLock = new object(); //임의의 object 객체를 lock을 위해 만들어준다.
public int Number { get => number; }
public void Increment()
{
number++;
}
public void IncrementLock()
{
lock (_numberLock)
{
number++;
}
}
}
static void ThreadFunc(object inst)
{
MyData data = inst as MyData;
Stopwatch st = new Stopwatch();
st.Start();
for (int i = 0; i < 100000; i++)
{
data.Increment();
}
st.Stop();
Console.WriteLine("lock을 쓰지 않았을 때의 시간 : {0} Ticks", st.Elapsed.Ticks);
st.Start();
for (int i = 0; i < 100000; i++)
{
data.IncrementLock();
}
st.Stop();
Console.WriteLine("lock을 썼을 때의 시간 : {0} Ticks", st.Elapsed.Ticks); //일반적으로 다섯 배 이상 차이가 난다.
}
}
Interlocked
원자적 연산에 대한 메서드를 제공한다. 원자적 연산 동안은 다른 스레드가 개입할 수 없기 때문에 별도의 동기화 작업이 필요없다.
class MyData
{
int number = 0;
public int Number { get => number; set => number = value; }
public void Increment()
{
Interlocked.Increment(ref number); //원자적 연산을 기본으로 제공해준다. 이건 +1의 예제이다.
}
//increase : 1증가.
//decrease : 1감소.
//Exchange : 대입.
//add(a,b) : a를 a+b값으로 바꿈.
//CompareExchange(a,b,c) : a,b가 같으면 a = c 수행.
}
class InterlockedEx
{
public static void Main()
{
MyData data = new MyData();
Thread t1 = new Thread(ThreadFunc);
Thread t2 = new Thread(ThreadFunc);
t1.Start(data);
t2.Start(data);
t1.Join();
t2.Join();
Console.WriteLine(data.Number);
}
static void ThreadFunc(object inst)
{
MyData data = inst as MyData;
for(int i = 1; i <= 100000; i++) data.Increment();
}
}
ThreadPool
스레드 중에는 특정 연산만을 수행할 목적으로 임시적으로 생겼다가 사라지는 일회성 스레드가 있다. 이런 일회성 스레드를 위해서 제공되는 것이 스레드 풀이다. 간단한 연산을 위해서 스레드를 생성/삭제하는 것 보다는 스레드 풀을 이용하는 것이 좀 더 나은 성능을 보인다.
public static void Main()
{
MyData data = new MyData();
//Thread t1 = new Thread(ThreadFunc);
//Thread t2 = new Thread(ThreadFunc);
ThreadPool.QueueUserWorkItem(ThreadFunc, data); //이렇게도 사용 가능하다.
ThreadPool.QueueUserWorkItem(ThreadFunc, data);
Thread.Sleep(1000); //스레드에 직접 Join()을 할 수 없어서 동작의 완료 여부를 알 수 없다. 그래서 임시로 sleep을 걸어준다.
Console.WriteLine(data.Number);
}
EventWaitHandle
Monitor 처럼 스레드 동기화 수단의 하나이다. EventWaitHandle 객체는 signal과 non-signal 상태를 가질 수 있고, WaitOne 메서드가 호출됐을때, 이벤트 객체의 상태가 signal이면 바로 수행하고 non-signal이면 대기 상태를 유지한다.
class EventWaitHandleEx //Join() 메서드의 역할을 EventWaitHandle로 구현
{
public static void Main()
{
//true면 signal,false면 non-signal 상태로 시작.
EventWaitHandle ewh = new EventWaitHandle(false, EventResetMode.ManualReset);
Thread t = new Thread(ThreadFunc);
t.IsBackground = true;
t.Start(ewh);
ewh.WaitOne(); //ewh가 signal 상태로 바뀔때까지 대기.
Console.WriteLine("주 스레드 종료!");
}
static void ThreadFunc(object obj)
{
EventWaitHandle ewh = obj as EventWaitHandle;
Console.WriteLine("6초 후에 프로그램 종료");
Thread.Sleep(6000);
Console.WriteLine("스레드 종료");
ewh.Set(); //ewh를 signal 상태로 바꿔준다.
}
}
ThreadPool에서 EventWaitHandler로 Join()메서드 기능을 구현
class EventWaitHandleEx2 //ThreadPool에서 EventWaitHandler로 Join()메서드 기능을 구현
{
public static void Main()
{
MyData data = new MyData();
Hashtable ht1 = new Hashtable();
//QueueUserWorkItem이 매개변수를 하나만 받으므로,
//data와 EventWaitHandle 두 개의 값을 전달하기 위해서 Hashtable로 만들었다.
ht1["data"] = data;
ht1["evt"] = new EventWaitHandle(false, EventResetMode.ManualReset);
//기본 상태를 non-signal로 설정.
//AutoReset으로 하면 Signal 상태가 자동으로 Non-Signal 상태로 전환된다.
//AutoReset으로 설정했을 때, set()하면 대기하던 하나의 스레드를 깨우고 곧바로 non-signal로 바뀐다.
ThreadPool.QueueUserWorkItem(ThreadFunc, ht1);
Hashtable ht2 = new Hashtable();
ht2["data"] = data;
ht2["evt"] = new EventWaitHandle(false, EventResetMode.ManualReset);
ThreadPool.QueueUserWorkItem(ThreadFunc, ht2);
(ht1["evt"] as EventWaitHandle).WaitOne();
(ht2["evt"] as EventWaitHandle).WaitOne();
Console.WriteLine(data.Number);
}
static void ThreadFunc(object obj)
{
Hashtable ht = obj as Hashtable; //object형으로 넘어온 Hashtable을 형변환해서 사용한다.
MyData data = ht["data"] as MyData;
for (int i = 0; i < 100000; i++)
{
data.IncrementLock(); //동기화 처리된 증가 메서드
}
(ht["evt"] as EventWaitHandle).Set();
//작업을 다 끝내면 EventWaitHandler를 set해서 WaitOne()의 대기 상태를 해제한다.
}
}
비동기 호출
디스크를 읽는 동안 스레드의 제어가 반환되지 않아서 스레드는 대기 상태로 머물러 있는다. 이를 동기 호출이라고 하는데, 비동기 호출은 디스크를 읽더라도 스레드는 계속해서 실행되고, 읽기 작업이 완료되면 ThreadPool로 부터 유휴 스레드를 얻어와 그 스레드에서 읽기 이후의 나머지 실행을 맡긴다.
비동기 호출 실행 예제
class FileState
{
public byte[] buffer;
public FileStream file;
}
class AsyncCallEx1
{
public static void Main()
{
FileStream fs = new FileStream("test.log", FileMode.Open);
FileState state = new FileState();
state.buffer = new Byte[fs.Length];
state.file = fs;
fs.BeginRead(state.buffer, 0, state.buffer.Length, ReadCompleted, state);
//읽은 데이터를 저장할 버퍼, 읽기를 시작할 바이트 위치, 읽을 바이트의 길이, 비동기 요청이 끝나면 실행될 메서드, 비동기 요청을 구분할 수 있는 객체
Console.WriteLine("비동기 호출 실행중!");
Console.ReadLine(); //비동기이기 때문에 읽기 작업과는 별개로 실행된다.
fs.Close();
}
//읽기 작업이 완료되면 호출되는 메서드이다. 스레드 풀의 스레드에서 실행된다.
static void ReadCompleted(IAsyncResult ar)
{
Console.WriteLine("비동기 호출 실행 완료!");
FileState state = ar.AsyncState as FileState; //state로 전달된 객체를 형변환을 통해 받을 수 있다.
state.file.EndRead(ar); //비동기 읽기 작업이 끝나기를 기다린다. BeginRead를 썼으면 꼭 써줘야한다.
//그렇지 않으면 교착상태와 같은 원치 않는 동작이 발생할 수 있다.
string txt = Encoding.UTF8.GetString(state.buffer);
Console.WriteLine(txt); //"test.log"에 있던 문자열들이 출력된다.
}
}
ThreadPool의 QueueUserWorkItem과 비교해서 비동기 호출로 얻는 이득은 크지 않다. 게임이나 웹 서버 등 동시 접속자 수가 많은 곳에서 유효하게 쓰인다.
Delegate의 비동기 호출
닷넷에서는 일반 메서드에 대해서도 비동기 호출을 할 수 있는 수단을 제공하는데, 델리게이트가 그 역할을 한다.
델리게이트의 비동기 호출 실행을 스레드 풀에서 하는 예제이다.
class DelegateAsyncCallEx1 //델리게이트를 이용한 비동기 호출 예제.
{
public delegate long CalcMethod(int start, int end);
//Cumsum 메서드를 받을 수 있는 델리게이트 선언.
//Cumsum 메서드는 start부터 end까지 수들의 합계를 구하는 것.
public static void Main()
{
CalcMethod calc = new CalcMethod(Util.Cumsum);
IAsyncResult ar = calc.BeginInvoke(1, 100, null, null);
//calc의 연산은 스레드 풀의 스레드에서 실행한다.
//끝나면 호출할 콜백함수나, 넘길 데이터가 없으므로 null,null
ar.AsyncWaitHandle.WaitOne();
//AsyncWaitHandle의 타입은 EventWaitHandle 타입이다. Cumsum이 완료되면 Signal로 바뀐다.
//Cumsum이 완료될 때까지 WaitOne에서 주 스레드는 대기한다.
long result = calc.EndInvoke(ar);
//Cumsum의 결과값을 얻기위해서 EndInvoke를 호출하였다.
//EndInvoke는 비동기 호출이 완료될 때 까지 호출하는 스레드를 차단한다.
//그래서 리턴 값이 없더라도 항상 EndInvoke를 호출하여 비동기 호출을 완료하는 것이 권장된다.
Console.WriteLine("합계 : {0}", result);
}
}
FileStream의 비동기 호출과 유사한 Delegate의 비동기 호출 예제
class DelegateAsyncCallEx2 //FileStream의 비동기 호출과 유사한 Delegate의 비동기 호출 예제
{
public delegate long CalcMethod(int start, int end);
public static void Main()
{
CalcMethod calc = new CalcMethod(Util.Cumsum);
calc.BeginInvoke(1, 100, calcCompleted, calc);
//비동기 호출이 시작된다. 끝나면 calcComplete를 스레드 풀에서 실행한다.
}
static void calcCompleted(IAsyncResult ar)
{
CalcMethod calc = ar.AsyncState as CalcMethod; //BeginInvoke에서 매개변수로 보낸 calc를 형변환해서 받는다.
long result = calc.EndInvoke(ar); //비동기 호출 결과를 리턴해준다.
Console.WriteLine(result);
}
}
네트워크
IPAddress
class IPAddressEx1
{
public static void Main()
{
IPAddress ipAddr = IPAddress.Parse("202.2.30.11");
Console.WriteLine(ipAddr);
IPAddress ipAddr2 = new IPAddress(new byte[] { 202, 131, 30, 11 });
Console.WriteLine(ipAddr2); //위 아래 모두 같은 결과.
//IPAddress ipAddr3 = IPAddress.Parse("2001:0000:85a3:1000:8a2w:0370:7334");
//8a2w 처럼 주소가 잘못된 경우, System.FormatException: 잘못된 IP 주소를 지정했습니다. 예외 출력
//자리 수가 모자라도 마찬가지
IPAddress ipAddr3 = IPAddress.Parse("2001::85a3:0042:1000:8a2e:0370:7334"); //중간에 0000은 ::로 축약가능.
Console.WriteLine(ipAddr3);
IPAddress ipAddr4 = IPAddress.Parse("2001::7334");
Console.WriteLine(ipAddr4);
}
}
포트
여러 프로그램을 서비스하는 하나의 서버에 여러 클라이언트들이 접근할 때, IP 주소만으로는 접근하려는 프로그램끼리의 구분을 할 수 없다. 그래서 포트를 이용한다. 서버에서 포트를 선점해서 통신을 대기해 두면 클라이언트는 자신이 이용하기 원하는 프로그램의 포트로 진입하여 서버와 연결할 수 있다.
IPEndPoint
EndPoint는 TCP/IP 통신에서는 "IP 주소 + 포트"를 말한다. 이 정보를 묶는 단일 클래스가 IPEndPoint 다.
class IPEndPointEx
{
public static void Main()
{
IPAddress ipAddr = IPAddress.Parse("192.168.1.10");
IPEndPoint endPoint = new IPEndPoint(ipAddr, 9000); //아이피 주소와 포트 번호를 담고있다.
Console.WriteLine(endPoint); //아이피주소:포트 꼴로 표현된다.
}
}
Dns
class DnsEx
{
public static void Main()
{
//인터넷 연결 안하면 System.Net.Sockets.SocketException: 알려진 호스트가 없습니다. 예외 출력
IPHostEntry entry = Dns.GetHostEntry("www.microsoft.com");
foreach(IPAddress ipAddress in entry.AddressList)
{
Console.WriteLine(ipAddress);
}
IPHostEntry entry2 = Dns.GetHostEntry("www.naver.com"); //네이버에 할당된 아이피는 2개
foreach (IPAddress ipAddress in entry2.AddressList)
{
Console.WriteLine(ipAddress);
}
string myComputer = Dns.GetHostName(); //내컴퓨터 이름과 지금 할당된 아이피 주소가 표시된다.
Console.WriteLine("컴퓨터 이름 : " + myComputer);
IPHostEntry entry3 = Dns.GetHostEntry(myComputer);
foreach (IPAddress ipAddress in entry3.AddressList)
{
Console.WriteLine(ipAddress.AddressFamily + ": " + ipAddress);
}
}
}
Socket
UDP 소켓 예제
class UDPSocketEx
{
public static void Main()
{
//서버 소켓 스레드
Thread serverThread = new Thread(ServerFunc);
serverThread.IsBackground = true;
serverThread.Start();
//Thread.Sleep(1000);
//클라이언트 소켓이 동작하는 스레드
Thread clientThread = new Thread(ClientFunc);
clientThread.IsBackground = true;
clientThread.Start();
Console.ReadLine();
}
static void ServerFunc(object obj)
{
using (Socket servSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) //UDP 소켓 생성
{
//ipAddress = IPAddress.Parse("0.0.0.0"); //"0.0.0.0" 이나 IPAddress.Any를 쓰면 모든 IP에 바인딩 할 수 있다.
//IPEndPoint endPoint = new IPEndPoint(ipAddress, 10200);
IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, 10200); //아이피주소:포트(10200)
servSocket.Bind(endPoint); //이 endPoint 정보로 소켓 바인딩
byte[] recvBytes = new byte[1024]; //받는 데이터를 저장할 바이트 배열
EndPoint clientEP = new IPEndPoint(IPAddress.None, 0); //클라이언트의 정보가 담겨질 EndPoint 객체
while (true)
{
int nRecv = servSocket.ReceiveFrom(recvBytes, ref clientEP); //받은 바이트 수와 클라이언트 endPoint를 리턴한다.
string txt = Encoding.UTF8.GetString(recvBytes, 0, nRecv); //받은 바이트 배열을 문자열으로 변환
byte[] sendBytes = Encoding.UTF8.GetBytes("Hello: " + txt); //클라이언트로부터 받은 문자열에 Hello를 붙인다.
servSocket.SendTo(sendBytes, clientEP); //다시 클라이언트에 전송.
}
//servSocket.Close();
//서버는 계속해서 서비스하는 입장이라 가정하고 무한루프로 해놓고 소켓을 닫지 않았다.
}
}
private static IPAddress GetCurrentIPAddress()
//이 메서드는 IPv4중 첫번째 ip 주소만을 리턴하므로 문제가 발생할 수 있다.
{
IPAddress[] addrs = Dns.GetHostEntry(Dns.GetHostName()).AddressList;
foreach (IPAddress addr in addrs)
{
if (addr.AddressFamily == AddressFamily.InterNetwork) return addr;
Console.WriteLine(addr);
}
return null;
}
static void ClientFunc()
{
Socket cliSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
for (int i = 0; i < 5; i++)
{
byte[] buf = Encoding.UTF8.GetBytes("검둥개 님의 시간은" + DateTime.Now.ToString() + "입니다.");
EndPoint serverEP = new IPEndPoint(IPAddress.Loopback, 10200);
//127.0.0.1 또는 IPAddress.Loopback로 해도 된다.
//서버가 다른 컴퓨터라면 그 컴퓨터의 아이피 주소가 들어가야 한다.
cliSocket.SendTo(buf, serverEP); //서버로 보낸다.
byte[] recvBytes = new byte[1024]; //서버로부터 응답받은 내용을 저장할 버퍼
int nRecv = cliSocket.ReceiveFrom(recvBytes, ref serverEP);
//서버에서 받은 바이트 recvByte에 저장, 바이트 길이 nRecv에 저장.
string txt = Encoding.UTF8.GetString(recvBytes, 0, nRecv); //UTF8로 변환해서 받는다.
Console.WriteLine(txt);
Thread.Sleep(1000);
}
cliSocket.Close(); // 소켓은 쓰고나면 꼭꼭 닫아준다.
}
}
TCP
class TCPSocketEx1
{
public static void Main()
{
//서버 소켓 스레드
Thread serverThread = new Thread(ServerFunc);
serverThread.IsBackground = true;
serverThread.Start();
Thread.Sleep(1000);
//클라이언트 소켓이 동작하는 스레드
Thread clientThread = new Thread(ClientFunc);
clientThread.IsBackground = false;
clientThread.Start();
Console.ReadLine();
}
static void ServerFunc()
{
using (Socket srvSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, 11200); //서버는 어떤 ip에서의 접근도 허용해야 하므로..
srvSocket.Bind(endPoint); //바인딩한다.
srvSocket.Listen(10); //최대 10개의 클라이언트 접속을 보관 할 수 있다. //0 이나 음수여도 되는데?
while (true)
{
Socket cliSocket = srvSocket.Accept();
//서버 소켓은 클라이언트와 직접적인 통신이 안된다.
//직접적인 통신은 서버 소켓의 Accept()로 반환된 소켓 인스턴스로만 할 수 있다.
//Accept()로 반환된 소켓은 클라이언트 쪽 소켓과 1:1 대응되므로 IPEndPoint 인자를 전달할 필요가 없다.
byte[] recvBytes = new byte[1024];
int nRecv = cliSocket.Receive(recvBytes); //recvBytes에 소켓으로 받아온 데이터를 넣는다. nRecv에는 바이트 수를 넣는다.
string txt = Encoding.UTF8.GetString(recvBytes, 0, nRecv);
byte[] sendBytes = Encoding.UTF8.GetBytes("Hello : " + txt); //받은 문자열에 Hello를 붙여서 다시 보낸다.
cliSocket.Send(sendBytes); //다시 보낸다.
cliSocket.Close(); //소켓을 닫아준다.
}
}
}
static void ClientFunc()
{
Socket cliSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
EndPoint serverEp = new IPEndPoint(IPAddress.Loopback, 11200);
cliSocket.Connect(serverEp); //클라이언트에서 Connect를 호출하는 시점에 서버는 Listen을 호출한 상태여야 한다.
byte[] buf = Encoding.UTF8.GetBytes("검둥개 님의 시간은" + DateTime.Now.ToString() + "입니다.");
cliSocket.Send(buf);
byte[] recvBytes = new byte[1024];
int nRecv = cliSocket.Receive(recvBytes);
string txt = Encoding.UTF8.GetString(recvBytes, 0, nRecv); //받은 문자열을 인코딩 후 출력한다.
Console.WriteLine(txt);
cliSocket.Close();
}
TCP 멀티 스레드 예제
class TCPSocketEx2 //TCP 다중 스레드
{
public static void Main()
{
//서버 소켓 스레드
Thread serverThread = new Thread(ServerFunc);
serverThread.IsBackground = true;
serverThread.Start();
Thread.Sleep(1000);
//클라이언트 소켓이 동작하는 스레드
Thread clientThread = new Thread(ClientFunc);
clientThread.IsBackground = true;
serverThread.Start();
Console.WriteLine("종료하려면 아무 키나 누르세요...");
Console.ReadLine();
}
static void ServerFunc()
{
using (Socket srvSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, 11200);
srvSocket.Bind(endPoint);
srvSocket.Listen(10);
while(true)
{
Socket cliSocket = srvSocket.Accept(); //Accept받은 소켓을 스레드풀의 처리를 스레드 풀의 스레드에게 맡긴다.
ThreadPool.QueueUserWorkItem((WaitCallback)clientSocketProcess, cliSocket);
//하지만 한 클라이언트 당 하나의 스레드가 생성되면서 동시 처리할 수 있는 클라이언트 개수에 제한이 생긴다.
}
}
}
private static void clientSocketProcess(object state)
{
Socket cliSocket = state as Socket;
byte[] recvBytes = new byte[1024];
int nRecv = cliSocket.Receive(recvBytes);
string txt = Encoding.UTF8.GetString(recvBytes, 0, nRecv);
byte[] sendBytes = Encoding.UTF8.GetBytes("Hello: " + txt);
cliSocket.Send(sendBytes);
cliSocket.Close();
}
static void ClientFunc()
{
//구현
}
}
TCP 비동기 통신 예제
public class AsyncStateData
{
public byte[] buffer;
public Socket socket;
}
class TCPSocketEx3 //TCP 비동기 통신 서버 예제
{
public static void Main()
{
//서버 소켓 스레드
Thread serverThread = new Thread(ServerFunc);
serverThread.IsBackground = true;
serverThread.Start();
Thread.Sleep(1000);
//클라이언트 소켓이 동작하는 스레드
Thread clientThread = new Thread(ClientFunc);
clientThread.IsBackground = true;
clientThread.Start();
Console.ReadLine();
}
static void ServerFunc()
{
using (Socket srvSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, 11200);
srvSocket.Bind(endPoint);
srvSocket.Listen(10);
while(true)
{
Socket cliSocket = srvSocket.Accept();
AsyncStateData data = new AsyncStateData();
data.buffer = new byte[1024];
data.socket = cliSocket;
cliSocket.BeginReceive(data.buffer, 0, data.buffer.Length, SocketFlags.None, asyncReceiveCallback, data);
//비동기 동작
//바로 While문의 시작으로 돌아가 다른 클라이언트와의 연결을 지연없이 받을 수 있다.
//BeginRecv -> EndRecv -> BeginSend -> EndSend 순서대로 진행된다.
}
}
}
private static void asyncReceiveCallback(IAsyncResult ar) //BeginReceive에서 콜백
{
AsyncStateData rcvData = ar.AsyncState as AsyncStateData;
int nRecv = rcvData.socket.EndReceive(ar);
string txt = Encoding.UTF8.GetString(rcvData.buffer, 0, nRecv); //문자열 데이터를 받는다.
byte[] sendBytes = Encoding.UTF8.GetBytes("Hello: " + txt); //받은 문자열에 Hello를 붙여서 다시 보낸다.
rcvData.socket.BeginSend(sendBytes, 0, sendBytes.Length, SocketFlags.None, asyncSendCallback, rcvData.socket);
}
private static void asyncSendCallback(IAsyncResult ar) //BeginSend에서 콜백
{
Socket socket = ar.AsyncState as Socket;
socket.EndSend(ar); //비동기 보내기 작업을 끝내준다.
socket.Close(); //소켓을 다 사용했으므로 닫아준다.
}
static void ClientFunc()
{
//구현
}
}
Http 통신 Client 예제
class HttpClientEx //HTTP 프로토콜의 기본은 요청과 응답이다.
{
public static void Main()
{
ClientFunc();
Console.WriteLine("종료하려면 아무 키나 누르세요...");
Console.ReadLine();
}
static void ClientFunc()
{
using (Socket cliSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
IPAddress ipAddr = Dns.GetHostEntry("www.naver.com").AddressList[0];
EndPoint serverEP = new IPEndPoint(ipAddr, 80);
cliSocket.Connect(serverEP);
/////Send와 Receive 구현
string request = "GET / HTTP/1.0\r\nHost: www.naver.com\r\n\r\n";
byte[] sendBuffer = Encoding.UTF8.GetBytes(request);
//네이버 웹 서버로 HTTP 요청을 전송
cliSocket.Send(sendBuffer);
//HTTP 요청이 전달되었으므로 네이버 웹 서버에서 응답 수신
StringBuilder sb = new StringBuilder();
while(true)
{
Console.WriteLine("몇 번 돌았나?");
byte[] recvBuffer = new byte[4096];
int nRecv = cliSocket.Receive(recvBuffer);
if (nRecv == 0)
{
//서버측에서 더는 받을 데이터가 없으면 Receive() 메서드에서 0을 반환.
Console.WriteLine("더 이상 수신받을 데이터가 없습니다.!");
break;
}
string text = Encoding.UTF8.GetString(recvBuffer, 0, nRecv);
sb.Append(text);
sb.Append("\r\n");
}
cliSocket.Close();
string response = sb.ToString();
Console.WriteLine(response);
}
}
}
HTTP 서버 예제
class HttpServerEx
{
public static void Main()
{
ServerFunc();
Console.WriteLine("종료하려면 아무 키나 누르세요...");
Console.ReadLine();
}
static void ServerFunc()
{
using (Socket servSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
Console.WriteLine("http://localhost:8000입니다.");
IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, 8000);
servSocket.Bind(endPoint);
servSocket.Listen(10);
while (true)
{
Socket cliSocket = servSocket.Accept();
Console.WriteLine("요청이 들어왔습니다!"); //요청이 들어올 때 마다 출력된다.
ThreadPool.QueueUserWorkItem(httpProcessFunc, cliSocket); //요청이 들어오면 httpProcessFunc가 호출된다.
}
}
}
private static void httpProcessFunc(object state)
{
Socket socket = state as Socket;
byte[] buf = new byte[4096];
socket.Receive(buf);
string header = "HTTP/1.0 200 OK\nContent-Type: text/html; charset=UTF-8\r\n\r\n";
//브라우저는 개행문자 2개 이후로 나온 내용을 화면에 보여준다. 개행문자 2개가 header와 body의 구분자가 된다.
string body = "<html><body><mark>테스트 HTML</mark> 웹 페이지입니다.</body></html>";
byte[] resbuf = Encoding.UTF8.GetBytes(header + body);
socket.Send(resbuf); //header + body를 합쳐 다시 전송한다.
socket.Close(); //다 쓴 소켓은 닫아준다.
}
}
HttpWebRequest
class HttpWebRequestEx
{
public static void Main()
{
ClientFunc();
Console.WriteLine("종료하려면 아무 키나 누르세요...");
Console.ReadLine();
}
static void ClientFunc()
{
//타입 내부에서 TCP 소켓을 생성한다.
HttpWebRequest req = WebRequest.Create("http://www.naver.com") as HttpWebRequest;
//GetResponse()를 호출하면서 지정 웹 서버로 HTTP 요청을 보내고 응답을 받는다.
HttpWebResponse resp = req.GetResponse() as HttpWebResponse;
//받은 응답을 읽어서 출력한다.
using(StreamReader sr = new StreamReader(resp.GetResponseStream()))
{
string responseText = sr.ReadToEnd();
Console.WriteLine(responseText);
}
}
}
WebClient
class HttpWebClientEx
{
public static void Main()
{
ClientFunc();
Console.WriteLine("종료하려면 아무 키나 누르세요...");
Console.ReadLine();
}
static void ClientFunc()
{
//WebClient 타입은 내부적으로 httpWebRequest를 이용해 통신한다.
WebClient wc = new WebClient();
//입력한 Url에서 받아온 데이터를 인코딩만 해서 출력하면 끝.
string responseText = Encoding.UTF8.GetString(wc.DownloadData("http://www.naver.com"));
Console.WriteLine(responseText);
}
}
기타
Nullable
nullable 형식이란 System.Nullable 구조체를 의미한다. 값 형식의 초기값은 0으로 채워진다. 하지만 그 0이 의미하는 것이 정말 숫자 0을 가지고 있는건지, 아니면 값이 없는 상태인지를 구분할 수 없다. Nullable 타입은 일반적인 값 형식에 대해 null 표현이 가능하도록 해준다.
class NullableEx
{
Nullable<bool> isGetMarried;
//bool? isGetMarried; 로 쓸 수 있다.
public Nullable<bool> IsGetMarried { get => isGetMarried; set => isGetMarried = value; }
//public bool? IsGetMarried { get => isGetMarried; set => isGetMarried = value; } 로 쓸 수 있다.
public static void Main()
{
NullableEx ne = new NullableEx();
ne.isGetMarried = null; //이상없음.
int? intValue = null;
//Value를 get 할 때,
//int target = intValue.Value;
int target = intValue.GetValueOrDefault();
//GetValueOrDefault()메서드를 쓰면 더 편하게 사용가능하다.
Console.WriteLine(target);
//알아서 set된다.
intValue = target;
double? temp = null;
//HasValue로 값이 있는지 없는지 알 수 있다.
Console.WriteLine(temp.HasValue);
}
}
StringFormat 보관소
//날짜 출력
Console.WriteLine(string.Format("{0:D}", DateTime.Now));
//시간 출력
Console.WriteLine(string.Format("{0:t}", DateTime.Now));
질문사항
-
p430의 thread race condition에 대한 상세 설명을 부탁드립니다.
키워드는 thread context, atomic operation, register 등입니다.
1. number 변수는 0으로 초기화돼 있다.
-> 힙 영역 내의 number 변수는 0인 상태이다.
2. CPU 1번에서 t1 스레드를 실행한다. t1은 메모리로부터 number 변수의 값을 가져온다.
-> 현재 t1 스레드가 실행중인 상태이고, 힙 영역 내의 number 변수의 값이 t1 스레드의 레지스터로 가져온 상태이다.
3. t1 스레드는 number 변수의 값에 1을 더한다.(아직 저장을 하지 못했다.)
-> CPU의 ALU에서 레지스터로 가져온 number 변수의 값에 1을 더한다. 아직 저장을 못했다고 한 이유는 연산을 마친 number 변수의 값을 다시 힙 영역에 기록하지 못했기 때문이다.
4. CPU 1번에서 t1 스레드의 실행이 멈추고 t2 스레드를 선택해 실행한다.
-> thread context switching이 발생한다. t1 스레드의 레지스터 값이 tcb에 저장된다.
5. t2 스레드는 메모리에서 number 변수의 값을 가져온다. 현재 number 변수가 가리키는 주소의 메모리에 있는 값은 0이다.
-> t2 스레드의 레지스터로 힙 영역 내의 number 변수의 값인 0을 가져온다.
6. t2 스레드는 number 변수의 값에 1을 더한다.
-> 마찬가지로 t2 스레드의 레지스터로 가져온 number 변수의 값에 1을 더한 것이다. 아직 힙 영역에 있는 number 변수의 값은 그대로 0인 상태이다.
7. t2 스레드는 1로 증가된 number 변수의 값을 메모리에 저장한다.
-> t2 스레드의 레지스터에 있는 number 변수의 값 1을 힙 영역의 number 변수에 저장한다.
8. CPU 1번에서 t2 스레드의 실행이 멈추고 다시 t1 스레드를 선택해 실행한다.
-> thread context switching이 발생한다. t2 스레드의 레지스터 값이 tcb에 저장되고, t1 스레드의 레지스터 값을 불러온다.
9. t1 스레드는 마지막으로 3번 작업까지 수행했었다. 따라서 1만큼 증가시켰던 number 변수의 값 1을 메모리에 저장한다.
-> t1 스레드도 t2와 마찬가지로 레지스터에 있는 number 변수의 값 1을 힙 영역에 저장한다. 힙 영역의 number 변수 값은 여전히 1이 된다.의도했던 결과는 t1과 t2가 number 변수를 각각 1씩 더해서 힙 영역의 결과가 2가 되는 것이었다. 하지만 2개의 스레드가 공유 자원으로 동시에 접근하여 의도와 다른 결과가 나타났다. 이처럼 다수의 스레드들이 공유 자원에 동시에 접근하여 나타나는 논리적 오류를 thread race codintion 이라 한다.
atomic operation은 thread race condition을 방지 할 수 있다. atomic operation은 실행 도중에 다른 operation으로부터 간섭을 받지 않고, 전체 연산 중 하나라도 실패하면 전체 연산이 시작되기 전의 상태로 복구한다는 특성을 가지고 있기 때문이다. 멀티스레드 프로그래밍에서 atomic operation은 스레드나 프로세스간의 동기화를 위한 Lock을 사용하지 않아도 되기 때문에 프로그램의 효율성과 속도를 높일수 있다
-
DateTime.Now를 문자열로 아래와 같이 출력하고 싶습니다.
어떻게 코딩하면 될까요?
"2017년 3월 23일 목요일" "PM 2:10"
//날짜 출력
Console.WriteLine(string.Format("{0:D}", DateTime.Now));
//시간 출력
CultureInfo info = new CultureInfo("en-US");
Console.WriteLine(string.Format(info,"{0:t}", DateTime.Now));
//AM/PM 표시를 앞으로.
Console.WriteLine(now.ToString("tt", new CultureInfo("en-US")) + " " + now.ToString("h:mm"));
- 아래와 같은 메소드를 구현해 주세요.
public static string ExtractString(string source, string beginDelim, string endDelim)
if error, return ""
usage1)
var code = ExtractString("실행중 문제가 발생했습니다. Error Code [20]", "[", "]")
// code == "20"
usage2)
var cmd = ExtractString("exec APP.COMMON.SET(NID=PN000101,CMD='ED-SYS::::1')", "CMD='", "'")
// cmd == "ED-SYS::::1"
class ExtractStringProgram
{
public static string ExtractString(string source, string beginDelim, string endDelim)
{
bool isBeginDelimExisting = false; //beginDelim의 존재 여부 나타내는 변수
bool isEndDelimExisting = false; //endDelim의 존재 여부 나타내는 변수
int indexOfBeginDelim = 0; //beginDelim 첫번째 글자의 번호
int indexOfEndDelim = 0; //endDelim 첫번째 글자의 번호
int nextIndexOfBeginDelim = 0; //beginDelim 마지막 글자 다음 번호
try
{
//문자열의 처음부터 beginDelim을 찾을 때까지 비교 수행
for (int i = 0; i <= source.Length - beginDelim.Length; i++)
{
if (source.Substring(i, beginDelim.Length).Equals(beginDelim))
{
isBeginDelimExisting = true;
indexOfBeginDelim = i; //저장하고 break;
break;
}
}
if (!isBeginDelimExisting) return ""; //입력받은 beginDelim가 존재하지 않으면 "" 리턴
nextIndexOfBeginDelim = indexOfBeginDelim + beginDelim.Length; //beginDelim 마지막 글자 다음 번호 저장
//beginDelim 뒤부터 EndDelim을 찾을 때까지 비교 수행
for (int i = nextIndexOfBeginDelim; i <= source.Length - endDelim.Length; i++)
{
if (source.Substring(i, endDelim.Length).Equals(endDelim))
{
isEndDelimExisting = true;
indexOfEndDelim = i; //저장하고 break;
break;
}
}
if (!isEndDelimExisting) return ""; //입력받은 endDelim가 존재하지 않으면 "" 리턴
//두 Delim 사이의 문자열 추출해서 리턴
return source.Substring(nextIndexOfBeginDelim, indexOfEndDelim - nextIndexOfBeginDelim);
}
catch(Exception e)
{
return ""; //그 외 에러 발생 시 "" 리턴
}
}
public static void Main()
{
var code = ExtractString("실행중 문제가 발생했습니다. Error Code [20]", "[", "]");
Console.WriteLine(code);
var cmd = ExtractString("exec APP.COMMON.SET(NID=PN000101,CMD='ED-SYS::::1')", "CMD='", "'");
Console.WriteLine(cmd);
}
}
- 아래와 같은 메소드를 구현해주세요.
public static List SortAidList(List unsortedList)
usage1)
var unsorted = new List();
unsorted.Add("TP6GC.9");
unsorted.Add("TP6GC.4");
unsorted.Add("TP3GC.1");
unsorted.Add("TP3GC.16");
unsorted.Add("TP3GC.10");
unsorted.Add("TP3GC.3");
var sortedList = SortAidList(unsorted);
// sortedList[0] == "TP3GC.1"
// sortedList[1] == "TP3GC.3"
// sortedList[2] == "TP3GC.10"
// sortedList[3] == "TP3GC.16"
// sortedList[4] == "TP6GC.4"
// sortedList[5] == "TP6GC.9"
class SortAidProgram
{
public static List<string> SortAidList(List<string> unsortedList)
{
unsortedList.Sort(); //맨 처음엔 문자열 기준으로 자동 정렬
int lowPos = 0; //swap을 위한 변수
for (int i = 0; i < unsortedList.Count; i++)
{
lowPos = i;
for (int j = i + 1; j < unsortedList.Count; j++)
{
//'.' 이전의 문자열이 같은 문자열끼리 '.'뒤의 숫자비교
if (ExtractStringProgram.ExtractString(unsortedList[i], "", ".")
.Equals(ExtractStringProgram.ExtractString(unsortedList[j], "", ".")))
{
//j,lowPos가 가리키는 값을 담기 위한 변수
int numOfj;
int numOfLowPos;
//'.'이후에 숫자만 있으면 바로 int로 형변환, 숫자와 문자가 섞여있다면 밑에서 ExtractString 이용해서 숫자만 추출
bool isConvertiblej = int.TryParse(unsortedList[j].Split('.').ElementAt(1), out numOfj);
bool isConvertibleLowPos = int.TryParse(unsortedList[lowPos].Split('.').ElementAt(1), out numOfLowPos);
if (!isConvertiblej) numOfj = int.Parse(ExtractStringProgram.ExtractString(unsortedList[j], ".", "-"));
if (!isConvertibleLowPos) numOfLowPos = int.Parse(ExtractStringProgram.ExtractString(unsortedList[lowPos], ".", "-"));
if (numOfLowPos > numOfj) lowPos = j; //'.' 뒤에 나오는 숫자만 추출해서 비교
}
}
string temp = unsortedList[i]; //swap
unsortedList[i] = unsortedList[lowPos];
unsortedList[lowPos] = temp;
}
return unsortedList;
}
public static void Main()
{
var unsorted = new List<string>();
unsorted.Add("TP6GC.9");
unsorted.Add("TP6GC.4");
unsorted.Add("TP3GC.1");
unsorted.Add("TP3GC.16");
unsorted.Add("TP3GC.10");
unsorted.Add("TP3GC.3");
unsorted.Add("TP3GC.1-C1");
unsorted.Add("TP3GC.1-C2");
unsorted.Add("TP3GC.3-C2");
unsorted.Add("TP3GC.3-C385");
unsorted.Add("TPXGU.5");
unsorted.Add("TPXGU.6");
var sortedList = SortAidList(unsorted);
sortedList.ForEach(Console.WriteLine); //출력
}
}