线程同步概念

2023-10-29

带着问题去思考:

  1. 什么是线程同步
  2. 线程同步能解决哪些问题
  3. 如何实现线程同步

线程同步是指两个或多个线程协同步调,按预期的顺序执行代码
若两个或多个线程同时写同一块内存或访问同一资源时,需线程同步
若线程A的执行依赖线程B的结果,需线程同步(输出A1B2C3问题)

解决:

  • 保证数据完整性,解决内存写覆盖,资源并发访问冲突/异常问题
  • 保证线程执行先后顺序,解决线程间先后执行的问题

实现线程同步的方式:
临界区对象、互斥锁、自旋锁、信号量、事件、条件变量

临界区对象:用户态对象,用于单个线程取得对某个数据区的访问权限
互斥锁:内核态对象,用于某个进程的单个线程取得对某个数据区的访问权限,可用于多进程同步
自旋锁:改进的互斥锁,线程在等待时不交回CPU控制权
信号量:内核态对象,用于资源计数的线程同步方式,可允许指定个数的线程取得对某个数据区的访问权限
事件:内核态对象,用来发信号通知其他线程,保证线程的先后执行
条件变量:一个可自定义的线程同步对象,即可实现取得对某个数据的访问权限,又可用于通知其他线程


1. 基本概念

种进程或线程同步互斥的控制方法

  1. 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
  2. 互斥量/互斥锁:为协调共同对一个共享资源的单独访问而设计的;因为进入内核模式,所以性能比临界区差;跨进程,可用于防止程序重复打开运行。
  3. 自旋锁:一种互斥锁的实现,等待的时候会占用CPU,通过循环判断锁是否被释放,因此比较快速,但是一直占用CPU时间。
  4. 信号量:为控制一个具有有限数量用户资源而设计,互斥锁可以理解为1个用户资源的信号量。
  5. 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。
  6. 条件变量:条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。

几个概念:

  • 临界区、竞态条件:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件;导致竞态条件发生的代码区称作临界区。

2. VC++下的4种线程同步方式

2.1 临界区(Critical Section)

保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。

临界区包含两个操作原语:

EnterCriticalSection() // 进入临界区
LeaveCriticalSection() // 离开临界区

EnterCriticalSection()语句执行后代码将进入临界区以后无论发生什么,必须确保与之匹配的 LeaveCriticalSection()都能够被执行到。否则临界区保护的共享资源将永远不会被释放。虽然临界区同步速度很快,但却只能用来同步本 进程内的线程,而不可用来同步多个进程中的线程。

MFC提供了很多功能完备的类,我用MFC实现了临界区。MFC为临界区提供有一个 CCriticalSection类,使用该类进行线程同步处理是 非常简单的。只需在线程函数中用CCriticalSection类成员函数Lock()和UnLock()标定出被保护代码片段即可。Lock()后代 码用到的资源自动被视为临界区内的资源被保护。UnLock后别的线程才能访问这些资源。

临界区示例(头文件Window.h):

CRITICAL_SECTION mCriticalSection; // 定义
InitializeCriticalSection(&mCriticalSection); // 初始化
EnterCriticalSection(&mCriticalSection); // 进入临界区
LeaveCriticalSection(&mCriticalSection); // 离开临界区
DeleteCriticalSection(&mCriticalSection); // 删除

2.2互斥量/互斥锁(Mutex)

互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
互斥量包含的几个操作原语:

CreateMutex()   // 创建一个互斥量
OpenMutex()     // 打开一个互斥量
ReleaseMutex()  // 释放互斥量
WaitForMultipleObjects()    // 等待互斥量对象

同样MFC为互斥量提供有一个CMutex类。使用CMutex类实现互斥量操作非常简单,但是要特别注意对CMutex的构造函数的调用

CMutex( BOOL bInitiallyOwn = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL)

不用的参数不能乱填,乱填会出现一些意想不到的运行结果。

2.3 信号量(Semaphores)

信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源 ,这与操作系统中的PV操作相同。它指出了同时访问共享 资源的线程 最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量 时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数 就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目, 不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可 用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。

PV操作及信号量的概念都是由荷兰科学家E.W.Dijkstra提出的。信号量S是一个整数,S大于等于零时代表可供并发进程使用的资源实体数,但S小于零时则表示正在等待使用共享资源的进程数。
P操作 申请资源:

(1) S减1;

(2) 若S减1后仍大于等于零,则进程继续执行;

