C++ 并发编程

news2025/1/19 8:19:13

文章目录

    • 基本概念
    • 编程
    • 创建线程
    • 启动
  • 共享数据相关
    • 条件变量
    • 时间相关
    • future相关——等待一次性事件
    • 读写锁
  • 原子操作与缓存一致性关系
  • 线程管理
    • 启动线程
    • 从类的方法来创建线程
    • 传参
    • 标识线程
    • 常用API
    • 等待线程完成
    • 后台运行
    • 线程移动
  • 线程间共享数据
    • 互斥量(mutex)
  • unique_ptr
  • 线程池
  • 内存
    • 内存对齐
    • 类的内存
  • 内存管理
    • atomic
    • 内存顺序
  • 内存模型
  • 并发存在的问题

基本概念

线程与进程:

任务并行与数据并行:两种方式利用并发提高性能:第一,将一个单个任务分成几部分,且各自并行运行,从而降低总运行时间。这就是任务并行(task parallelism)。虽然这听起来很直观,但它是一个相当 复杂的过程,因为在各个部分之间可能存在着依赖。区别可能是在过程方面——一个线程执行算法的一部分,而另一个线程执行算法的另一个部分——或是在数据方面——每个线程在不同的数据部分上执行相同的操作(第二种方式)。后一种方法被称为数据并行(data parallelism

何时使用并发:除非潜在的性能增益足够大或关注点分离地足够清晰,能抵消所需的额外的开发时间以及与维护多线程代码相关的额外成本(代码正确的前提下);否则,别用并发。

编程

多线程编程实例:

  1. 添加头文件:#include <thread>
  2. 创建线程对象:std::thread t1(foo2);或者std::thread t1(foo2, params);其中第一个表示通过无参数的函数创建线程对象,另外一个表示通过函数和相对于的参数创建线程对象
  3. t1.join()等待线程结束

创建线程

  1. 从普通函数名创建

std::thread th(func, args)

  1. 从类的成员函数创建

类内部:非成员函数创建线程时,传入this指针,类外部则改为指向类实例对象的指针。

静态成员函数:

std::thread th(&Class::Func, args)

非静态成员函数:

std::thread th(&Class::Func,this,args)

  1. 使用std::bind函数

std::thread th(std::bind(&Class::Func,this, args))

  1. 使用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));

共享数据相关

常见问题:

  1. 条件竞争(race condition);当不变量遭到破坏时,才会产生条件竞争,对数据的条件竞争通常表示为“恶性”(problematic)条件竞争。
  2. 数据竞争(data race)一种特殊的条件竞争:并发的去修改一个独立对象(参见5.1.2节)。

解决办法:

  1. 互斥量、锁:mutex
  2. 事务:transacting

避免死锁:

  1. 避免嵌套锁:尽量保证每个线程只持有一个锁,锁上就不会产生死锁;如果持有多个锁,那么使用std::lock();
  2. 避免在持有锁时调用用户提供的代码
  3. 使用固定顺序获取锁

当其中一个成员函数返回的是保护数据的指针或引用时,会破坏对数据的保护。

互斥量保护的数据需要对接口的设计相当谨慎,要确保互斥量能锁住任何对保护数据的访问,并且不留后门。

