Thread Pool

Source code

https://github.com/teamsmiley/gitbook-sample/tree/main/ThreadTest

Thread

Thread는 하나의 프로세스에서 실행되는 작업의 단위이다. Thread를 사용하면 여러 작업을 동시에 실행할 수 있다. Thread를 사용하면 여러 작업을 동시에 실행할 수 있지만, Thread를 생성하고 관리하는 것은 비용이 많이 든다. 그래서 .NET에서는 Thread Pool을 제공한다.

Thread Pool

Thread Pool은 Thread를 관리하는 기능을 제공한다. Thread Pool을 사용하면 Thread를 생성하고 관리하는 비용을 줄일 수 있다. Thread Pool은 Thread를 생성하고 관리하는 것을 자동으로 처리한다. Thread Pool을 사용하면 Thread를 생성하고 관리하는 것을 자동으로 처리하기 때문에 Thread를 사용할 때보다 더 적은 비용으로 Thread를 사용할 수 있다.

webapi

프로젝트마다 다르겟지만 dotnet으로 webapi를 사용시 kestrel이라는 웹서버를 사용한다. kestrel은 기본적으로 thread pool을 사용한다. 확인해보자.

cd ~/Desktop
dotnet new webapi --name ThreadTest

cd ThreadTest

vi controllers/ValuesController.cs
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;

namespace ThreadTest.Controllers;

[ApiController]
[Route("values")]
public class ValuesController : ControllerBase
{
    private readonly ILogger<ValuesController> _logger;
    public ValuesController(ILogger<ValuesController> logger)
    {
        _logger = logger;
    }

    [HttpGet(Name = "GetValues")]
    public int Get()
    {
        Console.WriteLine($"The number of processors on this computer is {Environment.ProcessorCount}.");

        var maxWorkerThreads = 0;
        var maxCompletionPortThreads = 0;

        ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxCompletionPortThreads);
        Console.WriteLine("Maximum worker threads: {0}", maxWorkerThreads);

        var availableWorkerThreads = 0;
        var completionPortThreads = 0;

        ThreadPool.GetAvailableThreads(out availableWorkerThreads, out completionPortThreads);
        Console.WriteLine($"Available Worker threads: {availableWorkerThreads}", availableWorkerThreads);

        var usedWorkerThread = maxWorkerThreads - availableWorkerThreads;
        Console.WriteLine($"Used worker threads: {usedWorkerThread}");

        Thread.Sleep(10000); // Sleep for 10 seconds

        return usedWorkerThread;
    }
}

컨트롤러를 만들고 실행해보자.

dotnet run

curl http://localhost:5007/values
  • 현재 컴퓨터는 6코어 12스레드이다. 12개의 cpu를 확인할수 있다.

  • 최대 worker thread는 32767개이다.

  • 사용된 worker thread는 2개이다. (1번의 호출을 테스트해보았다.) 1개는 기본쓰레드인가? 호출 한번에 2번째 스레드가 생성이 되었다.

  • 사용가능한 worker thread는 32765개이다.

The number of processors on this computer is 12.
Maximum worker threads: 32767
Available Worker threads: 32765
Used worker threads: 2

테스트를 계속해보면 usedWorkerThread가 계속 늘어나는것을 알수 있다. 그리고 시간이 되고 나면 usedWorkerThread가 줄어드는것을 확인할수 있다.

alt text

쓰레드가 증가햇다가 리턴되는것을 볼수 있다.

32000개의 쓰레드가 풀에는 만들어져있는거같다. 그러면 32000개를 미리 다 만들어두는것인가? 아니면 필요할때마다 만드는것인가? 이것을 확인해보자.

dotnet-counters

dotnet tool install --global dotnet-counters

dotnet-counters monitor -n ThreadTest # 프로세스 이름
alt text

요청을 보내면 올라가는 쓰레드를 확인할수 있다.

alt text

요청을 계속 만들어보면서 que에 들어가는지 확인해보자.

locust.io

https://locust.io/

많은 요청을 만들기 위해서 이걸 사용한다.

pip install locust
mkdir locust
cd locust

cat > locust.py <<EOF
import time
from locust import HttpUser, between, task

class WebsiteUser(HttpUser):
  wait_time = between(1, 2)

  def on_start(self):
    self.client.verify = False

  @task
  def launch(self):
      self.client.get(url="/values")
