线程同步

    同属于一个进程的不同线程是共享内存的,因而在执行过程中需要考虑数据的一致性。
    假设:进程有一变量i=0,线程A执行i++,线程B执行i++,那么最终i的取值是多少呢?似乎一定是i=2;其实不然,如果没有考虑线程同步,i的取值可能是1.我们先考虑自加操作的过程:a,首先将内存中i的值copy到寄存器;b,对寄存器中i的copy进行自加;c,将寄存器中自加的结果返回到内存中。回到例子,如果线程A执行完abc三个步骤,线程B在执行者三个步骤,那么结果就应该为2.但是自加不是原子操作,假如执行过程是a(A),
a(B), b(A),b(B),c(A),c(B)。那么执行的过程就是AB都把i原值i=0copy到寄存器,进行自加,得到结果1,然后分别有把值1赋值到内存中的i,导致i结果为1.
    之所以可能出现上面非期望的结果(i=1而不是i=2),是因为i++不是原子操作。为了解决这个问题,引入互斥锁。

互斥锁 Mutexes

    互斥锁通过pthreads的互斥接口来保证数据在某一时间里只能被一个线程访问。
    互斥锁的使用过程大致是:1)访问共享资源前加锁,2)访问共享资源,3)访问加速后解锁。(在这个过程中互斥量扮演一个锁的角色)
    互斥量使用结构体pthread_mutex_t表示,使用的接口如下:
互斥量的初始化与销毁:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t*restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
Both return: 0 if OK, error number on failure
    动态创建的互斥量mutex才需要使用pthread_mutex_destroy,静态创建互斥量时,可以直接mutex=PTHREAD_MUTEX_INITIALIZER
进行初始化,而不调用pthread_mutex_init函数。
    参数attr表示互斥量的属性。

加锁与解锁:
    
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict tsptr);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
All return: 0 if OK, error number on failure
 使用pthread_mutex_lock 时,如果互斥量已经被其他线程加锁,那么会阻塞,知道该互斥量被解锁再对互斥量加锁。
 使用pthread_mutex_trylock时,如果互斥量已经被其他线程加锁,则函数返回错误。如果未被加锁,那么就对互斥量加锁。
 使用pthread_mutex_timedlock 时,尝试加锁,阻塞(最多阻塞到时间点tsptr),如果在tsptr还无法加锁,则返回错误。
关于tsptr,它是一个时间点,比如我们要等待3min,则结果tsptr应该是当前时间加上3min。

死锁与避免死锁

产生死锁的几种情况:
1)线程对同一互斥量加锁两次,类似下面的情况:
pthread_mutex_lock(mutex);
pthread_mutex_lock(mutex);
pthread_mutex_unlock(mutex);
pthread_mutex_unlock(mutex);
在第二行发生阻塞,死锁。
2)多个互斥量,且不同线程各自锁住一个互斥量,并都在请求另一个互斥量时阻塞:
//thread A
pthread_mutex_lock(mutex1);
pthread_mutex_lock(mutex2);
pthread_mutex_unlock(mutex2);
pthread_mutex_unlock(mutex1);
//thread B
pthread_mutex_lock(mutex2);
pthread_mutex_lock(mutex1);
pthread_mutex_unlock(mutex1);
pthread_mutex_unlock(mutex2);
A锁住mutex1,在请求mutex2时,B已经锁住了mutex2.   而B在请求mutex1时,又被A锁住了。

避免死锁
    对于上面两种死锁情况,第一种是比较容易避免的,因为大多数情况下不会对同一个互斥量加锁两次,产生该类错误一般是程序员问题。
    针对第二种情况, 我们通过要求同一进程的各个进程都对所有的互斥量以相同的顺序进行加锁,来避免产生死锁。也就是对于任何一个线程pthread_mutex_lock(mutex1);一定
在pthread_mutex_lock(mutex2); 之前。
    一般我们使用一个散列列表锁来帮助我们实现,类似:
struct foo {
    int f_count;
    pthread_mutex_t f_lock;
    int f_id;
    /* ... more stuff here ... */
};

   

读写锁

    读写锁的作用是针对不同方式的共享资源访问,提供了更高的并行性。
    关键点:
    1)读写锁在读加锁状态时,任何其他的读加锁都可以获得访问权。
    2)写加锁只有在读写锁没有被加锁(不管在读加锁还是写加锁)的情况下才能成功,否则阻塞
    3)读写锁在写加锁状态时,任何其他的针对该读写锁的加锁都被阻塞。
    在实现过程中,为避免一直发生读加锁,导致写加锁一直阻塞,通常当发生写加锁请求时,会先阻塞随后的读加锁。以此避免等待着的写加锁一直得不到满足。
    下面列出读写锁的一些接口(类似互斥锁):
