线程与同步问题总结

68 篇文章 3 订阅
34 篇文章 0 订阅

0)Linux 的内置原子操作,不用使用锁

相关函数:

// 先fetch当然是返回旧值
type __sync_fetch_and_add (type *ptr, type value);
type __sync_fetch_and_sub (type *ptr, type value);
type __sync_fetch_and_or (type *ptr, type value);
type __sync_fetch_and_and (type *ptr, type value);
type __sync_fetch_and_xor (type *ptr, type value);
type __sync_fetch_and_nand (type *ptr, type value);

// 后fetch当然是返回新值
type __sync_add_and_fetch (type *ptr, type value);
type __sync_sub_and_fetch (type *ptr, type value);
type __sync_or_and_fetch (type *ptr, type value);
type __sync_and_and_fetch (type *ptr, type value);
type __sync_xor_and_fetch (type *ptr, type value);
type __sync_nand_and_fetch (type *ptr, type value);

bool __sync_bool_compare_and_swap (type*ptr, type oldval, type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval,  type newval, ...)
//这两个函数提供原子的比较和交换,如果*ptr == oldval,就将newval写入*ptr,
//第一个函数在相等并写入的情况下返回true.
//第二个函数在返回操作之前的值。

比如这样的一段代码:

// Type your code here, or load an example.
#include <iostream>
#include <string>
#include <unistd.h>
#include <stdint.h>

using namespace std;

static int g_status = 0;
 
void set_status(int stat)
{
    //LOCKWAIT(casLock_umfState);
    __sync_val_compare_and_swap(&g_status, 0,stat);
    //UNLOCK(casLock_umfState);
}


int main()
{
    set_status(1);
    printf(".2f\n", 0.623);
}

 其中的函数汇编后如下:

set_status(int):
 push   rbp
 mov    rbp,rsp
 mov    DWORD PTR [rbp-0x4],edi
 mov    edx,DWORD PTR [rbp-0x4]
 mov    eax,0x0
 lock cmpxchg DWORD PTR [rip+0x2ee3],edx        # 404050 <g_status>
 nop
 pop    rbp
 ret 

 最核心的就是lock  xchg一句,将内存与eax中比较,如果相同则设置为edx的内容;

具体原理摘抄如下:

从 P6 处理器开始,如果指令访问的内存区域已经存在于处理器的内部缓存中,则“lock” 前缀并不将引线 LOCK 的电位拉低,而是锁住本处理器的内部缓存,然后依靠缓存一致性协议保证操作的原子性。

4.2) IA32 CPU调用有lock前缀的指令,或者如xchg这样的指令,会导致其它的CPU也触发一定的动作来同步自己的Cache。
CPU的#lock引脚链接到北桥芯片(North Bridge)的#lock引脚,当带lock前缀的执行执行时,北桥芯片会拉起#lock
电平,从而锁住总线,直到该指令执行完毕再放开。 而总线加锁会自动invalidate所有CPU对 _该指令涉及的内存_
的Cache,因此barrier就能保证所有CPU的Cache一致性。

4.3) 接着解释。
lock前缀(或cpuid、xchg等指令)使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。
IA32在每个CPU内部实现了Snoopying(BUS-Watching)技术,监视着总线上是否发生了写内存操作(由某个CPU或DMA控
制器发出的),只要发生了,就invalidate相关的Cache line。 因此,只要lock前缀导致本CPU写内存,就必将导致
所有CPU去invalidate其相关的Cache line。

http://www.unixresources.net/linux/clf/linuxK/archive/00/00/65/37/653778.html
http://www.bitscn.com/linux/kernel/200806/144491_2.html

 

// 参考muduo中的代码:
  
volatile T value_;

  T getAndSet(T newValue)
  {
    // in gcc >= 4.7: __atomic_exchange_n(&value_, newValue, __ATOMIC_SEQ_CST)
    return __sync_lock_test_and_set(&value_, newValue);
  }

  T get()
  {
    // in gcc >= 4.7: __atomic_load_n(&value_, __ATOMIC_SEQ_CST)
    return __sync_val_compare_and_swap(&value_, 0, 0);
  }

  T getAndAdd(T x)
  {
    // in gcc >= 4.7: __atomic_fetch_add(&value_, x, __ATOMIC_SEQ_CST)
    return __sync_fetch_and_add(&value_, x);
  }

Linux线程同步的几种机制:

互斥量:pthread_mutex_t

读写锁:pthread_rwlock_t 

条件变量:pthread_cond_t

自旋锁:pthread_spinlock_t

自旋是指忙等待,不会和mutex一样进入睡眠,一般锁的开销是10个时钟周期,pthread spinlocks - Alex on Linux

