C++并发:并发操作的同步

有时我们不仅要共享数据,也要让独立线程上的行为同步。例如,某线程只有先等待另一线程的任务完成,才可以执行自己的任务。

C++提供了处理工具:条件变量future

并且进行了扩充:线程闩(latch),线程卡(barrier)

1 等待事件或等待其他条件

如果线程甲需要等待线程乙完成任务,可以采取几种不同的方式:

方式一:在共享数据内部维护一标志(受互斥保护),线程乙完成任务后,就设置标志成立。

该方式存在双重浪费:

1 线程甲须不断检查标志,耗费资源。

2 互斥一旦被锁住,其他线程无法再加锁(包括想要设置标志成立时的线程乙)。

方式二:让线程甲调用std::this_thread::sleep_for(),在各次查验之间短期休眠。

#include <mutex>
#include <thread>
bool flag;
std::mutex m;
void wait_for_flag() {
    std::unique_lock<std::mutex> lk(m);
    while (!flag) {
        lk.unlock();
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        lk.lock();
    }
}

让线程释放锁并休眠,让别的线程获取锁,这种写死的时间很难确定合适的值,而且合适的值也可能会随着系统运行而动态变化。

方式三:使用C++标准库的工具等待事件发生,优先使用这种方式。

比如条件变量。

1.1 使用条件变量来等待条件成立

两种条件变量的实现:

std::condition_variable和std::condition_variable_any。在头文件<condition_variable>内声明。

std::condition_variable仅限于和std::mutex一起使用,有更好的性能。

std::condition_variable_any可以和足以充当互斥的任一类型配合使用,更加通用,但是可能产生额外开销。

1.1.1 std::condition_variable

#include <condition_variable>
#include <mutex>
#include <queue>
std::mutex mut;
std::queue<data_chunk> data_queue;
std::condition_variable data_cond;

void data_preparation_thread() {
    while (more_data_to_prepare()) {
        data_chunk const data = prepare_data();
        {
            std::lock_guard<std::mutex> lk(mut);
            data_queue.push(data);
        }
        data_cond.notify_one();
    }
}

void data_processing_thread() {
    while (true) {
        std::unique_lock<std::mutex> lk(mut);
        data_cond.wait(lk, []{return !data_queue.empty();});
        data_chunk data=data_queue.front();
        data_queue.pop();
        lk.unlock();
        process(data);
        if (is_lask_chunk(data)) {
            break;
        }
    }
}

线程data_preparation_thread,在数据准备完成后,使用条件变量的notify_one(),通知一个正在等待的条件变量。

线程data_processing_thread,正在wait的条件变量收到通知后,继续往下进行,处理完成后对lk解锁(来让其他线程能够获取锁),这种需要加解锁灵活性的场景,让我们在这里选择了unique_lock而不是lock_guard。

1.1.2 伪唤醒

伪唤醒:如果线程data_processing_thread重新获得互斥,并且查验条件,但是这个行为不是直接响应线程data_preparation_thread的通知,就是伪唤醒。

这种伪唤醒出现的数量和频率都不确定。因此,若判定函数有副作用,则不建议选取它来查验条件。如果真的要这么做,有可能产生多次副作用。例如:每次被调用时提升线程优先级,多次伪唤醒可以使线程优先级非常高。

1.1.3 std::condition_variable::wait()

本质上是忙等的优化。

1.2 使用条件变量构建线程安全的队列

#include <queue>
#include <memory>
#include <mutex>
#include <condition_variable>

template<typename T>
class threadsafe_queue {
private:
    mutable std::mutex mut;
    std::queue<T> data_queue;
    std::condition_variable data_cond;

public:
    threadsafe_queue()
    {}

    threadsafe_queue(threadsafe_queue const& other) {
        std::lock_guard<std::mutex> lk(other.mut);
        data_queue=other.data_queue;
    }

    void push(T new_value) {
        std::lock_guard<std::mutex> lk(mut);
        data_queue.push(new_value);
        data_cond.notify_one();
    }

    void wait_and_pop(T& value) {
        std::unique_lock<std::mutex> lk(mut);
        data_cond.wait(lk, [this]{return !data_queue.empty();});
        value = data_queue.front();
        data_queue.pop();
    }

