做网站电话销售,手机网站开发一个多少钱,dedecms确定网站风格,信息流广告投放目录
一、C中的线程使用
二、C的线程安全问题
1. 加锁
2. 变为原子操作
3. 递归里面的锁
4. 定时锁
5. RAII的锁
三、条件变量
1. 为什么需要条件变量
2. 条件变量的使用
2.1 条件变量的相关函数
2.2 wait函数 一、C中的线程使用
线程的概念在linux中的线程栏已经…目录
一、C中的线程使用
二、C的线程安全问题
1. 加锁
2. 变为原子操作
3. 递归里面的锁
4. 定时锁
5. RAII的锁
三、条件变量
1. 为什么需要条件变量
2. 条件变量的使用
2.1 条件变量的相关函数
2.2 wait函数 一、C中的线程使用
线程的概念在linux中的线程栏已经详细介绍过了所以这里就不再赘述以下的内容大部分都默认读者对线程概念、线程控制和线程的同步与互斥有一定了解。在这里主要了解一下C中的线程。 C中的线程其实就是对各个平台的线程进行的一层封装以便于多平台通用。上图就是在C中的线程可能会使用到的接口。
首先来了解一下C中的线程创建 从上图可以看到C中的线程是禁止拷贝构造的但是支持移动拷贝。原因很简单在OS中的每个线程都有自己唯一的id如果用拷贝构造就可能出现两个具有相同id的线程。而移动拷贝是交换数据就没有这个问题。
同时C中的线程也是用类来封装的。与linux中的线程创建出来后就必须执行任务不同C中可以创建空线程即一个什么都不做的线程。 利用这个特性就可以结合C中的一些容器实现创建多个线程 这里就是使用一个vector一次性创建了3个线程。要让这些线程运行起来也比较简单向线程传一个可调用对象即可。这个可调用对象也可以是一个lambda表达式。为了方便看到这些不同的线程打印这些不同的线程的id。可以使用get_id来获取线程id 写出如下程序 在这个程序里面的“this_thread::”是一个命名空间它里面保存了线程相关的接口 注意多线程中一定要写join来等待回收线程资源。否则主线程可能会直接运行结束导致整个程序结束。使用join后主线程就会阻塞式等待从线程结束。运行程序 通过线程id我们确实可以知道这里有3个不同的线程在并发运行。但是为什么这里的打印结果是乱的呢原因很简单在这里是在向屏幕输出数据在没有使用锁的情况下会有线程安全问题。这里就是一个线程的打印还未结束调度器就将另一个线程切进来继续打印造成了打印不完整的问题。
如果想让一个线程被创建出来后就立即运行就可以使用带参的构造函数 在这里面第一个fn是一个可调用对象。args则是一个可变参数包是传给fn这个可调用对象的参数的 二、C的线程安全问题
1. 加锁
先来看以下一个程序 在这个程序里面有两个线程分别执行不同的任务。但是它们的执行函数中都对全局变量val进行了。运行该程序 可以看到运行结果并不是正确的。 因为val这个动作并不是原子的。我们知道要一个变量至少要经过“从内存读取数据到寄存器”“用计算器进行计算”和“将数据写回内存”三个步骤。此时这个变量被两个不同的线程修改此时就可能出现线程安全问题。例如当val 10时线程A刚val还没有将11写回到内存里调度器就将线程A切换为线程B由于val未被修改所以线程B也拿到val 10并进行然后将数据写回到内存里。此时线程A被切回来也将11写回内存。此时就导致这两个线程将相同的值写回内存。但是线程A和线程B的循环次数已经增加了也就导致少了一次。多次的少就会导致上面的结果。
要解决这个问题就可以进行“加锁” 在这里面lock是上锁unlock是解锁。try_lock也是用于上锁的但是和lock让线程阻塞式等待不同try_lock在线程获取锁成功后会正常执行。但如果获取失败该线程就不再是在此阻塞式等待而是可以去执行其他操作。当线程获取锁成功后执行锁内的代码如果失败则返回false。写出如下程序 运行该程序 可以看到如果是lock那么线程就会阻塞式等待不会进入else条件。但是这里是try_lock所以当线程获取锁失败后它并不是阻塞等待而是继续向下运行。
再来看lock。这里就是阻塞等待。修改程序 通过加锁的方式让线程在加锁的范围内串行运行。运行程序 此时程序的运行结果就是正常的。
上面是两个线程执行两个不同的函数的情况。如果将其改为执行同一个函数并运行 依然没有问题。那有人就奇怪了为什么val会有线程安全问题而这里的i却没有呢其实是因为每个线程都有自己的栈结构这也就导致每个线程都会有一份自己的函数体。因此虽然它们执行的是同一个函数但是它们的栈中都有一份单独的函数体里面的局部变量都是属于线程本身的。只有该进程中的共享资源例如全局变量才会被所有线程看到同一份。
2. 变为原子操作
要保护数据安全还有一种方法就是将数据的修改变为原子。要变为原子就需要使用系统提供的“CAS操作”。CAS操作用户是无法直接使用的只能使用系统提供的接口来实现。如果大家有兴趣可以去了解一下CAS的具体实现。这里只简单的介绍一下。
以val为例假设val为1在进行VAS操作时会将1写到两个寄存器里面假设这两个寄存器为eax分别存放在两个CPU中。当val需要时会将val的值写入到另外两个寄存器中进行运算并在寄存器中保存“预期原值”1。当需要将数据写回到内存中时会先拿预期原值与CPU中的val的值进行比对如果相等就写回如果不相等就继续计算并更新预期原值和需要写回的值直到可以写回。通过这种方式就实现了在同一时刻只能写回一个的结果。 如果想让我们自己的计算实现原子操作就可以使用“atomic” atomic是一个封装过的类支持以下的原子操作 它的底层就是使用了CAS来实现的原子操作。所以将代码修改如下并运行 可以看到在原本的情况中这里由于没有锁的保护且val是一个全局变量所以会出现线程安全问题val的值不正确。但使用了atomic后就可以得到正确的值了。当然因为CAS的局限性并不是所有场景都可以使用atomic。例如打印字符串。
当然在实际中是不推荐在多线程中使用全局变量的。那如果我们想让两个线程看到同一个变量又不使用全局变量该怎么做呢这里就可以使用lambda表达式 因为传给线程的是可调用对象所以lambda表达式也是可以传入的。在这里使用lambda表达式就可以让两个线程在main函数内就看见同一个变量。
3. 递归里面的锁
在递归中最好不要用锁。因为可能造成死锁的情况。例如如下程序 当一个线程进入该函数后会先申请锁然后去执行下面的代码。但是在锁保护的代码中刚好就要进行递归。调用函数此时线程就会进入下一层的递归调用中。于是线程又需要去申请锁。但是此时所已经被改线程拿走了于是该线程无法申请到锁在这里等待。 为了解决这种情况在C的线程库中就提供了一个“递归互斥锁” 它的接口和普通的锁是一样的 但是它就能解决上面的递归造成的死锁问题。解决方法很简单一个线程要申请锁那么就必定要进入到这个锁的函数内此时这个函数就可以获取到该线程的id。当锁已经被申请走情况下又有线程过来申请锁就先对比线程id看申请锁的线程id与它保存的线程id是否相等相等就直接进入不相等就阻塞等待。
4. 定时锁
一般来讲锁申请后都需要线程运行完后用unlock释放。但有时可能会有一种特殊需求那就是一个线程可以通过unlock解锁但是这个线程最多只能持有锁固定时间。一旦过了这个时间无论是否执行完都需要释放锁。此时就可以使用“timed_mutex” 这个类里面既提供了正常的锁也提供了通过时间控制的锁 至于如何具体使用这里就不再阐述了因为实际中的用处并不大。如果有兴趣可以对照文档使用。
5. RAII的锁
在实际中我们可能遇到锁保护的代码会抛异常的情况。一旦锁保护的代码中抛了异常就会让执行流跳转到外部的catch中进而导致线程未执行unlock形成死锁。
因此遇到锁保护的代码中可能抛异常的情况最好使用lock_guard 这个类是对锁的封装利用RAII的方法将锁的生命周期与作用域相绑定。一旦离开该对象的作用域这个对象就会调用析构函数自动释放锁。 在C中除了lock_guardunique_lock也可以做到同样的事。 虽然unique_lock的作用和lock_guard的作用是一样的都是在对象销毁时自动调用析构函数释放锁。但是unique与lock_guard相比起来提供了更多的成员函数以供操作 使用起来也比较灵活。
三、条件变量
1. 为什么需要条件变量
在了解条件变量前先来看这么一个题现在让你用两个线程交替打印奇数和偶数打印的结果要到100。要完成这个题也是比较简单的写出如下代码 运行该程序 结果没有问题。但我们再多运行几次 可以发现多运行几次后就会发现打印的结果里面有时有100有时又没有。这其实就是因为线程问题导致的。假设i 99当线程t1执行完后i 100。此时线程t2刚好停留在if判断的位置在它刚准备从内存获取i的值时线程t1将i修改为100于是线程t2获取的i 100。满足判断条件进入if内执行代码。执行完后ii 101不满足循环条件退出。但是线程t2的循环条件是i 100而100刚好因为t2就处于判断条件时被修改导致t2获取到错误的i并执行对应代码。
要解决这个问题当然可以将线程t2的循环条件改为i 100此时就必定会打印100。
但是这个程序依然存在一个问题那就是线程在反复的执行过程中如果条件不满足就会在循环内一直进行判断而调度器也会频繁的调度这个线程导致效率降低。这就好比你点了一份外卖你每隔几分钟就问一下商家有没有做好不仅让商家感到烦还会让商家因为要频繁接听你的电话和回答你的问题导致制作效率降低。
既然要让线程不频繁的判断而是进入阻塞等待有人就想出用锁的方式来完成 这种写法大家乍一看可能觉得就是线程t1先定义先运行所以线程t1会先获取锁然后执行代码并释放锁然后线程t2运行获取锁并执行代码释放锁通过这种方式形成交替打印。但是实际并不是这样的运行程序 在这个程序中绝大部分都是线程t1打印的。因为线程的互斥其实并不是遵循交替而是“竞争”。在线程互斥中每个线程都是竞争式的去争夺锁而不是遵循一定的次序。这就会导致竞争力高的线程可以频繁的获取锁而竞争力低的线程只能长期阻塞等待偶尔才能争夺到锁。
因此锁的方法是不可行的。面对这种情况最好就是使用条件变量。
2. 条件变量的使用
2.1 条件变量的相关函数 条件变量可以让线程按照有序的方式进行等待。每次需要唤醒线程时如果是唤醒单个线程就唤醒队列中的第一个线程。当然也可以一次性唤醒全部线程。 在这里面wait就是让线程有序进入阻塞等待的函数。而notify_one函数就是唤醒单个线程的函数notify_all就是唤醒全部线程的函数。而wait_for和wait_until就是在固定时间后唤醒线程的函数一般很少用。
2.2 wait函数 条件变量因为需要被不同线程看到同一份所以它本身也是共享资源需要锁的保护。所以在使用条件变量时都需要使用锁。
但是从上图可以看到wait函数的参数要求是unique_lock的引用即RAII的锁。原因很简单因为条件变量需要锁的保护所以锁是写在wait之前的。要让多个线程在wait这里等待就表示不能让这些线程在锁的地方等待。因此传入一个unique_lock这个锁被传入到wait后一旦wait函数执行完毕就会调用它的析构函数释放锁进而让其他线程可以获取到锁并进入wait函数等待。这里的unique_lock还有一个作用就是在持有锁的线程被切走时它会手动释放锁这样就可以在持有锁的线程被切走的时候让其他线程能够获取锁。
修改代码如下 该程序中就使用了条件变量。注意这里的条件变量的判断语句使用的是while。如果使用if就可能出现多个线程进入同一个if判断一旦唤醒多个线程时就可能会导致多个线程同时离开if去执行下面的代码进而出现错误。while循环就保证了一次最多只会有一个线程离开。
运行该程序 打印结果正确。C的条件变量其实还支持将判断条件放在wait函数中 这里的pred就是判断条件wait会根据传入的可调用对象的返回值来判断是否阻塞true就不阻塞false就阻塞。修改程序如下并运行 程序依然可以正常运行。