Linux多线程之线程同步

简介:

  线程最大的特点就是资源的共享性,所以也就有了一个难点线程同步,实现线程同步的方法最常用的方法是:互斥锁,条件变量和信号量。接下来就让我们来看下这几种同步的方法。

一、互斥锁(Mutex)

  获得锁的线程可以完成“读-修改-写”的操作,然后释放锁给其它线程,没有获得锁的线程只能等待而不能访问共享数据,这样“读-修改-写”三步操作组成一个原子操作,要么都执行,要么都不执行,不会执行到中间被打断,也不会在其它处理器上并行做这个操作。

1、锁的初始化和销毁(Mutex用pthread_mutex_t类型的变量表示)

wKioL1nmpo_Swv8XAABm7CySx6g673.png

  注意:如果Mutex变量是静态分配的(全局变量 或static变量),也可以用宏PTHREAD_MUTEX_INITIALIZER来初始化,相当于用pthread_mutex_init初始化并且attr参数为NULL。用函数初始化则是动态分配。

2、加锁解锁

wKioL1nmp7GwBHHHAABTcKT1dPo031.png

  一个线程可以调用pthread_mutex_lock获得Mutex,如果这时另一个线程已经调用pthread_mutex_lock获得了该Mutex,则当前线程需要挂起等待,直到另一个线程调用
pthread_mutex_unlock释放Mutex,当前线程被唤醒,才能获得该Mutex并继续执行。如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#define LOOP 5000
static  int  count = 0;
pthread_mutex_t mutex_lock = PTHREAD_MUTEX_INITIALIZER;
void  *read_write( void  *val)
{
     int  _val = 0;
     int  i = 0;
     for ( ; i < LOOP; i++ ){
         pthread_mutex_lock(&mutex_lock);
         _val = count;
         printf ( "pthread id is :%x,count : %d\n" ,
                 (unsigned  long )pthread_self(),count);
         count = _val+1;
         pthread_mutex_unlock(&mutex_lock);
     }
}
int  main()
{
     pthread_t tid1;
     pthread_t tid2;
     pthread_create(&tid1,NULL,read_write,NULL); //为了简单没有判断是否创建成功
     pthread_create(&tid2,NULL,read_write,NULL);
     pthread_join(tid1,NULL);
     pthread_join(tid2,NULL);
     printf ( "count final value is : %d\n" ,count);
     return  0;
}

如果没有加入互斥锁,运行结果不会是10000。

3、互斥锁的缺点

  死锁的情况:1)一般情况下,如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了。2)线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了。

  解决办法:首先要尽量避免同时获得多个锁。如果所有线程在需要多个锁时都按相同的先后顺序(常见的是按Mutex变量的地址顺序)获得锁,则不会出现死锁。

二、条件变量

  互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。条件变量分为两部分: 条件和变量。条件本身是由互斥量保护的。线程在改变条件状态前先要锁住互斥量。条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。  

1、条件变量的初始化和销毁

wKiom1nms0jBDzejAABgwD25S88063.png

  条件变量初始化和互斥锁初始化类似,宏表示静态分配,函数表示动态分配,当函数的第二个参数为NULL则两者等价。

2、等待条件成立和激活条件变量

wKiom1nmtcWTdwg6AAByOUDxKcs887.png

wKioL1nmskCzx6YlAAA7Ziy_vK0848.png

  可见,一个条件变量总是和一个Mutex搭配使用的。 一个线程可以调用pthread_cond_wait在一个条件变量上阻塞等待,这个函数做以下三步操作:
1)释放Mutex
2)阻塞等待
3)当被唤醒时,重新获得Mutex并返回
  pthread_cond_timedwait函数还有一个额外的参数可以设定等待超时,如果到达了abstime所指定的时刻仍然没有别的线程来唤醒当前线程,就返回ETIMEDOUT。一个线程可以调用pthread_cond_signal唤醒在某个条件变量上等待的另一个线程,也可以调用pthread_cond_broadcast唤醒在这个条件变量上等待的所有线程。

  下面写一个生产者和消费者的实现(链表):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
typedef  struct  list{
     struct  list *next;
     int  val;
}product_list;
product_list *head = NULL;
static  pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static  pthread_cond_t need_product=PTHREAD_COND_INITIALIZER;
/*init list*/
void  init_list(product_list *list)
{
     if (NULL != list){
         list->next = NULL;
         list->val = 0;
     }
}
/*consumer*/
void  *consumer( void  *_val)
{
     product_list *p = NULL;
     while (1){
         pthread_mutex_lock(&lock);
         while (NULL == head){
             pthread_cond_wait(&need_product,&lock);
         }
         p = head;
         head = head->next;
         p->next = NULL;
         pthread_mutex_unlock(&lock);
         printf ( "consum success,val is : %d\n" ,p->val);
         free (p);
         p = NULL;
     }
}
/*product*/
void  *product( void  *_val)
{
     while (1){
         sleep( rand ()%2);
         product_list *p = (product_list*) malloc ( sizeof (product_list));
         pthread_mutex_lock(&lock);
         init_list(p);
         p->val =  rand ()%1000;
         p->next = NULL;
         head = p;
         pthread_mutex_unlock(&lock);
         printf ( "product success,val : %d\n" ,p->val);
         pthread_cond_signal(&need_product);
     }
}
int  main()
{
     pthread_t t_product;
     pthread_t t_consumer;
     pthread_create(&t_product,NULL,product,NULL); //no check
         pthread_create(&t_consumer,NULL,consumer,NULL); //no check
 
     pthread_join(t_product,NULL); //wait product thread
     pthread_join(t_consumer,NULL); //wait consumer thread
     return  0;
}