性能比较见Mutex与spin lock的性能对比_继续微笑lsj的博客-CSDN博客

这个主要取决于临界区代码长度,因为mutex要进入睡眠模式,会有线程切换,如果临界区太短,就不划算。

1)线程互斥量

#include <pthread.h>

pthread_mutex_t  
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

// 比如:
pthread_mutex_t lock;

pthread_mutex_init(&lock, NULL);

pthread_mutex_lock(&lock);
pthread_mutex_trylock(&lock);
// 函数pthread_mutex_trylock会尝试对互斥量加锁,如果该互斥量已经被锁住,函数调用失败,返回EBUSY,
// 否则加锁成功返回0,线程不会被阻塞。

2)条件变量同步各个线程初始化完毕

// 主线程,网上好多示例都是错的,会造成死锁
// 最早我很困惑于这里主线程锁住了mutex,工作线程如何初始化,事实上关键在于理解pthread_cond_wait内部执行过程中会先解锁,然后等待,
 
	pthread_mutex_lock(&init_lock);
		
	while (init_count < nthreads) {

            // 则此函数内部就将线程放入等待队列
            // 再将 init_lock互斥量解锁 ,
            // ,此时第主线程挂起 (不占用任何CPU周期)
			pthread_cond_wait(&init_cond, &init_lock);
            // 首先要再次锁init_lock , 如果锁成功,再进行条件的判断
		}
		
	pthread_mutex_unlock(&init_lock);
		
	 

// 工作线程

    pthread_mutex_lock(&init_lock);
	init_count++;  // 这里加锁了,就不用原子操作了
    if (init_count >= nthreads)
    {
        pthread_cond_broadcast(&init_cond);

    }
    //pthread_cond_signal(&init_cond);

	pthread_mutex_unlock(&init_lock);
	
// 我个人理解:之所以需要使用init_lock,是需要保障“条件判断,加入唤醒队列”是原子操作,
// 并且与修改条件互斥,否则,不能保证判断条件后加入唤醒队列前,是否有人更改了条件并激发信号,
// 这样会丢失唤醒
// 至于信号发送位置,有很多讨论

   pthread_cond_signal 函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.

如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。

 使用pthread_cond_signal一般不会有“惊群现象”产生,他最多只给一个线程发信号。

假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。

如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发信一次。

 但是,  pthread_cond_signal 在多处理器上可能同时唤醒多个线程,当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续 wait,

备注1:pthread_cond_wait()函数的源码:pthread_cond_wait.c source code [glibc/nptl/pthread_cond_wait.c] - Woboq Code Browser

这个函数内部是先使用原子操作将自己添加到等待队列中,然后解锁,然后自旋等待一下,然后再阻塞睡眠,这里阻塞多久呢?

循环maxspin次操作。函数首内部设置const in maxspin = 1;也就是先检查了一次信号;

备注2:

pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。

但使用pthread_cond_signal不会有“惊群现象”产生,他最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发信一次。

另外,互斥量的作用一般是用于对某个资源进行互斥性的存取,很多时候是用来保证操作是一个原子性的操作,是不可中断的。

用法:

pthread_cond_wait必须放在pthread_mutex_lock和pthread_mutex_unlock之间,因为他要根据共享变量的状态来决定是否要等待,而为了不永远等待下去所以必须要在lock/unlock队中

共享变量的状态改变必须遵守lock/unlock的规则

pthread_cond_signal即可以放在pthread_mutex_lock和pthread_mutex_unlock之间,也可以放在pthread_mutex_lock和pthread_mutex_unlock之后,但是各有各缺点。

之间:

    pthread_mutex_lock(&init_lock);

	init_count++;  // 这里加锁了,就不用原子操作了
    if (init_count >= nthreads)
    {
        //pthread_cond_broadcast(&init_cond);
         pthread_cond_signal(&init_cond);
    }

   

	pthread_mutex_unlock(&init_lock);

缺点:在某下线程的实现中,会造成等待线程从内核中唤醒(由于cond_signal)然后又回到内核空间(因为cond_wait返回后会有原子加锁的行为),所以一来一回会有性能的问题。

在code review中,我会发现很多人喜欢在pthread_mutex_lock()和pthread_mutex_unlock(()之间调用 pthread_cond_signal或者pthread_cond_broadcast函数,从逻辑上来说,这种使用方法是完全正确的。但是在多线程环境中,这种使用方法可能是低效的。posix1标准说,pthread_cond_signal与pthread_cond_broadcast无需考虑调用线程是否是mutex的拥有者,也就是说,可以在lock与unlock以外的区域调用。如果我们对调用行为不关心,那么请在lock区域之外调用吧。这里举个例子:
       我们假设系统中有线程1和线程2,他们都想获取mutex后处理共享数据,再释放mutex。请看这种序列:
       1)线程1获取mutex,在进行数据处理的时候,线程2也想获取mutex,但是此时被线程1所占用,线程2进入休眠,等待mutex被释放。
       2)线程1做完数据处理后,调用pthread_cond_signal()唤醒等待队列中某个线程,在本例中也就是线程2。线程1在调用pthread_mutex_unlock()前,因为系统调度的原因,线程2获取使用CPU的权利,那么它就想要开始处理数据,但是在开始处理之前,mutex必须被获取,很遗憾,线程1正在使用mutex,所以线程2被迫再次进入休眠。
       3)然后就是线程1执行pthread_mutex_unlock()后,线程2方能被再次唤醒。
       从这里看,使用的效率是比较低的,如果再多线程环境中,这种情况频繁发生的话,是一件比较痛苦的事情。

但是在LinuxThreads或者NPTL里面,就不会有这个问题,因为在Linux 线程中,有两个队列,分别是cond_wait队列和mutex_lock队列, cond_signal只是让线程从cond_wait队列移到mutex_lock队列,而不用返回到用户空间,不会有性能的损耗。

所以在Linux中推荐使用这种模式。

之后:

pthread_mutex_lock(&init_lock);
init_count++;  // 这里加锁了,就不用原子操作了  
pthread_mutex_unlock(&init_lock);

if (init_count >= nthreads)
{
    //pthread_cond_broadcast(&init_cond);
    pthread_cond_signal(&init_cond);
}

优点:不会出现之前说的那个潜在的性能损耗,因为在signal之前就已经释放锁了

缺点:如果unlock和signal之前,有个低优先级的线程正在mutex上等待的话,那么这个低优先级的线程就会抢占高优先级的线程(cond_wait的线程),而这在上面的放中间的模式下是不会出现的。

所以,在Linux下最好pthread_cond_signal放中间,但从编程规则上说,其他两种都可以

3)使用modern c++ 的std::condition_variable  参考:C++11: 并发指南五(std::condition_variable 详解)_zzhongcy的博客-CSDN博客

#include <iostream>
#include <atomic>
#include <condition_variable>
#include <thread>
#include <chrono>
using namespace std::chrono_literals;
 
std::condition_variable cv;
std::mutex cv_m;
int i;
 
void waits(int idx)
{
    // 同样的需要先锁定
    std::unique_lock<std::mutex> lk(cv_m);

    // 这里不是无限等待,而是等待一个时间,并且提供了一个lamda表达式用于(在收到通知时候)判断是否继续等待(防止虚假唤醒)
    // 这里的函数签名为:bool pred();
    if(cv.wait_for(lk, idx*100ms, []{return i == 1;})) 
        std::cerr << "Thread " << idx << " finished waiting. i == " << i << '\n';
    else
        std::cerr << "Thread " << idx << " timed out. i == " << i << '\n';
}
 
void signals()
{
    std::this_thread::sleep_for(120ms);
    std::cerr << "Notifying...\n";
    cv.notify_all();
    std::this_thread::sleep_for(100ms);
    {
        std::lock_guard<std::mutex> lk(cv_m);
        i = 1;
    }
    std::cerr << "Notifying again...\n";
    cv.notify_all();
}
 
// 生成3个等待线程,分别等待100毫秒,200毫秒,300毫秒;
// 1个通知线程: 通知了2次,120毫秒时候,和220毫秒
// 线程1等不到通知就超时了;
// 线程2虽然收到了唤醒,但是发现条件不满足,就继续等待,结果超时了
// 线程3在220秒唤醒时候条件满足,高高兴兴的返回了
int main()
{
    std::thread t1(waits, 1), t2(waits, 2), t3(waits, 3), t4(signals);
    t1.join(); t2.join(), t3.join(), t4.join();
}
/*
输出结果是:
Thread 1 timed out. i == 0
Notifying...
Thread 2 timed out. i == 0
Notifying again...
Thread 3 finished waiting. i == 1

*/

不适用表达式的用法如下:

if (cv_.wait_for(lk,std::chrono::milliseconds(500))==std::cv_status::timeout) 
{
      
    std::cout<<"timeout"<<std::endl;

}

4)信号


#include <signal.h>
void handle_pipe(int sig)
{
//do nothing
}
int main()
{
    struct sigaction sa;
    sa.sa_handler = handle_pipe;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGPIPE,&sa,NULL);
//do something
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值