From 1ee21addf37fcafe06943566ad3ee5dfc8e8d14b Mon Sep 17 00:00:00 2001 From: Ra1nz0r Date: Sat, 22 Jun 2024 20:14:15 +0300 Subject: [PATCH] Complete. --- .github/workflows/autotests.yaml | 21 +++++ README.md | 23 ++++- cmd/api/app.go | 28 +++++++ go.mod | 11 +++ go.sum | 10 +++ internal/dye/color_for_err.go | 4 + internal/service/all_service_test.go | 41 +++++++++ internal/service/generator_test.go | 47 +++++++++++ internal/service/service_func.go | 121 +++++++++++++++++++++++++++ internal/service/worker_test.go | 51 +++++++++++ web/example.jpg | Bin 0 -> 6759 bytes 11 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/autotests.yaml create mode 100644 cmd/api/app.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/dye/color_for_err.go create mode 100644 internal/service/all_service_test.go create mode 100644 internal/service/generator_test.go create mode 100644 internal/service/service_func.go create mode 100644 internal/service/worker_test.go create mode 100644 web/example.jpg diff --git a/.github/workflows/autotests.yaml b/.github/workflows/autotests.yaml new file mode 100644 index 0000000..4bc81ef --- /dev/null +++ b/.github/workflows/autotests.yaml @@ -0,0 +1,21 @@ +name: autotests + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: Running test. + runs-on: ubuntu-latest + container: golang:1.22.3 + steps: + - uses: actions/checkout@v4 + + - name: Run Unit Tests + run: GOOS=linux GOARCH=amd64 go test -v ./... -count=1 + + - name: Vet + run: go vet ./... diff --git a/README.md b/README.md index d3c6361..b4a81e2 100644 --- a/README.md +++ b/README.md @@ -1 +1,22 @@ -# counting_concurrency \ No newline at end of file +

Счётчик "многопоточности".

