同步与锁

为了避免多个线程同事读写同一个数据而产生不可预料的后果,我们要将各个线程对同一数据的访问同步。所谓同步,即指在一个线程访问数据未结束的时候,其他线程不得不对同一个数据进行访问。如此,对数据的访问被原子化了。
同步的最常见的方法是使用锁(Lock).锁是一种非强制机制,每一个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁。在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。

  1. 二元信号量是最简单的一种锁,他只有两种状态:占用与非占用。它适合只能被唯一一个线程独占访问的资源。当二元信号量处于非占用状态时,第一试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,伺候其他的所有试图获取该二元信号量的线程会等待,直到该锁被释放。
    对于允许多个线程并发访问的资源,多元信号量简称信号量,它是一个很好的选择。一个初始值为N的信号量允许N个线程并发访问。线程访问资源的时候首先获取信号量,进行如下操作:
    • 将信号量 的值减一
    • 如果信号量的值小于0,则进入等待状态,否则继续执行。访问完资源之后,线程释放信号量,进行如下操作。
    • 将信号量加1.
    • 如果信号量的值小于1,唤醒一个等待中的线程。
      常用的POSIX信号量函数
      #include <semaphore.h>
      sem_init函数用于初始化一个未命名的信号量,pshared参数指定信号量的类型。如果其值为0,就表示这个信号量是当前信号量的局部信号量,否则该信号量就可以在多个进程之间共享。value参数指定信号量的初始值。此外,初始化一个已经被初始化的信号量将导致不可预期的后果
      int sem_init(sem_t *sem, int pshared, unsigned int value);
      sem_destroy函数用于销毁信号量,已释放其占用的内核资源。如果销毁一个正在被其他线程等待的信号量,则将导致不可预期的后果。
      int sem_destroy(sem_t *sem);
      //以原子操作的方式将信号量的值减一,如果信号量的值为0,则sem_wait将被阻塞,直到这个信号为非0值
      int sem_wait(sem_t *sem);
      相当与sem_wait的非阻塞版本。
      int sem_trywait(sem_t *sem);
      sem_post函数以原子操作的方式将信号量的值加1.当信号量的值大于0时,其他正在调用sem_wait等待信号的线程将被唤醒。
      int sem_post(sem_t *sem);
#include <iostream>
#include <pthread.h>
#include <exception>
#include <semaphore.h>
#include <unistd.h>
using namespace std;

class sem {
public:
    sem()
    {
        if (sem_init( &m_sem, 0, 1  ) != 0) {
            throw std::exception();
        }
    }
    ~sem() { sem_destroy(&m_sem);  }
    bool wait() { return sem_wait(&m_sem) == 0;  }
    bool post() { return sem_post(&m_sem) == 0;  }
private:
    sem_t m_sem;
};

void *thread_fun(void *arg)
{
    printf("child thread\n");
    sem sem_t;
    sem_t.wait();
    printf("main thread 5 seconds ,post after\n");
    sem_t.post();
}

int main()
{
    pthread_t id;
    sem sem_t;
    pthread_create(&id, NULL, thread_fun, NULL);
    printf("main thread\n");
    sem_t.wait();
    sleep(2);
    sem_t.post();
}
  1. 互斥量和二元信号量很类似,资源仅同时允许一个线程访问,但和信号量不同的是,信号量在整个系统可以被任意线程获取并释放,也就是说,同一个信号量可以被系统中的一个线程之后由另一个线程释放。而互斥量则要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁,其他线程越俎代庖去释放互斥量是无效的。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <exception>

int a = 0;

class locker {
public:
    locker()
    {
        if (pthread_mutex_init(&m_mutex, NULL) != 0) {
            throw std::exception();                   
        }             
    }
    ~locker() { pthread_mutex_destroy(&m_mutex); }
    bool lock() { return pthread_mutex_lock(&m_mutex) == 0; }     
    bool unlock() { return pthread_mutex_unlock(&m_mutex) == 0; }
private:
    pthread_mutex_t m_mutex;
};

locker mutex_t;

void *thread_fun(void *arg)
{
    printf("thread_fun1 %d\n",a);
    mutex_t.lock();
    //printf("thread_fun1 %d\n",a);
    a--;
    mutex_t.unlock();
    printf("thread_fun2 %d\n",a);
}

int main()
{
    pthread_t id;
    pthread_create(&id, NULL, thread_fun, NULL);
    printf("%d\n",a);
    mutex_t.lock();
    a++;
    //sleep(5);   //主线程加锁,子线程会等待主线程解锁后加锁。
    mutex_t.unlock();
    printf("%d\n",a);
    pthread_join(id, NULL);
}
  1. 临界区是比互斥量更加严格的同步手段。在术语中,把临界区的获取称为进入临界区,而把锁的释放称为离开临界区。临界区和互斥量与信号量的区别在于,互斥量和信号量在系统中的任何进程里都是可见的,也就是说,一个进程创建了一个互斥量或信号量,另一个进程试图去获取该锁是合法的。然而,临界区的作用范围仅限于本进程,其他的进程无法获取该锁。除此之外,临界区具有和互斥量相同的性质。
  2. 读写锁致力于一种更加特定的场合的同步。对于一段数据,多个线程同时读取总是没有问题的,但假设操作都不是原子型,只要有任何一个线程试图对这个数据进行修改,就必须使用同步手段来避免出错。如果我们使用上述信号量,互斥量或临界区中的任何一种来进行同步,尽管可以保证程序正确,但对于读取频繁,而仅仅偶尔写入的情况,会显得非常低效。读写锁可以避免这个问题。对于同一个锁,读写锁有两种获取方式,共享的或独占的。当锁处于自由的状态时,试图以任何一种方式获取锁都能成功,并将锁置于对应的状态。如果锁处于共享状态,其他线程以共享的方式获取仍然会成功,此时这个锁分配给了多个线程。然而,如果其他线程试图以独占的方式获取已经处于共享状态的锁,那么他将必须等待锁被所有线程释放。相应的,处于独占状态的锁将阻止任何其他线程获取该锁,不论他们试图以哪种方式获取。读写锁的行为可以总结如下图
读写锁以共享方式获取以独占方式获取
自由成功成功
共享成功等待
独占等待等待
  1. 条件变量作为一种同步手段,作用类似与一个栅栏。对于条件变量,线程可以有两种操作,首先线程可以等待条件变量,一个条件变量可以被多个线程等待。其次,线程可以唤醒条件变量,此时某个或所有等待此条件变量的线程都会被唤醒并继续支持。也就是说,使用条件变量可以让许多线程一起等待某个事件的发生,当事件发生时(条件变量被唤醒),所有的线程可以一起恢复执行。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <exception>
using namespace std;

class cond {
public:
    cond() {
        if (pthread_mutex_init(&m_mutex, NULL) != 0) {
            throw std::exception();
                    
        }
        if (pthread_cond_init(&m_cond, NULL) != 0) {
            pthread_mutex_destroy(&m_mutex);
            throw std::exception();
                    
        }    
    }
    ~cond()
    {
        pthread_mutex_destroy(&m_mutex);
        pthread_cond_destroy(&m_cond);
            
    }
    bool wait()
    {
        int ret = 0;
        pthread_mutex_lock(&m_mutex);
        ret = pthread_cond_wait(&m_cond, &m_mutex);
        pthread_mutex_unlock(&m_mutex);
        return ret = 0;    
    }
    bool signal() {
        return pthread_cond_signal(&m_cond) == 0;    
    }
private:
    pthread_mutex_t m_mutex;
    pthread_cond_t m_cond;
};

cond cond_t;

void *thread_fun(void *arg)
{
    cond_t.wait();
    printf("child thread is signal\n");
}

int main()
{
    pthread_t id;
    pthread_create(&id, NULL, thread_fun, NULL);
    int a = 1;
    if (a) {
        sleep(1);
        cond_t.signal();
        printf("signal\n");
    }
    pthread_join(id, NULL);
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值