皮皮网
皮皮网
dubbo源码扩展

【zabbix agent安装源码】【php 底层源码】【投稿php源码】sync源码

时间:2024-11-20 13:22:54 分类:热点 编辑:osmand 源码
1.golang的源码对象池sync.pool源码解读
2.PyTorch 源码解读之 BN & SyncBN:BN 与 多卡同步 BN 详解
3.从项目的一个 panic 说起:Go 中 Sync 包的分析应用
4.Rust并发:标准库sync::Once源码分析
5.Golang sync.Cond 条件变量源码分析
6.Go并发编程 — sync.Once

sync源码

golang的对象池sync.pool源码解读

       Go语言对象池sync.pool源码深度解析

       对象池在Go语言中被设计用于解决频繁创建和销毁对象导致的性能问题。sync.pool的源码核心理念是复用已创建对象,减轻垃圾收集(GC)压力。源码以下是源码关键点的理解和代码分析:

       对象池的动机

       新对象的创建会消耗内存,并可能对GC造成负担。源码sync.pool就是源码zabbix agent安装源码为了解决这个问题,通过预先创建和存储对象,源码减少创建成本,源码提高性能。源码

       池与缓存的源码相似性

       无论是连接池、线程池还是源码对象池,它们都体现了池化和缓存的源码思想:复用资源,减少临时创建,源码提升响应速度。源码池化和缓存都是源码为了减少资源消耗,提升服务效率。

       go1.原理与用法

       对象池使用简单,通过New函数创建,Get和Put操作实现对象的复用。go1.之前的版本可能频繁清空池,导致性能损失。1.改进了设计,引入了victim cache机制,php 底层源码通过双向链表优化获取和存储对象,减少锁竞争。

       源码解析

       从pool的结构体可以看到,victim和victimSize用于管理受害缓存,popTail函数通过无锁操作处理链表,保证了高性能。put操作时,根据对象状态决定放入private或shared区域。

       总结

       对象池通过复用对象、提前准备和性能优化的存储提高性能。理解对象池的关键在于:复用、存储策略和并发控制。在Go 1.中,通过victim cache和链表操作,进一步提升了性能和并发处理能力。

       深入理解

       理解对象池的细节包括如何禁用抢占P以防止GC影响,以及如何通过noCopy防止对象拷贝导致的潜在问题。同时,伪共享的处理也是优化对象池性能的关键点。

       持续学习和实践是技术成长的基石,让我们保持对技术的热情,不断探索和优化。投稿php源码

PyTorch 源码解读之 BN & SyncBN:BN 与 多卡同步 BN 详解

       BatchNorm原理

       BatchNorm最早在全连接网络中提出,旨在对每个神经元的输入进行归一化操作。在卷积神经网络(CNN)中,这一原理被扩展为对每个卷积核的输入进行归一化,即在channel维度之外的所有维度上进行归一化。BatchNorm带来的优势包括提高网络的收敛速度、稳定训练过程、减少过拟合现象等。

       BatchNorm的数学表达式为公式[1],引入缩放因子γ和移位因子β,作者在文章中解释了它们的作用。

       PyTorch中与BatchNorm相关的类主要位于torch.nn.modules.batchnorm模块中,包括如下的类:_NormBase、BatchNormNd。

       具体实现细节如下:

       _NormBase类定义了BN相关的一些属性。

       初始化过程。

       模拟BN的forward过程。

       running_mean、running_var的更新逻辑。

       γ、β参数的更新方式。

       BN在eval模式下的90坦克 源码行为。

       BatchNormNd类包括BatchNorm1d、BatchNorm2d、BatchNorm3d,它们的区别在于检查输入的合法性,BatchNorm1d接受2D或3D的输入,BatchNorm2d接受4D的输入,BatchNorm3d接受5D的输入。

       接着,介绍SyncBatchNorm的实现。

       BN性能与batch size密切相关。在batch size较小的场景中,如检测任务,内存占用较高,单张显卡难以处理较多,导致BN效果不佳。SyncBatchNorm提供了解决方案,其原理是所有计算设备共享同一组BN参数,从而获得全局统计量。

       SyncBatchNorm在torch/nn/modules/batchnorm.py和torch/nn/modules/_functions.py中实现,前者负责输入合法性检查以及参数设置,后者负责单卡统计量计算和进程间通信。

       SyncBatchNorm的识图的源码forward过程。

       复习方差计算方式。

       单卡计算均值、方差,进行归一化处理。

       同步所有卡的数据,得到全局均值mean_all和逆标准差invstd_all,计算全局统计量。

       接着,介绍SyncBatchNorm的backward过程。

       在backward过程中,需要在BN前后进行进程间通信。这在_functions.SyncBatchNorm中实现。

       计算weight、bias的梯度以及γ、β,进一步用于计算梯度。