(3) 若S减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转入进程调度。
V操作 释放资源:

(1) S加1;

(2) 若相加结果大于零,则进程继续执行;

(3) 若相加结果小于等于零,则从该信号的等待队列中唤醒一个等待进程,然后再返回原进程继续执行或转入进程调度。

信号量包含的几个操作原语:

CreateSemaphore()   // 创建一个信号量
OpenSemaphore()     // 打开一个信号量
ReleaseSemaphore()  // 释放信号量
WaitForSingleObject()   // 等待信号量

2.4 事件(Event)

事件对象也可以通过通知操作的方式来保持线程的同步。并且可以实现不同进程中的线程同步操作。
信号量包含的几个操作原语:

CreateEvent()   // 创建一个事件
OpenEvent()     // 打开一个事件
SetEvent()      // 回置事件
WaitForSingleObject()       // 等待一个事件
WaitForMultipleObjects()    // 等待多个事件

WaitForMultipleObjects 函数原型:

WaitForMultipleObjects(
    IN DWORD nCount,            // 等待句柄数
    IN CONST HANDLE *lpHandles, // 指向句柄数组
    IN BOOL bWaitAll,           // 是否完全等待标志
    IN DWORD dwMilliseconds     // 等待时间
)

参数nCount指定了要等待的内核对象的数目,存放这些内核对象的数组由lpHandles来指向。fWaitAll对指定的这nCount个内核对象的两种等待方式进行了指定,为TRUE时当所有对象都被通知时函数才会返回,为FALSE则只要其中任何一个得到通知就可以返回。 dwMilliseconds在这里的作用与在WaitForSingleObject()中的作用是完全一致的。如果等待超时,函数将返回 WAIT_TIMEOUT。

3. pthread种条件变量的使用

/*

pthread 条件变量学习

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;
另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

1. 创建和注销
静态创建方法: pthread_cond_t   cond=PTHREAD_COND_INITIALIZER
动态调用pthread_cond_init()函数

注销:pthread_cond_destroy()

2. 等待和激发
// 无条件等待
int pthread_cond_wait(pthread_cond_t   *cond,   pthread_mutex_t   *mutex)   
// 计时等待,超时退出等待
int pthread_cond_timedwait(pthread_cond_t   *cond,   pthread_mutex_t   *mutex,   const   struct   timespec   *abstime) 

// 激活一个等待的线程
pthread_cond_signal()
// 激活全部等待的线程
pthread_cond_broadcast()

*/
#include <pthread.h>
#include <unistd.h>

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

struct node
{
    int n_number;
    struct node *n_next;
} *head = NULL;

/*[thread_func]*/
static void cleanup_handler(void *arg)
{
    printf("Cleanup handler of second thread./n");
    free(arg);
    (void)pthread_mutex_unlock(&mtx);
}

static void *thread_func(void *arg)
{
    struct node *p = NULL;

    pthread_cleanup_push(cleanup_handler, p);
    while (1)
    {
        pthread_mutex_lock(&mtx); //这个mutex主要是用来保证pthread_cond_wait的并发性
        while (head == NULL)
        {                                   //这个while要特别说明一下,单个pthread_cond_wait功能很完善,为何这里要有一个while (head == NULL)呢?因为pthread_cond_wait里的线程可能会被意外唤醒,如果这个时候head != NULL,则不是我们想要的情况。这个时候,应该让线程继续进入pthread_cond_wait
            pthread_cond_wait(&cond, &mtx); // pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mtx,然后阻塞在等待对列里休眠,直到再次被唤醒(大多数情况下是等待的条件成立而被唤醒,唤醒后,该进程会先锁定先pthread_mutex_lock(&mtx);,再读取资源
                                            //用这个流程是比较清楚的/*lock-->unlock-->wait() return-->lock*/
        }
        p = head;
        head = head->n_next;
        printf("Got %d from front of queue/n", p->n_number);
        free(p);
        pthread_mutex_unlock(&mtx); //临界区数据操作完毕,释放互斥锁
    }
    pthread_cleanup_pop(0);
    return 0;
}

