前言

上一篇说过,系统会为线程mmap一块内存,每个线程有自己的私有栈,使用局部变量没啥问题。但是实际场景中不可避免的需要线程之间共享数据,这就需要确保每个线程看到的数据是一样的,如果大家都只需要读这块数据没有问题,但是当有了修改共享区域的需求时就会出现数据不一致的问题。甚至线程2的任务在执行到某个地方的时候,需要线程1先做好准备工作,出现顺序依赖的情况。为了解决这些问题,Linux提供了多种API来适用于不同的场景。

互斥量 mutex

排他的访问共享数据,锁竞争激烈的场景使用。锁竞争不激烈的情况可以使用自旋锁(忙等)

当我们用trace -f 去追踪多线程的时候会看到执行加锁解锁的调用是futex,glibc通过futex(fast user space mutex)实现互斥量。通过FUTEX_WAIT_PRIVATE标志的futex调用内核的futex_wait挂起线程,通过FUTEX_WAKE_PRIVATE的futex调用内核的futex_wake来唤醒等待的线程。这之中glibc做了优化:

  • 加锁时,当前mutex没有被加锁,则直接加锁,不做系统调用,自然不需要做上下文切换。如果已经加锁则需要系统调用futex_wait让内核将线程挂起到等待队列
  • 解锁时,没有其他线程在等待该mutex,直接解锁,不做系统调用。如果有其他线程在等待,则通过系统调用futex_wake唤醒等待队列中的一个线程

初始化互斥量

#include <pthread.h>
// 动态初始化并设置互斥量属性,用完需要销毁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
// attr 设置mutex的属性,NULL为使用默认属性
// 返回值:成功返回0,失败返回错误编号 // 静态初始化,无需销毁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

销毁互斥量

// 销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 返回值:成功返回0,失败返回错误编号。
// 如果互斥量是锁定状态,或者正在和条件变量共同使用,销毁会返回EBUSY

加锁和解锁

  1. 使用pthread_mutex_lock加锁
#include <pthread.h>
// 阻塞
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 返回值:成功返回0,失败返回错误编号 // 非阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 返回值:加锁成功直接返回0,加锁失败返回EBUSY int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 返回值:成功返回0,失败返回错误编号

调用状态:

  • 调用时互斥量未锁定,该函数所在线程争取到mutex,返回。
  • 调用时已有其他线程对mutex加锁,则阻塞等待mutex被释放后重新尝试加锁

重复调用问题,即本线程已经对mutex加锁,再次调用加锁操作时,根据互斥量的类型不同会有不同表现:

  • PTHREAD_MUTEX_TIMED_NP:重复加锁导致死锁,该调用线程永久阻塞,并且其他线程无法申请到该mutex
  • PTHREAD_MUTEX_ERRORCHECK_NP:内部记录着调用线程,重复加锁返回EDEADLK,如果解锁的线程不是锁记录的线程,返回EPERM
  • PTHREAD_MUTEX_RECURSIVE_NP:允许重复加锁,锁内部维护着引用计数和调用线程。如果解锁的线程不是锁记录的线程,返回EPERM
  • PTHREAD_MUTEX_ADAPTIVE_NP(自适应锁):先自旋一段时间,自旋的时间由__spins和MAX_ADAPTIVE_COUNT共同决定,自动调整__spin的大小但是不会超过MAX_ADAPTIVE_COUNT。超过自旋时间让出CPU等待,比自旋锁温柔,比normal mutex激进。

设置mutex属性

// 设置mutex为ADAPTER模式
pthread_mutexattr_t mutexattr;
pthread_mutexattr_init(&mutexattr);
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_ADAPTIVE_NP); // 获取mutex模式
int kind;
pthread_mutexattr_gettype(&mutexattr, &kind);
if (kind == PTHREAD_MUTEX_ADAPTIVE_NP) {
printf("mutex type is %s", "PTHREAD_MUTEX_ADAPTIVE_NP\n");
}