从项目的一个 panic 说起:Go 中 Sync 包的分析应用

       在项目开发过程中,遇到一个常见的错误——"fatal error: concurrent map read and map write",这是由于Golang内建的map在并发环境下不安全导致的。解决这个问题的方法并不复杂,就是转向使用sync包提供的并发安全的map。

       sync包在Golang 1.9之后被官方支持,其中包含了丰富的同步原语,是并发编程的关键部分。在Golang 1.9之前,解决map并发问题通常会借助sync包中的sync.RWMutex或其他锁机制。Golang作为支持用户态进程的编程语言,对并发编程的处理自然离不开锁,这是一种确保多个Goroutine在同一片内存中协同工作的同步机制。

       sync包的源码目录结构清晰,包含Mutex、RWmutex、WaitGroup、Map、Once、Cond、Pool等组件。接下来,我们将逐个分析这些同步原语的用途和使用注意事项,重点讨论在项目中常见的sync.Map。

       sync.Map是sync包中的一种高效并发安全的map实现,与内建map相比,它提供了Load、Store、LoadOrStore、Delete和Range等方法,并且具有更高的并发性能。虽然sync.Map没有len方法,但其内部机制使得在并发环境中的操作更加稳健。

       通过结合实际项目案例和面试题中的陷阱,本文简要探讨了sync包中Mutex、RWMutex、WaitGroup、Once以及Map的使用技巧和注意事项。在实际编程中,正确使用这些同步原语对于避免并发问题至关重要。

Rust并发:标准库sync::Once源码分析

       一次初始化同步原语Once,其核心功能在于确保闭包仅被执行一次。常见应用包括FFI库初始化、静态变量延迟初始化等。

       标准库中的Once实现更为复杂,其关键在于如何高效地模拟Mutex阻塞与唤醒机制。这一机制依赖于线程暂停和唤醒原语thread::park/unpark,它们是实现多线程同步对象如Mutex、Condvar等的基础。

       具体实现中,Once维护四个内部状态,状态与等待队列头指针共同存储于AtomicUsize中,利用4字节对齐优化空间。

       构造Once实例时,初始化状态为Incomplete。调用Once::call_once或Once::call_once_force时,分别检查是否已完成初始化,未完成则执行闭包,闭包执行路径标记为冷路径以节省资源,同时避免泛型导致的代码膨胀。

       闭包执行逻辑由Once::call_inner负责,线程尝试获取执行权限,未能获取则进入等待状态,获取成功后执行闭包,结束后唤醒等待线程。

       等待队列通过无锁侵入式链表实现,节点在栈上分配,以优化内存使用。Once::wait函数实现等待线程逻辑,WaiterQueue的drop方法用于唤醒所有等待线程,需按特定顺序操作栈节点,以避免use after free等潜在问题。

       思考题:如何在实际项目中利用Once实现资源安全共享?如何评估Once与Mutex等同步原语在不同场景下的性能差异?