int main(void)
{
    pthread_t tid;
    int i;
    struct node *p;
    pthread_create(&tid, NULL, thread_func, NULL); //子线程会一直等待资源,类似生产者和消费者,但是这里的消费者可以是多个消费者,而不仅仅支持普通的单个消费者,这个模型虽然简单,但是很强大
    /*[tx6-main]*/
    for (i = 0; i < 10; i++)
    {
        p = malloc(sizeof(struct node));
        p->n_number = i;
        pthread_mutex_lock(&mtx); //需要操作head这个临界资源,先加锁,
        p->n_next = head;
        head = p;
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mtx); //解锁
        sleep(1);
    }
    printf("thread 1 wanna end the line.So cancel thread 2./n");
    pthread_cancel(tid); //关于pthread_cancel,有一点额外的说明,它是从外部终止子线程,子线程会在最近的取消点,退出线程,而在我们的代码里,最近的取消点肯定就是pthread_cond_wait()了。关于取消点的信息,有兴趣可以google,这里不多说了
    pthread_join(tid, NULL);
    
    // 激活全部等待的线程
    // pthread_cond_broadcast(&cond);
    printf("All done -- exiting/n");
    return 0;
}

3. 总结

  • 互斥量/互斥锁与临界区的作用非常相似,但互斥量是可以命名的,也就是说它可以跨越进程使用。所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量 。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。
  • 互斥量(Mutex),信号量(Semaphore),事件(Event)都可以被跨越进程使用来进行同步数据操作,而其他的对象与数据同步操作无关,但对于进程和线程来讲,如果进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以可以使用WaitForSingleObject来等待进程和线程退出。
  • 通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号量对象可以说是一种资源计数器。

关于更详细的一篇介绍,请参考:
http://www.cppblog.com/killsound/archive/2009/07/15/16147.html

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

