Форум программистов, компьютерный форум, киберфорум
С++ для начинающих
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  
 
 
Рейтинг 4.86/64: Рейтинг темы: голосов - 64, средняя оценка - 4.86
Заблокирован

Std::thread приостановка потока

05.08.2015, 21:47. Показов 12863. Ответов 25
Метки нет (Все метки)

Студворк — интернет-сервис помощи студентам
Есть прога в C++ Builder. Три функции описаные внизу запускаются в трех разных потоках. Первый поток генерирует данные для для двух других, запускает их и сам приостанавливается. Каждый из двух других потоков после выполнения увеличивает переменную flag и приостанавливается, и тот поток который отработал последним проверяет и если flag==2, то запускает первый поток(тот который генерирует данные), а сам останавливается.

То есть работает первый поток, потом два других в паралели, потом опять первый, потом опять два других в паралели...

Кликните здесь для просмотра всего текста
C++
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
void __fastcall TForm1::Execute() {
        n=0;
        list <TTicks> listData;
        GenThread->SetContainer(&listData);
        DelThread->SetContainer(&listData);
        for ( list<int>::iterator i = listStart.begin() ; i != listStart.end() ; ++i ) {
                        flag = 0;
                        it = *i;
                        n++;
                        DelThread->NewIter(listData.end().operator --());
                        GenThread->Resume();
                        DelThread->Resume();
                        MainThread->Suspend();
        }
        terminate = 1;
        GenThread->Resume();
        DelThread->Resume();
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Generation(list <TTicks> *listData) {
        TTicks tempTicks;
        while (!terminate) {
                for ( int i = 1 ; i < 6; ++i) {
                        tempTicks.Init(it,i);
                        EnterCriticalSection(&csD);
                                listData->push_back(tempTicks);
                        LeaveCriticalSection(&csD);
                }
                EnterCriticalSection(&cs);
                        flag++;
                LeaveCriticalSection(&cs);
                if ( flag == 2 ) MainThread->Resume();
                GenThread->Suspend();
        }
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Delete(list <TTicks> *listData,list <TTicks>::iterator *ItEnd) {
        bool key;
        while (!terminate) {
                key = false;
                for ( list<TTicks>::iterator jt = listData->begin() ; jt!=listData->end() ; ) {
                        if ( jt == *ItEnd) key = true;
                        if (jt->getTimer(it)) {
                                EnterCriticalSection(&csD);
                                        jt = listData->erase(jt);
                                LeaveCriticalSection(&csD);
                        } else {
                                ++jt;
                        }
                        if ( key ) break;
                }
                EnterCriticalSection(&cs);
                        flag++;
                LeaveCriticalSection(&cs);
                if ( flag == 2 ) MainThread->Resume();
                DelThread->Suspend();
        }
}

интересует как в std::thread реализовать подобие работы методов ->Suspend() и ->Resume()

если кому интересно вся программа лежит тут
Вложения
Тип файла: zip thread.zip (104.2 Кб, 11 просмотров)
0
cpp_developer
Эксперт
20123 / 5690 / 1417
Регистрация: 09.04.2010
Сообщений: 22,546
Блог
05.08.2015, 21:47
Ответы с готовыми решениями:

Присоединение потока std::thread
Есть некая функция: int arr(){ do_something(); std::thread th1({ func1(); func2(); func3();}); th1.join() do_something2(); ...

Ошибка при создании потока std::thread
Внутри класса есть функция void move(); Нужно, чтобы она работала в отдельном потоке. Проблема : неправильно указываю параметры....

Использование std::function в std::thread
Нужно вызвать function fnc в новом потоке. Как сделать? function &lt;void(vector&lt;char&gt;)&gt; fnc; void test(vector&lt;char&gt; data) { ...

25
 Аватар для Kastaneda
5232 / 3205 / 362
Регистрация: 12.12.2009
Сообщений: 8,143
Записей в блоге: 2
05.08.2015, 22:31
Тут отсылки в гугл не приветствуются, но все же советую погуглить "c++11 producer consumer", это классика.
0
Эксперт по математике/физикеЭксперт С++
 Аватар для Ilot
2224 / 1426 / 420
Регистрация: 16.05.2013
Сообщений: 3,646
Записей в блоге: 6
06.08.2015, 11:58
Цитата Сообщение от iiieoi Посмотреть сообщение
интересует как в std::thread реализовать подобие работы методов ->Suspend() и ->Resume()
Условные переменные, а еще лучше будующие результаты:
C++
1
2
3
4
5
6
7
// < асинхронный запуск потоков потоков 
std::future<void> ft1 = std::async(std::launch::async, ...);
std::future<void> ft2 = std::async(std::launch::async, ...);
// < ждем завершения потоков
ft1.get();
ft2.get();
// < идем дальше
Непонятно зачем вообще нужен флаг flag...
0
Заблокирован
06.08.2015, 15:14  [ТС]
тут я привел сильно упрощенный вариант, чтобы было понятно как все должно работать.

1. основной поток(Execute): читает данные из массива, текуший элемент (it) передается в потоки, после этого он (основной поток) размораживает поток 1 и поток 2, а сам останавливается.
2. поток 1(Generation): в зависимости от значения it генерирует данные и добавляет их в конец списка (listData), после того как все данные добавлены поток останавливается
3. поток 2(Delete): в зависимости от значения (it) удаляет элементы из списка(listData), причем не трогает элементы которые возможно уже добавил поток 1 на текущей итерации, после того как все данные по условию удалены поток останавливается

для того чтобы вновь разморозить основной поток, когда все паралельные потоки завершили работу и был введен флаг, то есть последний из параллельных потоков по этому флагу смотрит, что он именно он последний завершает текущую итерацию, и запускает основной поток, который прочитает новый элемент из входного массива, относительно которого и будет запущена новая итерация обработки в потоке 1 и потоке 2.

Как это можно было сделать в Builder хитрее я незнаю.

Потоки создаются в самом начале, а удаляются в самом конце, т.к. тратить время и ресурсы на постоянное их создание не есть хорошо, учитывая что входной массив это больше 10 млн элементов.
0
Эксперт по математике/физикеЭксперт С++
 Аватар для Ilot
2224 / 1426 / 420
Регистрация: 16.05.2013
Сообщений: 3,646
Записей в блоге: 6
06.08.2015, 15:54
Приведенный выше код полностью подходит под ваше описание.
Начнем с конца.
Цитата Сообщение от iiieoi Посмотреть сообщение
для того чтобы вновь разморозить основной поток, когда все паралельные потоки завершили работу и был введен флаг, то есть последний из параллельных потоков по этому флагу смотрит, что он именно он последний завершает текущую итерацию, и запускает основной поток, который прочитает новый элемент из входного массива, относительно которого и будет запущена новая итерация обработки в потоке 1 и потоке 2.
Основной поток будет ждать завершения обоих потоков. Причем никакой флаг здесь не нужен:
C++
1
2
ft1.get();
ft2.get();
Цитата Сообщение от iiieoi Посмотреть сообщение
3. поток 2(Delete): в зависимости от значения (it) удаляет элементы из списка(listData), причем не трогает элементы которые возможно уже добавил поток 1 на текущей итерации, после того как все данные по условию удалены поток останавливается
Учитывая условия я бы рекомедовал данные подготовленные 1 потоком вставлять в список после завершения потока 2. Таким образом вам во-первых не потребуется синхронизация так как в данные момент со списком будет работать только один поток, а во-вторых потоки будут работать истино параллельно.
Уведомлять поток 1 о завершении работы потока 2 можно с помощью условных переменных.
1
Заблокирован
06.08.2015, 16:17  [ТС]
Цитата Сообщение от Ilot Посмотреть сообщение
Приведенный выше код полностью подходит под ваше описание.
Вы издеваетесь? С точки зрения ресурсов, это расточительно, если я правильно понял Вы предлагаете для всех элементов входного массива создавать по 2 потока которые потом убивать, то есть 20млн созданий потоков?

А за идею добавления данных блоком спасибо)
0
Эксперт С++
 Аватар для Avazart
8488 / 6155 / 615
Регистрация: 10.12.2010
Сообщений: 28,683
Записей в блоге: 30
06.08.2015, 16:19
Цитата Сообщение от iiieoi Посмотреть сообщение
Как это можно было сделать в Builder хитрее я незнаю.
В С++Builder тоже не рекомендовано использовать >Suspend() и ->Resume() Это устаревшие методы, ведущие зачастую к взаимным блокировкам.
Обычно виндой используют объекты-события (CreateEvent() + WaitForSingleObject() и кстати тут тоже нужно быть внимательным дедлокам) или системную очередь сообщений.

Если пользоваться стандартными средствами С++, то вероятно стоит смотреть в std::mutex+std::condition_variable
0
Эксперт по математике/физикеЭксперт С++
 Аватар для Ilot
2224 / 1426 / 420
Регистрация: 16.05.2013
Сообщений: 3,646
Записей в блоге: 6
06.08.2015, 16:52
Цитата Сообщение от iiieoi Посмотреть сообщение
Вы издеваетесь?
Мой косяк. Хотел сперва написать, но потом решил, что будет сложно объяснить. Но и сейчас подробно объяснять не стану...
Ну так вот. Вместо потоков используйте упакованные задачи, а сами потоки организуйте как пул. Подробнее можно почитать у Энтони Уильямса Параллельное программирование в действии.
0
Эксперт С++
 Аватар для Avazart
8488 / 6155 / 615
Регистрация: 10.12.2010
Сообщений: 28,683
Записей в блоге: 30
06.08.2015, 17:13
Ilot, Это если "одинаковые", а если нет то тут сложнее будет.
Тем более вроде же нет "готовых" пулов стандартной библиотеке.
0
Заблокирован
07.08.2015, 23:59  [ТС]
я правильно понимаю что для каждого из параллельных потоков нужна своя условная переменная, т.к. если использовать одну, то один из потоков после выполнения .notify_all() в основном потоке, заблокирует mutex и выйдет из .wait, а второй будет ждать когда он его освободит и никакой параллельности не будет?
0
Эксперт С++
 Аватар для hoggy
8973 / 4319 / 960
Регистрация: 15.11.2014
Сообщений: 9,760
08.08.2015, 12:11
Цитата Сообщение от iiieoi Посмотреть сообщение
я правильно понимаю что для каждого из параллельных потоков нужна своя условная переменная, т.к. если использовать одну, то один из потоков после выполнения .notify_all() в основном потоке, заблокирует mutex и выйдет из .wait, а второй будет ждать когда он его освободит и никакой параллельности не будет?
по-хорошему, рекомендую вам обратить свой взор на boost::thread_pool
профессиональный кросс-платформенный инструмент.

если же ваша цель - научиться самому строить подобные механизмы,
тогда предлагаю вашему вниманию рассмотреть конструкцию учебного тред-пула.

она очень простая, и должно быть, содержит ответы на все ваши вопросы.

итак:

файлы проекта:

Code
1
2
3
4
5
6
7
8
9
10
11
thread_pool
 |
 |--- include
 |      `-- thread_pool
 |            |--- atomic_stream.h
 |            |--- task_queue.h
 |             `-- thread_pool.h
  `-- src
       |--- main.cpp
       |--- task_queue.cpp
        `-- thread_pool.cpp
main.cpp
содержит пример-иллюстрацию использования

atomic_stream.h
атомарный вывод данных в консоль
напоминает std::cout, но вывод не коверкается,
когда несколько потоков одновременно пишут в консоль
только для отладочных целей.

task_queue
очередь задач.
по сути, это просто thread-safe очередь из std::function

thread_pool
учебная модель тред-пула

-----------------------------------------------------------------------------

main.cpp

C++
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
59
60
61
62
63
64
65
66
67
#include "thread_pool/atomic_stream.h"
#include "thread_pool/thread_pool.h"
 
// --- в качестве задачи просто будем выводить в консоль данные
void Worker()
{
    const auto id 
        = std::this_thread::get_id();
 
    for(size_t n = 0; n<10; ++n)
        // --- атомик-стрим используется исключительно в учебных целях
        // его использование сводит на нет весь перфоманс
        // от использования потоков
        // поэтому, в боевых условиях его не нужно использовать
        service::AStream() << "[" << id << "]" << n << "\n";
}
 
// --- эту функцию будем вызывать для каждого потока
// в момент его завершения
void Stop()
{
    const auto id
        = std::this_thread::get_id();
    service::AStream() << "[" << id << "] finished\n";
}
 
// --- последняя задача для тред-пула
// она должна просигналить пулу о том,
// что нужно остановиться
void Finish()
{
    service::AStream() << "[ALL TASK FINISHED]\n";
 
    // --- иногда по факту завершению работы потока
    // необходимо выполнить дополнительную функцию-зачистки
    // например, если требуется доступ к TLS (thread local storage) данным
    // в этом случае можно указать дополнительную функцию
    // которая будет вызвана для каждого остановившегося потока
 
    // если такая зачистка не нужна
    // то можно вызвать Stop() без аргументов
    service::ThreadPool::Get().Stop(::Stop);
}
 
int main()
{
    std::cout<<"hello\n";
 
    // --- получаем доступ к статическому тред-пулу
    auto& pool = service::ThreadPool::Get();
 
    // --- забиваем в тред-пул 100 задач
    for(size_t n=0; n<100; ++n)
        pool.AddTask(Worker);
 
    // тред пул будет работать хоть до бесконечности,
    // пока какой либо из тредов не просигналит об остановке
    // поэтому, добавляем последнею задачу,
    // которая пошлет сигнал остановки
    pool.AddTask(Finish);
 
    // запуск и ожидание завершения работы
    // всех тредов
    pool.Start();
 
    std::cout<<"success\n";
}
-----------------------------------------------------------------------------

atomic_stream.h

C++
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#pragma once
 
#include <iostream>
#include <sstream>
#include <mutex>
 
//-----------------------------------------------------------------------
// атомарный вывод данных в консоль
//-----------------------------------------------------------------------
 
namespace service{
 
    template<class T>
    class BasicAtomicStream
    {
        typedef std::mutex 
            mutex_type;
 
        typedef typename T::char_type
            char_type;
 
        typedef std::basic_ostringstream<char_type>
            buffer_type;
    public:
        BasicAtomicStream(T& s)
            : mMutex()
            , mStream()
            , mDst(s)
        {}
 
       ~BasicAtomicStream()
        {
            mMutex.lock();
            mDst << mStream.str();
            mMutex.unlock();
        }
 
        template<class U> 
        buffer_type& operator << (U&& v)
        { 
            mStream << v;
            return mStream;
        }
 
    private:
        mutex_type  
            mMutex;
 
        buffer_type 
            mStream;
 
        T& mDst;
    };
 
    template<class T> 
    class AtomicStream 
        : public BasicAtomicStream<T>
    {
        typedef BasicAtomicStream<T>
            base;
 
        AtomicStream(T& os)
            :base(os)
        {}
 
    };
 
    template<>
    class AtomicStream<std::ostream>
        : public BasicAtomicStream<std::ostream>
    {
        typedef BasicAtomicStream<std::ostream>
            base;
    public:
        AtomicStream(std::ostream& os = std::cout)
            :base(os)
        {}
    };
 
    template<>
    class AtomicStream<std::wostream>
        : public BasicAtomicStream<std::wostream>
    {
        typedef BasicAtomicStream<std::wostream>
            base;
    public:
        AtomicStream(std::wostream& os = std::wcout)
            :base(os)
        {}
    };
 
    typedef AtomicStream<std::ostream>
        AStream;
    typedef AtomicStream<std::wostream>
        WAStream;
 
 
}//namespace service
-----------------------------------------------------------------------------

task_queue.h

C++
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
#pragma once
 
#include <functional>
#include <atomic>
#include <mutex>
#include <queue>
 
 
struct TaskQueue final 
{
    typedef ::std::function<void()> 
        Task;
 
   ~TaskQueue(){}
    TaskQueue();
 
    // добавляйте std::function/функцию/лямбду/функтор
    template<class T>
    void AddTask(T&& task)
    {
        ::std::lock_guard<decltype(mGuard)>
            lock(mGuard);
 
        mTasks.push(task);
    }
 
    // запускает 1 задачу
    // вернёт true, если ещё остались задачи
    bool RunOne(); 
 
    // запускает все задачи
    size_t Run() ; 
 
    // очищает очередь задач
    void Clear() ; 
 
    // true, если очередь задач пуста
    bool Empty()const; 
private:
    //удалены
    TaskQueue(const TaskQueue&) ;
    TaskQueue& operator=(const TaskQueue&);
    
private:
    ::std::queue<Task>   
        mTasks;
    mutable ::std::mutex 
        mGuard;
};
task_queue.cpp

C++
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
#include "thread_pool/task_queue.h"
 
 
TaskQueue::TaskQueue()
    :mTasks()
    ,mGuard()
{}
 
bool TaskQueue::RunOne()
{
    Task task;
    {
        ::std::lock_guard<decltype(mGuard)> 
            lock(mGuard);
        if(!mTasks.empty())
            task = mTasks.front(),
            mTasks.pop();
        else
            return false;
    }
    return task(), 
        true;
}
 
size_t TaskQueue::Run()
{
    size_t count = 0;
    while(RunOne())
        ++count;
    return count;
}
 
void TaskQueue::Clear()
{
    ::std::lock_guard<decltype(mGuard)>
        lock(mGuard);
    while(!mTasks.empty())
        mTasks.pop();
}
 
bool TaskQueue::Empty()const
{
    ::std::lock_guard<decltype(mGuard)> 
        lock(mGuard);
    return mTasks.empty();
}
-----------------------------------------------------------------------------

thread_pool.h

C++
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#pragma once
 
#include "task_queue.h"
 
#include <condition_variable>
#include <thread>
#include <vector>
 
 
namespace service{
 
    struct ThreadPool
    {
        typedef std::function<void()>
            Task;
 
        //если 0 - количество потоков 
        //будет определено автоматически
        ThreadPool(const size_t num_threads = 0);
 
        // добавляйте std::function/функцию/лямбду/функтор
        template<class T>
        ThreadPool& AddTask(T&& task) 
        { 
            mTaskQueue.AddTask(task); 
 
            // --- если пул уже запущен
            // то при добавлении 1 задачи, будем 1 поток
            if(mStarted)
                mCondition.notify_one();
            return *this;
        }
 
        // эта задача будет выполнена для каждого треда
        // по завершению работы тред-пула
        // укажите std::function/функцию/лямбду/функтор
        template<class T>
        ThreadPool& SetFinished(T&& task)
        {
            ::std::lock_guard<decltype(mGuard)>
                lock(mGuard);
            mTaskStop = task;
            return *this;
        }
 
        // сигнал остановки работы тред пула
        // по завершению работы, тред запустит эту задачу
        // (std::function/функцию/лямбду/функтор)
        template<class T> void Stop(T&& task)
        {
            SetFinished( std::forward<T>(task)  );
            Stop();
        }
 
        // запуск обработки задач
        // и ожидание завершения всех потоков
        void Start();
 
        // потоки доделывают текущие задачи и завершаются
        void Stop();
 
        // тред-пул сингелтон
        static ThreadPool& 
            Get(const size_t num_threads=0);
 
    private:
        // ждать завершения работы всех потоков
        void Wait();
 
        std::atomic<bool>
            mStarted;
        std::atomic<bool>
            mStopped;
        ::std::mutex
            mGuard;
        Task
            mTaskStop;
        TaskQueue
            mTaskQueue;
        std::condition_variable 
            mCondition;
        std::vector<std::thread> 
            mThreads;
    };
 
}//namespace service
thread_pool.cpp

C++
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#include "thread_pool/thread_pool.h"
 
namespace service{
 
    ThreadPool::ThreadPool(const size_t num_threads)
        :mStarted(false)
        ,mStopped(false)
        ,mTaskStop()
        ,mTaskQueue()
        ,mCondition()
        ,mThreads()
    {
 
        // --- если количество тредов 0
        // тогда будет выбрано оптимальное количество
        // (равное количеству ядер процессора)
        const auto num = num_threads==0?
                std::thread::hardware_concurrency():
                num_threads;
 
        mThreads.reserve(num);
        for(size_t n=0; n<num; ++n)
            mThreads.emplace_back([this, n]()
            { 
                std::mutex mutex;
 
                // --- если тред-пул ещё не стартовал, 
                // --- то тред засыпает
                if(!mStarted)
                {
                    std::unique_lock<std::mutex>
                        locker(mutex);
                    mCondition.wait(locker);
                }
 
                // до тех пор, пока не будет получен сигнал о завершении
                while(!mStopped)
                {
                    std::unique_lock<std::mutex>
                        locker(mutex);
 
                    // --- тред извлекает и исполняет очередную задачу
                    const bool empty = !mTaskQueue.RunOne();
 
                    // --- если по окончанию выполнения очередной задачи
                    // был получен сигнал остановки,
                    // то тред завершается
                    if(mStopped)
                        break;
 
                    // --- если мы здесь, значит сигнала остановки ещё не было
                    if(empty)
                        // --- если задача была последней - тред засыпает
                        mCondition.wait(locker);
                }
 
                // --- если мы здесь, значит был получен сигнал остановки
                // если была указана функция зачистки
                // тред запускает её, и полностью завершается
                if(mTaskStop)
                    mTaskStop();
            });
    }
 
    void ThreadPool::Start()
    { 
        mStarted = true;
        // --- запускает все треды на выполнение
        // и ожидает их завершения
        mCondition.notify_all(); 
        Wait();
    }
 
    void ThreadPool::Stop()
    {
        // --- выставляет флаг остановки,
        // --- и пробуждает все спящие треды
        // --- что бы проснувшись, они корректно завершились
        mStopped  = true;
        mCondition.notify_all();
    }
 
    void ThreadPool::Wait()
    {
        // --- ждем завершения всех тредов
        for(auto& i: mThreads)
            i.join();
    }
 
    ThreadPool& ThreadPool::Get(const size_t num_threads)
    {
        // --- thread-safe singelton
        static ThreadPool p(num_threads);
        return p;
    }
 
 
}//namespace service
-----------------------------------------------------------------------------------
-----------------------------------------------------------------------------------

тестировался на компиляторах:
cl(visual studio 2012/2013), mingw482, gcc(версию запамятовал).

если что нибудь не понятно,
или будут какие то ошибки - спрашивайте.
8
Игогошка!
 Аватар для ct0r
1801 / 708 / 44
Регистрация: 19.08.2012
Сообщений: 1,367
08.08.2015, 12:48
Цитата Сообщение от hoggy Посмотреть сообщение
если что нибудь не понятно,
или будут какие то ошибки - спрашивайте.
Почему где-то при работе с universal references используется std::forward, а где-то нет?
Можно еще typedef заменить на using, а объявления в private-секции без определений на =delete.
1
Эксперт С++
 Аватар для hoggy
8973 / 4319 / 960
Регистрация: 15.11.2014
Сообщений: 9,760
08.08.2015, 16:14
Цитата Сообщение от ct0r Посмотреть сообщение
Почему где-то при работе с universal references используется std::forward, а где-то нет?
std::forward вроде только в одном месте фигурирует.
и эта правка была сделанна уже в более поздний период.

вообще, это не принципиально.
можно вообще убрать универсальные ссылки,
заменив их константными ссылками.

смысл от этого не изменится.
поэтому, и заморачиваться особо здесь смысла не имеет.

Цитата Сообщение от ct0r Посмотреть сообщение
Можно еще typedef заменить на using, а объявления в private-секции без определений на =delete.
можно, только зачем?
он бы тогда на msvc2012 не взлетел.
0
Игогошка!
 Аватар для ct0r
1801 / 708 / 44
Регистрация: 19.08.2012
Сообщений: 1,367
08.08.2015, 16:52
Цитата Сообщение от hoggy Посмотреть сообщение
вообще, это не принципиально.
можно вообще убрать универсальные ссылки,
заменив их константными ссылками.
смысл от этого не изменится.
поэтому, и заморачиваться особо здесь смысла не имеет.
Тем не менее, код должен находиться в согласованном для людей состоянии. Если мы используем универсальные ссылки - должны использовать std::forward. Если не используем - не должны. Иначе у читающего появляются вопросы по поводу того, что задумывал тут автор.

Цитата Сообщение от hoggy Посмотреть сообщение
можно, только зачем?
он бы тогда на msvc2012 не взлетел.
Красивее. Читабельней. Ограничиваемся ошибками на этапе компиляции, а не линковки. По поводу msvs2012 я не в курсе, что там.
0
Эксперт С++
 Аватар для hoggy
8973 / 4319 / 960
Регистрация: 15.11.2014
Сообщений: 9,760
08.08.2015, 17:05
Цитата Сообщение от ct0r Посмотреть сообщение
Тем не менее, код должен находиться в согласованном для людей состоянии. Если мы используем универсальные ссылки - должны использовать std::forward. Если не используем - не должны. Иначе у читающего появляются вопросы по поводу того, что задумывал тут автор.
ну и какие вопросы возникли конкретно у вас?

я перефразирую:
что именно в коде лично вам не понятно?

Цитата Сообщение от ct0r Посмотреть сообщение
Красивее. Читабельней.
смысл читабельности: код должен быть понятным.

читабельность ради читабельности не нужна.

если вы понимаете смысл подобной записи:

C++
1
2
3
4
private:
    //удалены
    TaskQueue(const TaskQueue&) ;
    TaskQueue& operator=(const TaskQueue&);
значит код уже достаточно читабельный.
делать его ещё читабельнее - можно, если есть время,
и нет причин этому припятствующих.

Цитата Сообщение от ct0r Посмотреть сообщение
По поводу msvs2012 я не в курсе, что там.
а это и есть как раз причина,
по которой я и не стал наводить такую косметику.
недостаточная поддержка с++11.

то есть, вы конечно можете ещё улучшить читабельность,
потратив время на рефактор того, что и так работает.
и при этом собирается на старых компиляторах.

лично я не вижу в этом никакого смысла.
0
Игогошка!
 Аватар для ct0r
1801 / 708 / 44
Регистрация: 19.08.2012
Сообщений: 1,367
08.08.2015, 22:42
Цитата Сообщение от hoggy Посмотреть сообщение
ну и какие вопросы возникли конкретно у вас?
я перефразирую:
что именно в коде лично вам не понятно?
Ну давай разберем код подробнее.
Цитата Сообщение от hoggy Посмотреть сообщение
struct TaskQueue final
final не нужно. Это слишком жесткое ограничение здесь. И так понятно, что класс не предназначен для построения иерархии - нету виртуального деструктора. Реальное применение final в контексте классов может быть таким: у нас есть иерархия с кучей виртуальных функций. Мы объявляем последний класс в этой иерархии - Foo. Если мы объявим его как final, то везде, где у нас будет Foo* или Foo&, компилятор сделает оптимизацию - поменяет динамическое связывание (вызов функции-члена через таблицу виртуальных функций) на обыкновенное статическое.

Цитата Сообщение от hoggy Посмотреть сообщение
template<class T>
void AddTask(T&& task)
Что я думаю, когда вижу такой интерфейс? Что внутри используется std::forward и преимущества семантики перемещения. То есть я ожидаю, что если я озабочусь и пихну скажем временный таск с дешевым мувом, то программа от этого выиграет. А по факту оказывается, что это не так. То есть интерфейс вроде как подсказывает, как именно нужно писать код для эффективного использования класса, но... тут лишь вводит в заблуждение. Аналогично с SetFinished.

Цитата Сообщение от hoggy Посмотреть сообщение
std::unique_lock<std::mutex>
Не совсем понял, зачем тут unique_lock. Почему не тот же более легковесный lock_guard?

Цитата Сообщение от hoggy Посмотреть сообщение
mStarted = true;
Лично я бы явно вызывал store. Потому как из этой строчки мне не видно, атомарно ли присваивание.

Цитата Сообщение от hoggy Посмотреть сообщение
size_t TaskQueue::Run()
Эту функцию лучше фтопку. Она очень легко реализуема, обращается только к открытому интерфейсу. Помним о принципе - API должно быть минимально, но не меньше. По крайней мере можем ее вынести из класса.

Цитата Сообщение от hoggy Посмотреть сообщение
ThreadPool(const size_t num_threads = 0);
explicit?

Цитата Сообщение от hoggy Посмотреть сообщение
значит код уже достаточно читабельный.
делать его ещё читабельнее - можно, если есть время,
и нет причин этому припятствующих.
О том и речь . У меня есть минутка и нет причин Давай отвлечемся от компиляторов. Вообще главный смысл =delete (для функций-членов) вот в чем. Их даже два.
1) При обращении из функций-членов класса и дружественных функций мы получаем ошибку компиляции, а не компоновки. Это как минимум меньше раздражает
2) Мне достаточно просмотреть public-секцию для того, чтобы иметь полное представление об интерфейсе класса. При использовании же костыля С++03 (согласись, это все-таки совсем не интуитивно для тех, кто видит такое в первый раз) мне нужно еще взглянуть и на private-секцию.
2
Заблокирован
09.08.2015, 14:13  [ТС]
я слабо представляю как можно применить пул потоков к моей задаче, вот временная диаграмма чтоб было понятнее в какое время какие потоки должны работать (см.вложение).

вот что у меня получилось пока, временами программа работает хорошо, но иногда зависает и не могу понять почему. может кто подскажет.

C++
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
 
using namespace std;
mutex m0,m1,m2,m3,m4;
condition_variable c1,c2;
vector <int> vInt;
bool b1 = false;
bool b2 = false;
int b0;
 
void gen() {
    unique_lock<mutex> lk1(m3);
    for (int i=1; i<6; ++i) {
        if (i>1) { // в перовой итерации цикла пропускаем ожидание окончания работы потоков ex1, ex2
            while (b0 < 2) {
                c2.wait(lk1); //ожидание окончания работы потоков ex1, ex2
            };
        };
        m0.lock();
            vInt.push_back(1);
        m0.unlock();
 
        m4.lock();
            b0 = 0;
            b1=true;
            b2=true;
            c1.notify_all(); //разблокируем потоки ex1, ex2
        m4.unlock();
 
    };
};
 
void ex1() {
    unique_lock<mutex> lk1(m1);
    for (int i = 1; i<6; ++i) {
        while (!b1) {
            c1.wait(lk1); //ожидание окончания работы потока gen
        };
        for (int i = 1; i<10; ++i) {
            m0.lock();
                vInt.push_back(2);//тут должна быть функция1 выполняемая в этом потоке
            m0.unlock();
        };
        m4.lock();
            b1=false;
            b0++;
            if (b0 == 2) //проверяем что этот поток последний завершает работу
                c2.notify_one(); //разблокируем поток gen
        m4.unlock();
    };
};
 
void ex2() {
    unique_lock<mutex> lk1(m2);
    for (int i = 1; i<6; ++i) {
        while (!b2) {
            c1.wait(lk1); //ожидание окончания работы потока gen
        };
        for (int i = 1; i<10; ++i) {
            m0.lock();
                vInt.push_back(3); //тут должна быть функция (не такая как в потоке ex1) выполняемая в этом потоке
            m0.unlock();
        };
        m4.lock();
            b2=false;
            b0++;
            if (b0 == 2) //проверяем что этот поток последний завершает работу
                c2.notify_one(); //разблокируем поток gen
        m4.unlock();
    };
};
 
int main()
{
    thread tgen(gen);
    thread t1(ex1);
    thread t2(ex2);
    tgen.join();
    t1.join();
    t2.join();
 
    for (vector <int>::iterator it=vInt.begin(); it != vInt.end(); ++it){
        cout << *it;
    };
 
    return 0;
}
на выходе должно получится чтото вроде
1222222222333333333132323232323232323212 3232323232323232313232323232323232321222
222232323333333
Миниатюры
Std::thread приостановка потока  
0
Эксперт С++
 Аватар для hoggy
8973 / 4319 / 960
Регистрация: 15.11.2014
Сообщений: 9,760
09.08.2015, 14:57
Цитата Сообщение от ct0r Посмотреть сообщение
final не нужно. Это слишком жесткое ограничение здесь. И так понятно, что класс не предназначен для построения иерархии - нету виртуального деструктора.
третие предложение противоречит первому.
поскольку нарушает правило Роббинсона:
"то, чего быть не должно - не должно произойти в принципе"

Цитата Сообщение от ct0r Посмотреть сообщение
Что я думаю, когда вижу такой интерфейс? Что внутри используется std::forward и преимущества семантики перемещения
инкапсуляция.
разработчику класса не интересны ваши домыслы.
внутри он сделает как захочет.

все, о чем вы можете думать,
глядя на интерфейс - это как работать с самим интерфейсом.

при этом вы не должны делать предположения о деталях реализации.

Цитата Сообщение от ct0r Посмотреть сообщение
То есть я ожидаю, что если я озабочусь и пихну скажем временный таск с дешевым мувом, то программа от этого выиграет.
пользователь вправе этого ожидать.
но он не вправе на это закладываться.

потому что он не знает, что там под капотом.
и это не его ума дело.

Цитата Сообщение от ct0r Посмотреть сообщение
А по факту оказывается, что это не так.
а по факту это зависит от реализации.

вам нужно четко определиться, с какой позиции вы рассуждаете:
с позиции пользователя?
или с позиции разработчика?

потому что если с позиции пользователя,
то у него нет на руках никаких "фактов".
потому что это не уго ума дело, что там под капотом.

а если с позиции разработчика - есть интерфейс.
и в рамках этого интерфейса вы можете менять реализацию, как захотите.
можете повтыкать std::forward, если угодно.
и казалось бы - манипулировать фактами, как захотите.

но давайте глянем на код добавления задачи:

C++
1
2
3
4
5
6
7
8
template<class T>
    void AddTask(T&& task)
    {
        ::std::lock_guard<decltype(mGuard)>
            lock(mGuard);
 
        mTasks.push(task);
    }
вот так это выглядело бы, если бы мы использовали std::forward

C++
1
2
3
4
5
6
7
8
template<class T>
    void AddTask(T&& task)
    {
        ::std::lock_guard<decltype(mGuard)>
            lock(mGuard);
 
        mTasks.push(  std::forward<T>(task) );
    }
где:

C++
1
void push (const value_type& val);
работает с константной ссылкой.

итого: разницы никакой.

я согласен, что использование std::forward - привентивно увеличивает эффективность кода.
однако, закладываться на то, что код действительно будет эффективнее нельзя.

поэтому, заблуждение здесь - именно полагать,
что раз мув, значит обязательно эффективнее.

Цитата Сообщение от ct0r Посмотреть сообщение
Не совсем понял, зачем тут unique_lock. Почему не тот же более легковесный lock_guard?
это связанно с std::condition_variable
сейчас не буду вдаваться в подробности:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
// --- если тред-пул ещё не стартовал, 
// --- то тред засыпает
if(!mStarted)
{
    // error C2664: 'void std::condition_variable::wait(std::unique_lock<std::mutex> &)' : 
    // cannot convert argument 1 from 'std::lock_guard<std::mutex>' 
    // to 'std::unique_lock<std::mutex> &'
 
    //std::unique_lock<std::mutex>
    ::std::lock_guard<std::mutex>
        locker(mutex);
    mCondition.wait(locker);
}
Цитата Сообщение от ct0r Посмотреть сообщение
Лично я бы явно вызывал store. Потому как из этой строчки мне не видно, атомарно ли присваивание.
не понял этой фразы.

Цитата Сообщение от ct0r Посмотреть сообщение
Эту функцию лучше фтопку. Она очень легко реализуема, обращается только к открытому интерфейсу. Помним о принципе - API должно быть минимально, но не меньше. По крайней мере можем ее вынести из класса.
на самом деле, TaskQueue не является собственностью представленного тред-пула.
в реальности, это отдельный класс, который уже в разных местах используется.
и там эта функция-член востребованна.

обуславливается нежеланием раз за разом
"легко реализовывать то,
что можно было бы не реализовывать,
а сразу же использовать".

потому что во многих местах требуется именно этот функционал.

можно было бы оформить в виде свободной функции.
однако есть причины так не делать:

1.
нарушает дизайн.

либо так:
C++
1
2
pool.RunOne();
pool.Run();
либо так:
C++
1
2
RunOne(pool);  
Run(pool);
а вот это уже неряшливый суржик:
C++
1
2
pool.RunOne();  
Run(pool);
2.
связанно с неймспейсами.
и кстати, на свой практике я неоднократно с подобным встречался.

существует требование к проекту, согласно которому,
собственность проекта должна жить в нейспейсе проекта.

когда разрабатывался класс TaskQueue,
как раз таки было такое требование.

допустим, пусть такой неймспейс называется sample.

тогда:

C++
1
2
3
4
5
6
7
8
9
#pragma once
 
#include "external/task_queue.h"
 
namespace sample
{
    typedef service::TaskQueue 
        TaskQueue;
}
и проблем никаких.

можно использовать:
C++
1
sample::TaskQueue taskqueue;
однако это не прокатывает со свободными функциями.

Резюмируя:
поэтому, лично я допускаю в апи класса некий наиболее востребованный функциал,
предпочитая это свободным функциям.

Цитата Сообщение от ct0r Посмотреть сообщение
explicit?
хорошее замечание.
+1.

Цитата Сообщение от ct0r Посмотреть сообщение
Мне достаточно просмотреть public-секцию для того, чтобы иметь полное представление об интерфейсе класса. При использовании же костыля С++03 (согласись, это все-таки совсем не интуитивно для тех, кто видит такое в первый раз) мне нужно еще взглянуть и на private-секцию.
это все здорово.
но это все - косметика.
и это не причина запарывать с++03.

а что касается новичков - ну все бывает в первый раз.

некоторые практикуют наседование от nocopyable.
но лично я предпочитаю минимизировать кол-во классов,
если такой аскетизм не причиняет мне боль и страдания.

Добавлено через 8 минут
Цитата Сообщение от iiieoi Посмотреть сообщение
я слабо представляю как можно применить пул потоков к моей задаче
один из потоков крутит бесконечную задачу "менеджер",
которая создает другие задачи.
вот она создала 3 таких задачи.
и подкрутила атомарный счетчик на 3.
и заснула.

эти 3 задачи начали исполняться в пуле потоков.
когда задача завершается, она уменьшает счетчик.
и проверяет:
если счетчик дошел до 0, то такая последняя задача пробуждает задачу "менеджер".
и отваливается.

задача "менеджер" просыпается, и все повторяется.
и так до тех пор, пока первая задача не просигналит об остановке пула.
0
Игогошка!
 Аватар для ct0r
1801 / 708 / 44
Регистрация: 19.08.2012
Сообщений: 1,367
09.08.2015, 16:47
Цитата Сообщение от hoggy Посмотреть сообщение
третие предложение противоречит первому.
поскольку нарушает правило Роббинсона:
"то, чего быть не должно - не должно произойти в принципе"
Это не про С++

Цитата Сообщение от hoggy Посмотреть сообщение
инкапсуляция.
разработчику класса не интересны ваши домыслы.
внутри он сделает как захочет.
все, о чем вы можете думать,
глядя на интерфейс - это как работать с самим интерфейсом.
при этом вы не должны делать предположения о деталях реализации.
Это не домыслы. Это логичный вывод из интерфейса такого вида. Если разработчик класса это не подразумевает, то именно он и должен это написать, мол, несмотря на то, что тут блаблабла, я тут блаблабла. Разработчик должен составлять API так, чтоб пользователь мог им пользоваться интуитивно на основании предыдущего опыта.

Цитата Сообщение от hoggy Посмотреть сообщение
пользователь вправе этого ожидать.
но он не вправе на это закладываться.
потому что он не знает, что там под капотом.
и это не его ума дело.
1) А почему не вправе закладываться? Это управление рисками. Тем более changelog (если мы говорим о библиотеке например), должен содержать сведения о регрессии производительности и в этом духе.
2) Это отчасти и его ума дело. Вот ты берешь ассоциативный контейнер. Тебе _нужно_ знать, как он устроен. Красно-черные деревья это или хэш-таблица. Если хэш-таблица, то separate chaining или open addressing. Если open addresing, то какой именно. Насколько дружелюбна к кэш-памяти. Это вроде как детали реализации, но они должны быть _статичны_, _почти не меняться со временем_, описаны в документации, потому что от них зависит принятие решения о выборе библиотеки. Так и тут - если ты используешь в интерфейсе универсальные ссылки и прочую мощь С++11, то будь добр, укажи явно, что семантика перемещений в пролете. Тогда я возьму другую библиотеку, потому что у меня в проекте дофига классов с тяжелым копированием и легким перемещением.
Еще пример: если ты используешь либу для вывода логов асинхронно, ты вправе знать и закладываться на то, используется там блокировка или лок-фри. Потому как это сильно влияет на производительность и выбор либы, если у тебя прямо в лог так и прет. Хотя, казалось бы, - деталь реализации.

