Первая бета-версия MTL

Мы рады объявить о выпуске первой бета-версии MTL (многопоточной библиотеки) под лицензией GPL-3.0.

ZigRazor / MTL

Многопоточная библиотека

MTL

Многопоточная библиотека

Дорожная карта

  • Класс потоков
  • Менеджер потоков
  • Пул потоков
  • Класс задач
  • Упорядоченная задача
  • Поток задач
  • Полная документация
  • Интеграция с Doxygen
  • Первая бета-версия
  • Тестовый фреймворк
  • Конвейер CI/CD
  • Первый стабильный релиз
  • Мониторинг потоков

Полный список предлагаемых функций (и известных проблем) смотрите в разделе «Открытые проблемы».

Начало работы

Это пример того, как вы можете дать инструкции по локальной настройке вашего проекта. Чтобы получить локальную копию и запустить ее, выполните следующие простые шаги.

Предварительные условия

Для запуска проекта необходимо следующее.

Google Test

GoogleTest

git clone https://github.com/google/googletest.git  # Dowload the Google Test repository
cd googletest                                       # Main directory of the cloned repository.
mkdir -p build                                      # Create a directory to hold the build output.
cd build                                            # Move into the build directory.   
cmake ..                                            # Generate native build scripts for GoogleTest.
make                                                # Compile
sudo make install                                   # Install in

Войдите в полноэкранный режим Выйти из полноэкранного режима
Просмотр на GitHub

Пример

Ниже приведены примеры использования библиотеки:

Класс Thread

Для того, чтобы класс потока знал, что делать, необходимо создать объект Runnable.

MyRunnable.hpp

#include <iostream>
#include "MTL.h"

class MyRunnable : public MTL::MTLRunnable
{
public:
    MyRunnable() = default;
    virtual ~MyRunnable() = default;
    void run(MTL::MTLThreadInterface* threadIf)
    {
        std::cout << "Hello World!" << std::endl;
        int counter = 0;
        while (true)
        {
            if(threadIf->getThreadState() == MTL::E_MTLThreadState::STOPPED){
                std::cout << "Stopped" << std::endl;
                std::this_thread::sleep_for(std::chrono::milliseconds(1000));
            }else if (threadIf->getThreadState() == MTL::E_MTLThreadState::SUSPENDED)
            {
                std::cout << "Suspended" << std::endl;
                std::this_thread::sleep_for(std::chrono::milliseconds(1000));
            }else if (threadIf->getThreadState() == MTL::E_MTLThreadState::EXITED)
            {
                std::cout << "Exited" << std::endl;
                break;
            } else if (threadIf->getThreadState() == MTL::E_MTLThreadState::RUNNING)
            {
                //Simulate 20 seconds of running work
                if(counter == 20)
                {
                    threadIf->setThreadState(MTL::E_MTLThreadState::EXITED);
                }
                std::cout << "Running" << std::endl;
                std::this_thread::sleep_for(std::chrono::milliseconds(1000));
                counter++;
            }
        }

    }

    void stop()
    {
        std::cout << "Stop!" << std::endl;
    }

    void suspend()
    {
        std::cout << "Pause!" << std::endl;
    }

    void resume()
    {
        std::cout << "Resume!" << std::endl;
    }

    void clean_exit()
    {
        std::cout << "Clean Exit!" << std::endl;
    }

    void force_exit()
    {
        std::cout << "Force Exit!" << std::endl;
        ::exit(0); // clean Exit
    }
};
Вход в полноэкранный режим Выйти из полноэкранного режима

Затем в функции main вы можете создать объект thread и передать ему объект runnable.

main.cpp

#include "MyRunnable.hpp"

