[c ++ 11]多线程编程(1)-初识

电脑杂谈  发布时间:2020-04-06 23:06:06  来源:网络整理

linux线程编程_多线程编程之一——问题提出_java线程编程

并发可以在生活中的任何地方看到,走路时说话,听歌时编写代码. 在计算机术语中,“并发”是指在单个系统中同时执行多个独立活动,而不是依次执行一个接一个. 对于单核CPU,一次只能处理一个任务,但不能完全执行一个任务,然后执行下一个任务,但是总是在任务之间切换,并且每个任务在完成后都会执行. 任务看起来好像任务是并行发生的. 尽管并非严格同时执行多个任务,但我们仍将其称为并发. 真正的并发是在多核CPU上,它实际上可以同时执行多个任务,称为硬件并发.

并发并非没有代价. 在单核CPU上同时执行两个任务需要上下文切换的时间成本. 如下图所示:

双核机器的并行执行与单核机器对比.png

假设将A和B的两个任务划分为10个相等大小的块,则单核CPU交替执行两个任务,每次执行一个块,所花费的时间就是不完成A任务在进入B之前,该任务花费的时间是原来的两倍,但更多. 这是因为系统需要执行从一个任务到另一个任务的上下文切换,这需要时间(图中的灰色框). 上下文切换要求操作系统为当前正在运行的任务保存CPU状态和指令指针,找出要切换到的任务,并为要切换的任务重新加载处理器状态. 然后将新任务的指令和数据加载到缓存中.

该应用程序分为多个独立的单线程进程,这些进程可以同时运行. 该过程的内部实现原理相对复杂,因此在此不再赘述.

并发运行的进程之间的通信.png

这些独立的进程可以通过常规的进程间通信机制进行通信,例如管道,信号,消息队列,共享内存,存储映射的I / O,信号量,套接字等.

linux线程编程_多线程编程之一——问题提出_java线程编程

缺点:

好处:

线程就像轻量级进程,但是进程中的所有线程共享相同的地址空间,并且线程之间的大多数数据可以共享. 线程之间的通信通常通过共享内存来实现.

并发运行的线程之间的通信.png

好处:

缺点:

主要原因有两个: 任务拆分和性能提高.

linux线程编程_多线程编程之一——问题提出_java线程编程

编写软件时,最好将相关代码放在一起并分离不相关的代码,这可以使程序更易于理解和测试. 将程序划分为不同的任务,每个线程执行一个任务或多个任务,这可以简化整个程序的逻辑.

在两种情况下,并发都可以提高性能.

任务并行性(task parallelism): 将单个任务分为几个部分并并行运行以减少运行时间. 尽管听起来很简单,但这实际上是一个相当复杂的过程. 想象一下,如果每个部分之间有很多部分,则一个部分的执行需要使用另一任务的执行结果. 此时,它不能很好地并行完成. 数据并行性: 每个线程对不同的数据部分执行相同的操作.

C ++ 98标准中没有线程库多线程编程之一——问题提出,但是在C ++ 11中最终提供了多线程标准库,该库提供了用于管理线程,保护共享数据,线程间同步操作,原子的类. 操作等.

与多线程库相对应的头文件是#include ,类名是std :: thread.

一个简单的串行程序如下:

#include <iostream>
#include <thread>
void function_1() {
    std::cout << "I'm function_1()" << std::endl;
}
int main() {
    function_1();
    return 0;
}

多线程编程之一——问题提出_linux线程编程_java线程编程

这是典型的单线程单进程程序. 任何程序都是一个进程,而main()函数是主线程. 单线程按顺序执行.

实际上,将上述程序转换为多线程程序非常简单. 让function_1()函数在另一个线程中执行:

#include <iostream>
#include <thread>
void function_1() {
    std::cout << "I'm function_1()" << std::endl;
}
int main() {
    std::thread t1(function_1);
    // do other things
    t1.join();
    return 0;
}

分析:

首先,构造一个std ::线程对象t1,并在构造过程中传递一个参数. 此参数是一个函数. 该函数是该线程的入口函数. 执行该函数后,将执行整个线程. 成功创建线程后,它将立即启动,并且没有类似于start的函数来显式启动线程. 线程开始运行后,您需要明确决定是等待它的连接(join)还是分离它以使其独立运行(分离). 注意: 您只需要在销毁std :: thread对象之前做出此决定即可. 在此示例中,对象t1是堆栈上的变量,并且在执行主函数后将被销毁,因此您需要在主函数结束之前做出决定. 在此示例中,选择了t1.join(). 主线程将阻塞,直到子线程完成. join()函数的另一任务是回收线程中使用的资源.

线程对象的生命周期与对象内部管理的线程不同. 如果线程快速执行,则内部线程可能已经结束,但是线程对象仍然存在,或者线程对象可能已被破坏. 内部线程仍在运行.

假设t1线程是一个非常慢的线程,主线程不想等待子线程的结束要结束整个任务,直接删除t1.join()是不够的,程序将被终止(破坏性的t1将调用std ::终止,并且程序将在没有活动异常的情况下打印终止调用).

多线程编程之一——问题提出_java线程编程_linux线程编程

相应地,我们可以调用t1.detach(),以便t1线程在后台运行,并将所有权和控制权转移到C ++运行时库,以确保可以使用与该线程关联的资源线程退出后. 已正确回收. 参考UNIX守护程序进程的概念,此分离的线程称为守护程序线程. 分离线程后,即使线程对象被破坏,线程仍然可以在后台运行,但是由于对象被破坏多线程编程之一——问题提出,主线程无法通过对象名称与此线程进行通信. 例如:

#include <iostream>
#include <thread>
void function_1() {
    //延时500ms 为了保证test()运行结束之后才打印
    std::this_thread::sleep_for(std::chrono::milliseconds(500)); 
    std::cout << "I'm function_1()" << std::endl;
}
void test() {
    std::thread t1(function_1);
    t1.detach();
    // t1.join();
    std::cout << "test() finished" << std::endl;
}
int main() {
    test();
    //让主线程晚于子线程结束
    std::this_thread::sleep_for(std::chrono::milliseconds(1000)); //延时1s
    return 0;
}
// 使用 t1.detach()时
// test() finished
// I'm function_1()
// 使用 t1.join()时
// I'm function_1()
// test() finished

分析:

由于线程输入函数内部有500ms的延迟,因此在打印之前已执行了test()且t1已被销毁,但是它负责的线程仍然可以运行,这就是detach()的作用. 如果消除主函数中的1s延迟,则会发现没有任何输出,因为主线程执行得太快,整个程序已结束,并且后台线程由C ++运行时库回收. 如果将t1.detach()替换为t1.join(),则在t1线程的执行结束之前,测试功能将不会执行.

一旦线程分离,就无法再次加入. 如果必须调用,程序将崩溃,可以使用joinable()函数确定线程对象是否可以调用join().

void test() {
    std::thread t1(function_1);
    t1.detach();
    if(t1.joinable())
        t1.join();
    assert(!t1.joinable());
}

链接到我的简书

C ++并发编程与C ++线程#1: 简介


本文来自电脑杂谈,转载请注明本文网址:
https://www.pc-fly.com/a/jisuanjixue/article-166378-1.html

    相关阅读
      发表评论  请自觉遵守互联网相关的政策法规,严禁发布、暴力、反动的言论

      热点图片
      拼命载入中...