    std::shared_ptr<T> wait_and_pop() {
        std::unique_lock<std::mutex> lk(mut);
        data_cond.wait(lk, [this]{return !data_queue.empty();});
        std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
        data_queue.pop();
        return res;
    }

    bool try_pop(T& value) {
        std::lock_guard<std::mutex> lk(mut);
        if (data_queue.empty()) {
            return false;
        }
        value = data_queue.front();
        data_queue.pop();
        return true;
    }

    std::shared_ptr<T> try_pop() {
        std::lock_guard<std::mutex> lk(mut);
        if (data_queue.empty()) {
            return std::shared_ptr<T>();
        }
        std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
        data_queue.pop();
        return res;
    }

    bool empty() const {
        std::lock_guard<std::mutex> lk(mut);
        return data_queue.empty();
    }
};

2 使用future等待一次事件发生

C++标准库使用future模拟一次性事件:若线程等待某个特定的一次性事件发生,则会以一个恰当的方式获取一个future,他代表目标时间;接着,该线程就能一边执行其他任务,一边在future上等待;同时,他以短暂的间隔反复查验目标事件是否已经发生。

这个线程也可以转换运行模式:先不等目标事件发生,直接暂缓当前任务,切换到别的任务,必要时再回头等待future准备就绪。

future可能与数据关联,也可能未关联,一旦目标事件发生,其future即进入就绪状态,无法重置。

C++标准库有两种future:独占future(std::future<>)和共享future(std::shared_future<>)他们的设计参照了unique_ptr和shared_ptr。同一个事件仅仅允许关联唯一一个std::future实例,但是可以关联多个shared_future实例。

future能用于线程间通信,但是本身不提供同步访问,若多个线程需要同时访问一个future对象,需要使用互斥或其他同步方式。

2.1 从后台任务返回值std::async

只要我们并不急需线程运算的值,就可以使用std::async()异步方式启动任务。我们从std::async()函数处获得std::future对象(而非std::thread对象),运行的函数一旦完成,其返回值就由该对象最后持有。若要用到这个值,只需再future对象上调用get(),当前线程就会阻塞,以便future准备妥当并返回该值。

#include <future>
#include <iostream>
int find_the_answer_to_ltuae();
void do_other_stuff();

int main() {
    std::future<int> the_answer = std::async(find_the_answer_to_ltuae);
    do_other_stuff();
    std::cout << "the answer is " << the_answer.get() << std::endl;
}

std::async的第一个参数是函数指针,第二个是用在调用函数之上的参数,其余类推。

如果std::async的参数是右值,则通过移动原始参数构建副本。

#include <string>
#include <future>

struct X {
    void foo(int, std::string const &);
    std::string bar(std::string const &);
};

X x;

调用了p->foo(42, "hello");,其中p的值是&x
auto f1 = std::async(&X::foo, &x, 42, "hello");
调用了tempx.bar("goodbye");,其中tempx是x的副本
auto f2 = std::async(&X::bar, x, "goodbye");

struct Y {
    double operator() (double);
};

Y y;
调用tmpy(3.141)。其中由Y()生成的一个匿名变量传递给std::async(),进而发生移动构造。
在std::async()内部产生对象tmpy,在tmpy上执行Y::operator()(3.141)
auto f3 = std::async(Y(), 3.141);
调用y(2.718);
auto f4 = std::async(std::ref(y), 2.718);

X baz(X&);
// 调用baz(x)
// std::async(baz, std::ref(x));

class move_only {
public:
    move_only();
    move_only(move_only&&);
    move_only(move_only const&) = delete;
    move_only& operator=(move_only&&);
    move_only& operator=(move_only const&) = delete;
    void operator() ();
};
调用tmp(),其中tmp等价于std::move(move_only());
它的产生过程与std::async(Y(), 3.141);类似
auto f5 = std::async(move_only());
运行新线程
auto f6 = std::async(std::launch::async, Y(), 1.2);
在wait或get内部运行任务函数
auto f7 = std::async(std::launch::deferred, baz, std::ref(x));
交由实现自行选择运行方式
auto f8 = std::async(std::launch::deferred | std::launch::async, baz, std::ref(x));
交由实现自行选择运行方式
auto f9 = std::async( baz, std::ref(x));