int main()
{
    MyRunnable myRunnable; // Create a runnable object
    MTL::MTLThread thread(myRunnable); // Create a thread object and pass it the runnable object.
    thread.run(); //Start the Thread
    std::this_thread::sleep_for(std::chrono::milliseconds(10000)); //Sleep for 10 seconds
    thread.suspend(); //Suspend the thread
    std::this_thread::sleep_for(std::chrono::milliseconds(5000)); //Sleep for 5 seconds
    thread.resume(); //Resume the thread
    int counter = 0;
    while(thread.isRunning()){ //Wait 10 seconds in the main thread
        if(counter == 10)
        {
            break;
        }
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << "Counter: " << counter << std::endl;
        counter++;
    }
    return 0;
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Класс рабочих потоков

Класс Worker Thread представляет собой runnable с двумя дополнительными функциями:
— Содержит очередь сообщений
— Должна быть реализована функция для обработки сообщений

Этот класс может быть использован для создания runnable для передачи объекту Thread, который получает входное сообщение и обрабатывает его.

Простой пример реализации класса Worker Thread может быть следующим:

MyWorkerThread.hpp

class MyWorker : public MTL::MTLWorkerThread
{
public:
    MyWorker() = default;
    virtual ~MyWorker() = default;
    virtual void processMessage(MTL::Message message) override
    {
        //When the message is dequeued it is passed to this function
        int* message_casted = static_cast< int *> (message.get()); //Cast the message to the correct type
        std::cout << "MyWorker::processMessage(" << *message_casted << ")" << std::endl; //Do somenthings with the message
    }
};
Вход в полноэкранный режим Выйти из полноэкранного режима

Класс диспетчера потоков

Класс Thread Manager представляет собой Runnable, который можно использовать для управления потоками.
Если вы хотите управлять группой потоков вместе, вы можете создать объект Thread Manager и передать его объекту Thread.
Потоки могут быть переданы объекту Thread Manager, а объект Thread Manager будет управлять потоками.
Таким образом, если у вас есть несколько задач или несколько рабочих, вы можете собрать их в менеджере потоков, и все действия, выполняемые менеджером потоков (запуск, остановка, приостановка, возобновление и т.д.), будут выполняться для всех потоков.
Простой пример использования может быть следующим:

#include "MTL.h"
#include "MyWorker.hpp"

int main(){
    // Three different kind of workers
    MyWorker1 myWorker1;
    MyWorker2 myWorker2;
    MyWorker3 myWorker3;

    MTL::MTLThreadManager threadManager; //Create a thread manager object

    threadManager.addThread(std::make_unique<MTL::MTLThread>(myWorker1)); //Add a thread to the thread manager
    threadManager.addThread(std::make_unique<MTL::MTLThread>(myWorker2)); //Add a thread to the thread manager

    MTL::MTLThread thread(threadManager); //Create a thread object and pass it the thread manager
    thread.run(); //Start the thread manager that will start all the threads
    std::this_thread::sleep_for(std::chrono::milliseconds(5000)); //Let him work for 5 seconds
    thread.suspend(); //Suspend the thread manager that will suspend all the threads
    std::this_thread::sleep_for(std::chrono::milliseconds(5000)); //Let suspended for 5 seconds
    thread.resume(); //Resume the thread manager that will resume all the threads
    threadManager.addThread(std::make_unique<MTL::MTLThread>(myWorker3)); //Add a thread to the thread manager
    thread.clean_exit(); // Clean Exit the thread manager that will clean exit all the threads   
    while (thread.isRunning()) //Wait for the thread manager to exit
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << "Counter: " << counter << std::endl;
        counter++;
    }
    std::cout << "Thread Manager Terminated" << std::endl;
    return 0;
}
Войти в полноэкранный режим Выход из полноэкранного режима

Класс Thread Pool

Класс Thread Pool — это менеджер потоков, который может быть использован для создания фиксированного числа рабочих, выполняющих одну и ту же задачу. Когда сообщение записывается в пул потоков, оно передается одному из рабочих.
Этот вид менеджера потоков полезен, когда вам нужно реализовать многопоточный потребитель.
Примером использования может быть следующее:

#include "MTL.h"