Golang sync.Cond 条件变量源码分析

       sync.Cond 是 Golang 标准库 sync 包中一个关键的条件变量类型,用于在多个goroutine间协调等待特定条件。它常用于生产者-消费者模型等场景,确保在某些条件满足后才能继续执行。本文基于 go-1. 源码,深入解析 sync.Cond 的核心机制与用法。

       sync.Cond 的基本用法包括创建条件变量、等待唤醒与发送信号。使用时,通常涉及到一个互斥锁(Locker)以确保并发安全性。首先,通过`sync.NewCond(l Locker)`创建条件变量。其次,`cond.Wait()`使当前执行的goroutine等待直到被唤醒,期间会释放锁并暂停执行。`cond.Signal()`和`Broadcast()`用于唤醒等待的goroutine,前者唤醒一个,后者唤醒所有。

       在底层实现中,sync.Cond 采用了一种称为 notifyList 的数据结构来管理等待和唤醒过程。notifyList 由一组元素构成,其中`wait`和`notify`表示当前最大ticket值和已唤醒的最大ticket值,而`head`和`tail`则分别代表等待的goroutine链表的头和尾。在`Wait`操作中,每次调用`runtime_notifyListAdd`生成唯一的ticket,并将当前goroutine添加到链表中。当调用`Signal`或`Broadcast`时,会查找并唤醒当前`notify`值对应的等待goroutine,并更新`notify`值。

       信号唤醒过程确保了FIFO的顺序,即最早等待的goroutine会首先被唤醒。这种机制有效地防止了并发操作下列表的乱序,确保了正确的唤醒顺序,尽管在实际执行中,遍历整个列表的过程在大多数情况下效率较高。

       在使用sync.Cond时,需注意避免潜在的死锁风险和错误的唤醒顺序。确保合理管理互斥锁的使用,以及在适当情况下使用`Signal`或`Broadcast`来唤醒等待的goroutine。正确理解和应用sync.Cond,能有效提升并发编程的效率与稳定性。

Go并发编程 — sync.Once

       ç®€ä»‹

       Once 可以用来执行某个函数,但是这个函数仅仅只会执行一次,常常用于单例对象的初始化场景。说到这,就不得不说一下单例模式了。

单例模式

       å•ä¾‹æ¨¡å¼æœ‰æ‡’汉式和饿汉式两种,上代码。

饿汉式

       é¥¿æ±‰å¼é¡¾åæ€ä¹‰å°±æ˜¯æ¯”较饥饿,所以就是上来就初始化。

var?instance?=?&Singleton{ }type?Singleton?struct?{ }func?GetInstance()?*Singleton?{ return?instance}懒汉式

       æ‡’汉式顾名思义就是偷懒,在获取实例的时候在进行初始化,但是懒汉式会有并发问题。并发问题主要发生在 instance == nil 这个判断条件上,有可能多个 goruntine 同时获取 instance 对象都是 nil ,然后都开始创建了 Singleton 实例,就不满足单例模式了。

var?instance?*Singletontype?Singleton?struct?{ }func?GetInstance()?*Singleton?{ if?instance?==?nil?{ ?instance?=?&Singleton{ }}return?instance}加锁

       æˆ‘们都知道并发问题出现后,可以通过加锁来进行解决,可以使用 sync.Metux 来对整个方法进行加锁,就例如下面这样。这种方式是解决了并发的问题,但是锁的粒度比较高,每次调用 GetInstance 方法的时候都需要获得锁才能获得 instance 实例,如果在调用频率比较高的场景下性能就不会很好。那有什么方式可以解决嘛?让我们接着往下看吧

var?mutex?sync.Mutexvar?instance?*Singletontype?Singleton?struct?{ }func?GetInstance()?*Singleton?{ mutex.Lock()defer?mutex.Unlock()if?instance?==?nil?{ ?instance?=?&Singleton{ }}return?instance}Double Check

       ä¸ºäº†è§£å†³é”çš„粒度问题,我们可以使用 Double Check 的方式来进行解决,例如下面的代码,第一次判断 instance == nil 之后需要进行加锁操作,然后再第二次判断 instance == nil 之后才能创建实例。这种方式对比上面的案例来说,锁的粒度更低,因为如果 instance != nil 的情况下是不需要加锁的。但是这种方式实现起来是不是比较麻烦,有没有什么方式可以解决呢?

var?mutex?sync.Mutexvar?instance?*Singletontype?Singleton?struct?{ }func?GetInstance()?*Singleton?{ if?instance?==?nil?{ ?mutex.Lock()?defer?mutex.Unlock()?if?instance?==?nil?{ ?instance?=?&Singleton{ }?}}return?instance}使用 sync.Once

       å¯ä»¥ä½¿ç”¨ sync.Once 来实现单例的初始化逻辑,因为这个逻辑至多只会跑一次。推荐使用这种方式来进行单例的初始化,当然也可以使用饿汉式。

var?once?sync.Oncevar?instance?*Singletontype?Singleton?struct?{ }func?GetInstance()?*Singleton?{ once.Do(func()?{ ?instance?=?&Singleton{ }})return?instance}源码分析

       ä¸‹é¢å°±æ˜¯ sync.Once 包的源码,我删除了注释,代码不多,Once 数据结构主要由 done 和 m 组成,其中 done 是存储 f 函数是否已执行,m 是一个锁实例。

