什么行业做网站多,宝应县天宇建设网站,wordpress 个人博客模板,wordpress速度优化简书文章目录基本概念编程创建线程启动共享数据相关条件变量时间相关future相关——等待一次性事件读写锁原子操作与缓存一致性关系线程管理启动线程从类的方法来创建线程传参标识线程常用API等待线程完成后台运行线程移动线程间共享数据互斥量#xff08;mutex#xff09;unique…
文章目录基本概念编程创建线程启动共享数据相关条件变量时间相关future相关——等待一次性事件读写锁原子操作与缓存一致性关系线程管理启动线程从类的方法来创建线程传参标识线程常用API等待线程完成后台运行线程移动线程间共享数据互斥量mutexunique_ptr线程池内存内存对齐类的内存内存管理atomic内存顺序内存模型并发存在的问题基本概念
线程与进程
任务并行与数据并行两种方式利用并发提高性能第一将一个单个任务分成几部分且各自并行运行从而降低总运行时间。这就是任务并行task parallelism。虽然这听起来很直观但它是一个相当 复杂的过程因为在各个部分之间可能存在着依赖。区别可能是在过程方面——一个线程执行算法的一部分而另一个线程执行算法的另一个部分——或是在数据方面——每个线程在不同的数据部分上执行相同的操作第二种方式。后一种方法被称为数据并行data parallelism
何时使用并发除非潜在的性能增益足够大或关注点分离地足够清晰能抵消所需的额外的开发时间以及与维护多线程代码相关的额外成本(代码正确的前提下)否则别用并发。
编程
多线程编程实例
添加头文件#include thread创建线程对象std::thread t1(foo2);或者std::thread t1(foo2, params);其中第一个表示通过无参数的函数创建线程对象另外一个表示通过函数和相对于的参数创建线程对象t1.join()等待线程结束
创建线程
从普通函数名创建
std::thread th(func, args)
从类的成员函数创建
类内部非成员函数创建线程时传入this指针类外部则改为指向类实例对象的指针。
静态成员函数
std::thread th(Class::Func, args)
非静态成员函数
std::thread th(Class::Func,this,args)
使用std::bind函数
std::thread th(std::bind(Class::Func,this, args))
使用lambda表达式创建
std::thread th([](){func();};)
启动
std::thread::hardware_concurrency()硬件线程数量
detach()
join()
joinable()返回是否可被join()
std::move(th);线程只能被移动不能被拷贝复制。
标识
std::thread::id
get_id()
暂停线程
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
量产
std::for_each(threads.begin(), threads.end(), std::mem_fn(std::thread::join));
共享数据相关
常见问题
条件竞争(race condition)当不变量遭到破坏时才会产生条件竞争对数据的条件竞争通常表示为“恶性”(problematic)条件竞争。数据竞争(data race)一种特殊的条件竞争并发的去修改一个独立对象(参见5.1.2节)。
解决办法
互斥量、锁mutex事务transacting
避免死锁
避免嵌套锁尽量保证每个线程只持有一个锁锁上就不会产生死锁如果持有多个锁那么使用std::lock();避免在持有锁时调用用户提供的代码使用固定顺序获取锁
当其中一个成员函数返回的是保护数据的指针或引用时会破坏对数据的保护。
互斥量保护的数据需要对接口的设计相当谨慎要确保互斥量能锁住任何对保护数据的访问并且不留后门。
已经对锁的粒度有所了解锁的粒度是一个“摆手术语”(hand-waving term)用来描述通过一个锁保护着的数据量大小。一个细粒度锁(a fine-grained lock)能够保护较小的数据量一个粗粒度锁(a coarse-grained lock)能够保护较多的数据量。
std::mutex mtx; lock();上锁 unlock();解锁 std::lock(mtx1, mtx2)一次性锁住多个互斥量并且没有副作用(死锁风险)
std::std::recursive_mutex嵌套锁一般用在可被多线程并发访问的类上所以其拥有一个互斥量保护其成员数据。每个公共成员函数都会对互斥量上锁然后完成对应的功能之后再解锁互斥量
std::lock_guardstd::mutex guard(mtx);
std::unique_lockstd::mutex ulck(mtx);
ulck(mtx, std::adopt_lock)等
条件变量
如果互斥量在线程休眠期间保持锁住状态准备数据的线程将无法锁住互斥量也无法添加数据到队列中同样的等待线程也永远不会知道条件何时满足。
std::condition_variable con_var;
wait(ulck)wait(ulck, bool)
wait()会去检查这些bool条件当条件满足返回(程序继续执行)。如果条件不满足wait()函数将解锁互斥量并且将这个线程置于阻塞或等待状态。
notify_one()
当准备数据的线程调用notify_one()通知条件变量时处理数据的线程从阻塞或者等待状态中苏醒重新获取互斥锁并且对条件再次检查——当条件不满足时线程将对互斥量解锁并且重新开始等待。在条件满足的情况下从wait()返回并继续持有锁。
应用场景当线程用来分解工作负载并且只有一个线程可以对通知做出反应 这就是为什么用 std::unique_lock 而不使用 std::lock_guard ——等待中的线程必须在等待期间解锁互斥量并在这这之后对互斥量再次上锁而 std::lock_guard 没有这么灵活。 notify_all()
应用场景很多线程等待同一事件对于通知他们都需要做出回应。这会发生在共享数据正在初始化的时候当处理线程可以使用同一数据时就要等待数据被初始化(有不错的机制可用来应对
时间相关
限定等待一个事件的时间wait_for()和wait_until()
std::cv_status::timeout
std::condition_variable cv;
std::mutex m;
auto now std::chrono::steady_clock::now();
std::unique_lockstd::mutex lk(m);
cv.wait_until(lk, now std::chrono::milliseconds(500));
cv.wait_for(lk, std::chrono::milliseconds(35));
if(cv.wait_until(lk, timeout)std::cv_status::timeout)break;
//
std::packageData promise;
std::futureData future promise.get_future();
future.wait_for(std::chrono::milliseconds(35));
//当线程因为指定时延而进入睡眠时可使用sleep_for()唤醒或因指定时间点睡眠的可使用sleep_until唤醒超时和延迟处理功能std::this_thread::sleep_for() 和 std::this_thread::sleep_until() future相关——等待一次性事件
应用场景当等待线程只等待一次当条件为true时它就不会再等待条件变量了所以一个条件变量可能并非同步机制的最好选择。尤其是条件在等待一组可用的数据块时。在这样的情况下期望(future)就是一个适合的选择。 “期望”对象本身并不提供同步访问。当多个线程需要访问一个独立“期望”对象时他们必须使用互斥量或类似同步机制对访问进行保护 唯一期望std::future 的实例只能与一个指定事件相关联
std::async()
std::futureData future std::async(std::launch::async, func)
std::futureData future std::async(std::launch::deffered, func)
Data data future.get();future.wait();
std::packaged_task 对一个函数或可调用对象绑定一个期望它就会调用相关函数或可调用对象将期望状态置为就绪返回值也会被存储为相关数据。
应用场景比如在任务所在线程上运行任务或将它们顺序的运行在一个特殊的后台线程上。当一个粒度较大的操作可以被分解为独立的子任务时其中每个子任务就可以包含在一个 std::packaged_task 实例中之后这个实例将传递到任务调度器或线程池中。
std::packaged_task 的模板参数是一个函数签名如std::packaged_taskdouble(double)
std::packaged_taskint(std::string) pk_task(func)func的函数签名必须是符合其模板参数。
调用pk_task(std::string s)是一个可调用对象。
pk_task.get_future()返回的future对象含有函数的调用结果。
std::promise/std::future
在future上可以阻塞等待线程同时提供数据的线程可以使用组合中的“promise”来对相关值进行设置以及将“future”的状态置为“ready”
”解决单线程多连接问题
std::promiseData promise; promise.set_value(Data); std::futureData future promise.getfuture();
std::advance()
共享期望 std::shared_future 的实例就能关联多个事件
std::shared_futureData
带返回值的后台任务因为 std::thread 并不提供直接接收返回值的机制。这里就需要 std::async 函数模板(也是在头文件中声明的)了。 每个线程都通过自己拥有的 std::shared_future 对象获取结果那么多个线程访问共享同步结果就是安全的 使用std::async 启动一个异步任务std::async 会返回一个 std::future 对象这个对象持有最终计算出来的结果当你需要这个值时你只需要调用这个对象的get()成员函数并且直到“期望”状态为就绪的情况下线程才会阻塞。
通过future来进行初始化
std::promiseData promise;
std::shared_futureData sf(promise.get_future());
std::futureData f(p.get_future());
std::shared_futureData sf(std::move(f))
auto sf f.share();
层次锁层级互斥量的实现。
hierarchical_mutex high_mtx(100);
函数化编程”(functional programming ( FP ))引用于一种编程方式这种方式中的函数结果只依赖于传入函数的参数并不依赖外部状态
替代同步的解决方案函数化模式编程FP完全独立执行的函数不会受到外部环境的影响消息传递模式FSP以消息子系统为中介向线程异步发送消息。
读写锁
原子操作与缓存一致性关系
当程序中的对同一内存地址中的数据访问存在竞争你可以使用原子操作来 避免未定义行为。当然这不会影响竞争的产生——原子操作并没有指定访问顺序——但原子操作把程序拉回了定义行为的区域内。 原子操作是一类不可分割的操作当这样操作在任意线程中进行一半的时候你是不能查看的它的状态要不就是完成要不就是未完成 std::atomic_flag()
解决缓存一致性问题
CPU是多核的写回策略会导致缓存一致性问题。 写传播——总线嗅探 事务的串行化——锁 优化尽量广播给持有相关数据的核心而不是所有核心
MSEI一致性协议来实现
M已修改某个数据块已修改到CPU cache但是没有同步到内存中
E独占某个数据块只在某个核心中此时缓存和内存中数据一致
S共享某个数据块在多个核心都有缓存和内存中数据一致。
I已失效某个数据块在核心中已失效不是最新的数据。
——多核下确保M、E这两个状态发生改变任何尝试改变这两种状态的操作进行阻塞实现原子操作。
线程管理
启动线程
void do_some_work();
std::thread my_thread(do_some_work);对于类可以通过重载()运算符来实现。
class background_task {
public: void operator()() const { do_something(); do_something_else(); }
};
background_task f;
std::thread my_thread(f);注意当把函数对象传入到线程构造函数中时需要避免“最令人头痛的语法解析”——如果传递一个临时变量而不是一个命名的变量C编译器会将其解析成函数声明而不是类型对象的定义。 std::thread my_thread(background_task()); 这里相当与声明了一个名为my_thread的函数这个函数带有一个参数(函数指针指向没有参数并返回background_task对象的函数)返回一个 std::thread 对象的函数而非启动了一个线程。
解决办法
std::thread my_thread((background_task())); // 1
std::thread my_thread{background_task()}; // 2必须在 std::thread 对象销毁之前做出决定——加入或分离线程之前。 如果线程就已经结束想再去分离它线程可能会在 std::thread 对象销毁之后继续运行下去 使用一个能访问局部变量的函数去创建线程是一个糟糕的主意(除非十分确定线程会在函数完成前结束) 从类的方法来创建线程
创建线程用的函数指针是类方法的指针foo::bar虽然这个方法没有声明参数但是熟悉C对象的朋友应该都知道类方法隐含了自身实例的self指针所以这里需要传给它的第一个参数就是指向实例foo_inst的指针。
class foo {void bar() {// ...}void create_thread() {this-my_thread new std::thread(foo::bar, this);}// ...private:std::thread* my_thread;};传参
传递引用std::ref(data)
标识线程
线程直接调用get_id函数来获取每个线程的id即std::thread::id 对象该对象可以自由的拷贝和对比,因为标识符就可以复用。如果两个对象的 std::thread::id 相等那它们就是同一个线程或者都“没有线程”。如果不等那么就代表了两个不同线程或者一个有线程另一没有。
说明std::thread::id 实例常用作检测线程是否需要进行一些操作例如
std::thread::id master_thread;
if(std::this_thread::get_id()master_thread)
{ do_master_thread_work();
}要保证ID比较结果相等的线程必须有相同的输出。 常用API
在前面加std::进行调用
API参数说明get_idvoid用于标识线程move
等待线程完成
就可以确保局部变量在线程完成后才被销毁
my_thread.join() 只能对一个线程使用一次join();一旦已经使用过join() std::thread 对象就不能再次加入了当对其使用**joinable()**时将返回否false 特殊情况下
需要对一个还未销毁的 std::thread 对象使用join()或detach()。如果想要分离一个线程可以在线程启动后直接使用**detach()进行分离。如果打算等待对应线程则需要细心挑选调用join()**的位置。 当倾向于在无异常的情况下使用join()时需要在异常处理过程中调用join()从而避免生命周期的问题 try { do_something_in_current_thread();
}
catch(...) { t.join(); // 1 throw;
}
t.join(); // 2后台运行
使用detach()会让线程在后台运行调用 std::thread 成员函数1detach()来分离一个线程。之后相应的 std::thread 对象就与实际执行的线程无关了并且这个线程也无法进行加入
std::thread t(do_background_work);
t.detach();
assert(!t.joinable());线程传递引用提供的参数可以移动(move)但不能拷贝(copy)。移动是指原始对象中的数 据转移给另一对象而转移的这些数据就不再在原始对象中保存了(译者比较像在文本编辑 时剪切操作)。 std::unique_ptr 就是这样一种类型(译者C11中的智能指针)这种类型 为动态分配的对象提供内存自动管理机制(译者类似垃圾回收)。同一时间内只允许一 个 std::unique_ptr 实现指向一个给定对象并且当这个实现销毁时指向的对象也将被删除 。 C标准库中有很多资源占有(resource-owning)类型比如 std::ifstream , std::unique_ptr 还有 std::thread 都是可移动(movable)但不可拷贝 (cpoyable) 线程移动
std::thread 支持移动就意味着线程的所有权可以在函数外进行转移。
std::thread f() { void some_function(); return std::thread(some_function);
}
std::thread g() { void some_other_function(int); std::thread t(some_other_function,42); return t;
}线程间共享数据
问题无论结果如何都是并行代码常见错误条件竞争(race condition)
数据竞争数据竞争(data race)这个术语一种特殊的条件竞争并发的去修改一个独立对象(参见5.1.2节)
恶性条件竞争通常发生于完成对多于一个的数据块的修改时例如对两个连接指针的修改 (如图3.1)。因为操作要访问两个独立的数据块独立的指令将会对数据块将进行修改并且其中一个线程可能正在进行时另一个线程就对数据块进行了访问。因为出现的概率太低条件竞争很难查找也很难复现。如CPU指令连续修改完成后即使数据结构可以让其他并发线程访问问题再次复现的几率也相当低。当系统负载增加时随着执行数量的增加执行序列的问题复现的概率也在增加这样的问题只可能会出现在负载比较大的情况下。 条件竞争通常是时间敏感的所以程序以调试模式运行时它们常会完全消失因为调试模式会影响程序的执行时间(即使影响不多)。 如何避免恶性条件竞争
最简单的办法就是对数据结构采用某种保护机制确保只有进行修改的线程才能看到不变量被破坏时的中间状态另一个选择是对数据结构和不变量的设计进行修改修改完的结构必须能完成一系列不可分割的变化也就是保证每个不变量保持稳定的状态这就是所谓的无锁编程(lock-free programming)使用事务(transacting)的方式去处理数据结构的更新(这里的处理就如同对数据库进行更新一样)。所需的一些数据和读取都存储在事务日志中然后将之前的操作合为一步再进行提交当数据结构被另一个线程修改后或处理已经重启的情况下提交就会无法进行这称作为“软件事务内存”(software transactional memory (STM))
互斥量mutex
unique_ptr
线程池
在用户线程中调用AfxGetMainWnd()函数获取的不是应用程序主框架类指针而是线程的m_pMainWnd。
https://blog.csdn.net/lidandan2016/article/details/72154490
内存
内存对齐
对于32位系统其内存对齐至少为一个地址长度也即2322^{32}232即4个字节长度可以使用alignas(4的倍数)来表示想要对齐的内存大小指针类型的大小为4个字节。
对于64位系统其内存对齐至少为一个地址长度也即2642^{64}264即8个字节长度可以使用alignas(8的倍数)来表示想要对齐的内存大小指针类型的大小为8个字节。
类的内存
空类的内存为1个字节为了保证类的唯一实例化以64位系统为例即指针大小为8个字节说明内存的详细情况
内存管理
原子操作std::atomic要么完成要么没完成不存在中间状态
互斥量与原子操作不同点 互斥量的加锁一般是针对一个代码段几行代码)原子操作针对的都是一个变量而不是针对一个代码段 原子操作比互斥量在效率上更高
步骤最常见的操作是原子读改写简称RMW
ReadModifiedWrite
如何保证读和写保持顺序一致下面这种是没有用锁和原子操作等机制会发现两个线程一直保持独立
int g_value;
void read() {while (true){cout g_value endl;}
}void write() {while (true){std::this_thread::sleep_for(std::chrono::seconds(1));g_value;}
}一般地使用条件变量锁asdsds
condition_variable con_var;
mutex g_mtx;
int g_value;
void read() {while (true){unique_lockmutex ul(g_mtx);con_var.wait(ul);cout g_value endl;}
}
void write() {while (true){std::this_thread::sleep_for(std::chrono::milliseconds(100));g_value;con_var.notify_one();}
}atomic
std::atomic_flag ato; .test_and_set() 1.如果atomic_flag true返回true 2.如果atomic_flagfalse返回false并设置atomic_flagtrue。 指定memory_order顺序设置。 .clear()设置atomic_flag false
自旋锁与互斥锁
一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁这种锁叫做自旋锁它不用将线程阻塞起来(NON-BLOCKING)
std::atomic_flag lock ATOMIC_FLAG_INIT;
while (lock.test_and_set(std::memory_order_acquire)) // acquire lock; // spin
dosomthing();
lock.clear(std::memory_order_release); 还有一种处理方式就是把自己阻塞起来等待重新调度请求这种叫做互斥锁
原子类型对象的主要特点 从不同线程访问不会导致数据竞争(data race)。 因此从不同线程访问某个原子对象是良性 (well-defined) 行为 通常对于非原子类型而言并发访问某个对象如果不做任何同步操作会导致未定义 (undifined) 行为发生。
std::atomicT不允许拷贝构造和拷贝赋值
std::atomicbool ato1 false;
std::atomicbool ato3(false);
// std::atomicbool ato2 ato1; // 错误不能拷贝赋值
// std::atomicbool ato3(ato1); // 错误不能拷贝构造.is_lock_free()判断对象是否可lock-free即多个线程访问对象时会不会导致线程阻塞。 .store(T val, memory_order)修改被封装的值 .load(memory_order)读取被封装的值 T .exchange(T val, memory_order) 修改值为新值并返回之前的值 bool .compare_exchange_weak(T expected, T val, memory_order)比较并交换被封装的值(weak)与参数 expected 所指定的值是否相等整个操作是原子的在某个线程读取和修改该原子对象时另外的线程不能读取和修改该原子对象如果 相等则原子变量值val返回true。 不相等则expected 原子变量值返回false。 调用该函数之后如果被该原子对象封装的值与参数 expected 所指定的值不相等expected 中的内容就是原子对象的旧值。 bool .compare_exchange_strong(T expected, T val, memory_order)等
std::atomicT基本操作有fetch_add()和fetch_sub()提供属于RMW操作。
fetch_add(T* ptr, memory_order)在存储地址上做原子加法fetch_sub(T* ptr, memory_order)在存储地址上做原子减法
std::atomic自定义类 这个类型必须有拷贝赋值运算符。这就意味着这个类型不能有任何虚函数或虚基类以及必须使用编译器创建的拷贝赋值操作。自定义类型中所有的基类和非静态数据成员也都需要支持拷贝赋值操作。一个UDT类型对象可以使用memcpy()进行拷贝还要确定其对象可以使用memcmp()对位进行比较。
对于特化的版本
内存顺序Memory Order
所谓的memory order其实就是限制编译器以及CPU对单线程当中的指令执行顺序进行重排的程度此外还包括对cache的控制方法。这种限制决定了以atom操作为基准点边界对其之前的内存访问命令以及之后的内存访问命令能够在多大的范围内自由重排或者反过来需要施加多大的保序限制。从而形成了6种模式。它本身与多线程无关是限制的单一线程当中指令执行顺序。
其他参考
relaxed, acquire, release, consume, acq_rel, seq_cst它们表示的是三种内存模型: sequential consistent(memory_order_seq_cst), relaxed(memory_order_relaxed). acquire release(memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel),
memory_order_relaxed: 只保证当前操作的原子性不考虑线程间的同步其他线程可能读到新值也可能读到旧值只保证操作的原子性和内存一致性。 在单个线程内所有原子操作是顺序进行的。按照什么顺序基本上就是代码顺序sequenced-before。这就是唯一的限制了两个来自不同线程的原子操作是什么顺序两个字任意。 memory_order_release:可以理解为 mutex 的 unlock 操作
对写入施加 release 语义store在代码中这条语句前面的所有读写操作都无法被重排到这个操作之后即 store-store 不能重排为 store-store, load-store 也无法重排为 store-load当前线程内的所有写操作对于其他对这个原子变量进行 acquire 的线程可见当前线程内的与这块内存有关的所有写操作对于其他对这个原子变量进行 consume 的线程可见
memory_order_acquire: 可以理解为 mutex 的 lock 操作
对读取施加 acquire 语义load在代码中这条语句后面所有读写操作都无法重排到这个操作之前即 load-store 不能重排为 store-load, load-load 也无法重排为 load-load在这个原子变量上施加 release 语义的操作发生之后acquire 可以保证读到所有在 release 前发生的写入。
memory_order_consume:
对当前要读取的内存施加 release 语义store在代码中这条语句后面所有与这块内存有关的读写操作都无法被重排到这个操作之前在这个原子变量上施加 release 语义的操作发生之后consume 可以保证读到所有在 release 前发生的并且与这块内存有关的写入举个例子
memory_order_acq_rel:
对读取和写入施加 acquire-release 语义无法被重排可以看见其他线程施加 release 语义的所有写入同时自己的 release 结束后所有写入对其他施加 acquire 语义的线程可见
memory_order_seq_cst:顺序一致性
如果是读取就是 acquire 语义如果是写入就是 release 语义如果是读取写入就是 acquire-release 语义同时会对所有使用此 memory order 的原子操作进行同步所有线程看到的内存操作的顺序都是一样的就像单个线程在执行所有线程的指令一样
内存栅栏Memory Barrier无锁lock-free数据结构的设计中指令的乱序执行会造成无法预测的行为。所以我们通常引入内存栅栏Memory Barrier这一概念来解决可能存在的并发问题。
内存顺序
如下图所示对于原子变量x,y,z
顺序类型说明与举例可操作类型relaxed只保证单个线程如线程1中对A的操作如A0、A3、A5的顺序性或者线程2中对x的操作如B2、B3、B5操作的顺序性都可以acquire不允许load操作后所有变量的操作被重排到load之前比如不允许将线程2的B4的y操作重排到B3操作之前但是在B3之后可以load\RMWconsume不允许load操作后的依赖于当前原子变量的变量的操作被重排到load之前比如线程2中z_x的操作B1不可以被重排到B3之后但是不依赖于x的y变量的操作B4就可以被移动到x的B3操作之前load\RMWrelease不允许store操作前所有变量的操作被重排到store之后比如不允许将线程1的A2、A1操作重排到A3操作之后但是可以在A3之前store\RMWacq_rel不允许跨越RMW操作重排即只能在其左右两端进行重排RMWseq_cst所有操作的前后语句不能跨越该操作进行重排所有线程语句以全局内存修改顺序为参照都可以happens-before
happens-before关系表示的不同线程之间的操作先后顺序。如果A happens-before B则A的内存状态将在B操作执行之前就可见
synchronizes-with
synchronizes-with关系强调的是变量被修改之后的传播关系propagate 即如果一个线程修改某变量的之后的结果能被其它线程可见那么就是满足synchronizes-with关系的。synchronizes-with可以被认为是跨线程间的happends-before关系
Carries dependency
同一个线程内表达式A sequenced-before 表达式B并且表达式B的值是受表达式A的影响的一种关系 称之为Carries dependency
内存模型
顺序连贯(sequential consistency, SC)SC有两点要求
在每个处理器内维护每个处理器的程序次序在所有处理器间维护单一的表征所有操作的次序。对于写操作W1, W2, 不能出现从处理器 P1 看来执行次序为 W1-W2; 从处理器 P2 看来执行次序却为 W2-W1 这种情况
缓存一致性协议(cache coherence protocols)
维护写原子性(maintaining write atomicity):
要求1针对同一地址的写操作被串行化(serialized). 图4阐述了对这个条件的需求如果对 A 的写操作不是序列化的那么 P3 和 P4 输出寄存器 1,2的结果将会不同,这违反了次序一致性对一个新写的值的读操作必须要等待所有别的缓存对该写操作都返回确认通知后才进行
为获得好的性能我们可以引入放松内存一致性模型(relaxed memory consistency models)这些模型主要通过两种方式优化程序读写
放松对程序次序的要求这种放松与此前所述的“在无缓存的体系结构中采用的优化”类似仅适用于对不同地址的操作对(opeartion pairs)间使用。
放松对写原子性的要求一些模型允许读操作在“一个写操作未对所有处理器可见”前返回值。这种放松仅适用于基于缓存的体系结构。
并发存在的问题
数据争用、兵乓缓存、伪共享
超额认购(oversubscription)如果有很多额外线程就会有很多线程准备执行而且数量远远大于可 用处理器的数量不过操作系统就会忙于在任务间切换以确保每个任务都有时间运行
Amdahl定律当程序“串行”部分的时间用fs来表示那么性能增益§就可以通过处理器数量(N)进行估计 并发分离关注可以将一个很长的任务交给一个全新的线程并且留下一个专用的 GUI线程来处理这些事件。