int main(){
    MyWorker1 myWorker1; //Create a worker

    MTL::MTLThreadPool threadPool(myWorker1, 4); //Create a thread pool with 4 workers

    MTL::MTLThread thread(threadPool); //Create a thread object and pass it the thread pool
    thread.run(); //Start the thread pool that will start all the workers
    for (int i = 0; i < 10; ++i) // Enqueue 10 messages
    {
        std::cout << "Inject Message " << i << std::endl;
        MTL::Message message(new int(i));
        threadPool.onMessage(message);        
    }
    std::this_thread::sleep_for(std::chrono::milliseconds(10000)); //Let him work for 10 seconds
    std::cout << "Suspend" << std::endl; 
    thread.suspend(); // Suspend the thread pool that will suspend all the workers
    for (int i = 10; i < 20; ++i) // Enqueue 10 messages
    {
        std::cout << "Inject Message " << i << std::endl;
        MTL::Message message(new int(i));
        threadPool.onMessage(message);       

    }
    std::this_thread::sleep_for(std::chrono::milliseconds(5000)); //Let Suspeded for 5 seconds
    std::cout << "Resume" << std::endl; 
    thread.resume(); // Resume the thread pool that will resume all the workers

    std::cout << "Exit" << std::endl;
    thread.clean_exit();  // Clean Exit the thread pool that will clean exit all the workers  
    while (thread.isRunning()) //Wait for the thread pool to exit
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        std::cout << "Counter: " << counter << std::endl;
        counter++;
    }
    std::cout << "Thread Pool Terminated" << std::endl;

    return 0;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Классы Shared Object и Shared Memory

Класс Shared Object — это класс, который управляет объектами общей памяти и может быть использован в потокобезопасной манере.

Класс Shared Memory — это класс, который управляет объектами общей памяти и может быть использован в потокобезопасном режиме.

Эти два класса могут быть использованы для создания потокобезопасной памяти для обмена данными между потоками.

Ниже приведен простой пример приложения:

MySharedObject.hpp

#include "MTL.h"

class MySharedObject : public MTL::MTLSharedObject {
public:
    MySharedObject(unsigned int id) : MTLSharedObject(id) {
        std::cout << "MySharedObject::MySharedObject()" << std::endl;
        value = 0;
    }
    ~MySharedObject() {
        std::cout << "MySharedObject::~MySharedObject()" << std::endl;
    }

    int getValue() {
        return value;
    }
    void setValue(int v) {
        value = v;
    }
private:
    int value; //The value of the shared object
};
Вход в полноэкранный режим Выйти из полноэкранного режима

MyRunnable.hpp

#include "MTL.h"

class MyRunnable : public MTL::MTLRunnable
{
public:
    MyRunnable(MTL::MTLSharedMemory* sharedMemory) : m_sharedMemory(sharedMemory)
    {};
    virtual ~MyRunnable() = default;
    void run(MTL::MTLThreadInterface* threadIf)
    {
        std::cout << "Hello World!" << std::endl;
        int counter = 0;
        while (true)
        {
            if(threadIf->getThreadState() == MTL::E_MTLThreadState::STOPPED){
                std::cout << "Stopped Thread Id: " << std::this_thread::get_id() << std::endl;
                std::this_thread::sleep_for(std::chrono::milliseconds(1000));
            }else if (threadIf->getThreadState() == MTL::E_MTLThreadState::SUSPENDED)
            {
                std::cout << "Suspended Thread Id: "<< std::this_thread::get_id() << std::endl;
                std::this_thread::sleep_for(std::chrono::milliseconds(1000));
            }else if (threadIf->getThreadState() == MTL::E_MTLThreadState::EXITED)
            {
                std::cout << "Exited Thread Id: " << std::this_thread::get_id() << std::endl;
                break;
            } else if (threadIf->getThreadState() == MTL::E_MTLThreadState::RUNNING)
            {
                // when running this runnable increment the shared memory value by 1
                MySharedObject& myObj = dynamic_cast<MySharedObject&>(m_sharedMemory->getSharedObjectById(1));
                std::cout << "Thread Id: " << std::this_thread::get_id()  << " starting Value: " << myObj.getValue() << " end Value: " << myObj.getValue() + 1 << std::endl;
                myObj.setValue(myObj.getValue() + 1);
                m_sharedMemory->releaseSharedObject(myObj);
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
                counter++;
            }
        }

    }

    void stop()
    {
        std::cout << "Stopping" << std::endl;
    }

    void suspend()
    {
        std::cout << "Suspending" << std::endl;
    }

    void resume()
    {
        std::cout << "Resuming" << std::endl;
    }

    void clean_exit()
    {
        std::cout << "Exiting" << std::endl;
    }

    void force_exit()
    {
        std::cout << "Force Exiting" << std::endl;
    }

private:
    MTL::MTLSharedMemory* m_sharedMemory;
};
Вход в полноэкранный режим Выход из полноэкранного режима

main.cpp

#include "MTL.h"
#include "MySharedObject.hpp"
#include "MyRunnable.hpp"