签名f7的任务函数调用被延后,到这里运行
f7.wait();

2.2 关联future实例和任务std::packaged_task<>

std::packaged_task<>连结了future对象函数。std::package_task<>对象在执行任务时,会调用关联的函数(或可调用对象),把返回值保存为future的内部数据,并令future准备就绪。

类模板std::package_task<>具有成员函数get_future(),它返回std::future<>实例,该future的特化类型取决于函数签名所指定的返回值。

std::package_task<>还具备函数调用操作符,他的参数取决于函数签名的参数列表。

#include <deque>
#include <mutex>
#include <future>
#include <thread>
#include <utility>

std::mutex m;

std::deque<std::packaged_task<void()>> tasks;
bool gui_shutdown_message_received();
void get_and_process_gui_message();
void gui_thread() {
    while (!gui_shutdown_message_received()) {
        get_and_process_gui_message();
        std::packaged_task<void()> task;
        {
            std::lock_guard<std::mutex> lk(m);
            if (tasks.empty()) {
                continue;
            }
            task = std::move(tasks.front());
            task.pop_front();
        }
        task();
    }
}

std::thread gui_bg_thread(gui_thread);

template<typename Func>
std::future<void> post_task_for_gui_thread(Func f) {
    std::packaged_task<void()> task(f);
    std::future<void> res = task.get_future();
    std::lock_guard<std::mutex> lk(m);
    tasks.push_back(std::move(task));
    return res;
}

2.3 创建std::promise

有些任务无法以简单的函数调用表达,还有一些任务的执行结果可能来自多个部分的代码。这种时候就需要用到std::promise显示异步求值。

在处理大量网络连接时,通常使用少量线程处理,来避免大量线程带来的上下文切换开销等。

配对的std::promise和std::future可实现下面的工作机制:等待数据的线程在future上阻塞,而提供数据的线程利用相配的promise设定关联的值,使future准备就绪。

若需从给定的std::promise实例获取关联的std::future对象,调用前者的get_future()即可。promise的值通过成员函数set_value()设置,只要设置好,future就准备就绪。

#include <future>
void process_connections(connection_set& connections) {
    while (!done(connections)) {
        for (connection_iterator connection=connections.begin(), end=connections.end(); connection!= end; ++connection) {
            if (connection->has_incoming_data()) {
                data_packet data = connection->incoming();
                std::promise<payload_type>& p = connection->get_promise(data.id);
                p.set_value(data.payload);
            }
            if (connection->has_outgoing_data()) {
                outgoing_packet data = connection->top_of_outgoing_queue();
                connection->send(data.payload);
                data.promise.set_value(true);
            }
        }
    }
}

2.4 将异常保存到future中

经由std::async()调用抛出的异常被保存到future中,等到get()调用,存储在内的异常会被抛出。

任务包装在packaged_task也是如此。std::promise也有这个功能,使用set_expection()。

#include <future>
extern std::promise<double> some_promise;
try {
    some_promise.set_value(value());
} catch (...) {
    some_promise.set_exception(std::current_exception());
}

2.5 多个线程同时等待std::shared_future

若在多个线程上访问同一个std::future,不采取措施会发生抢占。std::shared_future则可以解决这个问题,他可以复制出副本,但是它们全指向同一异步任务的状态数据。

std::shared_future的实例依据std::future的实例构造而得,前者的异步状态由后者决定。由于std::future独占异步状态,因此想要创建shared_future,需要使用std::move向其默认构造函数传递归属权。

#include <assert.h>
#include <future>
std::promise<int> p;
std::future<int> f(p.get_future());
// assert(f.valid());
std::shared_future<int> sf(std::move(f));

隐式的归属权转换
std::shared_future<int> sf(p.get_future());

转移给sf后,对象f不再有效。

3 限时等待

有两种超时机制:

1 迟延超时,线程根据指定的时长而继续等待。