带有超时的mutex

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
// abstime表示在该时间之前阻塞,不是时间间隔
// 成功返回0,失败返回错误编号,超时返回ETIMIEOUT

demo

对已经加锁的mutex继续使用timedlock加锁,timedlock超时返回,之后mutex解锁

#define _DEFAULT_SOURCE 1
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h> char* now_time(char buf[]) {
struct timespec abstime;
abstime.tv_sec = time(0);
strftime(buf, 1024, "%r", localtime(&abstime.tv_sec));
return buf;
} int main() {
char buf[1024];
pthread_mutex_t mutex;
struct timespec abstime;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
char* now = now_time(buf);
printf("mutex locked, now: %s\n", buf);
// 设置超时的绝对时间,不设置tv_nsec会返回22,EINVAL
abstime.tv_sec = time(0) + 10;
abstime.tv_nsec = 0;
int ret = pthread_mutex_timedlock(&mutex, &abstime);
fprintf(stderr, "error %d\n", ret);
if (ret == ETIMEDOUT) {
printf("lock mutex timeout\n");
} else if (ret == 0) {
printf("lock mutex successfully\n");
} else if (ret == EINVAL) {
printf("timedlock param invalid!\n");
} else {
printf("other error\n");
}
pthread_mutex_unlock(&mutex);
memset(buf, '\0', 1024);
now = now_time(buf);
printf("mutex unlocked, now: %s\n", buf);
pthread_mutex_destroy(&mutex);
return 0;
} // -----------------------------
root@yielde:~/workspace/code-container/cpp/blog_demo# ./test
mutex locked, now: 08:18:34 PM
error 110
lock mutex timeout
mutex unlocked, now: 08:18:44 PM

读写锁

读写锁适用于临界区很大并且在大多数情况下读取共享资源,极少数情况下需要写的场景

  1. 未加锁:加读、写锁都可以
  2. 加读锁:再次尝试加读锁成功,写锁阻塞
  3. 加写锁:再次尝试加读、写锁阻塞

常用接口与mutex类似,用的时候查https://man7.org/linux/man-pages/dir_section_3.html,读写锁有两种策略:

PTHREAD_RWLOCK_PREFER_READER_NP, // 读者优先
PTHREAD_RWLOCK_PREFER_WRITER_NP, // 读者优先
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP, // 写者优先
PTHREAD_RWLOCK_DEFAULT_NP = PTHREAD_RWLOCK_PREFER_READER_NP // 通过以下函数设置
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
int pthread_rwlockattr_getkind_np(const pthread_rwlockattr_t *attr, int *pref);

读写锁存在的问题:

  1. 如果临界区小,锁内部维护的数据结构多于mutex,性能不如mutex
  2. 因为有读优先和写优先的策略,使用不当会出现读或写线程饿死的现象
  3. 如果是写策略优先,线程1持有读锁,线程2等待加写锁,线程1再次加读锁,就出现了死锁情况

demo

启动5个线程共同对一个变量累加1,使用读写锁让线程并发,用自适应锁对共享变量加锁。