int main()
{
    /**
     *  This example demostrate how the shared memory and shared object classes 
     *  can be used to share data between threads. Without alter the atomicity 
     *  of the execution over the memory objects


     **/

    std::unique_ptr<MySharedObject> myObj(new MySharedObject(1)); //Create a shared object
    MTL::MTLSharedMemory sharedMemory; //Create a shared memory
    sharedMemory.addSharedObject(std::move(myObj)); //Add the shared object to the shared memory
    MyRunnable myRunnable1(&sharedMemory); //Create a runnable
    MyRunnable myRunnable2(&sharedMemory); //Create another runnable

    MTL::MTLThread thread1(myRunnable1); //Create a thread from the runnable
    MTL::MTLThread thread2(myRunnable2); //Create another thread from the runnable
    thread1.run(); //Run the thread
    thread2.run(); //Run the thread

    std::this_thread::sleep_for(std::chrono::milliseconds(1000)); //Let the threads run for 1 second
    std::cout << "Suspend Thread 1" << std::endl;
    thread1.suspend(); //Suspend the thread 1
    std::this_thread::sleep_for(std::chrono::milliseconds(1000)); //Let the thread 2 runs for 1 second
    std::cout << "Resume Thread 1" << std::endl;  
    thread1.resume(); //Resume the thread 1
    std::this_thread::sleep_for(std::chrono::milliseconds(1000)); //Let the threads run for 1 second
    std::cout << "Exit the Threads" << std::endl;
    thread1.clean_exit();    //Exit the thread 1
    thread2.clean_exit();    //Exit the thread 2

    thread1.join(); //Wait for the thread 1 to finish
    thread2.join(); //Wait for the thread 2 to finish

    std::cout << "Threads Joined" << std::endl;

    return 0;
}
Войти в полноэкранный режим Выход из полноэкранного режима

Класс задач

Класс задач является оберткой класса задач C++.
Для его выполнения требуется запускаемая задача.
Простым примером может быть следующий:

MyRunnableTask.hpp

#include <iostream>
#include "MTL.h"

