对文章“ 如何正确和不正确地睡觉 ”的评论启发了我写这篇文章。
本文将重点介绍多线程应用程序的开发,无锁在LAppS工作过程中出现的某些情况下的适用性 , nanosleep功能以及对任务计划程序的暴力行为。
NB: C++ Linux, POSIX.1-2008 a ( ).
总的来说,一切都很混乱,我希望演示文稿中的思路清晰。 如果有兴趣的话,我要一只猫。
面向事件的软件总是在等待某些东西。 无论是GUI还是网络服务器,他们都在等待任何事件:键盘输入,鼠标事件,数据包通过网络到达。 但是所有软件的等待时间不同。 无锁系统根本不必等待。 至少应该在不需要等待的地方使用无锁算法,甚至有害。 但是,我们谈论的是竞争性(多线程)系统,奇怪的是,无锁算法也在等待中。 是的,它们不会阻塞并行线程的执行,但是它们本身正在等待机会而不会阻塞。
LAppS非常积极地使用互斥量和信号量。 同时,C ++标准中没有信号灯。 该机制非常重要且方便,但是C ++应该在没有信号灯支持的系统上工作,因此该信号灯不包含在标准中。 而且,如果我使用信号量是因为它们很方便,则使用互斥量是因为我必须这样做。
在竞争性锁()的情况下,互斥锁的行为(如Linux中的sem_wait())将等待线程放在任务调度程序队列的末尾,当它位于顶部时,将重复执行检查而不会返回到userland,如果线程处于等待状态,则将该线程放回队列预期的事件尚未发生。 这是非常重要的一点。
然后,我决定检查是否可以拒绝std :: mutex和POSIX信号量,并使用std :: atomic模拟它们,并将负载主要转移到userland。 实际失败了,但首先是第一件事。
首先,我在几个部分中这些实验可能有用:
- 锁定LibreSSL(情况1);
- 在将有效负载接收的数据包传输到Lua应用程序时发生阻塞(情况2);
- 等待准备好由Lua应用程序处理的有效负载事件(情况3)。
让我们从非阻塞锁开始。 让我们使用原子来编写互斥体,如H. Sutter的一些讲话中所示(没有原始代码,因此,从内存中获取的代码与原始100%不一致,因此在Satter中,此代码与C ++ 20的进展有关,因此存在差异)。 尽管这段代码很简单,但其中也存在一些缺陷。
#include <atomic> #include <pthread.h> namespace test { class mutex { private: std::atomic<pthread_t> mLock; public: explicit mutex():mLock{0} { } mutex(const mutex&)=delete; mutex(mutex&)=delete; void lock() { pthread_t locked_by=0;
与std :: mutex :: unlock()不同,尝试从另一个线程解锁时,test :: mutex:unlock()的行为是确定性的。 将引发异常。 这很好,尽管与标准行为不符。 这堂课有什么不好呢? 坏消息是,test :: mutex:lock()方法将无耻地消耗分配给该线程的时间配额中的CPU资源,以尝试接管另一个线程已拥有的互斥量。 即 test :: Mutex:lock()中的循环将浪费CPU资源。 我们有什么选择来克服这种情况?
我们可以使用sched_yield()(如上述文章的评论之一所建议)。 这么简单吗? 首先,为了使用sched_yield(),执行线程必须在任务调度程序中使用SCHED_RR,SCHED_FIFO策略对其进行优先级排序。 否则,调用sched_yield()将浪费CPU资源。 其次,非常频繁地调用sched_yield()仍会增加CPU消耗。 此外,在系统中没有其他实时应用程序的情况下,如果在应用程序中使用实时策略,则将具有所选策略的调度程序队列限制为仅线程。 看来这很好! 不,不好 整个系统的响应速度将降低,因为 忙于优先任务。 CFQ将在笔中。 但是应用程序中还有其他线程,通常将捕获互斥锁的线程放在队列的末尾(配额已过期),而等待互斥锁释放的线程就在这种情况下。 在我的实验(案例2)中,该方法的结果与std :: mutex大致相同(差3.8%),但是系统的响应速度较慢,CPU消耗增加了5%-7%。
您可以尝试像这样更改test :: Mutex :: lock()(也很糟糕):
void lock() { pthread_t locked_by=0; while(!mLock.compare_exchange_strong(locked_by,pthread_self())) { static thread_local const struct timespec pause{0,4};
在这里,您可以试验睡眠时间(以纳秒为单位),对于我的CPU来说,延迟为4 ns最佳,并且在相同情况下2相对于std :: mutex的性能下降为1.2%。 纳米睡眠不睡4ns的事实。 实际上,更多或更多(在一般情况下)或更少(如果被打断)。 CPU消耗下降(!)为12%-20%。 即 这是一个健康的梦想。
OpenSSL和LibreSSL具有两个函数,这些函数设置在多线程环境中使用这些库时阻止的回调。 看起来像这样:
// callback void openssl_crypt_locking_function_callback(int mode, int n, const char* file, const int line) { static std::vector<std::mutex> locks(CRYPTO_num_locks()); if(n>=static_cast<int>(locks.size())) { abort(); } if(mode & CRYPTO_LOCK) locks[n].lock(); else locks[n].unlock(); } // callback-a CRYPTO_set_locking_callback(openssl_crypt_locking_function_callback); // id CRYPTO_set_id_callback(pthread_self);
现在最糟糕的是,在LibreSSL中使用上述测试:: Mutex互斥体会使LAppS的性能降低近2倍。 而且,无论选项如何(空等待循环,sched_yield(),nanosleep())。
通常,我们删除情况2和情况1,并保留std :: Mutex。
让我们继续进行信号量。 关于如何使用std :: condition_variable实现信号量的例子很多。 它们也都使用std :: Mutex。 并且这种信号量模拟器比POSIX系统信号量要慢(根据我的测试)。
因此,我们将对原子进行信号量处理:
class semaphore { private: std::atomic<bool> mayRun; mutable std::atomic<int64_t> counter; public: explicit semaphore() : mayRun{true},counter{0} { } semaphore(const semaphore&)=delete; semaphore(semaphore&)=delete; const bool post() const { ++counter; return mayRun.load(); } const bool try_wait() { if(mayRun.load()) { if(counter.fetch_sub(1)>0) return true; else { ++counter; return false; } }else{ throw std::system_error(ENOENT,std::system_category(),"Semaphore is destroyed"); } } void wait() { while(!try_wait()) { static thread_local const struct timespec pause{0,4}; nanosleep(&pause,nullptr); } } void destroy() { mayRun.store(false); } const int64_t decrimentOn(const size_t value) { if(mayRun.load()) { return counter.fetch_sub(value); }else{ throw std::system_error(ENOENT,std::system_category(),"Semaphore is destroyed"); } } ~semaphore() { destroy(); } };
哦,这个信号量比系统信号量快许多倍。 使用一个提供者和20个consumers对该信号量进行单独测试的结果:
OS semaphores test. Started 20 threads waiting on a semaphore Thread(OS): wakes: 500321 Thread(OS): wakes: 500473 Thread(OS): wakes: 501504 Thread(OS): wakes: 502337 Thread(OS): wakes: 498324 Thread(OS): wakes: 502755 Thread(OS): wakes: 500212 Thread(OS): wakes: 498579 Thread(OS): wakes: 499504 Thread(OS): wakes: 500228 Thread(OS): wakes: 499696 Thread(OS): wakes: 501978 Thread(OS): wakes: 498617 Thread(OS): wakes: 502238 Thread(OS): wakes: 497797 Thread(OS): wakes: 498089 Thread(OS): wakes: 499292 Thread(OS): wakes: 498011 Thread(OS): wakes: 498749 Thread(OS): wakes: 501296 OS semaphores test. 10000000 of posts for 20 waiting threads have taken 9924 milliseconds OS semaphores test. Post latency: 0.9924ns ======================================= AtomicEmu semaphores test. Started 20 threads waiting on a semaphore Thread(EmuAtomic) wakes: 492748 Thread(EmuAtomic) wakes: 546860 Thread(EmuAtomic) wakes: 479375 Thread(EmuAtomic) wakes: 534676 Thread(EmuAtomic) wakes: 501014 Thread(EmuAtomic) wakes: 528220 Thread(EmuAtomic) wakes: 496783 Thread(EmuAtomic) wakes: 467563 Thread(EmuAtomic) wakes: 608086 Thread(EmuAtomic) wakes: 489825 Thread(EmuAtomic) wakes: 479799 Thread(EmuAtomic) wakes: 539634 Thread(EmuAtomic) wakes: 479559 Thread(EmuAtomic) wakes: 495377 Thread(EmuAtomic) wakes: 454759 Thread(EmuAtomic) wakes: 482375 Thread(EmuAtomic) wakes: 512442 Thread(EmuAtomic) wakes: 453303 Thread(EmuAtomic) wakes: 480227 Thread(EmuAtomic) wakes: 477375 AtomicEmu semaphores test. 10000000 of posts for 20 waiting threads have taken 341 milliseconds AtomicEmu semaphores test. Post latency: 0.0341ns
这个信号量几乎是免费的post(),比系统速度快29倍,它在唤醒等待它的线程方面也非常快:每秒29325次唤醒,而系统每毫秒1007次唤醒。 它具有确定的行为,具有被破坏的信号量或可破坏的信号量。 当然,在尝试使用已损坏的段时会出现段错误。
(¹)实际上,调度器无法在一毫秒内多次延迟或唤醒流。 因为 post()没有阻塞,在此综合测试中,wait()通常会发现自己不需要睡觉。 同时,至少有7个并行线程读取信号量的值。
但是,无论睡眠时间如何,在LAppS的情况3中使用它都会导致性能损失。 他太醒来无法检查,并且LAppS中的事件到达的速度要慢得多(网络延迟,生成负载的客户端延迟等)。 减少检查次数也意味着会损失性能。
此外,在这种情况下以类似方式使用睡眠是完全有害的,因为 在另一种硬件上,结果可能会完全不同(如汇编程序指令暂停的情况),并且对于每种CPU型号,还必须选择延迟时间。
系统互斥量和信号量的优点是,执行线程不会唤醒,直到发生事件(解锁互斥量或增加信号量)为止。 不会浪费额外的CPU周期-利润。
总的来说,从我的系统上禁用iptables到这个邪恶的东西,从12%(使用TLS)到30%(没有TLS)的性能提升...