C++多线程编程之创建线程的几种方法

简介: C++多线程编程之创建线程的几种方法

1.线程基础知识


可执行程序运行起来,就会生成一个进程,该进程所属的主线程开始自动运行。请看下面的示例程序:


#include <iostream>
using namespace std;
int main(){
    cout << "I love China!" << endl;  // 实际上这个是主线程在执行,主线程从main()函数返回,则整个进程执行完毕。
    return 0;
}


主线程从main()函数开始执行,当我们自己创建线程时,也需要从一个函数开始运行(初始函数),一旦这个函数运行完毕,就代表我们这个线程运行结束。整个进程是否执行完毕的标志主线程是否执行完,如果主线程执行完毕,就代表整个进程执行完毕。此时,如果其他子线程还没有执行完,那么这些子线程也会被操作系统强行终止。因此,一般情况下,如果想保持子线程(自己用代码创建的线程)的运行状态,则必须要让主线程一直保持运行,不要让主线程运行结束。


2.创建线程的几种常用方法



2.1 使用初始函数创建线程


  • 主要流程如下:
  • 包含一个头文件thread
  • 编写初始函数
  • 在main()函数中开始写代码


#include <iostream>
#include <thread>
using namespace std;
// 自己创建的线程也要从一个函数(初始函数)开始运行
void myprint()
{
    cout << "我的线程开始执行了\n";
    /*
        中间包含其他的业务逻辑代码
    */
    cout << "我的线程执行完毕了\n";
}
int main(){
    thread mytobj(myprint);
    mytobj.join();
    cout << "I love China!" << endl;
    return 0;
}


   观察上面的代码发现:有两个线程在运行,相当于整个程序的执行有两条线在同时走。所以可以同时干两件事。即使一条线被堵住了,另外一条线仍然是可以通行的,这就是多线程。

73.png

(1).thread:是标准库中的类


// myprint是可调用对象
thread mytobj(myprint);
}


  • 上面的代码段做了两件事:
  • a.创建了线程,线程执行起点(入口)myprint();
  • b.myprint线程开始执行


(2).join()


加入/汇合,简单来说就是阻塞。阻塞主线程,让主线程等待子线程执行完毕,然后子线程和主线程汇合。然后主线程再继续往下走!


thread mytobj(myprint); 
mytobj.join();


  • 上面代码段的分析:
  • a.主线程阻塞到这里等待myprint子线程执行完,当子线程执行完毕,这个join()就执行完毕,主线程再继续往下执行。换言之,即阻塞主线程并等待myprint()子线程执行完
  • b.如果主线程执行完毕,但子线程没执行完毕,这样的程序是不合格的!程序也是不稳定的。一个良好的程序,应该是主线程等待子线程执行完毕后,主线程才能最终退出


(3).detach()  


detach()函数即主线程不再和子线程汇合。主线程执行自己的,子线程也执行自己的,主线程也不必等待子线程运行结束。主线程可以先执行结束,这并不影响子线程的执行    由于我们创建了很多子线程,让主线程逐个等待子线程结束,这种方法不太好,所以引入detach()方法。    一旦detach()之后,与主线程关联的thread对象就会失去与主线程的关联性。此时,子线程就会驻留在后台运行了(主线程与该子线程失去联系),子线程就相当于被C++运行时库接管。当子线程执行完毕后,由运行时库负责清理该线程相关的资源(守护线程)。    detach()使线程myprint失去我们对其进行的控制。一旦调用了detach(),就不能再用join(),否则系统会报告异常


(4).joinable()


判断是否可以成功使用join()或者detach()。若返回true,则可以使用join或者detach。否则,则不能使用。


thread mytobj(myprint); 
    if (mytobj.joinable())
    {
        cout << "1:joinable() == true" << endl;
    }
    else
    {
        cout << "1:joinable() == false" << endl;
    }
    mytobj.detach(); // 阻塞主线程并等待myprint()子线程执行完
    if (mytobj.joinable())
    {
        cout << "2:joinable() == true" << endl;
    }
    else
    {
        cout << "2:joinable() == false" << endl;
    }


2.2 使用类对象创建线程


#include <iostream>
#include <thread>
using namespace std;
class TA
{
public:
    void operator()() // 类对象变成可调用对象,即后面的括号中不能带参数
    {
        cout << "我的线程operator()开始执行了\n";
        /*
            中间包含其他的业务逻辑代码
        */
        cout << "我的线程operator()结束执行了\n";
    }
};
int main(){
    TA ta;  // ta是一个类的对象
    thread mytobj3(ta); // 同时,ta也是一个可调用对象
    mytobj3.join(); // 等待子线程执行结束
    cout << "I Love China" << endl;
    return 0;
}


使用类对象创建线程的代码如上所示,但需要注意一个问题。请看如下代码


#include <iostream>
#include <thread>
using namespace std;
class TA
{
public:
    int &m_i;
    TA(int& i) : m_i(i){}
    void operator()() // 类对象变成可调用对象,即后面的括号中不能带参数
    {
        cout << "m_i1的值为: " << m_i << endl; // 产生不可意料的结果!
        cout << "m_i2的值为: " << m_i << endl;
        cout << "m_i3的值为: " << m_i << endl;
        cout << "m_i4的值为: " << m_i << endl;
        cout << "m_i5的值为: " << m_i << endl;
        cout << "m_i6的值为: " << m_i << endl;
    }
};
int main(){
    int myi = 6;
    TA ta(myi);  // ta是一个类的对象
    thread mytobj3(ta); // 同时,ta也是一个可调用对象
    // mytobj3.join(); // 等待子线程执行结束
    mytobj3.detach();
    cout << "I Love China" << endl;
    return 0;
}


分析上述代码的输出结果可知:由于使用了mytobj3.detach(),因此主线程与子线程之间是无关的。主线程执行主线程的,子线程执行子线程的。现在假设出现一种情况,主线程先执行完了,子线程后执行完的。由于子线程的函数中使用的是引用成员变量m_i,m_i是与形参引用变量i绑定在一起的。而形参变量i是主线程中实参myi的引用,myi是主线程中的局部变量,当主线程执行结束后,变量myi的内存空间就会被系统所回收。但是,子线程仍然没有执行完,还在打印已经被销毁的变量m_i的内容,那么就会产生不可意料的结果了。


对上述代码段还有一个疑问:一旦调用了detach(),当主线程执行结束了,主线程中用的这个ta对象还存在吗?答:这个ta对象已经不存在了,因为这个对象ta实际上是被复制到子线程中去的!所以执行完主线程后,ta对象会被销毁,但是所复制的ta对象依旧存在。所以,只要你这个TA类对象里没有引用、没有引用,那么就不会产生问题。验证代码如下所示:


#include <iostream>
#include <thread>
using namespace std;
class TA
{
public:
    int &m_i;
    TA(int& i) : m_i(i){
        cout << "TA构造函数被执行" << endl;
    }
    TA(const TA& ta) : m_i(ta.m_i)
    {
        cout << "TA()拷贝构造函数被执行" << endl;
    }
    ~TA()
    {
        cout << "TA()析构函数被执行" << endl;
    }
    void operator()() // 类对象变成可调用对象,即后面的括号中不能带参数
    {
        cout << "m_i1的值为: " << m_i << endl;
        cout << "m_i2的值为: " << m_i << endl;
        cout << "m_i3的值为: " << m_i << endl;
        cout << "m_i4的值为: " << m_i << endl;
        cout << "m_i5的值为: " << m_i << endl;
        cout << "m_i6的值为: " << m_i << endl;
    }
};
int main(){
    int myi = 6;
    TA ta(myi);  // ta是一个类的对象
    thread mytobj3(ta); // 同时,ta也是一个可调用对象
    // mytobj3.join(); // 等待子线程执行结束
    mytobj3.detach();
    cout << "I Love China" << endl;
    return 0;
}

74.png


2.3 使用lambda表达式创建线程


int main(){
    auto mylamthread = []
    {
        cout << "我的线程3开始执行了" << endl;
        // ...
        cout << "我的线程3执行结束了" << endl;
    };
    thread mytobj4(mylamthread);
    mytobj4.join();
    return 0;
}
相关文章
|
2天前
|
数据采集 存储 数据处理
Python中的多线程编程及其在数据处理中的应用
本文深入探讨了Python中多线程编程的概念、原理和实现方法,并详细介绍了其在数据处理领域的应用。通过对比单线程与多线程的性能差异,展示了多线程编程在提升程序运行效率方面的显著优势。文章还提供了实际案例,帮助读者更好地理解和掌握多线程编程技术。
|
1天前
|
API Android开发 iOS开发
深入探索Android与iOS的多线程编程差异
在移动应用开发领域,多线程编程是提高应用性能和响应性的关键。本文将对比分析Android和iOS两大平台在多线程处理上的不同实现机制,探讨它们各自的优势与局限性,并通过实例展示如何在这两个平台上进行有效的多线程编程。通过深入了解这些差异,开发者可以更好地选择适合自己项目需求的技术和策略,从而优化应用的性能和用户体验。
|
6天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
15天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
12天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
2天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
14 2
|
8天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
33 5
|
14天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
46 4
|
15天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
43 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
28 4