class MyRunnableTask : public MTL::MTLRunnableTask
{
public:
    MyRunnableTask() = default;
    virtual ~MyRunnableTask() = default;
    std::shared_ptr<void> run(MTL::MTLTaskInterface *interface = nullptr)
    {
        std::cout << "Hello World!" << std::endl;
        std::cout << "Simulating Working for 3 seconds" << std::endl;

        for (int i = 0; i < 3; i++)
        {
            std::cout << "." << std::flush;
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
        std::cout << std::endl;
        std::cout << "This is a Task that return 1" << std::endl;

        int i = 1;
        std::shared_ptr<void> result(new int(i));
        return result;
    }
};
Вход в полноэкранный режим Выйти из полноэкранного режима

main.cpp

#include "MyRunnableTask.hpp"

int main()
{
    MyRunnableTask myRunnableTask; //Create a runnable task
    MTL::MTLTask task(myRunnableTask); //Create a task from the runnable task
    task.run(); //Run the task
    std::shared_ptr<void> result = task.getResult(); //Get the result of the task
    std::cout << "Result: " << *(static_cast<int *>(result.get())) << std::endl; //Print the result
    return 0;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Классы Ordered Task и Task Flow

Класс Ordered Task является производным от класса Task и имеет список предшественников и список преемников.

Класс Task Flow — это класс, который позволяет последовательно выполнять упорядоченные задачи и возвращать конечный результат.

Простым примером может быть следующий:

DerivedTasks.hpp

#include <iostream>
#include "MTL.h"

//Return 2
class Var2Task : public MTL::MTLRunnableTask
{
public:
    Var2Task() = default;
    virtual ~Var2Task() = default;
    std::shared_ptr<void> run(MTL::MTLTaskInterface *interface = nullptr)
    {
        MTL::MTLOrderedTaskInterface *orderedTaskIf = dynamic_cast<MTL::MTLOrderedTaskInterface *>(interface);
        std::cout << orderedTaskIf->getTaskName() << ": "
                  << "Start Task " << std::endl
                  << std::flush;
        std::cout << orderedTaskIf->getTaskName() << ": "
                  << "Simulating Working for 3 seconds" << std::endl
                  << std::flush;

        for (int i = 0; i < 3; i++)
        {
            std::cout << "." << std::flush;
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }

        std::cout << std::endl;

        auto previousResult = orderedTaskIf->getPredecessorsResults();

        std::shared_ptr<void> result(new int(2));
        std::cout << orderedTaskIf->getTaskName() << ": "
                  << "Result = " << *((int *)(result.get())) << std::endl
                  << std::flush;
        return result;
    }
};

// Sum predecessors
class SumTask : public MTL::MTLRunnableTask
{
public:
    SumTask() = default;
    virtual ~SumTask() = default;
    std::shared_ptr<void> run(MTL::MTLTaskInterface *interface = nullptr)
    {
        MTL::MTLOrderedTaskInterface *orderedTaskIf = dynamic_cast<MTL::MTLOrderedTaskInterface *>(interface);
        std::cout << orderedTaskIf->getTaskName() << ": "
                  << "Start Task " << std::endl
                  << std::flush;
        std::cout << orderedTaskIf->getTaskName() << ": "
                  << "Simulating Working for 3 seconds" << std::endl
                  << std::flush;

        for (int i = 0; i < 3; i++)
        {
            std::cout << "." << std::flush;
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }

        std::cout << std::endl;

        auto predecessorsResults = orderedTaskIf->getPredecessorsResults();
        std::shared_ptr<void> result(new int(0));
        if (predecessorsResults.empty())
        {
            std::shared_ptr<void> result(new int(0));
            return result;
        }
        else
        {
            auto it = predecessorsResults.begin();
            std::shared_ptr<void> result((int *)it->second.get());
            ++it;
            for (it; it != predecessorsResults.end(); ++it)
            {
                int *previousResult = (int *)(it->second.get());
                int *currentResult = (int *)(result.get());
                *currentResult = (*currentResult) + (*previousResult);
            }
            std::cout << orderedTaskIf->getTaskName() << ": "
                      << "Result = " << *((int *)(result.get())) << std::endl
                      << std::flush;
            return result;
        }
    }
};

// Multiply predecessors
class MulTask : public MTL::MTLRunnableTask
{
public:
    MulTask() = default;
    virtual ~MulTask() = default;
    std::shared_ptr<void> run(MTL::MTLTaskInterface *interface = nullptr)
    {
        MTL::MTLOrderedTaskInterface *orderedTaskIf = dynamic_cast<MTL::MTLOrderedTaskInterface *>(interface);
        std::cout << orderedTaskIf->getTaskName() << ": "
                  << "Start Task " << std::endl
                  << std::flush;
        std::cout << orderedTaskIf->getTaskName() << ": "
                  << "Simulating Working for 3 seconds" << std::endl
                  << std::flush;

        for (int i = 0; i < 3; i++)
        {
            std::cout << "." << std::flush;
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }

        std::cout << std::endl
                  << std::flush;

        auto predecessorsResults = orderedTaskIf->getPredecessorsResults();
        std::shared_ptr<void> result(new int(0));
        if (predecessorsResults.empty())
        {
            std::shared_ptr<void> result(new int(0));
            return result;
        }
        else
        {
            auto it = predecessorsResults.begin();
            std::shared_ptr<void> result((int *)it->second.get());
            ++it;
            for (it; it != predecessorsResults.end(); ++it)
            {
                int *previousResult = (int *)(it->second.get());
                int *currentResult = (int *)(result.get());
                *currentResult = (*currentResult) * (*previousResult);
            }
            std::cout << orderedTaskIf->getTaskName() << ": "
                      << "Result = " << *((int *)(result.get())) << std::endl
                      << std::flush;
            return result;
        }
    }
};

Вход в полноэкранный режим Выйти из полноэкранного режима

main.cpp

#include "DerivedTasks.hpp"

// This Example simulate the execution of the following expression ((a+b)+c)*(d+e) where a,b,c,d,e are value 2
int main()
{
    std::cout << "Running Example 7 for MTL Version " << MTL_VERSION_MAJOR << "." << MTL_VERSION_MINOR << "." << MTL_VERSION_PATCH << std::endl;
    Var2Task var2Task; // Task that return 2
    SumTask sum2Task; // Task that return the sum of the predecessors 
    MulTask mul2Task; // Task that return the multiplication of the predecessors

    auto taskA = std::make_shared<MTL::MTLOrderedTask>("a", var2Task); // Task that return 2
    auto taskB = std::make_shared<MTL::MTLOrderedTask>("b", var2Task); // Task that return 2
    auto taskC = std::make_shared<MTL::MTLOrderedTask>("c", var2Task); // Task that return 2
    auto taskD = std::make_shared<MTL::MTLOrderedTask>("d", var2Task); // Task that return 2
    auto taskE = std::make_shared<MTL::MTLOrderedTask>("e", var2Task); // Task that return 2

    auto taskF = std::make_shared<MTL::MTLOrderedTask>("f", sum2Task); // Task that return the sum of the predecessors
    auto taskG = std::make_shared<MTL::MTLOrderedTask>("g", sum2Task); // Task that return the sum of the predecessors
    auto taskH = std::make_shared<MTL::MTLOrderedTask>("h", sum2Task); // Task that return the sum of the predecessors

    auto taskI = std::make_shared<MTL::MTLOrderedTask>("i", mul2Task); // Task that return the multiplication of the predecessors

    MTL::MTLTaskFlow taskFlow; // Task Flow
    taskFlow.precede(taskA, taskF); // Task A precede Task F
    taskFlow.precede(taskB, taskF); // Task B precede Task F
    taskFlow.precede(taskC, taskG); // Task C precede Task G
    taskFlow.precede(taskF, taskG); // Task F precede Task G
    taskFlow.precede(taskD, taskH); // Task D precede Task H
    taskFlow.precede(taskE, taskH); // Task E precede Task H
    taskFlow.precede(taskG, taskI); // Task G precede Task I
    taskFlow.precede(taskH, taskI); // Task H precede Task I

    taskFlow.run(); // Run the task flow
    std::shared_ptr<void> result = taskFlow.getResult(); // Get the result of the task flow
    std::cout << "Result: " << *(static_cast<int *>(result.get())) << std::endl; // Print the result
    return 0;
}

Войти в полноэкранный режим Выход из полноэкранного режима

Для получения дополнительных примеров, пожалуйста, обратитесь к директории примеров

Компиляция и использование

Необходимые условия

Для запуска проекта в работу необходимо следующее.

Google Test

GoogleTest

git clone https://github.com/google/googletest.git  # Dowload the Google Test repository
cd googletest                                       # Main directory of the cloned repository.
mkdir -p build                                      # Create a directory to hold the build output.
cd build                                            # Move into the build directory.   
cmake ..                                            # Generate native build scripts for GoogleTest.
make                                                # Compile
sudo make install                                   # Install in /usr/local/ by default
Войдите в полноэкранный режим Выйти из полноэкранного режима

Установка

git clone https://github.com/ZigRazor/MTL.git       # Dowload the MTL repository
cd MTL                                              # Main directory of the cloned repository.
mkdir -p build                                      # Create a directory to hold the build output.
cd build                                            # Move into the build directory.   
cmake ..                                            # Generate native build scripts for MTL.
make                                                # Compile
sudo make install                                   # Install in /usr/local/ by default
Войти в полноэкранный режим Выход из полноэкранного режима

Использование

Чтобы получить библиотеку в вашем проекте, вам нужно сделать только одно:
— Включить заголовочный файл MTL.h в ваш проект.
— Подключить библиотеку MTL.so к вашему проекту.

Как внести свой вклад

Вклад — это то, что делает сообщество открытого кода таким удивительным местом для обучения, вдохновения и творчества. Любой ваш вклад будет высоко оценен.

Если у вас есть предложение, которое могло бы сделать этот проект лучше, пожалуйста, сделайте форк репозитория и создайте запрос на исправление. Вы также можете просто открыть проблему с тегом «улучшение».
Не забудьте поставить проекту звезду! Еще раз спасибо!
Перед выполнением этих шагов, пожалуйста, прочитайте Руководство по внесению вклада и Кодекс поведения.

  1. Форк проекта
  2. Создайте свою ветку Feature (git checkout -b feature/AmazingFeature)
  3. Зафиксируйте свои изменения (git commit -m 'Add some AmazingFeature')
  4. Переместите изменения в ветку (git push origin feature/AmazingFeature)
  5. Открыть Pull Request

Контакты

ZigRazor — zigrazor@gmail.com

Ссылка на проект: https://github.com/ZigRazor/MTL

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *