1、进程与线程
- 进程是一个应用程序被操作系统拉起来加载到内存之后从开始执行到执行结束的这样一个过程。简单来说,进程是程序(可执行文件exe)的一次执行。比如双击打开一个桌面应用软件就是开启一个进程。
- 线程是进程中的一个实体,是被操作系统独立分配和调度的基本单位。线程是CPU可执行调度的最小单位,也就是说进程本身不能获取CPU时间,只有线程才可以。
- 从属关系:进程 > 线程。一个进程可以拥有多个线程。
- 每个线程共享同样的内存空间,开销比较小。
- 每个进程拥有独立的内存空间,因此开销大。
- 对于高性能并行计算,或者说高性能服务器等,更好的是多线程。
2、std::thread的构造函数
// ①
thread() noexcept;
// ②
thread( thread&& other ) noexcept;
// ③
template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );
// ④
thread( const thread& ) = delete;
- 构造函数①:默认构造函数,构造一个线程对象,在这个线程中不执行任何处理动作。
- 构造函数②:移动构造函数,将other的线程所有权转移给新的thread对象。之后other不再表示执行线程。
- 构造函数③:创建线程对象,并在该线程中执行函数f中的业务逻辑,args是要传递给函数f的参数。任务函数f的可选类型很多,包括普通函数、类成员函数、匿名函数、仿函数等等。
- 构造函数④:使用=delete显示删除拷贝赋值构造函数,不允许线程对象之间拷贝。
3、std::thread的成员函数
3.1、get_id()
应用程序启动之后默认只有一个线程,这个线程一般称为主线程或者父线程,通过线程类创建出的线程一般称为子线程。每个被创建出的线程实例都对应一个线程ID,这个ID是唯一的,可以通过这个ID来区分和识别各个已经存在的线程实例,这个获取线程ID的函数叫做get_id(),函数原型为:
std::thread::id get_id() const noexcept;
测试程序如下:
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
void func(int num, string str)
{
for (int i = 0; i < 10; ++i)
{
cout << "子线程: i = " << i << "num: "
<< num << ", str: " << str << endl;
}
}
void func1()
{
for (int i = 0; i < 10; ++i)
{
cout << "子线程: i = " << i << endl;
}
}
int main()
{
cout << "主线程的线程ID: " << this_thread::get_id() << endl;
thread t(func, 520, "i love you");
thread t1(func1);
cout << "线程t 的线程ID: " << t.get_id() << endl;
cout << "线程t1的线程ID: " << t1.get_id() << endl;
return 0;
}
在上面的示例程序中有一个 bug,在主线程中依次创建出两个子线程,打印两个子线程的线程 ID,最后主线程执行完毕就退出了(主线程就是执行 main () 函数的那个线程)。默认情况下,主线程销毁时会将与其关联的两个子线程也一并销毁,但是这时有可能子线程中的任务还没有执行完毕,最后也就得不到我们想要的结果了。
当启动一个线程(创建一个thread对象)之后,在这个线程结束的时候,我们如何回收线程所使用的资源呢?thread库给了我们两种选择:
- 加入式(join()):也叫阻塞式。
- 分离式(detach())。
我们必须在线程销毁前在二者之间作出选择,否则会出现异常。
3.2、join()
join()字面意思是连接一个线程,意味着主动等待线程的终止(线程阻塞)。在某个线程中通过子线程对象调动join()函数,调用这个函数的线程被阻塞。但是子线程对象中的任务函数会继续执行,当任务执行完毕之后join()会清理当前子线程的相关资源然后返回,同时,调用该函数的线程解除阻塞继续向下执行。join函数的原型如下:
void join();
为了更好理解join()的使用,再举个例子,如下是一个多线程模拟下载文件的例子:
#include<iostream>
#include<chrono>
#include<thread>
#include<string>
/*模拟下载任务-多线程的异步处理*/
void downLoad(std::string file)
{
for (int i = 0; i < 10; i++)
{
std::cout << "DownLoading" << file << "(" << i * 10 << "%)" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "DownLoading completed:" << file<< std::endl;
}
void interact()
{
std::string name;
std::cin >> name;
std::cout << "Hello" << " " << name<<std::endl;
}
int main()
{
std::thread t([&](){
downLoad("hello.zip");
});
t.join();
interact();
return 0;
}
测试结果如下:
如上我们在主线程中执行interact()这个交互函数,子线程执行文件下载的函数,当主线程执行完成后,不会立马退出程序和回收资源,程序会陷入阻塞,等待下载文件的子线程完成,才会退出程序。
3.3、detach()
detach() 函数的作用是进行线程分离,分离主线程和创建出的子线程。在线程分离之后,主线程退出也会一并销毁创建出的所有子线程,在主线程退出之前,它可以脱离主线程继续独立的运行,任务执行完毕之后,这个子线程会自动释放自己占用的系统资源。(其实就是孩子翅膀硬了,和家里断绝关系,自己外出闯荡了,如果家里被诛九族还是会受牵连)。该函数的原型如下:
void detach();
测试代码如下:
#include<iostream>
#include<chrono>
#include<thread>
#include<string>
/*模拟下载任务-多线程的异步处理*/
void downLoad(std::string file)
{
for (int i = 0; i < 10; i++)
{
std::cout << "DownLoading" << file << "(" << i * 10 << "%)" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "DownLoading completed:" << file<< std::endl;
}
void interact()
{
std::string name;
std::cin >> name;
std::cout << "Hello" << " " << name<<std::endl;
}
void myfunc()
{
std::thread t([&](){
downLoad("hello.zip");
});
t.detach();
}
int main()
{
myfunc();
interact();
return 0;
}
测试结果如下:
在上述案例中,调用成员函数detach()分离该线程,意味着线程的生命周期不再受当前std::thread对象所管理,而是在线程退出后自动销毁。
注意:线程分离函数 detach () 不会阻塞线程,子线程和主线程分离之后,在主线程中就不能再对这个子线程做任何控制了,比如:通过 join () 阻塞主线程等待子线程中的任务执行完毕,或者调用 get_id () 获取子线程的线程 ID。有利就有弊,鱼和熊掌不可兼得,建议使用 join ()。