C#다중 스레드 실습-잠금 및 스레드 보안
13013 단어 스레드 보안
class ThreadUnsafe { static int val1, val2; static void Go() { if (val2 != 0) Console.WriteLine (val1 / val2); val2 = 0; } }
이것은 스레드가 안전하지 않습니다. 만약 Go 방법이 두 스레드에 동시에 호출된다면, 어떤 스레드에서 0을 나누는 오류가 발생할 수 있습니다.val2는 한 스레드가 0으로 설정되었을 수도 있고, 다른 스레드는if와Console로 마침 실행되었을 수도 있습니다.WriteLine 문
다음은 c#의 lock을 사용하여 이 문제를 수정합니다.
class ThreadSafe { static object locker = new object(); static int val1, val2; static void Go() { lock (locker) { if (val2 != 0) Console.WriteLine (val1 / val2); val2 = 0; } } }
같은 시간에 하나의 라인만 동기화 대상을 잠글 수 있습니다. (여기는locker) 경쟁하는 다른 라인은 이 자물쇠가 풀릴 때까지 차단됩니다.만약 한 라인보다 큰 라인이 이 자물쇠를 경쟁한다면 그들은'준비대열'이라고 불리는 대열을 형성하여 선착순으로 자물쇠를 수여할 것이다.한 라인의 접근이 다른 라인과 중첩될 수 없기 때문에, 상호 배척 자물쇠는 때때로 자물쇠가 보호하는 내용에 대한 강제 직렬화 접근이라고 불린다.이 예에서 고 방법의 논리와 val1과val2 필드의 논리를 보호했다.경쟁 자물쇠를 기다리는 라인이 막히면 Thread State에서Wait Sleep Join 상태가 됩니다.잠시 후 토론할 라인은 다른 라인을 통해 인터럽트나 Abort 방법을 사용해서 강제로 풀려날 것입니다.이것은 작업 라인을 끝내는 데 상당히 효율적인 기술이다.C#의 lock 문은 실제로 Monitor를 호출합니다.Enter 및 Monitor.Exit, try-finally 문이 섞인 간략한 버전, 다음은 실제 이전 예에서 발생한 Go 방법입니다.
Monitor.Enter (locker);
try { if (val2 != 0) Console.WriteLine (val1 / val2); val2 = 0; } finally { Monitor.Exit (locker); }
같은 대상에서 첫 번째 모니터를 호출합니다.Ente가 먼저 모니터를 호출했습니다.Exit에서 예외가 발생합니다.모니터도 TryEnter 방법을 제공하여 시간 초과 기능을 실현했다. 밀리초나 Timespan을 사용하지만, 자물쇠를 얻으면true로 되돌아오고, 반대로false로 되돌아오지 않는다.TryEnter도 시간 초과 파라미터가 없으면 자물쇠를 테스트해서 가져올 수 없으면 즉시 시간을 초과할 수 있습니다.
동기화 객체 선택
모든 관계가 있는 라인에 대해 볼 수 있는 대상은 동기화 대상이 될 수 있지만, 경직된 규정을 충족시키려면 인용 유형이어야 한다.동기화 대상은 클래스 (예를 들어 하나의 개인 실례 필드) 에서 무심코 외부에서 같은 대상을 잠그는 것을 방지하는 것이 좋습니다.이러한 규칙을 충족시키면 동기화 대상은 대상과 보호의 두 가지 역할을 겸할 수 있다.예를 들면 다음과 같습니다.
class ThreadSafe { List <string> list = new List <string>(); void Test() { lock (list) { list.Add ("Item 1"); ...
자물쇠의 범위와 입도를 정확하게 제어할 수 있기 때문에, 전문 필드 (예:locker) 는 자주 사용된다.객체 또는 클래스 자체의 유형을 동기화 객체로 사용합니다. 즉,
lock (this) { ... }
또는:
lock (typeof (Widget)) { ... } //
이러한 대상을 공공적으로 방문할 수 있는 잠재적인 위험이 존재하기 때문에 방식은 좋지 않다.
자물쇠는 동기화 대상 자체에 대한 접근을 어떤 방식으로도 막지 않습니다. 다시 말하면 x.ToString () 은 다른 라인이 lock (x) 를 호출해서 막히지 않습니다.
중첩 잠금
스레드는 같은 대상을 반복해서 잠글 수 있으며 여러 번 모니터를 호출할 수 있습니다.Enter 또는 lock 문을 사용합니다.대응하는 번호의 모니터입니다.Exit가 호출되거나 가장 바깥쪽 lock 문이 완료되면 객체가 잠금 해제됩니다.이것은 가장 간단한 문법으로 한 방법의 자물쇠를 다른 자물쇠로 호출할 수 있도록 허용한다.
static object x = new object(); static void Main() { lock (x) { Console.WriteLine ("I have the lock"); Nest(); Console.WriteLine ("I still have the lock"); } // } static void Nest() { lock (x) { ... } // ? ! }
라인은 처음 자물쇠나 가장 바깥쪽 자물쇠만 막을 수 있습니다.
잠금 시기
기본 규칙으로서, 다중 루트와 관련된 모든 읽기와 쓰기를 할 필드는 잠궈야 한다.심지어 매우 일반적인 일인 단일 필드의 값 부여 작업도 동기화 문제를 고려해야 한다.다음 예에서 Increment 및 Assign 은 모두 스레드가 안전하지 않습니다.
class ThreadUnsafe { static int x; static void Increment() { x++; } static void Assign() { x = 123; } }
다음은 Increment 및 Assign 스레드 보안 버전입니다.
class ThreadUnsafe { static object locker = new object(); static int x; static void Increment() { lock (locker) x++; } static void Assign() { lock (locker) x = 123; } }
자물쇠를 잠그는 또 다른 선택으로 간단한 상황에서도 동기화를 막지 않는 것을 사용할 수 있다. 이런 문장이 동기화되어야 하는 이유도 뒤에서 논의할 것이다.
자물쇠와 원자 조작
만약 많은 변수가 일부 자물쇠에서 항상 읽기와 쓰기를 한다면, 너는 원자 조작이라고 할 수 있다.우리는 x와 y가 끊임없이 값을 읽고 부여한다고 가정한다. 그들은 자물쇠 안에서 locker를 통해 잠긴다.
lock (locker) { if (x != 0) y /= x; }
너는 x와 y가 원자를 통해 접근한다고 생각할 수 있다. 코드 세그먼트가 다른 세그먼트에 의해 분리되거나 점령되지 않았기 때문에, 다른 세그먼트가 x와 y를 바꾸는 것은 무효한 출력이다. 너는 영원히 0으로 나누는 오류를 얻지 못할 것이다. x와 y가 항상 같은 배타적 자물쇠에 접근할 수 있도록 보장한다.
성능 고려
자물쇠 자체가 매우 빠르기 때문에 자물쇠 하나가 막히지 않은 상황에서 보통 몇 십 나노초(10억분의 1초)밖에 걸리지 않는다.만약 막히면, 작업 전환이 가져오는 비용은 수미초 (백만분의 1초) 의 범위 내에 가깝다. 비록 라인이 실제 안배 시간을 재편성하기 전에 수밀리초 (천분의 1초) 가 걸릴 수도 있다.반면 자물쇠를 사용해서 사용하지 않는 것은 더 많은 시간을 들일 수 있다.만약 사라진 자물쇠와 경쟁 자물쇠가 발생한다면 자물쇠는 반작용을 가져올 것이다. 너무 많은 코드가 자물쇠 문장에 놓여 다른 라인이 불필요하게 막히기 때문이다.사라진 자물쇠는 두 라인이 서로 잠긴 내용을 기다리기 때문에 둘 다 계속할 수 없다.경합 자물쇠는 두 개의 라인 중 어느 하나라도 내용을 잠글 수 있으며, 만약 '오류' 라인이 자물쇠를 가져오면 프로그램 오류를 초래할 수 있다.
동기화 대상이 사라지기 쉬운 경우 비교적 좋은 처리 방식은 비교적 적은 자물쇠를 설계하는 것이다.믿을 만한 상황에서 차단이 많으면 자물쇠의 입도를 높이는 것을 고려할 수 있다.
스레드 보안
루틴 안전 코드는 어떤 다중 루틴에 직면해도 코드가 확실하지 않은 요소가 없다는 것을 말한다.라인 안전은 우선 자물쇠를 완성한 다음에 온라인 라인 간의 상호작용 가능성을 줄인다.
어떤 상황에서도 다시 호출할 수 있는 안전한 방법인용 유형이 매우 적은 것이 라인이 안전하다는 이유는 다음과 같다.
4
4
4
특정한 다중 노드 상황을 처리하기 위해서, 노드 안전은 항상 실현이 필요한 곳에서만 실현된다.그러나 희생 자물쇠의 입도를 통해 큰 단락의 코드를 포함하고 심지어 배타 자물쇠에서 전역 대상을 방문함으로써 더욱 높은 단계에서 직렬화 접근을 실현하고 방대하고 복잡한 종류가 다중 노드 환경에서 안전하게 운행하도록 하는 특수한 상황도 있다.이런 방법은 비선정 안전 대상을 선정 안전 코드에 사용하고 같은 상호 배척 자물쇠가 비선정 안전 대상에 대한 모든 속성, 방법, 필드에 대한 접근을 보호하는 데 사용되는 것을 피한다.또는 공유 데이터를 최소화함으로써 루트의 상호작용을 최소화하고'약한 상태'의 중간 프로그램과 웹 서버에서 인용 유형의 루트 안전을 실현하는 데 많이 사용된다.여러 개의 클라이언트 요청이 동시에 도착하지만, 모든 요청은 그 자체의 루트 (예를 들어 ASP.NET, 웹 서버 또는 원격 시스템 구조) 에서 온다. 그들이 호출하는 방법은 루트가 안전하다.약한 상태 설계(신축성이 좋아 유행)는 본질적으로 상호작용 능력을 제한하기 때문에 클래스는 모든 요청 간에 데이터를 오래 보존할 수 없다.루틴 상호작용은 선택적으로 만들 수 있는 정적 필드에만 한정되며, 일반적으로 메모리에 자주 사용하는 데이터를 캐시하고 인증과 심사 같은 인프라 시설 서비스를 제공하는 데 사용된다.
라인 안전과.NET Framework 유형
자물쇠는 비선정 안전 코드를 선정 안전 코드로 변환하는 데 사용할 수 있다.에 있습니다.NET 프레임워크 구현 중 거의 모든 비기본 유형의 실례는 라인이 안전하지 않다.다중 루틴 코드에 비기본 형식을 사용하려면 방문한 대상에게 잠금 보호가 필요합니다.다음 예제에서는 두 스레드가 동시에 동일한 List에 엔트리를 추가한 다음 열거합니다.
class ThreadSafe { static List <string> list = new List <string>(); static void Main() { new Thread (AddItems).Start(); new Thread (AddItems).Start(); } static void AddItems() { for (int i = 0; i < 100; i++) lock (list)list.Add ("Item " + list.Count); string[] items; lock (list) items = list.ToArray(); foreach (string s in items) Console.WriteLine (s); } }
이런 상황에서list 대상 자체를 잠그는 것이 좋은 방법일 수도 있다.매거NET의 집합도 라인이 안전한 것이 아니다. 일일이 열거할 때 다른 라인이list를 바꾸면 이상을 던진다.매거 과정을 직접적으로 잠그지 않기 위해서, 우리는 우선 항목을 수조에 복제하여 매거 과정에서 잠재적인 소모 시간이 있기 때문에 자물쇠를 고정시키지 않도록 한다.
흥미로운 가설: 만약에 List가 실제적으로 라인이 안전하다면, 우리의 가상적인 라인이 안전한list에 항목을 추가해야 한다. 다음과 같다.
if (!myList.Contains (newItem)) myList.Add (newItem);
list가 라인이 안전하든 그렇지 않든 이 문장은 분명히 아니다. 즉, 완전한 라인이 안전한 통용 집합류는 기본적으로 존재하지 않는다는 것이다.net4.0에서 마이크로소프트는 안전한 병렬 집합 클래스를 제공했지만 그들은 모두 특수 처리를 거쳐 접근 방식에 제한을 두었다.위의 문장은 라인 안전을 실현하려면 전체if문장은 반드시 하나의 자물쇠에 넣어서 유무를 판단하고 새로운 점유율을 증가시키는 데 사용해야 한다.유사한 자물쇠는 다음 문장과 같이 리스트를 수정해야 하는 모든 곳에 사용됩니다.
myList.Clear();
다시 말하면, 우리는 많은 비선정적인 안전한 집합류들을 잠궈야 한다.내부 라인 안전, 시간 낭비!이런 이유로..NET 프레임워크에서 정적 구성원은 라인이 안전하지만, 실례적인 구성원은 그렇지 않다.따라서 사용자 정의 형식을 쓸 때 안전한 사용자 정의 구성 요소를 만들려고 시도하지 마십시오.공용 구성 요소를 쓸 때 정적 구성원을 단독으로 조심스럽게 처리하는 것은 좋은 인코딩 습관이다.