2 绝对超时,在某特定时间点来临之前,线程一直等待。

3.1 时钟类

使用std::chrono::system_clock::now()来获取系统当前时刻。

若时钟类每秒计数25次,那么表示为std::ratio<1, 25>

若时钟类每2.5秒计数1次,那么表示为std::ratio<5, 2>

std::chrono::steady_clock:恒温时钟类

std::chrono::system_clock:系统时钟类

std::chrono::high_resolution_clock:高精度时钟类

3.2 时长类

std::chrono::duration<>,是类模板,具有两个模板参数,前者指明采用何种类型表示计时单元的数量,后者是一个分数,设定该时长类的每一个计时单元代表多少秒。

例如:采用short值计数的分钟时长类:

std::chrono::duration<short, std::ratio<60, 1>>(1分钟60秒)

采用double值计数的毫秒时长类:

std::chrono::duration<short, std::ratio<1, 1000>>(1毫秒是1/1000秒)

std::chrono::milliseconds ms(22222);

std::chrono::seconds s = std::duration_cast<std::chrono::seconds>(ms);

3.3 时间点类

由类模板std::chrono::time_point<>的实例表示,第一个参数指明所参考的时钟,第二个参数指明计时单元。

例如:以系统时钟为参考,计时单元为分钟

std::chrono::time_point<std::chrono::system_clock, std::chrono::minutes>

可以将时间点加减时长,从而得出新的时间点。

比如:

std::chrono::system_clock::now() + std::chrono::minutes(423)

如果两个时间点共享一个时钟,我们也可以用它相减来得到时长。

3.4 接受超时时限的函数

用来设定休眠时间,延迟线程执行,设置超时时长等。

4 运用同步操作简化代码

4.1 利用future进行函数式编程

在并发实战中使用非常贴近函数式编程的风格。线程间不会直接共享数据,而是由各任务分别预先准备自己所需的数据,并借助future将结果发送到其他有需要的线程。

之前章节讨论到的共享内存的种种问题,上述模式令它们大部分都不复存在,这便于我们把代码考虑周全,特别是涉及并发的场景。

4.1.1 函数式编程风格的快速排序

#include <algorithm>
#include <iostream>
#include <list>
template<typename T>
std::list<T> sequential_quick_sort(std::list<T> input) {
    if (input.empty()) {
        return input;
    }
    std::list<T> result;
    result.splice(result.begin(), input, input.begin());
    T const& pivot =*result.begin();

    auto divide_point = std::partition(input.begin(), input.end(), [&](T const& t){return t<pivot;});
    std::list<T> lower_part;
    lower_part.splice(lower_part.end(), input, input.begin(), divide_point);
    auto new_lower(sequential_quick_sort(std::move(lower_part)));
    auto new_higher(sequential_quick_sort(std::move(input)));
    result.splice(result.end(), new_higher);
    result.splice(result.begin(), new_lower);
    return result;
}

splice将原链表第一个元素作为基准元素切除。使用std::partition()把原链表切割成两组,一组小于基准元素,另一组不然。之后std::partition()返回一个迭代器,指向大于等于基准元素的第一个元素。

4.1.2 函数式编程风格的并行快速排序

#include <algorithm>
#include <future>
#include <iostream>
#include <list>
template<typename T>
std::list<T> parallel_quick_sort(std::list<T> input) {
    if (input.empty()) {
        return input;
    }
    std::list<T> result;
    result.splice(result.begin(), input, input.begin());
    T const& pivot =*result.begin();

    auto divide_point = std::partition(input.begin(), input.end(), [&](T const& t){return t<pivot;});
    std::list<T> lower_part;
    lower_part.splice(lower_part.end(), input, input.begin(), divide_point);

    std::future<std::list<T>> new_lower(std::async(&parallel_quick_sort<T>, std::move(lower_part)));
    auto new_higher(parallel_quick_sort(std::move(input)));
    result.splice(result.end(), new_higher);
    result.splice(result.begin(), new_lower.get());
    return result;
}

4.2 使用消息传递进行同步handle()

通信式串行进程CSP:假设不存在共享数据,线程只接收消息,那么单纯地依据其反应行为,就能独立的对线程进行完成的逻辑判断。

因此,每个CSP线程实际上都与状态机等效:它们从原始状态起步,只要接收到消息就按某种方式更新自身状态,也许还会像其他CSP线程发送消息。

#include <string>

struct card_inserted {
    std::string account;
};

class atm {
    messageing::receiver incomming;
    messageing::sender bank;
    messageing::sender interface_hardware;
    void (atm::*state) ();
    std::string account;
    std::string pin;

    void waiting_for_card() {
        interface_hardware.send(display_enter_card());
        incomming.wait().handle<card_inserted>(
            [&](card_inserted const& msg) {
                account=msg.account;
                pin = "";
                interface_hardware.send(display_enter_pin());
                state=&atm::getting_pin;
            }
        );
    }
    void getting_pin() {
        incomming.wait().handle<digit_pressed> (
            [&](digit_pressed const& msg) {
                unsigned const pin_length = 4;
                pin += msg.digit;
                if (pin.length() == pin_length) {
                    bank.send(verify_pin(account, pin, incomming));
                    state =&atm::verifying_pin; 
                }
            }
        ).handle<clear_last_pressed>(
            [&](clear_last_pressed const& msg) {
                if (!pin.empty()) {
                    pin.resize(pin.length()-1);
                }
            }
        ).handle<cancel_pressed>(
            [&](cancel_pressed const& msg) {
                state = &atm::done_processing;
            }
        );
    }
public:
    void run() {
        state=&atm::waiting_for_card;
        try {
            for(;;) {
                (this->*state)();
            }
        }catch (messaging::close_queue const&) {
            
        }
    }
};

handle()是wait()的链式后继,他们发生连锁调用,若接收的消息与指定类型不符,就会被丢弃,执行线程继续等待,直到收到与类型匹配的消息。

这种编程方式是CSP,大幅简化并发系统设计工作,能够完全独立地处理每个线程(handle)

4.3 符合并发技术规约的后续并发风格std::experimental::future

std::experimental::future

如下,只要调用了then(),原future的值就会被取出,原future就会失效,then的调用也会返回新的future对象,后续函数的结果由他持有。但是我们无法向then中的后续函数传递参数,因为参数已经由程序库设计好,是先前准备就绪的future

#include <thread>
#include <utility>

std::experimental::future<int> find_the_answer();
auto fut = find_the_answer();
auto fut2 = fut.then();
assert(!fut.valid());
assert(fut2.valid());

// 既与std::async等价,又支持std::experimental::future的实现
template<typename Func>
std::experimental::future<decltype(std::declval<Func>()())> spawn_async(Func&& func) {
    std::experimental::promise<decltype(std::declval<Func>()())> p;
    auto res = p.get_future();
    std::thread t(
        [p = std::move(p), f = std::decay_t<Func>(func)]()
            mutable {
            try {
                p.set_value_at_thread_exit(f());
            } catch (...) {
                p.set_exception_at_thread_exit(std::current_exception());
            }
        });
    t.detach();
    return res;
}

4.4 后续函数的连锁调用std::experimental::shared_future

std::experimental::future<void> process_login() {
    return spawn_async([=]() {
        return backend.authticate_user(username, password);
    }).then([](std::experimental::future<user_id> id) {
        return backend.request_current_info(id.get());
    }).then([](std::experimental::future<user_data> info_to_display) {
        try {
            update_display(info_to_display.get());
        } catch (std::exception& e) {
            display_error(e);
        }
    });
}

若调用链中的任何后续函数抛出了异常,那么异常会沿着调用链向外传递,并且在最末尾的后续函数中,经由info_to_display.get()的调用抛出。该处的catch能集中处理全部异常。但是它仍然是链式,中间若有一步需要等待较长时间,那么就只能持续阻塞。

std::experimental::future<void> process_login() {
    return backend.async_authenticate_user(username, password)
        .then([](std::experimental::future<user_id> id) {
            return backend.async_request_current_info(id.get());
        }).then([](std::experimental::future<user_data> info_to_display) {
            try {
                update_display(info_to_display.get());
            } catch (std::exception& e) {
                display_error(e);
            }
        });
}

