从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(上):https://developer.aliyun.com/article/1522526
1.4 atomic+CAS
C++11提供了原子操作,我们知道,线程不安全的主要原因就是访问某些公共资源的时候,操作不是原子的,如果让这些操作变成原子的后,就不会存在线程安全问题了。
CAS原理:
原子操作的原理就是CAS(compare and swap)。
- CAS包含三个操作数:内存位置(V),预期原值(A)和新值(B)。
- 如果内存位置的值与预期原值相等,那么处理器就会自定将该位置的值更新为新值。
- 如果内存位置的值与预期原值不相等,那么处理器不会做任何操作。
val是临界资源,两个线程t1和t2同时对这个值进行加加操作,每个线程都是将该值先拿到寄存器eax中。
- 线程将val值拿到寄存器eax中时,同时将该值放入原值V中。
- 在修改val值之前,CPU会先判断eax中的值与原值V中的值是否相等,如果相等则修改并且更新值,如果不相等则不修改。
伪代码原理:
while(1) { eax = val; // 将val值取到寄存器eax中 if(eax == V) // 和原值相同可以修改 { eax++; V = eax; // 修改原值 val = eax; // 修改val值 break; // 访问结束,跳出循环 } }
- t1和t2虽然同时运行,但是时间粒度划分到极小的时候,CPU仍然是一个个在执行。
t1
线程将val
值拿到寄存器中,并且赋原值,经过判断发现和原值相同,所以修改val
值,并放回到va
l的地址中。
此时t2
线程被唤醒,它将val
值拿到寄存器中后与最开始的原值V相比,发现不相同了,所以就不进行修改,而且继续循环,知道寄存器中的值和原值相等才会改变。
- 原子操作虽然保证了线程安全,但是另一个无法写的的线程会不停的循环,而这也会占用一定的CPU资源。
CAS具体的原理有兴趣可以自行去了解,深入了解后写在简历是加分项。
atomic也是一个类,所以也有构造函数:
经常使用的是atomic(T val),在创建的时候传入我们想要进行原子操作的变量。
int a = atomic(1);
此时变量a的操作就都成了原子操作了,在多线程访问的时候可以保证线程安全。
成员函数:
该类重载了++,-- 等运算符,可以直接对变量进行操作。
看看没用atomic也没加锁的:
int main() { mutex mtx; int x = 0; int n = 100000; int m = 2; vector<thread> v(m); for (int i = 0; i < m; ++i) { // 移动赋值给vector中线程对象 v[i] = thread([&](){ for (int i = 0; i < n; ++i) { ++x; } }); } for (auto& t : v) { t.join(); } cout << x << endl; return 0; }
两个线程互相抢着加,就会出现有一个线程没加的情况,看看加锁的:
再看看用atomic的:
和加锁效果一样。
1.5 condition_variable
C++11中同样也有条件变量,用来实现线程的同步。
构造函数:
在创建条件变量的时候不用传入参数,同样是不允许被拷贝的。
其他成员函数:
放入等待队列:
wait(unique_lock& lock),该接口是将调用它的线程放入到条件变量的等待队列中。
wait(unique_lock& lck, Predicate pred),该接口和上面的作用一样,只是多了一个pred参数,当这个参数为true的话不放入等待队列,为false时放入等待队列。
这里传入的锁是unique_lock而不是lock_guard。这是因为,当一个线程申请到锁进入临界区,但是条件不满足被放入条件变量的等待队列中时,会将申请到的锁释放。
lock_guard只能在对象生命周期结束时自动释放锁。
unique_lock可以在任意位置释放锁。
如果使用了lock_guard的话就无法在进入等待队列的时候释放锁了。
wait_for和wait_until都是等待指定时间,一个是在等待队列中待指定时间,另一个是在等待队列中带到固定的时间点后自定唤醒。
notify_one唤醒等待队列中的一个线程,notify_all唤醒等待队列中的所有线程。
1.6 分别打印奇数和偶数
写一个程序:支持两个线程交替打印,一个打印奇数,一个打印偶数。
分析:
- 首先创建一个全局的变量
val
,让两个线程去访问该变量并且进行加一操作。 - 考虑到线程安全,所以需要给对应的临界区加互斥锁
mutex
- 又是交替打印,所以要使用条件变量
condition_variable
来控制顺序,为了方便管理,使用的锁是unique_lock
。
代码实现:
int main() { int val = 0; int n = 10; // 打印的范围 mutex mtx; // 创建互斥锁 condition_variable cond; // 创建条件变量 thread t1([&](){ while (val < n) { unique_lock<mutex> lock(mtx); // 加锁 while (val % 2 == 0)// 判断是否是偶数 { // 是偶数则放入等待队列中等待 cond.wait(lock); } // 是奇数时打印 cout << "thread1:" << this_thread::get_id() << "->" << val++ << endl; cond.notify_one(); // 唤醒等待队列中的一个线程去打印偶数 } }); this_thread::sleep_for(chrono::microseconds(100)); thread t2([&](){ while (val < n) { unique_lock<mutex> lock(mtx); while (val % 2 == 1) { cond.wait(lock); } cout << "thread2:" << this_thread::get_id() << "->" << val++ << endl; cond.notify_one();//唤醒等待队列中的一个线程去打印奇数 } }); t1.join(); t2.join(); return 0; }
上面代码两个线程执行的函数对象是lambda表达式,所以创建线程对象时,调用的是移动构造函数。
- wait()的第二个参数是false的时候,该线程被挂起到等待队列中,是true的时候不挂起,而且执行向下执行。
- 第二个参数的false和true可以是返回值,如代码就是使用的lambda表达式的返回值。
线程t1负责打印奇数,t2负责打印偶数,两个线程通过条件变量的控制交替打印。
还可以这么用:
int main() { int val = 0; int n = 10; // 打印值的范围 mutex mtx; condition_variable cond; bool ready = true; // t1线程打印奇数 thread t1([&](){ while (val < n) { { unique_lock<mutex> lock(mtx); cond.wait(lock, [&ready](){return !ready; }); cout << "thread1:" << this_thread::get_id() << "->" << val << endl; val += 1; ready = true; cond.notify_one(); } //this_thread::yield(); this_thread::sleep_for(chrono::microseconds(10)); } }); // t2线程打印偶数 thread t2([&]() { while (val < n) { unique_lock<mutex> lock(mtx); cond.wait(lock, [&ready](){return ready; }); cout << "thread2:" << this_thread::get_id() << "->" << val << endl; val += 1; ready = false; cond.notify_one(); } }); t1.join(); t2.join(); return 0; }
成功按照预期打印。
从C语言到C++_40(多线程相关)C++线程接口+线程安全问题加锁(shared_ptr+STL+单例)(下):https://developer.aliyun.com/article/1522544