Цитата Сообщение от hoggy Посмотреть сообщение
а вот это уже неряшливый суржик:
Не вижу в этом ничего страшного. Но да ладно.
Цитата Сообщение от hoggy Посмотреть сообщение
однако это не прокатывает со свободными функциями.
Может я что-то не понял, но чем не подходит using?

Добавлено через 12 минут
Цитата Сообщение от hoggy Посмотреть сообщение
работает с константной ссылкой.
итого: разницы никакой.
Кстати тут ты не прав:
void push (const value_type& val);
void push (value_type&& val);
0
Эксперт С++
 Аватар для hoggy
8973 / 4319 / 960
Регистрация: 15.11.2014
Сообщений: 9,760
09.08.2015, 16:55
Цитата Сообщение от ct0r Посмотреть сообщение
Если разработчик класса это не подразумевает, то именно он и должен это написать, мол, несмотря на то, что тут блаблабла, я тут блаблабла.
не должен.
ещё раз: детали реализации - его личное дело.

Цитата Сообщение от ct0r Посмотреть сообщение
А почему не вправе закладываться?
потому что не факт.
выше я привел пример,
где форвард конструкция не имеет профита.

Цитата Сообщение от ct0r Посмотреть сообщение
Тебе _нужно_ знать, как он устроен.
мне не нужно этого знать, что бы пользоваться.