上述代码将async_request_current_info修改为返回future对象,让这个函数对应的一系列操作由其他线程执行,来避免阻塞当前线程。

auto fut = spawn_async(some_function).share();
auto fut2 = fut.then([](std::experimental::shared_future<some_data> data) {
    do_stuff(data);
});

auto fut3 = fut.then([](std::experimental::shared_future<some_data> data) {
    return do_other_stuff(data);
});

std::experimental::shared_future允许有多个后续,不过要注意fut.then返回的仍然是std::experimental::future,并且入参是std::experimental::shared_future

4.5 等待多个futurestd::experimental::when_all

#include <future>
#include <vector>
std::future<FinalResult> process_data(std::vector<Mydata>& vec) {
    size_t const chunk_size = whatever;
    std::vector<std::future<ChunkResult>> results;
    for (auto begin = vec.begin(), end = vec.end(); begin != end;) {
        size_t const remaining_size = end - begin;
        size_t const this_chunk_size = std::min(remaining_size, chunk_size);
        results.push_back(std::async(process_chunk, begin, begin + this_chunk_size));
        begin += this_chunk_size;
    }
    return std::async([all_results = std::move(results)]() {
        std::vector<ChunkResult> v;
        v.reserve(all_results.size());
        for (auto& f : all_results) {
            v.push_back(f.get());
        }
        return gather_results(v);
    });
}

上述代码将分项计算交由其他线程进行,但是最终结果合并时,若有分项没有计算完,则需要阻塞在哪里等待结果返回。

#include <future>
#include <vector>

std::experimental::future<FinalResult> process_data(std::vector<MyData>& vec) {
    size_t const chunk_size = whatever;
    std::vector<std::experimental::future<ChunkResult>> results;
    for (auto begin = vec.begin(), end = vec.end(); begin!=end;) {
        size_t const remaining_size = end - begin;
        size_t const this_chunk_size = std::min(remaining_size, chunk_size);
        results.push_back(
            spawn_async(process_chunk, begin, begin + this_chunk_size)
        );
        begin+=this_chunk_size;
    }
    return std::experimental::when_all(results.begin(), results.end()).then(
        [](std::future<std::vector<std::experimental::future<ChunkResult>>> ready_results) {
            std::vector<std::experimental::future<ChunkResult>> all_results = ready_results.get();
            std::vector<ChunkResult> v;
            v.reserve(all_results.size());
            for (auto& f : all_results) {
                v.push_back(f.get());
            }
            return gather_results(v);
        }
    );
}

上述代码在原来的基础上使用了std::experimental::when_all使得参数不再从捕获列表获得,而是直接接收传入的future实例,其内包装了一个vector容器,里面装载着各分项结果。因此获取全部分项结果之后,再让该线程继续执行结果合并操作。

4.6 运用std::experimental::when_any()函数等待多个future,直到其中一个准备就绪

#include <thread>
#include <vector>
std::experimental::future<FinalResult> find_and_process_value(std::vector<MyData>& data) {
    unsigned const concurrency = std::thread::hardware_concurrency();
    unsigned const num_tasks = (concurrency > 0) ? concurrency : 2;
    std::vector<std::experimental::future<MyData *>> results;
    auto const chunk_size = (data.size() + num_tasks - 1) / num_tasks;
    auto chunk_begin = data.begin();
    std::shared_ptr<std::atomic<bool>> done_flag = std::make_shared<std::atomic<bool>>(false);
    for (unsigned i = 0; i < num_tasks; ++i) {
        auto chunk_end = (i < (num_tasks - 1)) ? chunk_begin + chunk_size : data.end();

        results.push_back(spawn_async([=] {
            for (auto entry = chunk_begin; !*done_flag && (entry != chunk_end); ++entry) {
                if (match_find_criteria(*entry)) {
                    *done_flag = true;
                    return &*entry;
                }
            }
            return (MyData *) nullptr;
        }));
        chunk_begin = chunk_end;
    }
    std::shared_ptr<std::experimental::promise<FinalResult>> final_result = std::make_shared<std::experimental::promise<FinalResult>>();
    struct DoneCheck {
        std::shared_ptr<std::experimental::promise<FinalResult>> final_result;
        DoneCheck(std::shared_ptr<std::experimental::promise<FinalResult>> final_result_) : final_result(std::move(final_result_)) {}

        void operator() (std::experimental::futureM<std::experimental::when_any_result<std::vector<std::experimental::future<MyData *>>>> results_param) {
            auto results = results_param.get();
            MyData *const ready_result = results.futures[results.index].get();
            if (ready_result) {
                final_result->set_value(process_found_value(*ready_result));
            } else {
                results.futures.erase(results.futures.begin() + results.index);
                if (!results.futures.empty()) {
                    std::experimental::when_any(results.futures.begin(), results.futures.end()).then(std::move(*this));
                } else {
                    final_result->set_exception(std::make_exception_ptr(std::runtime_error("Not found")));
                }
            }
        }
    };

    std::experimental::when_any(results.begin(), results.end()).then(DoneCheck(final_result));
    return final_result->get_future();
}

详解比较复杂,可见《C++并发编程实战》p118-p119

4.7 线程闩和线程卡——并发计数规约提出的新特性

有时候我们需要等待某些线程,待其运行到代码中的某个特定的地方,或者等它们相互配合完成一定量的数据项处理,此时适合使用线程闩和线程卡。

线程闩:是一个同步对象,内含计数器,一旦减到0,就会进入就绪状态。它对线程加闩,并保持封禁状态(只要它就绪,就保持该状态不变,除非对象销毁)。同一线程能让线程闩计数器多次减持,多个线程也可以让线程闩计数器分别减持一次,或者两者兼有。

线程卡:可重复使用的同步构件,针对一组给定的线程,在她们之间进行同步。在线程卡的每个同步周期内只准许每个线程唯一一次运行到其所在之处。线程运行到线程卡所在之处就会被阻塞,一直等到同组线程全部抵达,在那一瞬间,它们全部被释放。

4.8 线程闩std::experimental::latch

#include <future>
#include <vector>

void foo() {
    unsigned const thread_count = ...;
    latch done(thread_count);
    my_data data(thread_count);
    std::vector<std::future<void>> threads;
    for (unsigned i = 0; i < thread_count; ++i) {
        threads.push_back(std::async(std::launch::async, [&, i] {
            data[i] = make_data(i);
            done.count_down();
            do_more_stuff();
        }));
    }
    done.wait();
    process_data(data, thread_count);
}

这里,传入std::async的lambda对象的i不使用引用是因为变量i是循环计数器,按引用捕获会导致数据竞争和未定义行为。

4.9 线程卡std::experimental::barrier

并发技术提出了两种线程卡:

std::experimental::barrier和std::experimental::flex_barrier

前者相对简单,后者更灵活但是开销更大。

线程闩的意义在于关闸拦截:一旦他进入了就绪状态,就始终保持不变。

线程卡的意义在于:线程卡会释放等待的线程并且自我重置,因此可以重复使用。只与一组固定的线程同步,若某线程不属于同步组,就不会被拦截。

只要在线程卡上调用arrive_and_drop(),即可令线程显示的脱离其同步组。

#include <thread>
#include <vector>
result_chunk_process(data_chunk);
std::vector<data_chunk>
divide_info_chunks(data_block data, unsigned num_threads);

void process_data(data_source& source, data_sink& sink) {
    unsigned const concurrency = std::thread::hardware_concurrency();
    unsigned const num_threads = (concurrency > 0) ? concurrency : 2;

    std::experimental::barrier sync(num_threads);
    std::vector<joining_thread> threads(num_threads);

    std::vector<data_chunk> chunks;
    result_block result;

    for (unsigned i = 0; i < num_threads; ++i) {
        threads[i] = joining_thread([&, i] {
            while(!source.done()) {
                if (!i) {
                    data_block current_block = source.get_next_data_block();
                    chunks = divide_info_chunks(current_block, num_threads);
                }
                sync.arrive_and_wait();
                result.set_chunk(i, num_threads, process(chunks[i]));
                sync.arrive_and_wait();
                if (!i) {
                    sink.write_data(std::move(result));
                }
            }
        });
    }
}

这里并发由for里面的joining_thread产生,arrive_and_wait()保证了分阶段的并发,所有线程在一个阶段内部完成全部处理后再进行下一阶段,并且由于它可以重复利用,因此可以在多个地方设卡。

4.10 std::experimental::flex_barrier

flex_barrier的构造函数既接收线程数目,也接受补全函数。只要全部线程都运行到线程卡处,该函数就会在其中一个线程上运行。

#include <thread>
#include <vector>

void process_data(data_source &source, data_sink &sink) {
    unsigned const concurrency = std::thread::hardware_concurrency();
    unsigned const num_threads = (concurrency > 0) ? concurrency : 2;

    std::vector<data_chunk> chunks;

    auto split_source = [&] {
        if (!source.done()) {
            data_block current_block = source.get_next_data_block();
            chunks = divide_info_chunks(current_block, num_threads);
        }
    };

    split_source();
    result_block result;

    std::experimental::flex_barrier sync(num_threads, [&] {
        sink.write_data(std::move(result));
        split_source();
        return -1;
    });
    std::vector<joining_thread> threads(num_threads);

    for (unsigned i = 0; i < num_threads; ++i) {
        threads[i] = joining_thread([&, i] {
            while (!source.done()) {
                result.set_chunk(i, num_threads, process(chunks[i]));
                sync.arrive_and_wait();
            }
        });
    }
}

这里提取出了一个lambda函数,其内部封装了之前循环开头的部分,它们原本由0号线程运行,功能是将下一个大块数据切分成小段。并且,当都达到线程卡的时候,他们当中的一个就会运行补全函数,循环内的代码也就在补全函数中开始运行。

5 小结

5.1 future 相关

std::async只要我们并不急需线程运算的值,就可以使用std::async()异步方式启动任务。我们从std::async()函数处获得std::future对象(而非std::thread对象),运行的函数一旦完成,其返回值就由该对象最后持有。
std::packaged_task<>std::packaged_task<>连结了future对象函数。std::package_task<>对象在执行任务时,会调用关联的函数(或可调用对象),把返回值保存为future的内部数据,并令future准备就绪。
std::promise配对的std::promise和std::future可实现下面的工作机制:等待数据的线程在future上阻塞,而提供数据的线程利用相配的promise设定关联的值,使future准备就绪。
std::future
std::shared_future若在多个线程上访问同一个std::future,不采取措施会发生抢占。std::shared_future则可以解决这个问题,他可以复制出副本,但是它们全指向同一异步任务的状态数据。

5.2 等待相关

std::chrono::steady_clock恒温时钟类
std::chrono::system_clock系统时钟
std::chrono::high_resolution_clock高精度时钟
std::chrono::duration<short, std::ratio<60, 1>>时长
std::chrono::time_point<std::chrono::system_clock, std::chrono::minutes>时间点

5.3 同步操作

wait()
handle()handle()是wait()的链式后继,他们发生连锁调用,若接收的消息与指定类型不符,就会被丢弃,执行线程继续等待,直到收到与类型匹配的消息。
then()只要调用了then(),原future的值就会被取出,原future就会失效,then的调用也会返回新的future对象,后续函数的结果由他持有。
std::experimental::future
std::experimental::shared_future
std::experimental::when_all使用了std::experimental::when_all,等待future列表内的所有异步线程全部执行完毕,再继续当前线程。
std::experimental::when_any使用了std::experimental::when_any,等待future列表内的异步线程有一个执行完毕,就继续当前线程。
std::experimental::latch一旦他进入了就绪状态,就始终保持不变。随着到达latch的线程数增加,countdown直至0后,释放阻塞在此的全部线程。
std::experimental::barrier线程卡会释放等待的线程并且自我重置,因此可以重复使用。只与一组固定的线程同步,若某线程不属于同步组,就不会被拦截。
std::experimental::flex_barrierflex_barrier的构造函数既接收线程数目,也接受补全函数。只要全部线程都运行到线程卡处,该函数就会在其中一个线程上运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值