线程同步概念 的相关文章

  • Qt5实现与单片机ATS89S51通信

    Qt实现与单片机直接的通信上位机 单片机代码 测试环境 项目目标 实现效果 关键通信类 QSerialport 总结 这是我大二下学期的单片机课设做的一个小项目 实现上位机与下位机之间的通信 测试环境 开发环境 Qt5 96 Mingw32
  • 每天都在谈SOA和微服务,但你真的理解什么是服务吗?

    近几年来 我一直从事着和面向服务相关的底层软件研发工作 逐渐的形成了一些自己的看法 其中我觉得比较重要的看法就是服务需要一个更准确细致的定义 简单来说 服务的本质就是行为 业务活动 的抽象 为了更好的阐述新服务的概念 并方便与传统的SOA中
  • C/C++语言实现WiFi(socket)数据收发(客户端和服务端)

    目录 客户端 client 服务端 server C C 实现TCP通信 接收WIFI数据 编程环境 VC 6 0 手机端 使用WiFi调试助手 提示 整个过程在局域网中进行 很多编程语言都可以实现socket通信 本博客将通过C C 实现
  • [原]Pro*C介绍-内嵌SQL

    Translate by Z Jingwei Document address http www db stanford edu ullman fcdb oracle or proc html Pro C介绍内嵌SQL 概要 Pro C语法
  • C++工程师复习题

    一 auto ptr 类使用必须满足下列限制 1 不要使用 auto ptr 对象保存指向静态分配对象的指针 2 不要使用两个 auto ptrs 对象指向同一对象 3 不要使用 auto ptr 对象保存指向动态分配数组的指针 4 不要将
  • 编写程序模拟完成动态分区存储管理方式的内存分配和回收。

    usr bin python coding utf 8 class Table object 空闲分区表 0 开始地址 1 长度 freeTable 占用分区表 0 程序名 1 开始地址 2 长度 useTable def init sel
  • 写时拷贝技术(copy-on-write)

    传统的fork 系统调用直接把所有的资源复制给新创建的进程 这种实现过于简单并且效率低下 因为它拷贝的数据也许并不共享 更糟的情况是 如果新进程打算立即执行一个新的映像 那么所有的拷贝都将前功尽弃 Linux的fork 使用写时拷贝 cop
  • typedef struct 用法详解

    typedef为C语言的关键字 作用是为一种数据类型定义一个新名字 当typedef与结构结合使用时 会有一些比较复杂的情况 而且在C语言和C 里面有略有差别 本文将详细讲解typedef struct的用法 第一篇 typedef str
  • 大端模式和小端模式转化

    在工作中遇到一个问题 数据是以大端模式存储的 而机器是小端模式 必须进行转换 否则使用时会出问题 一 定义 大端模式 Big Endian 数据的高字节 保存在内存的低地址中 数据的低字节 保存在内存的高地址中 小端模式 Little En
  • 【C++】VS code如何配置使用C++(手把手教学)

    博 主 米码收割机 技 能 C Python语言 公众号 测试开发自动化 获取源码 商业合作 荣 誉 阿里云博客专家博主 51CTO技术博主 专 注 专注主流机器人 人工智能等相关领域的开发 测试技术 VS code如何配置使用C 手把手教
  • 经典面试题之new和malloc的区别

    new和malloc的区别是C C 一道经典的面试题 我也遇到过几次 回答的都不是很好 今天特意整理了一下 0 属性 new delete是C 关键字 需要编译器支持 malloc free是库函数 需要头文件支持 1 参数 使用new操作
  • Dev-C++之开启装逼效果

    Dev C 是个不错的C IDE 在10年前 它是很不错 在现在 它是个以界面丑陋和调试像吃粑粑这两点著称 如下图 实在是丑到离谱 丑到无法忍受 可是没办法呀 人家CCF规定比赛用这个 你个小蒟蒻吵什么 我现在就来讲讲怎么把你的Dev C
  • 手把手教你如何写一个三子棋/N子棋的小游戏

    这里写目录标题 第一步 游戏进入界面 第二步 初始化棋盘 第三步 打印棋盘 第四步 玩家和电脑下棋 第五步 判断输赢 三子棋或者N子棋怎么写 让我们先来玩一把 再来看看怎么写 程序运行界面 1为玩游戏 2为清屏 0为退出游戏 我们选1 然后
  • 一个简单的参数帮助框架,c实现

    文章目录 具体实现如下 include
  • enable_shared_from_this使用介绍

    文章目录 enable shared from this定义 使用场合 源码实现 注意 enable shared from this定义 定义于头文件 template lt class T gt class enable shared
  • 【C++】运算符重载

    加号运算符重载 include
  • C++中的并发多线程网络通讯

    C 中的并发多线程网络通讯 一 引言 C 作为一种高效且功能强大的编程语言 为开发者提供了多种工具来处理多线程和网络通信 多线程编程允许多个任务同时执行 而网络通信则是现代应用程序的基石 本文将深入探讨如何使用C 实现并发多线程网络通信 并
  • C/C++编程:令人印象深刻的高级技巧案例

    C C 编程语言在软件开发领域有着悠久的历史 由于其高效 灵活和底层访问能力 至今仍然被广泛应用 本文将介绍一些在C C 编程中令人印象深刻的高级技巧 帮助读者提升编程水平 更加高效地使用这两种强大的编程语言 一 指针运算与内存管理 C C
  • C 语言运算符详解

    C 语言中的运算符 运算符用于对变量和值进行操作 在下面的示例中 我们使用 运算符将两个值相加 int myNum 100 50 虽然 运算符通常用于将两个值相加 就像上面的示例一样 它还可以用于将变量和值相加 或者将变量和另一个变量相加
  • Woocommerce:添加第二个电子邮件地址不起作用,除非收件人是管理员

    我尝试了多种方法来向 Woocommerce 电子邮件添加其他收件人 但它似乎仅适用于主要收件人是管理员的测试订单 这些是我尝试过的片段 如果订单的客户是管理员 则电子邮件将发送到这两个地址 如果订单包含客户电子邮件地址 则仅发送至该电子邮

