После поста про потоки в Go я прочитал другое мнение про общую целесообразность Go в плане продвинутости в многопоточном программировании.
Признаюсь, я не боец в бусте и новом C++, но благодаря предоставленному примеру, было очевидно, что и на С++ решение получается весьма изящное.
Интересно было сравнить производительнось потоков во обоих языках в плане скорости из создания и назначения им работы. Как я понял, это битва между pthreads и системой Go-рутин, которые не являются потоками операционной системы. Как сказано в документации:
Goroutines are multiplexed onto multiple OS threads so if one should block, such as while waiting for I/O, others continue to run. Their design hides many of the complexities of thread creation and management.
Я взял последний boost, и на той же восьми процессорной машине провел эксперимент.
Программе надо будет выполнить множество однотипной работы (фактически, вызвать функцию). Задачи будут мультиплексироваться между несколькими параллельными потоками. Сама функция будет элементарной и быстрой. Надеюсь, этим удастся сфокусировать тестирование именно на подсистеме потоков, нежели на полезной нагрузке.
Итак, программа на Go:
package main
import (
"flag"
"fmt"
)
var jobs *int = flag.Int("jobs", 8, "number of concurrent jobs")
var n *int = flag.Int("tasks", 1000000, "number of tasks")
func main() {
flag.Parse()
fmt.Printf("- running %d concurrent job(s)\n", *jobs)
fmt.Printf("- running %d tasks\n", *n)
tasks := make(chan int, *jobs)
done := make(chan bool)
for i := 0; i < *jobs; i++ {
go runner(tasks, done)
}
for i := 1; i <= *n; i++ {
tasks <- i
}
for i := 0; i < *jobs; i++ {
tasks <- 0
<- done
}
}
func runner(tasks chan int, done chan bool) {
for {
if arg := <- tasks; arg == 0 {
break
}
worker()
}
done <- true
}
func worker() int {
return 0
}
Makefile для прогона по серии параметров:
target = go_threading
all: build
build:
6g $(target).go
6l -o $(target) $(target).6
run:
(time -p ./$(target) -tasks=$(args) \
1>/dev/null) 2>&1 | head -1 | awk '{ print $$2 }'
n = \
10000 \
100000 \
1000000 \
10000000 \
100000000
test:
@for i in $(n); do \
echo "`printf '% 10d' $$i`" `$(MAKE) args=$$i run`; \
done
Программа на C++:
#include <iostream>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
#include <queue>
#include <string>
#include <sstream>
class thread_pool {
typedef boost::function0<void> worker;
boost::thread_group threads_;
std::queue<worker> queue_;
boost::mutex mutex_;
boost::condition_variable cv_;
bool done_;
public:
thread_pool() : done_(false) {
for(int i = 0; i < boost::thread::hardware_concurrency(); ++i)
threads_.create_thread(boost::bind(&thread_pool::run, this));
}
void join() {
threads_.join_all();
}
void run() {
while (true) {
worker job;
{
boost::mutex::scoped_lock lock(mutex_);
while (queue_.empty() && !done_)
cv_.wait(lock);
if (queue_.empty() && done_) return;
job = queue_.front();
queue_.pop();
}
execute(job);
}
}
void execute(const worker& job) {
job();
}
void add(const worker& job) {
boost::mutex::scoped_lock lock(mutex_);
queue_.push(job);
cv_.notify_one();
}
void finish() {
boost::mutex::scoped_lock lock(mutex_);
done_ = true;
cv_.notify_all();
}
};
void task() {
volatile int r = 0;
}
int main(int argc, char* argv[]) {
thread_pool pool;
int n = argc > 1 ? std::atoi(argv[1]) : 10000;
int threads = boost::thread::hardware_concurrency();
std::cout << "- executing " << threads << " concurrent job(s)" << std::endl;
std::cout << "- running " << n << " tasks" << std::endl;
for (int i = 0; i < n; ++i) {
pool.add(task);
}
pool.finish();
pool.join();
return 0;
}
Makefile:
BOOST = ~/opt/boost-1.46.1
target = boost_threading
build:
g++ -O2 -I $(BOOST) -o $(target) \
-lpthread \
-lboost_thread \
-L $(BOOST)/stage/lib \
$(target).cpp
run:
(time -p LD_LIBRARY_PATH=$(BOOST)/stage/lib ./$(target) $(args) \
1>/dev/null) 2>&1 | head -1 | awk '{ print $$2 }'
n = \
10000 \
100000 \
1000000 \
10000000 \
100000000
test:
@for i in $(n); do \
echo "`printf '% 10d' $$i`" `$(MAKE) args=$$i run`; \
done
В обоих языках число потоков будет равно количеству процессоров - 8. Количество задач, прогоняемых через эти восемь поток будет варьироваться.
Запускаем программу на C++:
make && make -s test
g++ -O2 -I ~/opt/boost-1.46.1 -o boost_threading \
-lpthread \
-lboost_thread \
-L ~/opt/boost-1.46.1/stage/lib \
boost_threading.cpp
(time -p LD_LIBRARY_PATH=~/opt/boost-1.46.1/stage/lib ./boost_threading \
1>/dev/null) 2>&1 | head -1 | awk '{ print $2 }'
10000 0.03
100000 0.35
1000000 3.43
10000000 29.57
100000000 327.37
Теперь Go:
make && make -s test
6g go_threading.go
6l -o go_threading go_threading.6
10000 0.00
100000 0.03
1000000 0.35
10000000 3.72
100000000 38.27
Разница очевидна.
Может быть я сравниваю соленое с красным, и результаты просто неадекватны. Будет очень признателен за подсказку, в каких попугаях на правильно измерять.
Посты по теме:
Комментариев нет:
Отправить комментарий