已经对锁的粒度有所了解:锁的粒度是一个“摆手术语”(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_guard<std::mutex> guard(mtx);

std::unique_lock<std::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_lock<std::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::package<Data> promise;
std::future<Data> 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::future<Data> future = std::async(std::launch::async, func)

std::future<Data> future = std::async(std::launch::deffered, func)

  • Data data = future.get();
  • future.wait();

std::packaged_task<> :对一个函数或可调用对象,绑定一个期望;它就会调用相关函数或可调用对象,将期望状态置为就绪,返回值也会被存储为相关数据。

应用场景:比如在任务所在线程上运行任务,或将它们顺序的运行在一个特殊的后台线程上。当一个粒度较大的操作可以被分解为独立的子任务时,其中每个子任务就可以包含在一个 std::packaged_task<> 实例中,之后这个实例将传递到任务调度器或线程池中。

  • std::packaged_task<> 的模板参数是一个函数签名,如std::packaged_task<double(double)>

std::packaged_task<int(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::promise<Data> promise;

  • promise.set_value(Data);

  • std::future<Data> future = promise.getfuture();

std::advance()

共享期望: std::shared_future 的实例就能关联多个事件

std::shared_future<Data>

带返回值的后台任务:因为 std::thread 并不提供直接接收返回值的机制。这里就需要 std::async 函数模板(也是在头文件中声明的)了。

每个线程都通过自己拥有的 std::shared_future 对象获取结果,那么多个线程访问共享同步结果就是安全的

  • 使用std::async 启动一个异步任务
  • std::async 会返回一个 std::future 对象,这个对象持有最终计算出来的结果
  • 当你需要这个值时,你只需要调用这个对象的get()成员函数;并且直到“期望”状态为就绪的情况下,线程才会阻塞。

通过future来进行初始化:

std::promise<Data> promise;

std::shared_future<Data> sf(promise.get_future());

std::future<Data> f(p.get_future());

std::shared_future<Data> 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 就是这样一种类型(译者:C++11中的智能指针),这种类型 为动态分配的对象提供内存自动管理机制(译者:类似垃圾回收)。同一时间内,只允许一 个 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指令连续修改完成后,即使数据结构可以让其他并发线程访问,问题再次复现的几率也相当低。当系统负载增加时,随着执行数量的增加,执行序列的问题复现的概率也在增加,这样的问题只可能会出现在负载比较大的情况下。

条件竞争通常是时间敏感的,所以程序以调试模式运行时,它们常会完全消失,因为调试模式会影响程序的执行时间(即使影响不多)。

如何避免恶性条件竞争?

  1. 最简单的办法就是对数据结构采用某种保护机制,确保只有进行修改的线程才能看到不变量被破坏时的中间状态
  2. 另一个选择是对数据结构和不变量的设计进行修改,修改完的结构必须能完成一系列不可分割的变化,也就是保证每个不变量保持稳定的状态,这就是所谓的无锁编程(lock-free programming)
  3. 使用事务(transacting)的方式去处理数据结构的更新(这里的"处理"就如同对数据库进行更新一样)。所需的一些数据和读取都存储在事务日志中,然后将之前的操作合为一步,再进行提交:当数据结构被另一个线程修改后,或处理已经重启的情况下,提交就会无法进行,这称作为“软件事务内存”(software transactional memory (STM))

互斥量(mutex)

unique_ptr

线程池

在用户线程中调用AfxGetMainWnd()函数,获取的不是应用程序主框架类指针,而是线程的m_pMainWnd。

https://blog.csdn.net/lidandan2016/article/details/72154490

内存

内存对齐

对于32位系统,其内存对齐至少为一个地址长度,也即 2 32 2^{32} 232即4个字节长度,可以使用alignas(4的倍数)来表示想要对齐的内存大小,指针类型的大小为4个字节。

对于64位系统,其内存对齐至少为一个地址长度,也即 2 64 2^{64} 264即8个字节长度,可以使用alignas(8的倍数)来表示想要对齐的内存大小,指针类型的大小为8个字节。

类的内存

空类的内存为1个字节,为了保证类的唯一实例化,以64位系统为例即指针大小为8个字节说明内存的详细情况:

内存管理

原子操作:std::atomic:要么完成,要么没完成,不存在中间状态

互斥量与原子操作不同点:

  • 互斥量的加锁一般是针对一个代码段(几行代码);原子操作针对的都是一个变量,而不是针对一个代码段;

  • 原子操作比互斥量在效率上更高

步骤:最常见的操作是原子读改写,简称RMW

  1. Read
  2. Modified
  3. Write

如何保证读和写保持顺序一致?下面这种是没有用锁和原子操作等机制,会发现两个线程一直保持独立!!!

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_lock<mutex> 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_flag==false,返回false,并设置atomic_flag=true。

    指定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::atomic<T>:不允许拷贝构造和拷贝赋值

std::atomic<bool> ato1 = false;
std::atomic<bool> ato3(false); 
// std::atomic<bool> ato2 = ato1; // 错误,不能拷贝赋值
// std::atomic<bool> 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::atomic<T>:基本操作有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 操作)

  1. 写入施加 release 语义(store),在代码中这条语句前面的所有读写操作都无法被重排到这个操作之后,即 store-store 不能重排为 store-store, load-store 也无法重排为 store-load
  2. 当前线程内的所有写操作,对于其他对这个原子变量进行 acquire 的线程可见
  3. 当前线程内的与这块内存有关所有写操作,对于其他对这个原子变量进行 consume 的线程可见

memory_order_acquire: (可以理解为 mutex 的 lock 操作)

  1. 读取施加 acquire 语义(load),在代码中这条语句后面所有读写操作都无法重排到这个操作之前,即 load-store 不能重排为 store-load, load-load 也无法重排为 load-load
  2. 在这个原子变量上施加 release 语义的操作发生之后,acquire 可以保证读到所有在 release 前发生的写入。

memory_order_consume:

  1. 对当前要读取的内存施加 release 语义(store),在代码中这条语句后面所有与这块内存有关的读写操作都无法被重排到这个操作之前
  2. 在这个原子变量上施加 release 语义的操作发生之后,consume 可以保证读到所有在 release 前发生的并且与这块内存有关的写入,举个例子:

memory_order_acq_rel:

  1. 对读取和写入施加 acquire-release 语义,无法被重排
  2. 可以看见其他线程施加 release 语义的所有写入,同时自己的 release 结束后所有写入对其他施加 acquire 语义的线程可见

memory_order_seq_cst:(顺序一致性)

  1. 如果是读取就是 acquire 语义,如果是写入就是 release 语义,如果是读取+写入就是 acquire-release 语义
  2. 同时会对所有使用此 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\RMW
consume不允许load操作后的依赖于当前原子变量的变量的操作被重排到load之前,比如线程2中z_x的操作B1不可以被重排到B3之后,但是不依赖于x的y变量的操作B4就可以被移动到x的B3操作之前load\RMW
release不允许store操作前所有变量的操作被重排到store之后,比如不允许将线程1的A2、A1操作重排到A3操作之后,但是可以在A3之前store\RMW
acq_rel不允许跨越RMW操作重排,即只能在其左右两端进行重排RMW
seq_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有两点要求

  1. 在每个处理器内,维护每个处理器的程序次序;在所有处理器间,维护单一的表征所有操作的次序。
  2. 对于写操作W1, W2, 不能出现从处理器 P1 看来,执行次序为 W1->W2; 从处理器 P2 看来,执行次序却为 W2->W1 这种情况

缓存一致性协议(cache coherence protocols)

维护写原子性(maintaining write atomicity):

  1. 要求1:针对同一地址的写操作被串行化(serialized). 图4阐述了对这个条件的需求:如果对 A 的写操作不是序列化的,那么 P3 和 P4 输出(寄存器 1,2)的结果将会不同,这违反了次序一致性
  2. 对一个新写的值的读操作,必须要等待所有(别的)缓存对该写操作都返回确认通知后才进行

为获得好的性能,我们可以引入放松内存一致性模型(relaxed memory consistency models),这些模型主要通过两种方式优化程序(读写):

放松对程序次序的要求:这种放松与此前所述的“在无缓存的体系结构中采用的优化”类似,仅适用于对不同地址的操作对(opeartion pairs)间使用。

放松对写原子性的要求:一些模型允许读操作在“一个写操作未对所有处理器可见”前返回值。这种放松仅适用于基于缓存的体系结构。

并发存在的问题

数据争用、兵乓缓存、伪共享

超额认购(oversubscription):如果有很多额外线程,就会有很多线程准备执行,而且数量远远大于可 用处理器的数量,不过操作系统就会忙于在任务间切换,以确保每个任务都有时间运行

Amdahl定律:当程序“串行”部分的时间用fs来表示,那么性能增益§就可以通过处理器数量(N)进行估计:

并发分离关注,可以将一个很长的任务交给一个全新的线程,并且留下一个专用的 GUI线程来处理这些事件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/422658.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

公网WebSocket Client远程连接本地WebSocket Server【内网穿透】

目录 1. Java 服务端demo环境 2. 在pom文件引入第三包封装的netty框架maven坐标 3. 创建服务端,以接口模式调用,方便外部调用 4. 启动服务,出现以下信息表示启动成功,暴露端口默认9999 5. 创建隧道映射内网端口 6. 创建隧道映射本地端口 7. 测试公网远程连接 1. Java 服…

一文轻松教会你基于Excel+关键字驱动的自动化测试框架封装

目录 一、概述 二、框架设计 测试用例管理 关键字库封装 数据驱动设计 日志记录和报告生成 三、框架实现 测试用例管理 关键字库封装 数据驱动设计 日志记录和报告生成 四、框架使用 编写测试用例 编写关键字库 执行测试 五、总结 一、概述 在软件开发过程中&a…

【Spring Security】| 从0到1编写一个权限认证 | 学会了吗?

目录一. &#x1f981; 认证前的工作1. 添加依赖2. 创建数据库表&#xff08;数据自行添加&#xff09;3. 编写用户实体类4. 编写Dao接口5. 在启动类中添加 MapperScan 注解6. 继续添加各种包二. &#x1f981; 自定义逻辑认证原理—UserDetailsService三. &#x1f981; 数据库…

Android Studio Flamingo | 2022.2.1 发布,快来看看有什么更新吧

原文链接 https://developer.android.com/studio/releases 新的 Android Studio 版本 Flamingo (火烈鸟) 已经发布&#xff0c;本次更改最有意思的点在于&#xff1a; Flamingo 自带的 JDK 是 JDK 17 而不再是 JDK 11&#xff0c;另外还有如 IDE 支持应用主题图标和动态颜色&am…

TikTok和国产抖音的发展路径和趋势

鑫优尚电子商务&#xff1a;以历史为镜子&#xff0c;我们可以知道变化。 纵观TikTok和国产抖音&#xff0c;我们会发现TikTok目前的发展路径和趋势与国产抖音的主线十分相似&#xff0c;直播也是如此。鑫优尚电子商务 国内抖音直播一般经历过四个时代&#xff1a;直播1.0时代…

AIGC周报|清华、北邮新研究:让文生图AI更懂你

AIGC通过借鉴现有的、人类创造的内容来快速完成内容创作。ChatGPT、Bard等AI聊天机器人以及DallE 2、Stable Diffusion等文生图模型都属于AIGC的典型案例。「AIGC技术周报」将为你带来最新的paper、博客等前瞻性研究。 OpenAGI&#xff1a;当大模型遇见领域专家 “愿原力与大型…

分子生物学 第五章 DNA损伤修复和突变

文章目录第五章 DNA损伤修复和突变第一节第二节 DNA损伤的类型1 造成DNA损伤的因素2 DNA损伤的类型3 DNA损伤修复机制3.1 直接修复3.2 切除修复3.3 双链断裂修复3.4 重组修复3.5 跨越合成第五章 DNA损伤修复和突变 第一节 损伤&#xff1a;比如碱基&#xff0c;甲基化 突变&…

JavaSE学习进阶day04_04 正则表达式和Lambda表达式

第六章 正则表达式&#xff08;超级重要&#xff09; 开发心得&#xff1a;看着正确数据&#xff0c;从左到右书写正则表达式 6.1 正则表达式的概念及演示 在Java中&#xff0c;我们经常需要验证一些字符串&#xff0c;例如&#xff1a;年龄必须是2位的数字、用户名必须是8位…

爬虫日常练习-协程方式爬取图片

文章目录前言代码设计前言 hello朋友们&#xff0c;欢迎回来。这里是无聊的网友。今天给大家分享另一种处理多任务的方法–协程 那么在开始之前我们首先要了解什么是协程。协程是在一个线程内&#xff1a;多个任务出现阻塞时&#xff0c;由envet_loop轮转查看阻塞状态&#…

Zeppelin0.9.0 连接 Hive 3.1.2(踩坑,亲测有效)

一、前提 已经安装好Hadoop、Hive&#xff08;可以启动hiveserver2&#xff09;、Zeppelin 1.启动Hadoop [roothurys24 ~]# start-all.sh 2.启动hiveserver2 [roothurys24 ~]# cd /opt/soft/hive312/conf/ [roothurys24 conf]# nohup ../bin/hive --service hiveserver2 &a…

kubernetes之Ingress介绍

Ingress 组成 ingress controller将新加入的Ingress转化成Nginx的配置文件并使之生效 ingress服务将Nginx的配置抽象成一个Ingress对象&#xff0c;每添加一个新的服务只需写一个新的Ingress的yaml文件即可工作原理 1.ingress controller通过和kubernetes api交互&#xff0…

【机器学习(五)】基于KNN模型对高炉发电量进行回归预测分析

文章目录专栏导读1、KNN简介2、KNN回归模型介绍3、KNN模型应用-高炉发电量预测3.1数据集信息&#xff1a;3.2属性信息3.3数据准备3.4数据标准化和划分数据集3.5寻找最佳K值3.6建立KNN模型预测4、完整代码专栏导读 ✍ 作者简介&#xff1a;i阿极&#xff0c;CSDN Python领域新星…

利用74373芯片进行单片机IO口扩展的方法介绍-成都控制设备订做

本文介绍用74373芯片进行微处理器IO口扩展的方法。 1.为什么要进行IO口扩展&#xff1f; 在电路设计的某些时候&#xff0c;微处理器&#xff08;如单片机&#xff09;IO口不够用了&#xff0c;此时该怎么办呢&#xff1f;利用辅助芯片进行IO口扩展是个简单直接的方法&#xff…

开源社与 Dev.Together 2022

思否与开源社携手11>2在 2023 年的春天&#xff0c;开源社走进了 Dev.Together 2022 的会场&#xff0c;一时间有种时空错觉。2022 年本该举办的开源聚会因为不可抗力的因素被延期&#xff0c;感谢思否一直坚持的理念&#xff1a;Dev.Together Summit 只做线下&#xff0c;将…

debian 10 安装神州通用数据库 V7.0

debian 10 安装神州通用数据库 V7.01、官方下载链接2、windows客户端下载链接3、官方安装手册4、安装前准备3.1、创建安装用户3.2、以root 用户修改神通数据库安装包的所有者为shentong 用户3.3、以root 用户创建神通数据库主目录并修改所有者为shentong 用户3.4、以root 用户临…

c/c++:顺序结构,if else分支语句,do while循环语句,switch case break语句

c/c:顺序结构&#xff0c;if else分支语句&#xff0c;do while循环语句&#xff0c;switch case break语句 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;此时学会c的话&#xff0c; 我所知道的周边的会c的同学&#xff…

一文读懂域名注册

本文深入浅出讲解域名的注册、建站和管理&#xff0c;通过文章可以了解以下问题&#xff1a; 域名注册及建站流程&#xff1b;域名注册的技术原理&#xff1b;域名管理&#xff08;修改 DNS 服务器、转入转出、自定义 DNSHost、whois 信息&#xff09;。 众所周知&#xff0c;…

【MYSQL】表的增删改查(基础)

文章目录&#x1f337; 1. 新增&#xff08;Create&#xff09;⭐️ 1.1 单行行数据 指定列插入⭐️ 1.2 多行数据 指定列插入&#x1f337; 2. 查询&#xff08;Retrieve&#xff09;⭐️ 2.1 全列查询⭐️ 2.2 指定列查询⭐️ 2.3 查询字段为表达式⭐️ 2.4 别名⭐️ 2.5 去…

十五、市场活动:excel导入

功能需求 ①用户在市场活动主页面,点击"导入"按钮,弹出导入市场活动的模态窗口; ②用户在导入市场活动的模态窗口选择要上传的文件,点击"导入"按钮,完成导入市场活动的功能. *只支持.xls *文件大小不超过5MB ③导入成功之后,提示成功导入记录条数,关闭…

(只需三步)如何用chatgpt自动生成思维导图

目录 chatgpt是可以生成思维导图的&#xff01;只需三步&#xff0c;非常简单&#xff01; 第一步&#xff1a;打开chatgpt&#xff0c;告诉它主题 第二步&#xff0c;完善思维导图 第三步&#xff1a;查看思维导图的效果 chatgpt是可以生成思维导图的&#xff01;只需三步&am…