/*
5个线程对total加1执行指定次数
*/ #define _DEFAULT_SOURCE 1 // 处理vscode 未定义 pthread_rwlock_t
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> #define THREAD_COUNT 5 int total = 0; // 最终和
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 初始化互斥量
pthread_rwlock_t rwlock; // 读写锁变量
typedef struct param { // 线程参数类型
int count;
int id;
} param; void *handler(void *arg) {
struct param *pa = (struct param *)arg;
pthread_rwlock_rdlock(&rwlock); // 当主线程不unlock写锁时,会阻塞在这里
for (int i = 0; i < pa->count; ++i) {
pthread_mutex_lock(&mutex); // 加互斥锁
++total;
pthread_mutex_unlock(&mutex);
}
pthread_rwlock_unlock(&rwlock);
printf("thread %d complete\n", pa->id);
return NULL;
} int main(int argc, char *argv[]) {
if (argc != 2) {
printf("usage: %s per_thread_loop_count\n", argv[0]);
return 1;
}
// 设置mutex为ADAPTER模式
pthread_mutexattr_t mutexattr;
pthread_mutexattr_init(&mutexattr);
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_ADAPTIVE_NP);
// 给handler传参
int loop_count = atoi(argv[1]);
// 存放线程id的数组
pthread_t tid[THREAD_COUNT];
param pa[THREAD_COUNT]; pthread_rwlock_init(&rwlock, NULL); // 动态初始化读写锁
pthread_rwlock_wrlock(&rwlock); // 给写加锁,等所有线程创建好后解锁,线程执行
for (int i = 0; i < THREAD_COUNT; ++i) { // 创建5个线程
pa[i].count = loop_count;
pa[i].id = i;
pthread_create(&tid[i], NULL, handler, &pa[i]);
} pthread_rwlock_unlock(&rwlock);
for (int i = 0; i < THREAD_COUNT; ++i) {
pthread_join(tid[i], NULL);
}
pthread_rwlock_destroy(&rwlock);
printf("thread count: %d\n", THREAD_COUNT);
printf("per thread loop count: %d\n", loop_count);
printf("total except: %d\n", loop_count * 5);
printf("total result: %d\n", total); int kind;
pthread_mutexattr_gettype(&mutexattr, &kind);
if (kind == PTHREAD_MUTEX_ADAPTIVE_NP) {
printf("mutex type is %s", "PTHREAD_MUTEX_ADAPTIVE_NP\n");
}
return 0;
} // --------------------------------
root@yielde:~/workspace/code-container/cpp/blog_demo# ./test 2000
thread 2 complete
thread 1 complete
thread 0 complete
thread 3 complete
thread 4 complete
thread count: 5
per thread loop count: 2000
total except: 10000
total result: 10000
mutex type is PTHREAD_MUTEX_ADAPTIVE_NP

自旋锁

等待锁的时候不会通知内个将线程挂起,而是忙等。适用于临界区很小,锁被持有的时间很短的情况,相比于互斥锁,节省了上下文切换的开销

线程同步-屏障

barrier可以同步多个线程,允许任意数量的线程等待,直到所有的线程完成工作,然后继续执行

#include <pthread.h>

int pthread_barrier_destroy(pthread_barrier_t *barrier);
// 返回值:成功返回0,失败返回错误号
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
const pthread_barrierattr_t *restrict attr, unsigned count);
// count指定有多少个线程到达屏障后再继续执行下去
// 返回值:成功返回0,失败返回错误号 int pthread_barrier_wait(pthread_barrier_t *barrier);
// 成功:给一个线程返回PTHREAD_BARRIER_SERIAL_THREAD,其他线程返回0
// 失败返回错误号

demo

使用4个线程,每个线程计算1+1+..+1=10,将结果放入数组的一个位置,完成后到达barrier。主线程创建好线程后到达barrier,等四个线程全部完成后,由主线程合计结果

#define _DEFAULT_SOURCE
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#define COUNT 10
#define THR_NUM 4 pthread_barrier_t barrier;
long total_arr[THR_NUM] = {0}; void *handler(void *arg) {
long idx = (long)arg;
long tmp = 0;
for (int i = 0; i < COUNT; ++i) {
++tmp;
sleep(1);
}
total_arr[idx] = tmp;
printf("thread %ld complete, count %ld\n", idx, tmp);
pthread_barrier_wait(&barrier); // 等待在barrier
return NULL;
} int main() {
pthread_t tids[THR_NUM];
unsigned long total = 0; pthread_barrier_init(&barrier, NULL, THR_NUM + 1); // 包含主线程
for (long i = 0; i < THR_NUM; ++i) {
pthread_create(&tids[i], NULL, handler, (void *)i);
}
pthread_barrier_wait(&barrier); // 到达barrier
for (int i = 0; i < THR_NUM; ++i) {
total += total_arr[i];
} for (int i = 0; i < THR_NUM; ++i) {
pthread_join(tids[i], NULL);
}
pthread_barrier_destroy(&barrier); // 销毁barrier
printf("total: %lu\n", total);
} // ---------------------
root@yielde:~/workspace/code-container/cpp/blog_demo# time ./test
thread 2 complete, count 10
thread 0 complete, count 10
thread 3 complete, count 10
thread 1 complete, count 10
total: 40 real 0m10.027s
user 0m0.005s
sys 0m0.003s