type?Once?struct?{ done?uint?//?f函数是否已执行mMutex?//?锁}func?(o?*Once)?Do(f?func())?{ if?atomic.LoadUint(&o.done)?==?0?{ ?o.doSlow(f)}}func?(o?*Once)?doSlow(f?func())?{ o.m.Lock()defer?o.m.Unlock()if?o.done?==?0?{ ?defer?atomic.StoreUint(&o.done,?1)?f()}}

       Do 方法

       ä¼ å…¥ä¸€ä¸ª function,然后 sync.Once 来保证只执行一次,在 Do 方法中使用 atomic 来读取 done 变量,如果是 0 ,就代码 f 函数没有被执行过,然后就调用 doSlow方法,传入 f 函数

       doShow 方法

       doShow 的第一个步骤就是先加锁,这里加锁的目的是保证同一时刻是能由一个 goruntine 来执行 doSlow 方法,然后再次判断 done 是否是 0 ,这个判断就相当于我们上面说的 DoubleCheck ,因为 doSlow 可能存在并发问题。然后执行 f 方法,然后执行使用 atomic 将 done 保存成 1。使用 DoubleCheck 保证了 f 方法只会被执行一次。

       æŽ¥ç€çœ‹ï¼Œé‚£å¯ä»¥è¿™æ ·å®žçŽ° sync.Once 嘛?

       è¿™æ ·ä¸æ˜¯æ›´ç®€å•ä¸€ç‚¹å˜›ï¼Œä½¿ç”¨åŽŸå­çš„ CAS 操作就可以解决并发问题呀,并发只执行一次 f 方法的问题是可以解决,但是 Do 方法可能并发,第一个调用者将 done 设置成了 1 然后调用 f 方法,如果 f 方法特别耗时间,那么第二个调用者获取到 done 为 1 就直接返回了,此时 f方法是没有执行过第二次,但是此时第二个调用者可以继续执行后面的代码,如果后面的代码中有用到 f 方法创建的实例,但是由于 f 方法还在执行中,所以可能会出现报错问题。所以官方采用的是Lock + DoubleCheck 的方式。

if?atomic.CompareAndSwapUint(&o.done,?0,?1)?{ f()}拓展

       æ‰§è¡Œå¼‚常后可继续执行的Once

       çœ‹æ‡‚了源码之后,我们就可以扩展 sync.Once 包了。例如 f 方法在执行的时候报错了,例如连接初始化失败,怎么办?我们可以实现一个高级版本的 Once 包,具体的 slowDo 代码可以参考下面的实现

func?(o?*Once)?slowDo(f?func()?error)?error?{ o.m.Lock()defer?o.m.Unlock()var?err?errorif?o.done?==?0?{ ?//?Double?Checkerr?=?f()if?err?==?nil?{ ?//?没有异常的时候记录done值atomic.StoreUint(&o.done,?1)}}return?err}

       å¸¦æ‰§è¡Œç»“果的 Once

       ç”±äºŽ Once 是不带执行结果的,我们不知道 Once 什么时候会执行结束,如果存在并发,需要知道是否执行成功的话,可以看下下面的案例,我这里是以 redis 连接的问题来进行说明的。Do 方法执行完毕后将 init 值设置成 1 ,然后其他 goruntine 可以通过 IsConnetion 来获取连接是否建立,然后做后续的操作。

type?RedisConn?struct?{ once?sync.Onceinit?uint}func?(this?*RedisConn)?Init()?{ this.once.Do(func()?{ ?//?do?redis?connection?atomic.StoreUint(&this.init,?1)})}func?(this?*RedisConn)?IsConnect()?bool?{ ?//?另外一个goroutinereturn?atomic.LoadUint(&this.init)?!=?0}

Go并发编程:goroutine,channel和sync详解

       ä¼˜é›…的并发编程范式,完善的并发支持,出色的并发性能是Go语言区别于其他语言的一大特色。

       åœ¨å½“今这个多核时代,并发编程的意义不言而喻。使用Go开发并发程序,操作起来非常简单,语言级别提供关键字go用于启动协程,并且在同一台机器上可以启动成千上万个协程。

       ä¸‹é¢å°±æ¥è¯¦ç»†ä»‹ç»ã€‚

goroutine

       Go语言的并发执行体称为goroutine,使用关键词go来启动一个goroutine。

       go关键词后面必须跟一个函数,可以是有名函数,也可以是无名函数,函数的返回值会被忽略。

       go的执行是非阻塞的。

       å…ˆæ¥çœ‹ä¸€ä¸ªä¾‹å­ï¼š

packagemainimport("fmt""time")funcmain(){ gospinner(*time.Millisecond)constn=fibN:=fib(n)fmt.Printf("\rFibonacci(%d)=%d\n",n,fibN)//Fibonacci()=}funcspinner(delaytime.Duration){ for{ for_,r:=range`-\|/`{ fmt.Printf("\r%c",r)time.Sleep(delay)}}}funcfib(xint)int{ ifx<2{ returnx}returnfib(x-1)+fib(x-2)}

       ä»Žæ‰§è¡Œç»“果来看,成功计算出了斐波那契数列的值,说明程序在spinner处并没有阻塞,而且spinner函数还一直在屏幕上打印提示字符,说明程序正在执行。

       å½“计算完斐波那契数列的值,main函数打印结果并退出,spinner也跟着退出。

       å†æ¥çœ‹ä¸€ä¸ªä¾‹å­ï¼Œå¾ªçŽ¯æ‰§è¡Œæ¬¡ï¼Œæ‰“印两个数的和:

packagemainimport"fmt"funcAdd(x,yint){ z:=x+yfmt.Println(z)}funcmain(){ fori:=0;i<;i++{ goAdd(i,i)}}

       æœ‰é—®é¢˜äº†ï¼Œå±å¹•ä¸Šä»€ä¹ˆéƒ½æ²¡æœ‰ï¼Œä¸ºä»€ä¹ˆå‘¢ï¼Ÿ

       è¿™å°±è¦çœ‹Go程序的执行机制了。当一个程序启动时,只有一个goroutine来调用main函数,称为主goroutine。新的goroutine通过go关键词创建,然后并发执行。当main函数返回时,不会等待其他goroutine执行完,而是直接暴力结束所有goroutine。

       é‚£æœ‰æ²¡æœ‰åŠžæ³•è§£å†³å‘¢ï¼Ÿå½“然是有的,请往下看。

channel

       ä¸€èˆ¬å†™å¤šè¿›ç¨‹ç¨‹åºæ—¶ï¼Œéƒ½ä¼šé‡åˆ°ä¸€ä¸ªé—®é¢˜ï¼šè¿›ç¨‹é—´é€šä¿¡ã€‚常见的通信方式有信号,共享内存等。goroutine之间的通信机制是通道channel。

       ä½¿ç”¨make创建通道:

ch:=make(chanint)//ch的类型是chanint

       é€šé“支持三个主要操作:send,receive和close。

ch<-x//发送x=<-ch//接收<-ch//接收,丢弃结果close(ch)//关闭无缓冲channel

       make函数接受两个参数,第二个参数是可选参数,表示通道容量。不传或者传0表示创建了一个无缓冲通道。

       æ— ç¼“冲通道上的发送操作将会阻塞,直到另一个goroutine在对应的通道上执行接收操作。相反,如果接收先执行,那么接收goroutine将会阻塞,直到另一个goroutine在对应通道上执行发送。

       æ‰€ä»¥ï¼Œæ— ç¼“冲通道是一种同步通道。

       ä¸‹é¢æˆ‘们使用无缓冲通道把上面例子中出现的问题解决一下。

packagemainimport"fmt"funcAdd(x,yint,chchanint){ z:=x+ych<-z}funcmain(){ ch:=make(chanint)fori:=0;i<;i++{ goAdd(i,i,ch)}fori:=0;i<;i++{ fmt.Println(<-ch)}}

       å¯ä»¥æ­£å¸¸è¾“出结果。

       ä¸»goroutine会阻塞,直到读取到通道中的值,程序继续执行,最后退出。

缓冲channel

       åˆ›å»ºä¸€ä¸ªå®¹é‡æ˜¯5的缓冲通道:

ch:=make(chanint,5)

       ç¼“冲通道的发送操作在通道尾部插入一个元素,接收操作从通道的头部移除一个元素。如果通道满了,发送会阻塞,直到另一个goroutine执行接收。相反,如果通道是空的,接收会阻塞,直到另一个goroutine执行发送。

       æœ‰æ²¡æœ‰æ„Ÿè§‰ï¼Œå…¶å®žç¼“冲通道和队列一样,把操作都解耦了。

单向channel

       ç±»åž‹chan<-int是一个只能发送的通道,类型<-chanint是一个只能接收的通道。

       ä»»ä½•åŒå‘通道都可以用作单向通道,但反过来不行。

       è¿˜æœ‰ä¸€ç‚¹éœ€è¦æ³¨æ„ï¼Œclose只能用在发送通道上,如果用在接收通道会报错。

       çœ‹ä¸€ä¸ªå•å‘通道的例子:

packagemainimport"fmt"funccounter(outchan<-int){ forx:=0;x<;x++{ out<-x}close(out)}funcsquarer(outchan<-int,in<-chanint){ forv:=rangein{ out<-v*v}close(out)}funcprinter(in<-chanint){ forv:=rangein{ fmt.Println(v)}}funcmain(){ n:=make(chanint)s:=make(chanint)gocounter(n)gosquarer(s,n)printer(s)}sync

       sync包提供了两种锁类型:sync.Mutex和sync.RWMutex,前者是互斥锁,后者是读写锁。

       å½“一个goroutine获取了Mutex后,其他goroutine不管读写,只能等待,直到锁被释放。

packagemainimport("fmt""sync""time")funcmain(){ varmutexsync.Mutexwg:=sync.WaitGroup{ }//主goroutine先获取锁fmt.Println("Locking(G0)")mutex.Lock()fmt.Println("locked(G0)")wg.Add(3)fori:=1;i<4;i++{ gofunc(iint){ //由于主goroutine先获取锁,程序开始5秒会阻塞在这里fmt.Printf("Locking(G%d)\n",i)mutex.Lock()fmt.Printf("locked(G%d)\n",i)time.Sleep(time.Second*2)mutex.Unlock()fmt.Printf("unlocked(G%d)\n",i)wg.Done()}(i)}//主goroutine5秒后释放锁time.Sleep(time.Second*5)fmt.Println("readyunlock(G0)")mutex.Unlock()fmt.Println("unlocked(G0)")wg.Wait()}

       RWMutex属于经典的单写多读模型,当读锁被占用时,会阻止写,但不阻止读。而写锁会阻止写和读。

packagemainimport("fmt""sync""time")funcmain(){ varrwMutexsync.RWMutexwg:=sync.WaitGroup{ }Data:=0wg.Add()fori:=0;i<;i++{ gofunc(tint){ //第一次运行后,写解锁。//循环到第二次时,读锁定后,goroutine没有阻塞,同时读成功。fmt.Println("Locking")rwMutex.RLock()deferrwMutex.RUnlock()fmt.Printf("Readdata:%v\n",Data)wg.Done()time.Sleep(2*time.Second)}(i)gofunc(tint){ //写锁定下是需要解锁后才能写的rwMutex.Lock()deferrwMutex.Unlock()Data+=tfmt.Printf("WriteData:%v%d\n",Data,t)wg.Done()time.Sleep(2*time.Second)}(i)}wg.Wait()}总结

       å¹¶å‘编程算是Go的特色,也是核心功能之一了,涉及的知识点其实是非常多的,本文也只是起到一个抛砖引玉的作用而已。

       æœ¬æ–‡å¼€å§‹ä»‹ç»äº†goroutine的简单用法,然后引出了通道的概念。

       é€šé“有三种:

       æ— ç¼“冲通道

       ç¼“冲通道

       å•å‘通道

       æœ€åŽä»‹ç»äº†Go中的锁机制,分别是sync包提供的sync.Mutex(互斥锁)和sync.RWMutex(读写锁)。

       goroutine博大精深,后面的坑还是要慢慢踩的。

       æ–‡ç« ä¸­çš„脑图和源码都上传到了GitHub,有需要的同学可自行下载。

       åœ°å€ï¼šgithub.com/yongxinz/gopher/tree/main/sc

       ä½œè€…:yongxinz

本文地址:http://8o.net.cn/news/00a146898531.html

copyright © 2016 powered by 皮皮网   sitemap