даже более того - очень многие ребята
понятия не имеют ни о каких "красно-черных деревьях",
и как std::map устроен внутри.
но это никак не мешает им пользоваться.

если для того, что бы просто пользоваться механизмом,
необходимо знать подробности устройства - это говнокод.

Цитата Сообщение от ct0r Посмотреть сообщение
Если хэш-таблица, то
то совершенно без разницы, какона там устроенна внутри.

официально, std::unordered_map - это неупорядочный ассоциативный массив.
это все, что вам гарантируется.
под капотом там может быть все что угодно.
сегодня может быть одно, а завтра - другое.

Цитата Сообщение от ct0r Посмотреть сообщение
Так и тут - если ты используешь в интерфейсе универсальные ссылки и прочую мощь С++11, то будь добр, укажи явно, что семантика перемещений в пролете.
с какой стати вы настаиваете на нарушении инкапсуляции?

Цитата Сообщение от ct0r Посмотреть сообщение
Может я что-то не понял, но чем не подходит using?
технически - подходит.
но как то гемморно каждую фряшку так обрабатывать.
удобно тайпдефнуть класс целиком один раз.
0
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
raxper
Эксперт
30234 / 6612 / 1498
Регистрация: 28.12.2010
Сообщений: 21,154
Блог
09.08.2015, 16:55
Помогаю со студенческими работами здесь

