• BSP Day56


    线程同步

    所谓同步,即同时起步,协调一致。不同的对象,对“同步”的理解方式各有不同。如,设备同步,是指 在两个设备之间规定一个共同的时间参考:数据库同步,是指让两个或多个数据库内容保持一致,或者按 需要部分保持一致:文件同步,是指让两个或多个文件夹里的文件保持一致。等等。 而,编程中、通信中所说的同步与生活中大家印象中的同步概念略有差异。“同”字应是指协同、协助、 互相配合。主旨在协同步调,按预定的先后次序运行。

    同步即协同步调,对公共区域数据按预定的先后次序访问,防止数据混乱。 线程同步,指一个线程发出某一功能调用时, 在没有得到结果之前,该调用不返回。同时其它线程为保 证数据一致性,不能调用该功能。

    临界区(Critical Section)、互斥对象(Mutex):主要用于互斥控制;都具有拥有权的控制方法,只有拥有该对象的线程才能执行任务,所以拥有,执行完任务后一定要释放该对象。

    信号量(Semaphore)、事件对象(Event):事件对象是以通知的方式进行控制,主要用于同步控制!

    临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。它并不是核心对象,不是属于操作系统维护的,而是属于进程维护的。

    总结下关键段:
    1)关键段共初始化化、销毁、进入和离开关键区域四个函数。
    2)关键段可以解决线程的互斥问题,但因为具有“线程所有权”,所以无法解决同步问题。
    3)推荐关键段与旋转锁配合使用。

    2、互斥对象:互斥对象和临界区很像,采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程同时访问。当前拥有互斥对象的线程处理完任务后必须将线程交出,以便其他线程访问该资源。

    总结下互斥量Mutex
    1)互斥量是内核对象,它与关键段都有“线程所有权”所以不能用于线程的同步。
    2)互斥量能够用于多个进程之间线程互斥问题,并且能完美的解决某进程意外终止所造成的“遗弃”问题。


    信号量:信号量也是内核对象。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目

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

    事件对象: 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作

    总结下事件
    1)事件是内核对象,事件分为手动置位事件和自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。
    2)事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。还可以由PulseEvent()来发出一个事件脉冲。
    3)事件可以解决线程间同步问题,因此也能解决互斥问题。

    数据混乱原因

    1.资源共享(独享资源则不会)

    2.调度随机(意味着数据访问会出现竞争)

    3.线程间缺乏必要的同步机制 以上

    3点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞 争。只要存在竞争关系,数据就很容易出现混乱。 所以只能从第三点着手解决。使多个线程在访问共享资源的时候,出现互斥。

    互斥量

    Linux中提供一把互斥锁 mutex (也称之为互斥量)。 每个线程在对资源操作前都尝试先加锁,成功加锁才能操作。操作结束解锁。 资源还是共享的,线程间也还是竞争的, 但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了

     但,应注意:同一时刻,只能有一个线程持有该锁。 当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访 问该全局变量,依然能够访问,但会出现数据混乱。 所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源 的时候使用该机制。但,并没有强制限定。 因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。

    例程:父子线程共同使用共享资源stdout,利用sleep造成时间差混乱

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. void *tfn(void *arg)
    7. {
    8. srand(time(NULL));
    9. while (1)
    10. {
    11. printf("hello ");
    12. sleep(rand() % 3); /*模拟长时间操作共享资源 ,导致cpu易主,产生与时间有关的错误*/
    13. printf("world\n" );
    14. sleep(rand() % 3);
    15. }
    16. return NULL;
    17. }
    18. int main(void)
    19. {
    20. pthread_t tid;
    21. srand(time(NULL));
    22. pthread_create(&tid, NULL, tfn, NULL);
    23. while (1)
    24. {
    25. printf("HELLO ");
    26. sleep(rand() % 3);
    27. printf("WORLD\n");
    28. sleep(rand() % 3);
    29. }
    30. pthread_join(tid, NULL);
    31. return 0;
    32. }

    主要应用函数

    pthread_mutex_init 函数。

    pthread_mutex_destroy函数。

    pthread_mutex_lock 函做。

    pthread_mutex_trylock 函数。

    pthread_mutex_unlock 函数。

    以上5个函数的返回值都是:成功返回0,失败返回错误号。 pthread_mutex_t 类型,其本质是一个结构体。 为简化理解,应用时可忽略其实现细节,简单当成 整数看待。 pthread_mutex_t mutex;变量mutex只有两种取值1、0。

    使用mutex(互斥量、互斥锁)一般步骤

    1. pthread_mutex_t mutex: 创建锁

    2. pthread_mutex_init :初始化

    3. pthread_mutex_lock:加锁

    4. 访问共享数据(stdout)

    5. pthread_mutex_unlock:解锁

    6. pthread_mutex_destroy:销毁锁 

    lock与unlock:

    lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其他线程解锁为止 unlock主动解锁函数。同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先 级、调度。默认:先阻塞、先唤醒。 例如: T1 T2 T3 T4 使用一把mutex锁。T1加锁成功,其他线程均阻塞,直至T1 解锁。T1解锁后,T2 T3 T4均被唤醒,并自动再次尝试加锁。 可假想mutex锁init 成功初值为1. lock 功能是将mutex--. 而unlock则将mutex++。

    lock与trylock

    lock加锁失败会阻塞,等待锁释放。

    trylock加锁失败直接返回错误号(如: EBUSY)不阻塞

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. //pthread_mutex_t 互斥锁类型名,
    7. pthread_mutex_t mutex;//定义另一个互斥锁变量mutex,全局变量,所有线程可以共享使用
    8. void *tfn2(void *arg)
    9. {//当某个线程,不进行对共享资源进行加锁操作时,仍旧会造成抢夺资源现象
    10. srand(time(NULL));
    11. while(1)
    12. {
    13. int ret=pthread_mutex_trylock(&mutex);
    14. if(ret!=0)
    15. {
    16. printf("pthread_mutx_trylock err:%s\n",strerror(ret));
    17. }
    18. else
    19. {
    20. printf("---tfn2()---print ");
    21. sleep(rand() % 3);
    22. printf("to stdout\n");
    23. sleep(rand() % 3);
    24. ret=pthread_mutex_unlock(&mutex);//解锁
    25. if(ret!=0)
    26. {
    27. fprintf(stderr,"pthread_mutex_unlock()
    28. error:%s\n",strerror(ret));
    29. }
    30. }
    31. printf("common tfn2()------\n");
    32. sleep(rand() % 3);
    33. }
    34. }
    35. void *tfn(void *arg)
    36. {
    37. srand(time(NULL));
    38. while (1)
    39. {
    40. int ret=pthread_mutex_lock(&mutex);//加锁
    41. if(ret!=0)
    42. {
    43. fprintf(stderr,"pthread_mutex_lock() error:%s\n",strerror(ret));
    44. }
    45. printf("hello ");
    46. sleep(rand() % 3);
    47. printf("world\n");
    48. //pthread_mutex_unlock(),对加锁成功的共享资源进行解锁
    49. ret=pthread_mutex_unlock(&mutex);//解锁
    50. if(ret!=0)
    51. {
    52. fprintf(stderr,"pthread_mutex_unlock() error:%s\n",strerror(ret));
    53. }
    54. sleep(rand() % 3);
    55. }
    56. return NULL;
    57. }
    58. int main(void)
    59. {
    60. pthread_t tid;
    61. srand(time(NULL));
    62. //pthread_mutex_init()初始化 互斥锁
    63. int ret=pthread_mutex_init(&mutex,NULL);//初始化锁
    64. if(ret!=0)
    65. {
    66. fprintf(stderr,"mutex init error:%s\n",strerror(ret));
    67. }
    68. //pthread_create() 创建线程1
    69. ret=pthread_create(&tid, NULL, tfn, NULL);
    70. if(ret!=0)
    71. {
    72. fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
    73. }
    74. //pthread_create() 创建线程2
    75. ret=pthread_create(&tid, NULL, tfn2, NULL);
    76. if(ret!=0)
    77. {
    78. fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
    79. }
    80. while (1)
    81. { //pthread_mutex_lock()给已初始化过的互斥锁变量,(站在当前线程角度)对共享资源进行加
    82. 锁操作
    83. //如果加锁失败,则阻塞,也就是一直等待
    84. ret=pthread_mutex_lock(&mutex);//加锁
    85. if(ret!=0)
    86. {
    87. fprintf(stderr,"pthread_mutex_lock() error:%s\n",strerror(ret));
    88. }
    89. printf("HELLO ");
    90. sleep(rand() % 3);
    91. printf("WORLD\n");
    92. //pthread_mutex_unlock(),对加锁成功的共享资源进行解锁
    93. ret=pthread_mutex_unlock(&mutex);//解锁
    94. if(ret!=0)
    95. {
    96. fprintf(stderr,"pthread_mutex_unlock() error:%s\n",strerror(ret));
    97. }
    98. sleep(rand() % 3);
    99. }
    100. pthread_join(tid, NULL);
    101. ret=pthread_mutex_destroy(&mutex);//销毁锁
    102. if(ret!=0)
    103. {
    104. fprintf(stderr,"pthread_mutex_destroy() error:%s\n",strerror(ret));
    105. }
    106. return 0;
    107. }

     例程:父子线程共同使用共享资源stdout,利用sleep造成时间差混乱。加锁进行共享资源控制。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. pthread_mutex_t mutex;
    7. void *tfn(void *arg)
    8. {
    9. srand(time(NULL));
    10. while (1)
    11. {
    12. pthread_mutex_lock(&mutex);//加锁
    13. printf("hello ");
    14. sleep(rand() % 3); /*模拟长时间操作共享资源 ,导致cpu易主,产生与时间有关的错误*/
    15. printf("world\n" );
    16. pthread_mutex_unlock(&mutex);//解锁
    17. sleep(rand() % 3);
    18. }
    19. return NULL;
    20. }
    21. int main(void)
    22. {
    23. pthread_t tid;
    24. srand(time(NULL));
    25. int ret=pthread_mutex_init(&mutex,NULL);//初始化锁
    26. if(ret!=0)
    27. {
    28. fprintf(stderr,"mutex init error:%s\n",strerror(ret));
    29. }
    30. pthread_create(&tid, NULL, tfn, NULL);
    31. while (1)
    32. {
    33. pthread_mutex_lock(&mutex);//加锁
    34. printf("HELLO ");
    35. sleep(rand() % 3);
    36. printf("WORLD\n");
    37. pthread_mutex_unlock(&mutex);//解锁
    38. sleep(rand() % 3);
    39. }
    40. pthread_join(tid, NULL);
    41. pthread_mutex_destroy(&mutex);//销毁锁
    42. return 0;
    43. }

    注意事项:

    尽量保证锁的粒度,越小越好。 (访问共享数据前, 加锁。访问结束后,应立即解锁)

    互斥锁:本质是结构体,我们可以看做是整数,初值为1 (pthread_mutex_init()函数调用成功)

    加锁:--操作,阻塞线程。

    解锁:++操作,唤醒阻塞在锁上的线程

    try锁:尝试加锁,成功--;失败,继续其他业务功能代码 

    死锁

    是使用锁不恰当导致的现象

    1.线程试图对同一个互斥量A加锁两次。(连续调用pthread_mutex_lock两次及以上)

    1.读写锁是“写模式加锁”成功时, 其他线程尝加锁都会被阻塞。 2.读写锁是“读模式加锁”成功时,其他线程以写模式加锁会阻塞。 3.读写锁是“ 读模式加锁”时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写 锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高. 读写锁也叫共享–独占锁。 a.当读写锁以读模式锁住时,它是以共享模式锁住的,其他读线程可以读取内容 b.当它以写模式锁住时,它是以独占模式锁住的,其他尝试操作的线程均会被阻塞 读写锁非常适合于对数据结构读的次数远大于写的情况。

    2.线程1拥有A锁,请求获得B锁:线程2拥有B锁,请求获得A锁。

    特别强调。读写锁只有一把,但其具备两种状态: 1.读模式下加锁状态(读锁)。 2.写模式下加锁状态(写锁)。 

    读写锁 

    与互斥量类似,但读写锁允许更高的并行性。 其特性为:锁只有一把;写独占,读共享;写锁优先级高;

    特别强调。读写锁只有一把,但其具备两种状态: 1.读模式下加锁状态(读锁)。 2.写模式下加锁状态(写锁)。

    读写锁特征

    1.读写锁是“写模式加锁”成功时, 其他线程尝加锁都会被阻塞。

    2.读写锁是“读模式加锁”成功时,其他线程以写模式加锁会阻塞。

    3.读写锁是“ 读模式加锁”时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写 锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高。

    读写锁也叫共享–独占锁。

    a.当读写锁以读模式锁住时,它是以共享模式锁住的,其他读线程可以读取内容。

    b.当它以写模式锁住时,它是以独占模式锁住的,其他尝试操作的线程均会被阻塞 读写锁非常适合于对数据结构读的次数远大于写的情况。

    主要应用函数

    pthread_rwlock_init 函数。

    pthread_rwlock_destroy 函数。

    pthread_rwlock_rdlock 函数。

    pthread_rwlock_wrlock函数。

    pthread_rwlock_tryrdlock 函数。

    pthread_rwlock_trywrlock 函数。

    pthread_rwlock_unlock函数。

    以上7个函数的返回值都是,成功返回0,失败直接返回惜误号 。

    pthread_rwlock_t 类型 用于定义一个读写锁变量 如:pthread_rwlock_t rwlock;

    初始化一把读写锁

    int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

    参1:传读写锁变量

    参2:attr表示读写锁属性,通常使用默认值,传NULL即可。

    销毁一把读写锁

    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

    参1:传读写锁变量 

    以读方式请求读写锁(简称:请求读锁)

    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

    int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

    参1:传读写锁变量 

    以写方式请求读写锁(简称:请求写锁)

    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

    int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

    参1:传读写锁变量 

    解锁

    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

    参1:传读写锁变量 

    例程:3个线程不定时写同一全局资源,5个线程不定时读同一全局资源

    1. #include
    2. #include
    3. #include
    4. int counter;
    5. pthread_rwlock_t rwlock;
    6. /* 3个线程不定时写同一全局资源,5个线程不定时读同一全局资源*/
    7. void *th_write(void *arg)
    8. {
    9. int t;
    10. int i = (long int)arg;
    11. while (1)
    12. {
    13. pthread_rwlock_wrlock(&rwlock);//以写模式加锁
    14. t = counter;
    15. sleep(1);
    16. printf("=====write %d: %lu: counter=%d ++counter=%d\n", i,
    17. pthread_self(), t, ++counter);
    18. pthread_rwlock_unlock(&rwlock);
    19. sleep(1);
    20. }
    21. return NULL;
    22. }
    23. void *th_read(void *arg)
    24. {
    25. int i = (long int)arg;
    26. while (1)
    27. {
    28. pthread_rwlock_rdlock(&rwlock);//读线程间,读锁共享
    29. printf("----------------------read %d: %lu: %d\n", i, pthread_self(),
    30. counter);
    31. pthread_rwlock_unlock(&rwlock);
    32. sleep(1);
    33. }
    34. return NULL;
    35. }
    36. int main(void)
    37. {
    38. long int i;
    39. pthread_t tid[8];
    40. pthread_rwlock_init(&rwlock, NULL);
    41. for(i=0;i<3;i++)
    42. {
    43. pthread_create(&tid[i], NULL, th_write, (void *)i);
    44. }
    45. for(i=0;i<5;i++)
    46. {
    47. pthread_create(&tid[i+3], NULL, th_read, (void *)i);
    48. }
    49. for(i=0;i<8;i++)
    50. {
    51. pthread_join(tid[i], NULL);
    52. }
    53. pthread_rwlock_destroy(&rwlock);
    54. return 0;
    55. }

     

     

     

     

  • 相关阅读:
    深度学习(12)之模型训练[训练集、验证集、过拟合、欠拟合]
    一个.NET开源的功能丰富、灵活易用的 Windows 窗口增强神器
    第7章 项目进阶,构建安全高效的企业服务(上)
    2309d用dmd重写dfmt
    开发者职场“生存状态”大调研报告分析 - 第四版
    Nacos的动态配置源码解析
    Go-Excelize API源码阅读(二十一)——GetDefinedName()、DeleteDefinedName()
    Prepared SQL 性能测试
    Nginx:优化和防盗链
    【React源码】(十七)React 算法之深度优先遍历
  • 原文地址:https://blog.csdn.net/weixiaxiao/article/details/127616072