EOF
locust -f locust.py  --host http://localhost:5007 --users 1000 --spawn-rate 200

http://localhost:8089

실행을 해보면 결과를 볼수 있다.

alt text
alt text

결과가 잘 확인됨을 알수 있다.

container

실제 서비스환경에서는 컨테이너를 사용한다. 이제 지금까지 프로그램을 컨테이너로 만들고 활용해보자.

vi Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build-env
WORKDIR /app
COPY . .
RUN dotnet publish "ThreadTest.csproj" -c Release -o /app/publish

# Install dotnet debug tools
RUN dotnet tool install --tool-path /tools dotnet-trace \
  && dotnet tool install --tool-path /tools dotnet-counters \
  && dotnet tool install --tool-path /tools dotnet-dump \
  && dotnet tool install --tool-path /tools dotnet-gcdump

# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine

# Copy dotnet-tools
WORKDIR /tools
COPY --from=build-env /tools .

RUN apk add icu-libs

ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false

WORKDIR /app

COPY --from=build-env /app/publish .
EXPOSE 80
ENTRYPOINT ["dotnet", "ThreadTest.dll"]
vi docker-compose.yaml
version: '3'
services:
  web:
    build:
      context: .
    ports:
      - 80:80
    deploy:
      resources:
        limits:
          cpus: '1'
        reservations:
          cpus: '1'

cpu를 1개만 할당을 하였다. 이제 컨테이너를 실행해보자.

docker-compose up --build
docker ps -a

이제 locust를 실행해보자. 그리고 로그를 봐보자.

docker logs -f threadtest-web-1

그리고 모니터링도 해보자.

docker exec -it threadtest-web-1 /tools/dotnet-counters monitor --process-id 1
alt text

잘 보인다.

테스트 결과

cpu를 1개만 할당하고 그리고 12개를 할당하고 테스트해보니 다음과 같다.

cpu
maxWorkerThread
usedWorkerThread
AvailableWorkerThread

1

32767

1

32766

12

32767

2

32765

cpu 갯수와 상관이 없이 Thread는 32767개로 고정되어있는것을 확인할수 있다.

쓰레드에 기본 갯수가 있고 그게 넘어가면 자동으로 생성을 하는것으로 알고 있다. 그럼 기본값이 다른것일가?

코드에 다음을 추가하고 실행해보자.

var usedWorkerThread = maxWorkerThreads - availableWorkerThreads;
Console.WriteLine($"Used worker threads: {usedWorkerThread}");

# 이거 추가
int minWorker, minIOC;
// Get the current settings.
ThreadPool.GetMinThreads(out minWorker, out minIOC);
Console.WriteLine("Minimum worker threads: {0}", minWorker);

Thread.Sleep(1000000); // Sleep for 10 seconds
Console.WriteLine("Thread completed. go back to the pool.");
return usedWorkerThread;

minWorker를 확인해보자.

cpu
minWorker

1

1

12

12

minWorker는 cpu 갯수와 같은것을 확인할수 있다.

cpu를 1개로 하고 minworker를 1000개로 해볼가?

vi ThreadTest.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ThreadPoolMinThreads>1000</ThreadPoolMinThreads> <!-- 이거 추가 -->
  </PropertyGroup>
</Project>

다시 실행해보자.

alt text

기본적으로 1000개의 쓰레드를 가지고 있으므로 usedWorkerThread가 1000개로 나온다.

리퀘스트가 생김으로써 쓰레드가 늘어나는것을 확인할수 없다. 1000이 넘으면 증가를 할거 같다.

alt text

1000이 넘어가면 큐에 들어가고 하나씩 생성이 됨을 알수 있다.

다시 고민

친구의 문제는 쓰레드가 생기는게 오래 걸린다는것이다. 그래서 쓰레드를 미리 만들어두는것이 좋을거 같다. 그런데 아무리 테스트를 해봐도 간단한 작업은 Thread가 만들어지는것이 처음 10개와 1000개의 차이가 없다. 아무래도 쓰레드의 생성이 문제는 아닌거 같다. 그럼 다른것을 찾아봐야겠다.

entity framework

entity framework를 사용시에 커넥션 pool이 문제를 일으키는건 아닐가?

다음 글에서 확인해보자.

Last updated

Was this helpful?