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); //지정하지 않았으므로 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(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이 표시된다.
}
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 test = br.ReadUInt16(); //억지로 4바이트를 2바이트로 쪼개면 65535출력
ushort test2 = br.ReadUInt16(); //15출력. 순서가 뒤바뀐 이유는 리틀 엔디안이기 때문에.
Console.WriteLine("{0}{1}{2} {3}", first, second, test, test2); //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 특성을 지정하면 된다..
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;
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 //직렬화 대상 클래스의 접근 제한에 영향을 받는다. 다른 클래스의 inner 클래스가되서 private되면 예외가 발생한다.
{ //inner 클래스가 됐어도 [DataContract],[DataMember] 특성을 정의해주면 직렬화가 가능하다.
//그 외 접근이 가능한 상황에서는 명시적으로 선언하지 않아도 작동한다.
[DataMember]
public int age;
[DataMember] //[DateContract]를 선언했을 때, [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되었기 때문에 아무것도 출력하지 않는다.
}
}
파일
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.Read))
//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으로 잠시 프로그램이 종료되는 것을 막는다.
//윈도우에서 접근 시, 다른 프로세스가 파일을 사용 중이기 때문에 프로세스가 액세스 할 수 없습니다. 라는 메시지가 출력된다.
//메모장에서 여는 건 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 메서드는 덮어쓰기 같은 옵션이 없으므로 다음과 같이 덮어쓰기를 구현 가능.
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.CreateDirectory("지워볼까!");
//경로에 존재하지 않는 디렉터리를 삭제할 경우 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"에 있던 문자열들이 출력된다.
}
}