随机推荐

  • Qt 插件路径(笔记)

    Qt Manual 已经专门介绍了Deploying Plugins 的问题 半年前Qt 插件学习 一 也简单整理了一点路径相关的问题 可是 一直以来没理清 图片插件 编解码插件 数据库插件 到底是如何被加载的 走马观花 如果我们需要打开或
  • arcgis墨卡托与经纬度之间的互相转换

    使用 esri geometry webMercatorUtils 方法 经纬度转墨卡托 webMercatorUtils lngLatToXY x y 返回墨卡托坐标 merx mery 墨卡托转经纬度 webMercatorUtils
  • EasyExcel读取excel读复杂表头文件

    最近在项目开发中 遇到的一个excel复杂表头的导入数据库操作 具体怎么做 直接上代码吧 1 文件上传 把你要导入的文件上传磁盘某个目录 当然你也可以导入到项目目录下都行 该类的位置就是controller层 给用户提供一个上传文件的接口
  • 子集生成算法——增量构造法

    我的个人博客 逐步前行STEP 思路是一次选出一个元素放入集合中 生成0 n的子集 每次选出最小的值放入集合中 通过从0递增得到下一个位置的值 include
  • String常量池问题的几个例子

    String常量池问题的几个例子 示例1 Java代码 String s0 kvill String s1 kvill String s2 kv ill System out println s0 s1 System out println
  • 剑指Offer-40

    题目 一个整型数组里除了两个数字之外 其他的数字都出现了两次 找出只出现一次的数字 要求时间复杂度是 O n 空间复杂度是 O 1 实现 coding java public class Solution40 用 Hashmap 的方式 时
  • ES3~ES6数组的方法总结

    ES3数组的方法 push arr push 值 向数组的最后一个位置添加一个元素 语法 arr push 返回值 改变之后的数组的长度 改变原数组 var arr aa bb cc var res arr push dd console
  • day08-编程题

    每日作业 JavaSE第8天 题目1 训练 现已知工人 Worker 类 属性包含姓名 name 工龄 year 请编写该类 提供构造方法和get set方法 在测试类中 请查看键盘录入Scanner类的API 创建工人类对象 属性值由键盘
  • 基于STM32红外避障小车的设计(有代码)

    什么是避障小车 用红外光电传感器 探测到物体即输出脉冲 输入到单片机中处 理一下 再对电机驱动模块进行控制 实现壁障的功能 这样的避障小车又称为简单的避障机器人 各种避障方法 1 红外线避障 2 超声波避障 红外避障原理 基本硬件 红外发射
  • FATAL Port 4000 has been used. Try other port instead.

    我在另一个powershell也打开了hexo s 关掉另一个powshell就好了
  • vue移动端项目屏幕适配--flexible rem

    开始 首先 我们使用 vue 的脚手架 vue cli 来初始化一个 webpack 项目 没有安装过 vue cli 的请先安装 vue cli 安装所需依赖后安装 lib flexible 和 px2rem loader 1 下载lib
  • AOP的切入点Pointcut中的execution表达式详解

    在面向切面编程 AOP 中 切入点 Pointcut 用于定义在哪些方法或代码段上应该应用切面的逻辑 切入点使用表达式来匹配目标方法的签名和执行位置 在 Spring AOP 中 常用的切入点表达式是基于方法的 execution 表达式
  • 理解Vulkan中的各种对象

    学习Vulkan API的一个重要部分是了解其中定义了哪些类型的对象 它们代表了什么 以及它们如何相互关联 为了帮助解决这个问题 创建了一个图表 展示了所有vulkan对象及其一些关系 尤其是从另一个对象创建对象的顺序 每个vulkan对象
  • java中局部变量、全局变量和static的区别(简单易懂)

    java中的变量类型有 1 类变量 独立于方法之外的变量 用 static 修饰 2 实例变量 独立于方法之外的变量 不过没有 static 修饰 3 局部变量 类的方法中的变量 比如 Java 局部变量 局部变量声明在方法 构造方法或者语
  • 回调函数的原理及运用

    第一个问题 什么是回调函数 来看一下百度百科的定义为 回调函数就是一个通过函数指针调用的函数 如果你把函数的指针 地址 作为参数传递给另一个函数 当这个指针被用来调用其所指向的函数时 我们就说这是回调函数 回调函数不是由该函数的实现方直接调
  • hadoop环境搭建之安装JDK

    判断是否安装了jdk 使用java version 和 javac命令判断是否安装了jdk root localhost ssh java version bash java command not found root localhost
  • 【重铸Java根基】为什么Java中只有值传递

    最近带应届新员工 教然后知不足 发现自己把很多基础知识已经还给了大学老师 因此开贴 温故而知新 从最基础的Java知识开始由浅入深 在某个知识点中遇到有疑惑的点会额外多写几句或者单独开帖子展开 先要搞清楚什么是形参 什么是实参 形参 定义方
  • 寄存器的映射过程

  • vue学习-02vue入门之组件

    删除Vue cli预设 在用户根目录下 C Users 你的用户名 这个地址里有一个 vuerc 文件 修改或删除配置 组件 Props 组件之间的数据传递 Prop 的大小写 camelCase vs kebab case 不敏感 Pro
  • 线程同步概念

    带着问题去思考 什么是线程同步 线程同步能解决哪些问题 如何实现线程同步 线程同步是指两个或多个线程协同步调 按预期的顺序执行代码 若两个或多个线程同时写同一块内存或访问同一资源时 需线程同步 若线程A的执行依赖线程B的结果 需线程同步 输