初始化与销毁
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t*restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
Both return: 0 if OK, error number on failure
加锁与解锁:
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
All return: 0 if OK, error number on failure
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
Both return: 0 if OK, error number on failure
#include <pthread.h>
#include <time.h>
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrictrwlock,const struct timespec*restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrictrwlock,const struct timespec *restric ttsptr);

读写锁的接口用法基本与互斥锁一样,知识其逻辑功能分读和写两种情况而已。

条件变量

    条件变量允许线程以无竞争的方式等待某个条件的发生。
    条件变量用结构体:pthread_cond_t表示。
    条件变量的接口函数:

#include <pthread.h>
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
Both return: 0 if OK, error number on failure
这是初始化和去初始化的过程。同样可以使用PTHREAD_COND_INITIALIZER对静态创建的条件变量进行初始化。
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,
const struct timespec *restrict tsptr);
pthread_cond_wait是理解条件变量的关键。该函数会发生下面过程
1)解锁互斥量mutex
2)阻塞等待条件变量唤醒,关于唤醒函数后面会介绍
3)加锁互斥量mutex。

关于pthread_cond_timedwait注意tsptr是表示一个时间点,比如等待3min中,则tsptr的值应该是当前时间加上3min

唤醒函数:
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
Both return: 0 if OK, error number on failure
Example:
#include <pthread.h>
struct msg {
struct msg *m_next;
    /* ... more stuff here ... */
};
struct msg *workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;

void
process_msg(void)
{
    struct msg *mp;
    for (;;) {
        pthread_mutex_lock(&qlock);
        while (workq == NULL)
            pthread_cond_wait(&qready, &qlock);
        mp = workq;
        workq = mp->m_next;
        pthread_mutex_unlock(&qlock);
        /* now process the message mp */
    }
}
void
enqueue_msg(struct msg *mp)
{
    pthread_mutex_lock(&qlock);
    mp->m_next = workq;
    workq = mp;
    pthread_mutex_unlock(&qlock);
    pthread_cond_signal(&qready);
}

Spin lock 自旋锁

    自旋锁与互斥锁类似。只是起阻塞的方式不同。对于互斥锁而言,如果互斥锁被其他线程锁住了,那么本线程要加锁时通过睡眠方式等待。而对于自旋锁,它加锁时,如果自旋锁被其它线程加锁,则本线程以循环方式等待,也就是在那里一直循环。 
    自旋锁的作用是提高加锁的效率。它一般用于使用者保持锁时间较短的情况。也就是说加锁和解锁时间很短的情况。
    关于自旋锁的接口:
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock,intpshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
Both return: 0 if OK, error number on failure
    
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
All return: 0 if OK, error number on failure

Barriers 计数锁

    计数锁用pthread_barrier_ 表示,它充当栏杆,阻塞线程,直到指定数目的线程到齐。
    接口:
#include <pthread.h>
int pthread_barrier_init(pthread_barrier_t *restrictbarrier,const pthread_barrierattr_t *restrict attr,unsigned int count);
int pthread_barrier_destroy(pthread_barrier_t *barrier);
Both return: 0 if OK, error number on failure
参数count用于指定要等待的线程数目。
#include <pthread.h>
int pthread_barrier_wait(pthread_barrier_t *barrier);
Returns: 0 orPTHREAD_BARRIER_SERIAL_THREADif OK, error number on failure
pthread_barrier_wait 用来是线程进入等待,然后检测计数锁是否满足释放条件了。
具体通俗的例子,8个人参加赛跑(count=0),每个运动员到起跑线 执行pthread_barrier_wait 等待其它运动员,并通知裁判(相当于内核)我到了,裁判在8个运动员都到了时,开始比赛 pthread_barrier_wait 返回。






