线程同步-条件变量

如果条件不满足,线程会等待在条件变量上,并且让出mutex,等待其他线程来执行。其他线程执行到条件满足后会发信号唤醒等待的线程。

// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond); // 初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr); // 等待条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex); // 通知条件变量满足
int pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒所有线程
int pthread_cond_signal(pthread_cond_t *cond); // 至少唤醒1个线程
//返回值成功返回0,失败返回错误号

对于 cond_wait,传递mutex保护条件变量,调用线程将锁住的mutex传给函数,函数将调用线程挂起到等待队列上,解锁互斥量。当函数返回时,互斥量再次被锁住。

demo

handler_hello往buf里输入字符串,由handler_print打印

#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 初始化互斥量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 初始化条件变量 char buf[8] = {0}; void *handler_hello(void *arg) {
for (;;) {
sleep(2);
pthread_mutex_lock(&mutex);
sprintf(buf, "%s", "hello !");
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond); // 唤醒wait的线程
} return NULL;
} void *handler_print(void *arg) {
for (;;) {
pthread_mutex_lock(&mutex);
while (buf[0] == 0) {
// 如果buf没有内容就等待,此处将线程挂入队列,然后解锁mutex,等收到handler_hello的signal后返回,加锁mutex
//
pthread_cond_wait(&cond, &mutex);
}
fprintf(stderr, "%s", buf);
memset(buf, '\0', 8);
pthread_mutex_unlock(&mutex);
}
return NULL;
} int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, handler_hello, NULL);
pthread_create(&tid2, NULL, handler_print, NULL); pthread_join(tid1, NULL);
pthread_join(tid2, NULL); printf("%s", buf);
return 0;
} // ------------------------
root@yielde:~/workspace/code-container/cpp/blog_demo# ./test
hello !hello !hello !hello !^C

学习自:

《UNIX环境高级编程》

《Linux环境编程从应用到内核》高峰 李彬 著

