[OS] Locking

Locking


Locking이란

멀티 프로세스 혹은 멀티 스레드 환경에서 임계영역에 대한 접근 제어를 위한 방법
임계영역에 대한 접근 제어를 하지 않는다면 결과를 예측할 수 없다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <thread>

int num = 0;

void thread_task(){
for(int i = 0; i < 5000; ++i)
num++; //Critical section
}

int main(){
std::thread threadA(thread_task);
std::thread threadB(thread_task);

threadA.join();
threadB.join();

std::cout<<num<<std::endl; //expect 10000 but unpredictable
}



세마포어(Semaphore)



임계영역에 하나 이상의 프로세스 혹은 스레드 진입이 가능하다

세마포어을 설정하지 않은 프로세스 혹은 스레드가 세마포어를 해제할 수 있다

락에 대한 소유개념이 없다

커널모드 동기화로 context switching이 이루어진다

무겁고 느리지만 palse나 wait같은 다양한 기능이 제공된다



뮤텍스(Mutex)



임계영역에 하나만의 프로세스 혹은 스레드만이 진입이 가능하다

뮤텍스를 설정한 프로세스 혹은 스레드만이 뮤텍스를 해제할 수 있다

락에 대한 소유개념이 있다

커널모드 동기화로 context switching이 이루어진다

무겁고 느리지만 palse나 wait같은 다양한 기능이 제공된다



스핀락(Spin Lock)



라이브러리 수준에서 제공된다

유저모드 동기화로 context switching이 이루어지지 않는다

가볍고 빠르지만 다양한 기능이 제공되기 어렵다

잘못된 방법으로 사용될 경우 CPU 사용률을 높히게 된다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

class CQ_EnqueueDequeuePeek{
// ConcurrentQueue<T>.Enqueue()
// ConcurrentQueue<T>.TryPeek()
// ConcurrentQueue<T>.TryDequeue()
// Enqueue가 Peek와 Dequeue 메소드보다 우선순위가 높다
// Concurrent Collection은 내부적으로 Interlock을 통한 동기화를 수행한다
static void Main (){
ConcurrentQueue<int> cq = new ConcurrentQueue<int>();

// Populate the queue.
for (int i = 0; i < 10000; i++){
cq.Enqueue(i);
}

int result;
if (!cq.TryPeek(out result)) {
Console.WriteLine("CQ: TryPeek failed when it should have succeeded");
}
else if (result != 0) {
Console.WriteLine("CQ: Expected TryPeek result of 0, got {0}", result);
}
}

Interlock의 예제를 보면 왜 TryDequeue인지, 왜 CPU 사용률이 증가되는지 알 수 있다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using System;
using System.Threading;

namespace InterlockedExchange_Example{
class MyInterlockedExchangeExampleClass{
//0 for false, 1 for true.
private static int usingResource = 0;

private const int numThreadIterations = 5;
private const int numThreads = 10;

static void Main(){
Thread myThread;
Random rnd = new Random();

for(int i = 0; i < numThreads; i++){
myThread = new Thread(new ThreadStart(MyThreadProc));
myThread.Name = String.Format("Thread{0}", i + 1);

//Wait a random amount of time before starting next thread.
Thread.Sleep(rnd.Next(0, 1000));
myThread.Start();
}
}

private static void MyThreadProc(){
for(int i = 0; i < numThreadIterations; i++){
UseResource();

//Wait 1 second before next attempt.
Thread.Sleep(1000);
}
}

//A simple method that denies reentrancy.
static bool UseResource(){
//0 indicates that the method is not in use.
if(0 == Interlocked.Exchange(ref usingResource, 1)){
Console.WriteLine("{0} acquired the lock", Thread.CurrentThread.Name);

//Code to access a resource that is not thread safe would go here.

//Simulate some work
Thread.Sleep(500);

Console.WriteLine("{0} exiting lock", Thread.CurrentThread.Name);

//Release the lock
Interlocked.Exchange(ref usingResource, 0);
return true;
}
else{
Console.WriteLine(" {0} was denied the lock", Thread.CurrentThread.Name);
return false;
}
}
}
}

참고

https://docs.microsoft.com/ko-kr/dotnet/api/system.collections.concurrent.concurrentqueue-1?view=netcore-3.1
https://docs.microsoft.com/ko-kr/dotnet/api/system.threading.interlocked?view=netcore-3.1

Author: Song Hayoung
Link: https://songhayoung.github.io/2020/06/15/OS/Locking/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.