APUE学习笔记——11 线程同步、互斥锁、自旋锁、条件变量的更多相关文章

  1. APUE 学习笔记(八) 线程同步

    1. 进程的所有信息对该进程内的所有线程都是共享的 包括 可执行的程序文本.程序全局内存.堆内存以及文件描述符 线程包含了表示进程内执行环境必需的信息,包括线程ID.寄存器值.栈.调度优先级和策略.信 ...

  2. APUE学习笔记——11 线程基础

    线程标识 线程由线程号进行标识.线程号仅在线程所属的进程环境中有效.也就是说属于不同进程的两个线程可能线程号一样. 线程标识用结构体pthread_t tid表示.与线程Id相关的函数如下: 比较两个 ...

  3. APUE学习笔记6——线程和线程同步

    1 概念 线程是程序执行流的最小单元.线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的 ...

  4. python学习笔记11 ----线程、进程、协程

    进程.线程.协程的概念 进程和线程是操作系统中两个很重要的概念,对于一般的程序,可能有若干个进程,每一个进程有若干个同时执行的线程.进程是资源管理的最小单位,线程是程序执行的最小单位(线程可共享同一进 ...

  5. linux学习笔记之线程同步机制

    一.基础知识. 1:线程同步机制:互斥量,读写锁,条件变量,自旋锁,屏障. 1,互斥量:每个进程访问被互斥量保护的资源时,都需要先对互斥量进行判断. 1)互斥量重要属性:进程共享属性,健壮属性,类型属 ...

  6. Linux学习笔记21——线程同步的两种方式

    一  用信号量同步 1 信号量函数的名字都以sem_开头,线程中使用的基本信号量函数有4个 2 创建信号量 #include<semaphore.h> int sem_init(sem_t ...

  7. 第8章 用户模式下的线程同步(4)_条件变量(Condition Variable)

    8.6 条件变量(Condition Variables)——可利用临界区或SRWLock锁来实现 8.6.1 条件变量的使用 (1)条件变量机制就是为了简化 “生产者-消费者”问题而设计的一种线程同 ...

  8. C#学习笔记之线程 - 同步上下文

    同步上下文(Synchronization Contexts) 手动使用锁的一个替代方案是去声明锁.通过派生ContextBoundObject和应用Synchronization属性,你告诉CLR自 ...

  9. C++11并发学习之三:线程同步(转载)

    C++11并发学习之三:线程同步 1.<mutex> 头文件介绍 Mutex又称互斥量,C++ 11中与 Mutex 相关的类(包括锁类型)和函数都声明在 <mutex> 头文 ...

随机推荐

  1. OpenLDAP搭建全过程

    目        的:搭建一套完整的OpenLDAP系统,实现账号的统一管理.                     1:OpenLDAP服务端的搭建                     2:P ...

  2. Ant Design 常用命令汇总

    Ant Design React 安装 1. 安装脚手架工具# antd-init 是一个用于演示 antd 如何使用的脚手架工具,真实项目建议使用 dva-cli. $ npm install an ...

  3. javascript里用php

    <script type="text/javascript" > <?php if (!empty($searchResult)):?> $.searchM ...

  4. spring mvc 之初体验

    Spring MVC最简单的配置 配置一个Spring MVC只需要三步: 在web.xml中配置Servlet: 创建Spring MVC的xml配置文件: 创建Controller和View &l ...

  5. 链表中的倒数第k个结点

    题目描述 输入一个链表,输出该链表中倒数第k个结点.   基本思想:定义两个指针a,b分别指向头节点, a指针先向前走k-1步(注意:因为倒数节点是从倒数第一个结点开始的,而不是零),然后a指针和b指 ...

  6. select * from table_name where 1=1的

    我们先来看看这个语句的结果:select * from table where 1=1,其中where 1=1,由于1=1永远是成立的,返回TRUE,条件为真:所以,这条语句,就相当于select * ...

  7. Sql索引

    1.为什么要给表加上主键?建表的时候都会为表加上主键, 在某些关系数据库中, 如果建表时不指定主键,数据库会拒绝建表的语句执行. 一个没加主键的表,并不能被称之为「表」.一个没加主键的表,它的数据无序 ...

  8. nignx的master进程和worker进程的作用

    ngnix进程启动启动后会有一个master进程和多个worker进程. master进程的主要作用: 1.读取并验证配置信息: 2.创建,绑定及关闭套接字: 3.启动,终止worker进程以及维护w ...

  9. 在Eclipse中快速添加main方法

    方法一: 在创建类时自动添加,勾选“public static void main(String[]   args)” 方法二: 输入main之后按"alt+/"组合键,选择如图所 ...

  10. 为什么CPU要从单核发展到多核?

    前言 这里首先直接给出结论:CPU从单核发展到多核的原因是如果维持单核,则为了提高CPU性能只能不断提高时钟频率,从而会导致CPU功耗急速上升,导致机箱过热,来不及散热. 历史 2004年,Intel ...