Linux线程间交互的更多相关文章

  1. linux线程间同步方式汇总

    抽空做了下linux所有线程间同步方式的汇总(原生的),包含以下几个: 1, mutex 2, condition variable 3, reader-writer lock 4, spin loc ...

  2. linux线程间同步方式总结梳理

    线程间一般无需特别的手段进行通信,由于线程间能够共享数据结构,也就是一个全局变量能够被两个线程同时使用.只是要注意的是线程间须要做好同步! 使用多线程的理由: 1. 一个是和进程相比,它是一种非常&q ...

  3. Linux线程间同步的几种方式

    信号量 信号量强调的是线程(或进程)间的同步:"信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在sem_wait的时候,就阻塞 ...

  4. linux 线程间发送信号

    线程间通过 pthread_kill(thid,signo)给指定的thid线程发送signo信号. 创建线程与线程屏蔽字顺序 1. pthread_create();    pthread_sigm ...

  5. linux线程间同步(1)读写锁

    读写锁比mutex有更高的适用性,能够多个线程同一时候占用读模式的读写锁.可是仅仅能一个线程占用写模式的读写锁. 1. 当读写锁是写加锁状态时,在这个锁被解锁之前,全部试图对这个锁加锁的线程都会被堵塞 ...

  6. Linux 线程间的同步与互斥

    在线程并发执行的时候,我们需要保证临界资源的安全访问,防止线程争抢资源,造成数据二义性. 线程同步: 条件变量 为什么使用条件变量? 对临界资源的时序可控性,条件满足会通知其他等待操作临界资源的线程, ...

  7. 多线程学习之AsyncOperation实现线程间交互

    1.首先我们要实现如下图的效果:                                                          a.主线程A运行方法段1时创建子线程B b.然后子线 ...

  8. Linux进程间通信与线程间同步详解(全面详细)

    引用:http://community.csdn.net/Expert/TopicView3.asp?id=4374496linux下进程间通信的几种主要手段简介: 1. 管道(Pipe)及有名管道( ...

  9. 【java线程系列】java线程系列之线程间的交互wait()/notify()/notifyAll()及生产者与消费者模型

    关于线程,博主写过java线程详解基本上把java线程的基础知识都讲解到位了,但是那还远远不够,多线程的存在就是为了让多个线程去协作来完成某一具体任务,比如生产者与消费者模型,因此了解线程间的协作是非 ...

  10. Linux的进程/线程间通信方式总结

    Linux系统中的进程间通信方式主要以下几种: 同一主机上的进程通信方式 * UNIX进程间通信方式: 包括管道(PIPE), 有名管道(FIFO), 和信号(Signal) * System V进程 ...

随机推荐

  1. MyBatis Mapper.XML 标签使用说明

    直接将值返回给对象 <select id="list" resultType="com.vipsoft.base.entity.UserInfo"> ...

  2. WebService autoconfigure.web.servlet.DispatcherServletPath

    SpringBoot WebService 源代码:https://gitee.com/VipSoft/VipWebService SpringBoot 整合 WebService cxf 报错 Co ...

  3. 声明式调用 —— SpringCloud OpenFeign

    Feign 简介 Spring Cloud Feign 是一个 HTTP 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 HTTP 请求,而不用通过封装 HTTP 请求报文的方式直接调用 ...

  4. 解密Prompt系列1. Tunning-Free Prompt:GPT2 & GPT3 & LAMA & AutoPrompt

    借着ChatGPT的东风,我们来梳理下prompt范式的相关模型.本系列会以A Systematic Survey of Prompting Methods in Natural Language P ...

  5. 接口文档 token发展史 jwt介绍和原理 drf-jwt快速使用

    目录 昨日回顾 认证 权限 频率 全局异常处理 接口文档 接口文档编写 drf自动生成接口文档 cookies-session-token发展史 jwt介绍和原理 jwt的构成 base64的编码和解 ...

  6. Grafana--变量(label_values)

    grafana官方文档:https://grafana.com/docs/grafana/latest/ 版本:6.5.2 模板与变量(摘抄自官方文档): 变量是值的占位符.您可以在度量查询和面板标题 ...

  7. 面试官:请聊一聊String、StringBuilder、StringBuffer三者的区别

    面试官:"小伙子,在日常的写代码过程中,使用过String,StringBuilder和StringBuffer没?" 我:"用过的呀!" 面试官:" ...

  8. 【Cxx 20】使用 std::span 代替数组指针传参

    我们知道std::string_view可以创建std::string的一个视图,视图本身并不拥有实例,它只是保持视图映射的状态.在不修改实例的情况下,使用std::string_view会让字符串处 ...

  9. Latex公式排版问题总结

    Latex写博客和论文,因为有模板,所以用起来还是很方便的. 但是在实际使用中,由于论文是双栏的,因此比较长的公式在排版时会比较困难.下面对Latex中的公式排版方法做一些记录. Latex公式排版( ...

  10. Gosper's Hack (生成 n元集合所有 k 元子集

    Gosper's Hack是一种生成 n元集合所有 k元子集的算法,它巧妙地利用了位运算 void GospersHack(int k, int n) { int cur = (1 << ...