线程
C++11 之前,C++ 语言没有对并发编程提供语言级别的支持,在编写可移植的并发程序时存在诸多的不便。
现在 C++11 中增加了线程以及线程相关的类,很方便地支持了并发编程,使得编写的多线程程序的可移植性得到了很大的提高。
C++11 中提供的线程类叫做 std::thread,基于这个类创建一个新的线程非常的简单,
只需要提供线程函数或者函数对象,并且可以同时指定线程函数的参数。
首先了解一下这个类提供的一些常用 API:
构造函数
1 2 3 4 5 6 7 8 9
| thread() noexcept;
thread(thread&& other) noexcept;
template<class Function, class... Args > explicit thread(Function&& f, Args&&... args);
thread(const thread&) = delete;
|
(1)构造函数1
默认构造函数。构造一个线程对象,在这个线程中不执行任何处理动作。
(2)构造函数2
移动构造函数。将 other 的线程所有权转移给新的 thread 对象。之后 other 不再表示执行线程。
(3)构造函数3
创建线程对象,并在该线程中执行函数 f 中的业务逻辑,args 是要传递给函数 f 的参数
任务函数 f 的可选类型有很多,具体如下:
普通函数、类成员函数、匿名函数、仿函数(这些都是可调用对象类型)
可以是可调用对象包装器类型,也可以是使用绑定器绑定之后得到的类型(仿函数)
(4)构造函数4
使用 =delete 删除拷贝构造,不允许线程对象之间的拷贝。
公共成员函数
get_id()
应用程序启动之后默认只有一个线程,这个线程一般称之为主线程或父线程,
通过线程类创建出的线程一般称之为子线程,每个被创建出的线程实例都对应一个线程 ID,这个 ID 是唯一的,
可以通过这个 ID 来区分和识别各个已经存在的线程实例,这个获取线程 ID 的函数 get_id()
,函数原型如下:
1
| std::thread::id get_id() const noexcept;
|
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream> #include <thread> using namespace std;
void func1(int num, string str) { for (int i = 0; i < 10; ++i) { cout << "child thread, i: " << i << ", num: " << num << ", str: " << str << endl; } }
void func2() { for (int i = 0; i < 10; ++i) { cout << "child thread, i: " << i << endl; } }
int main() { cout << "main thread, id: " << this_thread::get_id() << endl; thread t1(func1, 520, "hello world"); thread t2(func2); cout << "thread t, id: " << t1.get_id() << endl; cout << "thread t1, id: " << t2.get_id() << endl; }
|
通过线程对象调用 get_id()
获取这个子线程的线程 ID。
示例代码中有一个 bug,在主线程中依次创建出两个子线程,打印两个子线程的线程 ID,最后主线程执行完毕就退出(主线程执行 main 函数)。默认情况下,主线程销毁时会将与其关联的两个子线程也一起销毁,但是可能子线程中的任务还没有执行完毕,最后得不到想要的结果。
当创建了一个 thread 对象之后,在这个线程结束的时候,如何回收线程所使用的资源呢?thread 库提供了两种选择:
(1)加入式,join()
(2)分离式,detach()
另外,必须在线程对象销毁之前在二者之间作出选择,否则程序运行期间就会有 bug 产生。
join()
join()
是连接一个线程,意味着主动地等待线程的终止(线程阻塞)。在某个线程中通过子线程对象调用 join()
函数,调用这个函数的线程被阻塞,但是子线程对象中的任务函数会继续执行,当任务执行完毕之后 join()
会清理当前子线程中的相关资源然后返回,调用该函数的线程解除阻塞继续向下执行。
注意:弄清楚这个函数阻塞的是哪一个线程,函数在哪个线程中被执行,函数就阻塞哪个线程。函数原型如下:
使用线程阻塞函数之后,就可以解决在上面的示例代码中的 bug。如果要阻塞主线程的执行,只需要在主线程中通过子线程对象调用这个方法,当调用这个方法的子线程对象中的任务函数执行完毕之后,主线程的阻塞也就随之解除。修改后的示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12
| int main() { cout << "main thread, id: " << this_thread::get_id() << endl; thread t1(func1, 520, "hello world"); thread t2(func2); cout << "thread t, id: " << t1.get_id() << endl; cout << "thread t1, id: " << t2.get_id() << endl; t1.join(); t2.join(); return 0; }
|
当主线程运行到 t.join()
,根据子线程对象 t 的任务函数 func()
的执行情况,主线程会做如下处理:
(1)如果任务函数 func()
还没执行完毕:主线程阻塞,直到任务执行完毕,主线程解除阻塞,继续向下运行。
(2)如果任务函数 func()
已经执行完毕:主线程不会阻塞,继续向下运行。
为了更好地理解 join()
的使用,举一个例子,场景如下:
程序中一共有三个线程,其中两个子线程负责分段下载同一个文件。下载完毕之后,由主线程对这个文件进行下一步处理,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| #include <iostream> #include <thread> #include <chrono> using namespace std;
void download1() { this_thread::sleep_for(chrono::microseconds(500)); cout << "thread t1, id: " << this_thread::get_id() << endl; }
void download2() { this_thread::sleep_for(chrono::microseconds(300)); cout << "thread t2, id: " << this_thread::get_id() << endl; }
void doSomething() { cout << "start..." << endl; cout << "..." << endl; cout << "end..." << endl; }
int main() { thread t1(download1); thread t2(download2); t1.join(); t2.join(); doSomething(); }
|
示例代码中最核心的处理是在主线程调用 doSomething()
。
之前通过子线程对象调用了 join()
方法,这样能够保证两个子线程的任务都执行完毕(此处表示文件内容已经全部下载完成),主线程再对文件进行后续处理。如果子线程的文件没有下载完毕,主线程就去处理文件,从逻辑上是有问题的。
detach()
detach()
函数的作用是进行线程分离,分离主线程和创建出的子线程。
在线程分离之后,主线程退出也会一并销毁创建出的所有子线程。在主线程退出之前,子线程可以脱离主线程继续独立的运行,任务执行完毕之后,这个子线程会自动释放自己占用的系统资源。函数原型如下:
线程分离函数没有参数也没有返回值,只需要在线程成功之后,通过线程对象调用该函数,修改示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #include <iostream> #include <thread> #include <chrono> using namespace std;
void func1(int num, string str) { for (int i = 0; i < 10; ++i) { cout << "child thread, i: " << i << ", num: " << num << ", str: " << str << endl; } }
void func2() { for (int i = 0; i < 10; ++i) { cout << "child thread, i: " << i << endl; } }
int main() { cout << "main thread, id: " << this_thread::get_id() << endl; thread t1(func1, 520, "hello world"); thread t2(func2); cout << "thread t, id: " << t1.get_id() << endl; cout << "thread t1, id: " << t2.get_id() << endl; t1.detach(); t2.detach(); this_thread::sleep_for(chrono::seconds(5)); return 0; }
|
注意:线程分离函数 detach()
不会阻塞线程,子线程和主线程分离之后,在主线程中就不能再对这个子线程做任何控制,建议使用 join()
。
joinable()
joinable()
函数用于判断主线程和子线程是否关联(连接)状态,该函数返回一个布尔类型:
(1)返回值为 true:主线程和子线程之间有关联(连接)关系
(2)返回值为 false:主线程和子线程之间没有关联(连接)关系
1
| bool joinable() const noexcept;
|
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <iostream> #include <thread> #include <chrono> using namespace std;
void foo() { this_thread::sleep_for(chrono::seconds(1)); }
int main() { thread t; cout << "before starting, joinable: " << t.joinable() << endl;
t = thread(foo); cout << "after starting, joinable: " << t.joinable() << endl;
t.join(); cout << "after joining, joinable: " << t.joinable() << endl;
thread t1(foo); cout << "after starting, joinable: " << t1.joinable() << endl; t1.detach(); cout << "after starting, joinable: " << t1.joinable() << endl; return 0; }
|
在创建的子线程对象的时候,如果没有指定任务函数,那么子线程不会启动,主线程和这个子线程也不会进行连接。
在创建的子线程对象的时候,如果指定了任务函数,子线程启动并执行任务,主线程和这个子线程自动连接成功。
在子线程调用了 join()
函数,子线程中的任务函数继续执行,直到任务处理完毕,join()
会回收当前子线程的相关资源,所以这个子线程和主线程的连接断开。因此,调用 join()
之后再调用 joinable()
会返回 false。
子线程调用了 detach()
函数之后,父子线程分离,同时二者的连接断开,调用 joinable()
返回 false。
operator=()
线程中的资源是不能被复制的,因此通过 = 操作符进行赋值操作最终并不会得到两个完全相同的对象。
1 2 3 4
| thread& operator= (thread&& other) noexcept;
thread& operator= (const other&) = delete;
|
通过以上 = 操作符的重载声明可知:
(1)如果 other 是一个右值,会进行资源所有权的转移。
(2)如果 other 不是右值,禁止拷贝,该函数被删除(=delete),不可用。
静态函数
thread 线程类还提供了一个静态方法,用于获取当前计算机的 CPU 核心数,根据这个结果在程序中创建出数量相等的线程。每个线程独自占有一个 CPU 核心,这些线程就不用分时复用 CPU 时间片,此时程序的并发效率是最高的。
1
| static unsigned hardware_concurrency() noexcept;
|
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12
| #include <iostream> #include <thread> using namespace std;
int main() { int num = thread::hardware_concurrency(); cout << "CPU number: " << num << endl; return 0; }
|
C线程库
C 语言提供的线程库不论在 window 还是 Linux 操作系统中都可以使用,C 语言中的线程函数和上面的 C++ 线程类使用类似(基于面向对象的思想进行了封装),但 C++ 的线程类用起来更简单一些。
参考资料
https://subingwen.cn/cpp/thread/