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();
相关推荐
ShineWinsu18 小时前
对于C++:继承的解析—上
开发语言·数据结构·c++·算法·面试·笔试·继承
小付同学呀18 小时前
C语言学习(五)——输入/输出
c语言·开发语言·学习
码农阿豪18 小时前
Nacos 日志与 Raft 数据清理指南:如何安全释放磁盘空间
java·安全·nacos
直有两条腿18 小时前
【大模型】Langchain4j
java·langchain
love530love18 小时前
Scoop 完整迁移指南:从 C 盘到 D 盘的无缝切换
java·服务器·前端·人工智能·windows·scoop
消失的旧时光-194319 小时前
C++ 多线程与并发系统取向(二)—— 资源保护:std::mutex 与 RAII(类比 Java synchronized)
java·开发语言·c++·并发
莫寒清19 小时前
ThreadLocal
java·面试
学编程的闹钟19 小时前
E语言计算器开发全攻略
学习
薛定e的猫咪19 小时前
Claude Code 完整学习手册:安装配置、CCR、MCP、插件与 Superpowers开发框架
学习
雾山大叔20 小时前
多会话浏览器串口调试助手
经验分享·笔记·学习