+ +__Горутина, генерирует числа и отправляет их в канал. Далее несколько горутин читают и распределяют их по каналам. Под конец производится обратное действие, из каналов пишутся все числа в один результирующий.__ + +- __При правильном выполнении кода:__ + - [x] Количество и сумма входящих чисел совпадает с количеством и суммой чисел, которые получены из канала вывода. + - [x] Количество проходящих чисел по каналам не должно сильно отличаться. + +Числа генерируются с помощью ```context.WithTimeout``` в течение одной секунды. +Количество обрабатывающих горутин зависит от числа ядер ```runtime.NumCPU()```. +Вышеуказанные параметры можно изменить в ```cmd/api/app.go``` + +*** +#### Инструкция по локальному запуску: + +Запуск производится по-умолчанию: ```go run ./...```\ +Тесты выполняются по-умолчанию: ```go test -v ./... -count=1``` + +*** +#### Пример: + +![logo](/web/example.jpg) diff --git a/cmd/api/app.go b/cmd/api/app.go new file mode 100644 index 0000000..c44a70c --- /dev/null +++ b/cmd/api/app.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "runtime" + "time" + + "github.com/ra1nz0r/counting_concurrency/internal/service" +) + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + var inSum int64 // сумма сгенерированных чисел + var inCnt int64 // количество сгенерированных чисел + + chIn := service.GenNum(ctx, &inSum, &inCnt) + + NumOut := runtime.NumCPU() // количество обрабатывающих горутин и каналов + outs := service.GenChanSliceWithNum(NumOut, chIn) + + chOut, amounts := service.SendNumInResChan(NumOut, outs) + + sumTOT, cntTOT := service.ReadFromResChan(chOut) + + service.CheckResult(amounts, inSum, inCnt, sumTOT, cntTOT) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e188ac0 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/ra1nz0r/counting_concurrency + +go 1.22.3 + +require github.com/stretchr/testify v1.9.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..60ce688 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/dye/color_for_err.go b/internal/dye/color_for_err.go new file mode 100644 index 0000000..0cabb15 --- /dev/null +++ b/internal/dye/color_for_err.go @@ -0,0 +1,4 @@ +package dye + +var Reset = "\033[0m" +var Red = "\033[31m" diff --git a/internal/service/all_service_test.go b/internal/service/all_service_test.go new file mode 100644 index 0000000..63a651a --- /dev/null +++ b/internal/service/all_service_test.go @@ -0,0 +1,41 @@ +package service + +import ( + "context" + "fmt" + "runtime" + "testing" + "time" + + "github.com/ra1nz0r/counting_concurrency/internal/dye" + + "github.com/stretchr/testify/assert" +) + +func TestAllService(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + var inSumT int64 + var inCntT int64 + + chIn := GenNum(ctx, &inSumT, &inCntT) + + NumOut := runtime.NumCPU() + outs := GenChanSliceWithNum(NumOut, chIn) + + chOut, amounts := SendNumInResChan(NumOut, outs) + + sumTestTOT, cntTestTOT := ReadFromResChan(chOut) + + assert.Equal(t, inCntT, cntTestTOT, + fmt.Sprintf("%sCount numbers is not equal.%s", dye.Red, dye.Reset)) + assert.Equal(t, inSumT, sumTestTOT, + fmt.Sprintf("%sSum numbers is not equal.%s", dye.Red, dye.Reset)) + + for _, v := range amounts { + inCntT -= v + } + assert.Zero(t, inCntT, + fmt.Sprintf("%sThe division of numbers by channel is incorrect.%s", dye.Red, dye.Reset)) +} diff --git a/internal/service/generator_test.go b/internal/service/generator_test.go new file mode 100644 index 0000000..b937fda --- /dev/null +++ b/internal/service/generator_test.go @@ -0,0 +1,47 @@ +package service + +import ( + "context" + "fmt" + "sync/atomic" + "testing" + "time" + + "github.com/ra1nz0r/counting_concurrency/internal/dye" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerator(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + var testCntTOT int64 + var testSumTOT int64 + + chInT := GenNum(ctx, &testSumTOT, &testCntTOT) + + require.NotEmpty(t, <-chInT, + fmt.Sprintf("%sChannel empty.%s", dye.Red, dye.Reset)) + + var expCount int64 = 1 + var expSum int64 = 1 + + for { + value, ok := <-chInT + if !ok { + break + } + atomic.AddInt64(&expSum, value) + atomic.AddInt64(&expCount, 1) + time.Sleep(25 * time.Microsecond) + } + + require.NotEqual(t, expCount, expSum, + fmt.Sprintf("%s`Generator` does not increment the value.%s", dye.Red, dye.Reset)) + assert.Equal(t, expCount, testCntTOT, + fmt.Sprintf("%sWrong count. Expect: %d, got: %d.%s", dye.Red, expCount, testCntTOT, dye.Reset)) + assert.Equal(t, expSum, testSumTOT, + fmt.Sprintf("%sWrong sum. Expect: %d, got: %d.%s", dye.Red, expSum, testSumTOT, dye.Reset)) +} diff --git a/internal/service/service_func.go b/internal/service/service_func.go new file mode 100644 index 0000000..52e5564 --- /dev/null +++ b/internal/service/service_func.go @@ -0,0 +1,121 @@ +package service + +import ( + "context" + "fmt" + "log" + "sync" + "sync/atomic" + "time" +) + +// Generator генерирует последовательность чисел 1,2,3 и т.д. и +// отправляет их в канал ch. При этом после записи в канал для каждого числа +// вызывается функция fn. Она служит для подсчёта количества и суммы +// сгенерированных чисел. +func Generator(ctx context.Context, ch chan<- int64, fn func(int64)) { + var num int64 = 1 + defer close(ch) + for { + select { + case <-ctx.Done(): + return + default: + ch <- num + fn(num) + num++ + } + } +} + +// Worker читает число из канала in и пишет его в канал out. +func Worker(in <-chan int64, out chan<- int64) { + defer close(out) + for { + value, ok := <-in + if !ok { + break + } + out <- value + time.Sleep(time.Millisecond) + } +} + +// Генерирует числа и считает их количество и сумму. +func GenNum(ctx context.Context, numSum, numCnt *int64) chan int64 { + chInput := make(chan int64) + go Generator(ctx, chInput, func(i int64) { + atomic.AddInt64((*int64)(numSum), i) + atomic.AddInt64((*int64)(numCnt), 1) + }) + return chInput +} + +// Возвращает слайс каналов, с количеством записанных чисел в каждый канал из входного канала. +func GenChanSliceWithNum(numOutput int, chInput chan int64) []chan int64 { + // outsChanSlice — слайс каналов, куда будут записываться числа из chIn + outsChanSlice := make([]chan int64, numOutput) + for i := 0; i < numOutput; i++ { + // создаём каналы и для каждого из них вызываем горутину Worker + outsChanSlice[i] = make(chan int64) + go Worker(chInput, outsChanSlice[i]) + } + return outsChanSlice +} + +// Отправляет числа из слайса каналов в результирующий канал, со статистикой по отработанным горутинам. +func SendNumInResChan(numOutput int, outsChanSlice []chan int64) (chan int64, []int64) { + // gorutStat — слайс, в который собирается статистика по горутинам + gorutStat := make([]int64, numOutput) + // chanRes — канал, в который будут отправляться числа из горутин `gorutStat[i]` + chanRes := make(chan int64, numOutput) + + var wg sync.WaitGroup + + // Передаем числа из каналов gorutStat в результирующий канал + for i := 0; i < len(outsChanSlice); i++ { + wg.Add(1) + go func(nextChan <-chan int64, numChan int64) { + defer wg.Done() + for v := range nextChan { + chanRes <- v + gorutStat[numChan]++ + } + }(outsChanSlice[i], int64(i)) + } + + go func() { + wg.Wait() + close(chanRes) + }() + return chanRes, gorutStat +} + +// Читаем числа из результирующего канала +func ReadFromResChan(chanRes chan int64) (checkSum, checkCnt int64) { + for v := range chanRes { + checkCnt++ + checkSum += v + } + return +} + +// Проверка результата и вывод данных по числам, каналам. +func CheckResult(gorutStat []int64, numSum, numCnt, checkSum, checkCnt int64) { + fmt.Println("Количество чисел", numCnt, checkCnt) + fmt.Println("Сумма чисел", numSum, checkSum) + fmt.Println("Разбивка по каналам", gorutStat) + + if numSum != checkSum { + log.Fatalf("Ошибка: суммы чисел не равны: %d != %d\n", numSum, checkSum) + } + if numCnt != checkCnt { + log.Fatalf("Ошибка: количество чисел не равно: %d != %d\n", numCnt, checkCnt) + } + for _, v := range gorutStat { + numCnt -= v + } + if numCnt != 0 { + log.Fatalf("Ошибка: разделение чисел по каналам неверное\n") + } +} diff --git a/internal/service/worker_test.go b/internal/service/worker_test.go new file mode 100644 index 0000000..fc5020f --- /dev/null +++ b/internal/service/worker_test.go @@ -0,0 +1,51 @@ +package service + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/ra1nz0r/counting_concurrency/internal/dye" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWorker(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + chanInT := make(chan int64) + var num int64 = 1 + var compareNum int64 + + go func() { + defer close(chanInT) + for { + select { + case <-ctx.Done(): + return + default: + chanInT <- num + compareNum = num + num++ + } + } + }() + + chanOutT := make(chan int64) + + go Worker(chanInT, chanOutT) + + require.NotEmpty(t, <-chanOutT, + fmt.Sprintf("%sChannel empty.%s", dye.Red, dye.Reset)) + + var count int64 + for v := range chanOutT { + count = v + } + + assert.Equal(t, compareNum, count, + fmt.Sprintf("%sWrong count. Expect: %d, got: %d.%s", dye.Red, compareNum, count, dye.Reset)) +} diff --git a/web/example.jpg b/web/example.jpg new file mode 100644 index 0000000000000000000000000000000000000000..259c9eb04947064302c3defcb9e025c88045d408 GIT binary patch literal 6759 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 literal 0 HcmV?d00001