目录
-
-
- 前言
- 最小化锁粒度
- 确保锁的正确性
- 线程启动时间
- 总结
-
前言
前面几部分都是讲的并发编程的一些理论知识,包括线程概念、互斥元的使用、异步线程的使用、原子类型等等。但是这都是纸面上的知识,还没有应用到实战上,而且关于并发还有一些设计技巧,比如
程序段1 // 无需独占
程序段2 // 访问内存空间
程序段3 // 打印到屏幕
有两种加锁方式
方式一: 方式二:
lock(); 程序段1
程序段1 lock();
程序段2 程序段2
程序段3 unlock();
程序段3
对于单线程程序来说这是无关紧要的,但是对于多线程来说则会差异很大。下面这个例子:
最小化锁粒度
mutex mu;
int sum = 0;
void fun_1() {
for (int i = 0; i < 100000; ++i) {
mu.lock();
sum ++;
mu.unlock();
}
}
void fun_2(){
mu.lock();
for (int i = 0; i < 100000; ++i) {
sum ++;
}
mu.unlock();
}
void test() {
auto t1=std::chrono::steady_clock::now();
//run code
thread th1(fun_1);
thread th2(fun_1);
thread th3(fun_1);
thread th4(fun_1);
th1.join();
th2.join();
th3.join();
th4.join();
auto t2=std::chrono::steady_clock::now();
double dr_ms=std::chrono::duration<double,std::milli>(t2-t1).count();
cout << "fun_1 执行时间:" << dr_ms << endl;
t1=std::chrono::steady_clock::now();
//run code
thread th5(fun_1);
thread th6(fun_1);
thread th7(fun_1);
thread th8(fun_1);
th6.join();
th7.join();
th8.join();
th5.join();
t2=std::chrono::steady_clock::now();
dr_ms=std::chrono::duration<double,std::milli>(t2-t1).count();
cout << "fun_2 执行时间:" << dr_ms;
}
执行结果
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLxEDOzQzN1kTM1AzNwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
执行结果大概相差一倍左右,而且随着线程和计算机线程数增加这个差值会更大。这是因为在
fun_2()
中循环除了
sum++
以外的部分应该不是独占的,线程在执行这一部分的时候可以把控制权交出去。但是
fun_2()
并没有这样做,导致大量的时间浪费。而且会空闲很多的线程资源。这就是多线程编程的第一个原则:确保锁的时间最小化。
确保锁的正确性
但是如果不加锁会出现什么问题呢? 来看第二个例子:
int sum = 0;
void fun() {
for (int i = 0; i < 100000; ++i) {
sum = sum + 1;
}
}
void test(){
thread t1(fun);
thread t2(fun);
thread t3(fun);
thread t4(fun);
t1.join();
t2.join();
t3.join();
t4.join();
cout << "sum = " << sum;
}
大家猜一下最后的结果是什么?
这是一个随机的值,因为:
同一块区域被多个线程访问,我以双线程为例:假设线程1拿到sum时值为100,在执行
sum++
过程中又被线程2访问,线程2获取的值也是100,此时线程1完成
sum++
,将执行后的结果放回原位置覆盖,此时sum变为了101,此时线程2也完成了操作,也将计算结果放回内存,覆盖之前的结果101。本来应该是两次
++
操作结果应该是102,结果由于多线程为加锁导致结果错误。这有引出了多线程的第二个原则:确保存取数据时要锁住正确的互斥量。
两条原则锁住正确的互斥量优先级更高,因为最小化互斥量只是会提高程序执行效率,而为正确锁住则会导致错误。
线程启动时间
在多线程中需要考虑线程启动的时间,下面先看例子:
mutex mut;
condition_variable cond;
void fun_1(){
while(true){
unique_lock<mutex> lk(mut);
cout << "做包子" << endl;
this_thread::sleep_for(chrono::seconds(1));
cout << "做好了" << endl;
lk.unlock();
cond.notify_one();
}
}
void fun_2(){
while (true){
unique_lock<mutex> lk(mut);
cout << "买包子" << endl;
cond.wait(lk);
cout << "买到包子" <<endl;
}
}
int main(){
thread t1(fun_1);
thread t2(fun_2);
t1.join();
t2.join();
}
这是一个典型的生产者-消费者模型,线程
fun_1
获得锁开始做包子,需要1秒时间,做好之后通知消费者前来购买;线程
fun_2
先到店里排队做好了就买到了。执行结果应该是一个做一个包子买一个包子,但是实际情况是这样吗?
会发现生产者一直在生产,但是没有消费者前来消费。这是因为生产做好之后会唤醒消费者,但是消费者在唤醒过程中,生产者进入了下一轮的生产中,继续占用的互斥量导致最终生产者-消费者模型的失败。
消费者线程从等待状态获得
notify()
转为锁池状态,获得互斥量,然后再进入执行状态;而生产者线程一直处于运行状态,会持续获得互斥量导致消费者线程无法从锁池状态转变为就绪状态,导致模型失效。
我们在每次生产者完成生产后等待1μs那么这个结果就不一样了:
这得到了我们所期望了生产者-消费者模型了,所以在并发编程中需要考虑线程唤醒的时间。
总结
其实书上关于这部分的内容并不是这样的,是关于容器的并发编程。这几个例子都是我和我师兄在学习过程中遇到的问题,然后分析讨论过后的结果。这一部分由于多线程会更多的接近底层,所以需要一定的操作系统的知识,至于带锁的方法,一个原则就是先正确再优化。希望能够帮助到大家!