多线程的概念就不需要多说了,多线程的主要难点在于争用条件,撕裂,死锁,和伪共享
争用条件很简单,也就是多个线程要访问共享资源。比如一个线程读取了内存的数据10,它将其增加了1变成了11,正当他写入到内存的时候,另一个线程登场,它读取了内存的数据,此时内存的数据还是10,它将其减少了1,然后写入到内存,内存现在的数据是9,之前的线程来了,它也要写入自己的数据11。这就偏离我们的目的。
其他的情况都类似,撕裂就是可能在读取文件的时候,因为多线程的影响只读取了一部分,另一部分被其他线程修改了,文件变成了四不像,缝合怪。死锁也是,就是一个线程进去访问资源,但是因为某些原因,比如没有权限,就阻塞在里面,因为外面加了锁,它不离开这个加锁的区域,也就没办法解锁,所以其他线程也被阻塞在外面,最终死锁了。伪共享是因为缓存行被多个线程同时使用了,这样修改的时候出现了交叉干扰,影响并发性能,java里面一般是字节填充
c++的线程对象允许传入多个参数,这和Linux不一样
#include
#include
void counter(int id, int numIteration)
{
for(int i = 0; i < numIteration; i++)
{
std::cout << "Counter " << id << " has value " << i << std::endl;
}
}
int main(int argc, char** argv) {
std::thread t1{counter, 1, 5};
std::thread t2{counter, 2, 8};
t1.join();
t2.join();
return 0;
}
对于c++创建线程,要习惯函数方法和函数对象方法,但是如果使用函数对象,需要实现operator(),特殊的调用方法还有lambda和成员函数
lambda的创建方法如下:
#include
#include
int main(int argc, char** argv) {
int id{1};
int num{5};
std::thread t1{
[id, num] {for(int i = 0; i < num; ++i)
{
std::cout << "Counter " << id << "has value " << i << std::endl;
}
}};
t1.join();
return 0;
}
通过成员函数:
#include
#include
class Counter{
public:
Counter(int a, int b){
id = a;
num = b;
}
void fun(){
for(int i{0}; i < num; ++i)
std::cout << id << ":" << i << std::endl;
}
private:
int id;
int num;
};
int main(int argc, char** argv) {
Counter c{10, 5};
std::thread t1{&Counter::fun, &c};
t1.join();
return 0;
}
对于一个全局的共享变量,不同的线程访问会导致这个变量发生变化,而c++创造了一种新的变量thread_local,可以将任何变量标记为线程本地数据
#include
#include
int k = 0;
thread_local int n;
void fun(int id)
{
std::cout << "id = " << id << " k = " << k++ << " n = " << n++ << std::endl;
}
int main(int argc, char** argv) {
std::thread t1{fun, 1};
std::thread t2{fun, 2};
t1.join();
t2.join();
return 0;
}
输出结果可以发现k发生了变化,但是n没有,这是因为n是每一个线程本地的,互不相干,但是k是共有的。
c++11里面实际上没有专门用来取消线程的方法,而c++20引进了jthread,在没有特定的线程库可使用的情况下,建议使用原子量和条件变量
多线程的异常处理用下面的函数展示
#include
#include
#include
#include
//抛出异常的函数
void doSomeWork(){
for(int i{0}; i < 5; ++i)
{
std::cout<< i << std::endl;
}
std::cout << "抛出一个runtime_error异常" << std::endl;
throw std::runtime_error {"Exception from thread"};
}
//线程引用的函数
void threadFunc(std::exception_ptr& err)
{
try{
doSomeWork();
}catch(...){
std::cout << "捕捉到异常"<
这个程序很简单,但是展示了抛出异常的处理过程
doWorkInthread函数创建一个线程,然后运行一个函数threadFunc,这个函数会调用doSomeWork,然后抛出一个异常,异常在threadFunc中获取,并被error给接受了,这是一个引用类型,最终在threadFunc中获取,传递给了主函数。
几个关键的函数:
(1).exception_ptr current_exception()
这个函数会获取当前正常处理的异常,要用在catch块中,如果异常存在,则返回exception_ptr对象,不存在则返回空指针
(2). void rethrow_exception(exception_ptr p)
重新抛出exception_ptr的异常
(3) template
这个是创建一个exception的对象,在需要的时候被自动调用,一般不会显式的被使用。
原子类型的数据不需要关注同步机制。
原子类型定义在
原子模板可以创建原子类型的变量,但是如果硬件不支持,使用原子模板则会出问题,有些硬件使用锁机制来实现原子,使用is_lock_free()检查是否支持无锁操作。关于原子操作可以去看专门的文章。原子操作的一个好处是不用费劲心思弄同步。
创建一个程序,将a每次加100,这个程序执行时间有点长,所以必然会发生线程竞争
比如下面这种情况,会导致输出不一定为1000
#include
#include
#include
#include
#include
using namespace std;
//对a加100
void add(int& a)
{
for(int i{0}; i < 100; ++i)
++a;
}
//创建十个线程一起加
int main(){
int a = 0;
vector t;
for(int i{0}; i < 10; i++)
t.push_back(thread{add, std::ref(a)});
for(int i{0}; i < 10; i++)
t[i].join();
cout << a << endl;//本来应该输出1000
return 0;
}
引入原子量之后,就可以得到预期结果
#include
#include
#include
#include
#include
using namespace std;
//对a加100
void add(atomic_int& a)
{
for(int i{0}; i < 100; ++i)
++a;
}
//创建十个线程一起加
int main(){
atomic_int a{0};
vector t;
for(int i{0}; i < 10; i++)
t.push_back(thread{add, std::ref(a)});
for(int i{0}; i < 10; i++)
t[i].join();
cout << a << endl;//本来应该输出1000
cout << t.size() << endl;
return 0;
}
线程等待原子变量(c++20)
有时候需要线程阻塞在某个地方,然后再某个条件发生之后重新苏醒,这个时候就可以使用下面的特性:
wait(oldValue) 阻塞线程,知道其他线程调用notify_one或者notify_all,告知原子变量已经被改变了
notify_one,通知一个线程变量已经改变
notify_all,通知所有线程变量改变
1.自旋锁
#include
#include
#include
#include
#include
using namespace std;
atomic_flag spinlock = ATOMIC_FLAG_INIT;
static const int n{50};
static const int loop{100};
void dowork(int threadNumber, vector& data){
for(int i{0}; i < loop; ++i){
while(spinlock.test_and_set());//线程获取锁,在这里忙等待
data.push_dack(threadNumber);//记录线程编号
spinlock.clear();//释放锁
}
}
int main(){
vector threads;
vector data;
for(int i{0}; i < n; ++i){
threads.push_back(thread{dowork, i ,ref(data)});
}
for(auto& t : threads){
t.join();
}
cout<< data.size() << " " << n * loop<
这里的自旋锁是使用atomic_flag实现,所有的线程在test_and_set试图获取锁,clear会解开锁
2.非定时互斥体
这种锁有三个,分别是mutex,recursive_mutex,shared_mutex。前两个类在
lock():调用线程将尝试获取锁
try_lock():线程会尝试调用锁,如果获取成功,则返回true,否则返回false
unlock();释放获取的锁
mutex是独占锁,最能锁一次,如果锁两次,就有点麻烦了。
recursive_metux可以锁多次,解开锁的时候,要使用同样多次数的unlock()
shared_mutex是一种读写锁,独占所有权是写锁,共享所有权是读锁,它有类似的方法:
lock_shared(),try_lock_shared(),unlock_shared()
4.定时的互斥体类
和上面提到的差不多,只是有时间限制,这里不多讲
锁也有不同的类型主要有以下三种:(1)lock_guard(),(2)unique_lock()(3)shared_lock()
他们都有多种构造函数,以互斥量和延迟时间为参数,获取互斥量的锁。
另外还有两种获取多个锁的对象,lock()和scoped_lock()
8.call_once和once_flag
结合call_once和once_flag保证函数只被调用一次,即便是不同的线程,也只会接续进行而已。这个其实是c++20的特性