C++11 多线程编程基础(上)
参考的学习视频:
【视频 - C++11 多线程编程 - 小白零基础到手撕线程池】
【视频 - C++11 多线程并发 基础入门教程 1.1 创建线程(thread)】
课程学习目录:
一、std::thread
线程库
01.基础知识
进程
:正在运行中的程序(是动态的)
线程
:可以让OS在同一时刻运行多个函数(进程中的进程)
OS可以有多个进程,一个进程也可以有多个线程
程序可利用的线程最大数量,取决于你环境的CPU核数
为什么要使用线程?
单线程是串行执行,效率是比较低的
利用好多线程可提高程序的运行效率
02.常见用法
#include
std::thread t(function_name, args…);
std::thread th(func, ...);
这里创建了一个名为th
的线程,入口函数是func
; 并且可以给入口函数传参;
th.join();
让整个程序在这个函数卡住,阻塞在这;
让主线程等待,直到子线程执行完毕;
th.detach();
让主线程与子线程分离,此时主线程是可以结束,子线程是挂在后台的;
注意:我们需要确保线程不会在主线程结束前退出,否则可能会导致未定义行为错误
一般是配合多进程使用;
th.joinable();
返回bool
值,用于判断线程是否可以使用join()
或detach()
方法;
其他用法:【视频 C++ 多线程并发 基础入门教程 1.1 创建线程(thread)】
参考:
【文档 std::thread - cppreference】
【文档 thead线程库的基本使用】
03.程序示例
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 39 40 41 42 43 44 45
| #include <iostream> #include <thread> #include <string>
void printT(std::string msg) { std::cout << msg << std::endl; for(int i = 0; i < 10; i++) { std::cout << i << std::endl; } }
int main() { std::thread thread_1(printT, "Hello Thread!");
bool isJoin = thread_1.joinable(); if(isJoin) { thread_1.join(); } std::cout << "over" << std::endl;
return 0; }
Output: Hello Thread! 0 1 2 3 4 5 6 7 8 9 over
|
04.常见错误
参考:【文档 thead线程库的基本使用】
在使用C++11线程库时,有一些常见的错误需要注意。例如:
上面提到的错误,后文会给出对应的解决方案。
二、线程函数中的数据未定义错误
主要有以下4种情况,
01.传递临时变量的问题
(1).问题描述
当给某个线程函数 std::thread th(foo, 1);
传入了一个临时值1或者单独一个临时变量,但入口函数要求的函数类型是引用类型,
这样会导致在线程函数执行时,会发生未定义错误;
(2).问题解决
我们需要定义一个变量来保存值,将该变量的引用传递给线程
在给线程的入口函数传递的参数是引用类型时,需要用std::ref()
来修饰
来确保传递的参数类型是一致的;
(3).程序示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <iostream> #include <thread>
void foo(int &x){ x += 1; }
int main() { int a = 1; std::thread th(foo, std::ref(a)); th.join(); std::cout << a << std::endl; return 0; }
|
02.传递指针或引用指向局部变量(指向已经释放的内存)的问题
参考:线程函数中的数据未定义错误
(1).问题描述
传递指针或引用指向局部变量的问题
当线程函数调用的入口函数的参数是引用类型的或指针类型(传入的是地址)时,
你将一个局部的变量传递给了线程函数,后续使用这个线程函数时,
由于局部变量离开作用域,找不到这个参数,发生未定义错误;
传递指针或引用指向已释放内存的问题
在线程开始执行之前,立即删除了ptr
指向的内存,这是不安全的操作
线程可能在主线程中删除ptr
后才开始执行,并且它仍然会尝试访问已经释放的内存,这会导致未定义的行为;
这个问题跟前面的描述类似,当线程函数执行期间,传递了一个已经被释放的内存,导致发生未定义错误;
(2).问题解决
传递指针或引用指向局部变量的问题:
将传入的参数改为全局变量,即可解决;
传递指针或引用指向已释放内存的问题:
不要在创建线程后立即释放内存,确保在线程完成之前内存仍然有效。
更好的办法是,
将指针或引用指向堆上的变量(new操作 + 适时delete)
或是 智能指针,
使用new操作
可以通过在线程函数中负责释放内存,或者等待线程完成后再释放内存来实现;
使用std::share_ptr
来管理对象的生命周期;
(3).程序示例
传入一个局部变量,会出现问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <iostream> #include <thread>
std::thread t;
int a = 1;
void foo(int &x){ x += 1; }
void test() { t = std::thread(foo, std::ref(a)); }
int main() { test(); t.join(); std::cout << a << std::endl; return 0; }
|
传递指针后,它被立即释放
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
| #include <iostream> #include <thread>
void foo(int* x){ *x += 1; std::cout << *x << std::endl; }
int main() { int* ptr = new int(1); std::thread t(foo, ptr); delete ptr;
t.join();
return 0; }
|
智能指针写法, 可避免上面的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <iostream> #include <thread> #include <memory>
void foo(std::unique_ptr<int> ptr) { std::cout << *ptr << std::endl; }
int main() { std::unique_ptr<int> ptr = std::make_unique<int>(1); std::thread t(foo, std::move(ptr)); t.join(); return 0; }
Output: 1
|
03.类的成员函数作为入口函数,类对象被提前释放
(1).问题描述
创建线程时,传入了一个类的成员函数func()
作为该线程的入口函数,&a
作为函数的参数
问题在于,如果在线程执行之前,main
函数中的a
对象已经被销毁,那么在线程执行时尝试访问a
对象的成员函数func()
将导致未定义的行为。
(2).问题解决
将t.join()
移动到了线程创建之后,
确保线程执行完毕后才继续执行主线程的操作。这样就能够避免在func
中访问无效的对象引用。
或者使用智能指针,可以保证代码的安全性,并且不会出现对象被提前释放的问题。
使用std::shared_ptr
来管理对象的生命周期是一种有效的方法,以确保在多线程环境中避免悬垂指针或无效引用的问题。
(3).程序示例
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
| #include <iostream> #include <thread> #include <memory>
class A { public: void func() { std::cout << "Thread " << std::this_thread::get_id() << " started" << std::endl; std::cout << "Hello" << std::endl; } };
int main() {
std::shared_ptr<A> a1 = std::make_shared<A>(); std::thread t1(&A::func, a1); t1.join();
return 0; }
Output: Thread 139623788812032 started Hello
|
04.线程的入口函数使用了类的私有成员函数
(1).问题描述
传递的函数需要访问类的私有方法,需要声明为友元类,否则会出错;
当线程的入口函数是调用了类的私有方法,需要将该方法声明为类的友元类,要求传递的参数是类对象指针;
(2).程序示例
线程的入口函数是调用了类的私有方法
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>
class MyClass{
private: friend void thread_func(MyClass* obj); void printT() { std::cout << "void printT()..." << std::endl; std::cout << "Thread " << std::this_thread::get_id() << std::endl; } };
void thread_func(MyClass* obj) { obj->printT(); }
int main() { MyClass obj; std::thread thread_1(thread_func, &obj); thread_1.join(); return 0; }
Output: void printT()... Thread 140095929181952
|
线程的入口函数是直接使用类的私有方法
将创建线程过程包含进友元函数中,
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 <memory>
class A { friend void test(); private: void func() { std::cout << "Thread " << std::this_thread::get_id() << " started" << std::endl; std::cout << "Hello" << std::endl; } };
void test() { std::shared_ptr<A> a1 = std::make_shared<A>(); std::thread t1(&A::func, a1); t1.join(); }
int main() { test(); return 0; }
Output: Thread 139892073989888 started Hello
|
三、互斥量解决多线程数据共享问题
参考:互斥量解决多线程数据共享问题
01.基础知识
线程安全概念:
如果多线程程序的每一次运行结果和单线程运行结果始终是一致的,那么你的线程就是安全的。
02.问题描述
如果多个线程同时访问同一个变量,并且其中至少有一个线程对该变量进行了写操作,
那么就会出现数据竞争问题(理解为同时访问时,只操作了一次,导致和你预期结果不一致)
而且数据竞争可能会导致程序崩溃、产生未定义的结果,或者得到错误的结果。
03.解决措施
数据竞争的解决措施,比如常见的加锁操作;
使用互斥量std::mutex
的最终目的就是保证线程的安全性,以避免数据之间发生竞争问题
为了避免数据竞争问题,需要使用同步机制来确保多个线程之间对共享数据的访问是安全的。
常见的同步机制包括互斥量、条件变量、原子操作等。
04.程序示例
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
| #include <iostream> #include <thread> #include <mutex>
int a = 0; std::mutex mtx;
void func() { for(int i = 0; i < 10000; i++) { mtx.lock(); a += 1; mtx.unlock(); } }
int main() { std::thread t1(func); std::thread t2(func); t1.join(); t2.join();
std::cout << "a: " << a << std::endl; return 0; }
|
四、std::atomic
原子操作解决多线程数据共享问题
01.什么是原子操作
std::atmoic
是C++11标准库中的一个模板类,用于实现多线程环境下的原子操作;
原子操作可以提供一种线程安全的方式来访问和修改共享变量,可以避免多线程环境中的数据竞争问题。
主要是用来多线程下解决数据共享的问题。
02.常见的API操作
load()
:将std::atomic
变量的值加载到当前线程的本地缓存中,并返回这个值。
store(val)
:将val
的值存储到 std::atomic
变量中,并保证这个操作是原子性的。
load()
就是输出值、**store(val)
就是赋值** 可以保证操作是原子性的,是线程安全的。
exchange(val)
:将val
的值存储到std::atomic
变量中,并返回原先的值。
compare_exchange_weak(expected, val)
和 compare_exchange_strong(expected, val)
:
比较std::atomic
变量的值和expected
的值是否相同,
如果相同,则将val
的值存储到std::atomic
变量中,并返回true
;
否则,将std::atomic
变量的值存储到expected
中,并返回false
。
03.程序示例
原始版本的代码,会出现数据竞争的问题,我们期望的结果是20000
但结果却不是;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <iostream> #include <thread>
int a = 0; void func() { for(int i = 0; i < 10000; i++) { a += 1; } }
int main() { std::thread t1(func); std::thread t2(func); t1.join(); t2.join();
std::cout << "a: " << a << std::endl; return 0; }
|
使用原子操作,执行效率会提升较多
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 39 40
| #include <iostream> #include <thread> #include <atomic> #include <mutex>
std::atomic<int> a;
void func() { for(int i = 0; i < 10000; i++) { a++; } }
int main() { auto start = std::chrono::duration_cast<std::chrono::microseconds>( std::chrono::system_clock::now().time_since_epoch()).count();
std::thread t1(func); std::thread t2(func); t1.join(); t2.join();
std::cout << "a: " << a << std::endl;
auto end = std::chrono::duration_cast<std::chrono::microseconds>( std::chrono::system_clock::now().time_since_epoch()).count();
std::cout << "cost time (us) : " << end - start << std::endl; return 0; }
|
五、互斥量死锁问题
参考:互斥量死锁
01.死锁现象
多个进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,
此时称系统处于死锁状态,这些永远在互相等待的进程称为死锁进程。【文档 什么是死锁? 是什么,为什么用,怎么用】
产生死锁的原因,主要包括:
- 系统资源不足;
- 程序执行的顺序有问题;
- 资源分配不当等。
02.发生死锁的原因
03.避免死锁的措施
上面的四个条件是死锁的发生必要条件,所以只要让上述条件之一不满足,就不会发生死锁现象。
例如,简单的解决措施是,可以改变线程获取锁的顺序,保证某一时刻共享资源只能被一个线程占有。
04.程序示例
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> #include <mutex> std::mutex mtx1, mtx2;
void func1() { for(int i = 0; i < 50; i++) { mtx1.lock(); mtx2.lock(); mtx1.unlock(); mtx2.unlock(); } }
void func2() { for(int i = 0; i < 50; i++) { mtx1.lock(); mtx2.lock(); mtx1.unlock(); mtx2.unlock(); } }
int main() { std::thread t1(func1); std::thread t2(func2); t1.join(); t2.join(); std::cout << "over" << std::endl;
return 0; }
|