wKioL1nmvBqzoRzKAABvD0kOj9U458.png

三、信号量

  Mutex变量是非0即1的,可看作一种资源的可用数量,初始化时Mutex是1,表示有一个可用资源,加锁时获得该资源,将Mutex减到0,表示不再有可用资源,解锁时释放该资源,将Mutex重新加到1,表示又有了一个可用资源。
  信号量(Semaphore)和Mutex类似,表示可用资源的数量,和Mutex不同的是这个数量可以大于1。即,如果信号量描述的资源数目是1时,此时的信号量和互斥锁相同!POSIX semaphore库函数,这种信号量不仅可用于同一进程的线程间同步,也可用于不同进程间的同步。

1、信号量的创建和销毁

1
2
int  sem_init(sem_t *sem ,  int  pshared, unsigned  int  value);
int  sem_destroy(sem_t *sem);

2、信号量的等待和释放

1
2
int  sem_wait(sem_t *sem); //阻塞等待
int  sem_trywait(sem_t *sem); //非阻塞等待

  调用sem_wait()可以获得资源(P操作),使semaphore的值减1,如果调用sem_wait()时semaphore的值已经是0,则挂起等待。如果不希望挂起等待,可以调用sem_trywait() 。调用sem_post() 可以释放资源(V操作),使semaphore 的值加1,同时唤醒挂起等待的线程。

  下面写一个生产者和消费者的实现(固定大小循环队列):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<semaphore.h>
 
#define SEM_PRO 10
#define SEM_COM 0
/*定义信号量*/
sem_t sem_product;
sem_t sem_consume;
int  bank[SEM_PRO];
/*消费者线程执行函数*/
void  *consumer( void  *_val)
{
     int  c = 0;
     while (1){
         sem_wait(&sem_consume);
         int  _consume = bank[c];
         printf ( "consumer done...,val : %d\n" ,_consume);
         sem_post(&sem_product);
         c = (c+1)%SEM_PRO;
         sleep( rand ()%5);
     }
}
/*生产者线程执行函数*/
void  *producter( void  *_val)
{
     int  p = 0;
     while (1){
         sem_wait(&sem_product);
         int  _product =  rand ()%100;
         bank[p] = _product;
         printf ( "product done... val is : %d\n" ,_product);
         sem_post(&sem_consume);
         p = (p+1)%SEM_PRO;
         sleep( rand ()%3);
     }
}
/*创建生产者消费者线程*/
void  run_product_consume()
{
     pthread_t tid_consumer;
     pthread_t tid_producter;
 
     pthread_create(&tid_consumer,NULL,consumer,NULL);
     pthread_create(&tid_producter,NULL,producter,NULL);
     
     pthread_join(tid_consumer,NULL);
     pthread_join(tid_producter,NULL);
}
/*销毁信号量*/
void  destroy_all_sem()
{
     printf ( "process done...\n" );
     sem_destroy(&sem_product);
     sem_destroy(&sem_consume);
     exit (0);
}
/*初始化信号量*/
void  init_all_sem()
{
     int  bank[SEM_PRO];
     int  num =  sizeof (bank)/ sizeof (bank[0]);
     int  i = 0;
     for (; i < num; i++ ){
         bank[i] = 0;
     }
     sem_init(&sem_product,0,SEM_PRO);
     sem_init(&sem_consume,0,SEM_COM);
}
 
int  main()
{
     init_all_sem();
     run_product_consume();
//  destroy_all_sem();//避免销毁信号量
     return  0;
}


wKioL1nmyyTDE2GvAABrfUyaGA8668.png




本文转自 8yi少女的夢 51CTO博客,原文链接:http://blog.51cto.com/zhaoxiaohu/1973694,如需转载请自行联系原作者

相关文章
|
2月前
|
安全 算法 Java
Java 多线程:线程安全与同步控制的深度解析
本文介绍了 Java 多线程开发的关键技术,涵盖线程的创建与启动、线程安全问题及其解决方案,包括 synchronized 关键字、原子类和线程间通信机制。通过示例代码讲解了多线程编程中的常见问题与优化方法,帮助开发者提升程序性能与稳定性。
117 0
|
7月前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
2月前
|
数据采集 监控 调度
干货分享“用 多线程 爬取数据”:单线程 + 协程的效率反超 3 倍,这才是 Python 异步的正确打开方式
在 Python 爬虫中,多线程因 GIL 和切换开销效率低下,而协程通过用户态调度实现高并发,大幅提升爬取效率。本文详解协程原理、实战对比多线程性能,并提供最佳实践,助你掌握异步爬虫核心技术。
|
3月前
|
Java 数据挖掘 调度
Java 多线程创建零基础入门新手指南:从零开始全面学习多线程创建方法
本文从零基础角度出发,深入浅出地讲解Java多线程的创建方式。内容涵盖继承`Thread`类、实现`Runnable`接口、使用`Callable`和`Future`接口以及线程池的创建与管理等核心知识点。通过代码示例与应用场景分析,帮助读者理解每种方式的特点及适用场景,理论结合实践,轻松掌握Java多线程编程 essentials。
219 5
|
5月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
236 67
|
7月前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
204 20
|
7月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
121 26
|
7月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
129 17
|
7月前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
9月前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
151 1