Ошибка компиляции "no instance of constructor 'std::thread::thread' matches the argument list"
Не могу сообразить почему возникает ошибка. У меня в классе есть метод, который должен работать в нескольких потоках одновременно. Вот он: ...

Boost::thread vs std::thread
Доброго времени суток, решил углубить свои знания, и решил почитать про потоки, бустовые и те что в 11 стандарте приняли, с бустом все ясно...

Boost::thread std::thread
чем отличается boost::thread( ) от std::thread (с++17)? я спрашиваю не о способе реализации потоков в целом, а конкретно о этих функциях....

Приостановка выполнения процесса/потока (windows)
Есть запущенный процесс, например, калькулятор или блокнот, известно ID процесса и его имя. Нужно сделать: на форме есть две кнопки, при...

std::thread
Возник вопрос: как создать массив потоков (точнее, как его инициализировать). То есть, мне нужно примерно такое: std::thread...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
20
Ответ Создать тему
Новые блоги и статьи
Ритм жизни
kumehtar 27.02.2026
Иногда приходится жить в ритме, где дел становится всё больше, а вовлечения в происходящее — всё меньше. Плотный график не даёт вниманию закрепиться ни на одном событии. Утро начинается с быстрых,. . .
SDL3 для Web (WebAssembly): Сборка SDL3 и Box2D из исходников с помощью CMake и Emscripten
8Observer8 27.02.2026
Недавно вышла версия 3. 4. 2 библиотеки SDL3. На странице официальной релиза доступны исходники, готовые DLL (для x86, x64, arm64), а также библиотеки для разработки под Android, MinGW и Visual Studio. . . .
SDL3 для Web (WebAssembly): Реализация движения на Box2D v3 - трение и коллизии с повёрнутыми стенами
8Observer8 20.02.2026
Содержание блога Box2D позволяет легко создать главного героя, который не проходит сквозь стены и перемещается с заданным трением о препятствия, которые можно располагать под углом, как верхнее. . .
Конвертировать закладки radiotray-ng в m3u-плейлист
damix 19.02.2026
Это можно сделать скриптом для PowerShell. Использование . \СonvertRadiotrayToM3U. ps1 <path_to_bookmarks. json> Рядом с файлом bookmarks. json появится файл bookmarks. m3u с результатом. # Check if. . .
Семь CDC на одном интерфейсе: 5 U[S]ARTов, 1 CAN и 1 SSI
Eddy_Em 18.02.2026
Постепенно допиливаю свою "многоинтерфейсную плату". Выглядит вот так: https:/ / www. cyberforum. ru/ blog_attachment. php?attachmentid=11617&stc=1&d=1771445347 Основана на STM32F303RBT6. На борту пять. . .
Камера Toupcam IUA500KMA
Eddy_Em 12.02.2026
Т. к. у всяких "хикроботов" слишком уж мелкий пиксель, для подсмотра в ESPriF они вообще плохо годятся: уже 14 величину можно рассмотреть еле-еле лишь на экспозициях под 3 секунды (а то и больше),. . .
И ясному Солнцу
zbw 12.02.2026
И ясному Солнцу, и светлой Луне. В мире покоя нет и люди не могут жить в тишине. А жить им немного лет.
«Знание-Сила»
zbw 12.02.2026
«Знание-Сила» «Время-Деньги» «Деньги -Пуля»
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru