C++并发编程学习(二)—— 线程所有权和管控

文章目录

一、线程归属权

移交线程归属权

std::thread支持移动语义,可以实现函数创建线程并将归属权移交给函数调用者,和创建线程并将其归属权传入某个函数的功能。

对于std::thread C++ 不允许其执行拷贝构造和拷贝赋值, 所以只能通过移动和局部变量返回的方式将线程变量管理的线程转移给其他变量管理。

c++ 复制代码
#include <thread>
#include <chrono>
#include <iostream>

void some_function() {
    while (true) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}
void some_other_function() {
    while (true) {
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

int main() {
    //t1 绑定some_function
    std::thread t1(some_function); 
    //2 转移t1管理的线程给t2,转移后t1无效   
    std::thread t2 =  std::move(t1);
    std::cout << "moved t1 to t2" << "\n";
    //3 t1 可继续绑定其他线程,执行some_other_function    
    t1 = std::thread(some_other_function);
    std::cout << "construction t1 again" << "\n";
    //4  创建一个线程变量t3
    std::thread t3;
    //5  转移t2管理的线程给t3
    t3 = std::move(t2);
    std::cout << "moved t2 to t3" << "\n";
    //6  转移t3管理的线程给t1
    t1 = std::move(t3);
    std::cout << "moved t3 to t1" << "\n";
    std::this_thread::sleep_for(std::chrono::seconds(2000));
}

上述代码打印信息:

复制代码
moved t1 to t2
construction t1 again
moved t2 to t3
terminate called without an active exception

上面代码将t2管理的线程交给t3

之后将t3管理的线程交给t1,此时t1管理线程运行着 some_function,

步骤6导致崩溃的原因就是将t3管理的线程交给t1,而此时t1正在管理线程运行some_other_function。

所以我们可以得出一个结论,就是不要将一个线程的管理权交给一个已经绑定线程的变量,否则会触发线程的terminate函数引发崩溃。赋值操作也有类似的原则:只要std::thread对象正管控着一个线程,就不能简单地向它赋新值,否则该线程会因此被遗弃。

std::thread支持移动操作的意义是,函数可以便捷地向外部转移线程的归属权:
从函数内部返回std::thread对象:

cpp 复制代码
std::thread f()
{
    void some_function();
    return std::thread(some_function);
}
std::thread g()
{
    void some_other_function(int);
    std::thread t(some_other_function,42);
    return t;
}

类似地,若归属权可以转移到函数内部,函数就能够接收std::thread实例作为按右值传递的参数:

cpp 复制代码
void f(std::thread t);
void g()
{
    void some_function();
    f(std::thread(some_function));
    std::thread t(some_function);
    f(std::move(t));
}

线程容器存储

容器存储线程时,比如 vector ,如果用 push_back 操作势必会调用 std::thread ,这样会引发编译错误,因为 std::thread 没有拷贝构造函数。我们可以使用emplace_back,避免调用thread的拷贝构造函数。

cpp 复制代码
void do_work(unsigned id);
void f()
{
    std::vector<std::thread> threads;
    for(unsigned i=0;i<20;++i)
    {
        threads.emplace_back(do_work,i); // 生成线程
    }
    for(auto& entry: threads) // 依次在各线程上调用join()函数
        entry.join();
}

二、在运行时选择线程数量

用C++标准库的std::thread::hardware_concurrency()函数,它的返回值是一个指标,表示程序在各次运行中可真正并发的线程数量。下面代码是并行版的std::accumulate()的简单实现

cpp 复制代码
#include <thread>
#include <vector>
#include <iostream>
#include <numeric>

template<typename Iterator, typename T>
struct accumulate_block
{
    void operator()(Iterator first, Iterator last, T& result)
    {
        result = std::accumulate(first, last, result);
    }
};

template<typename Iterator, typename T>
T parallel_accumulate(Iterator first, Iterator last, T init)
{
    unsigned long const length = std::distance(first, last);
    if (!length)
        return init;
    unsigned long const min_per_thread = 25;
    unsigned long const max_threads = (length + min_per_thread - 1) / min_per_thread;
    // 真正的可并行线程数,等于CPU核数
    unsigned long const hardware_threads = std::thread::hardware_concurrency();
    unsigned long const num_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads);
    std::cout << "hardware threads num: " << hardware_threads << std::endl;
    unsigned long const block_size = length / num_threads; // 各线程需分担的元素数量
    std::vector<T> results(num_threads);
    std::vector<std::thread> threads(num_threads - 1);
    Iterator block_start = first;
    for (unsigned long i = 0; i < num_threads - 1; ++i)
    {
        Iterator block_end = block_start;
        std::advance(block_end, block_size); // 将迭代器从当前位置向前移动 block_size 个元素
        threads[i] = std::thread(accumulate_block<Iterator, T>(), block_start, block_end, std::ref(results[i]));
        block_start = block_end; // 下一小块的起始位置即为本小块的末端
    } 

    accumulate_block<Iterator, T>()(block_start, last, results[num_threads - 1]); // 发起全部线程后,主线程随之处理最后一个小块
    for(auto& entry : threads)
        entry.join();
    return std::accumulate(results.begin(), results.end(), init);
}

void use_parallel_acc()
{
    std::vector<int> vec(1000000);
    for (int i = 0; i < 1000000; ++i)
        vec.push_back(i);

    int sum = 0;
    sum = parallel_accumulate<std::vector<int>::iterator, int>(vec.begin(), vec.end(), sum);
    std::cout << "sum: " << sum << std::endl;
}

int main()
{
    use_parallel_acc();
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 0;
}

三、识别线程

程ID所属类型是std::thread::id,它有两种获取方法。首先,在与线程关联的std::thread对象上调用成员函数get_id(),即可得到该线程的ID。如果std::thread对象没有关联任何执行线程,调用get_id()则会返回一个std::thread::id对象,它按默认构造方式生成,表示"线程不存在"​。其次,当前线程的ID可以通过调用std::this_thread::get_id()获得,函数定义位于头文件<thread>

C++标准库容许我们随意判断两个线程ID是否相同,并且std::thread::id型别具备全套完整的比较运算符

cpp 复制代码
std::thread::id master_thread;
void some_core_part_of_algorithm()
{
    if(std::this_thread::get_id()==master_thread)
    {
        do_master_thread_work();
    }
    do_common_work();
}

也可以输出线程ID:

cpp 复制代码
std::cout<<std::this_thread::get_id();
相关推荐
Demon_Hao2 小时前
JAVA缓存的使用RedisCache、LocalCache、复合缓存
java·开发语言·缓存
踏雪羽翼2 小时前
android 解决混淆导致AGPBI: {“kind“:“error“,“text“:“Type a.a is defined multiple times
android·java·开发语言·混淆·混淆打包出现a.a
lang201509282 小时前
Tomcat Maven插件:部署与卸载的架构设计
java·tomcat·maven
sinat_267611912 小时前
跟着官网学习协程随笔
学习·kotlin
一切尽在,你来2 小时前
C++ 零基础教程 - 第 5 讲 变量和数据类型
开发语言·c++
serve the people2 小时前
python环境搭建 (六) Makefile 简单使用方法
java·服务器·python
重生之后端学习2 小时前
146. LRU 缓存
java·数据结构·算法·leetcode·职场和发展
王老师青少年编程2 小时前
2022信奥赛C++提高组csp-s复赛真题及题解:假期计划
c++·真题·csp·信奥赛·csp-s·提高组·假期计划
萧曵 丶2 小时前
懒加载单例模式中DCL方式和原理解析
java·开发语言·单例模式·dcl