Linux下的系统编程——线程同步(十三)

简介: Linux下的系统编程——线程同步(十三)

一、同步概念:

       

       所谓同步,即同时起步,协调一致。不同的对象,对“同步”的理解方式略有不同。如,设备同步,是指在两 个设备之间规定一个共同的时间参考;数据库同步,是指让两个或多个数据库内容保持一致,或者按需要部分保持 一致;文件同步,是指让两个或多个文件夹里的文件保持一致。等等

       而,编程中、通信中所说的同步与生活中大家印象中的同步概念略有差异。“同”字应是指协同、协助、互相 配合。主旨在协同步调,按预定的先后次序运行

1.线程同步:

协同步调,对公共区域数据按序访问。防止数据混乱,产生与时间有关的错误

因此,所有“多个控制流,共同操作一个共享资源”的情况,都需要同步

2.数据混乱原因:

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

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

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

        以上 3 点中,前两点不能改变,欲提高效率,传递数据,资源必须共享。只要共享资源,就一定会出现竞争。 只要存在竞争关系,数据就很容易出现混乱。

二、互斥量 mutex

Linux 中提供一把互斥锁 mutex(也称之为互斥量)。

每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁

资源还是共享的,线程间也还是竞争的, 但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了。

1.线程同步与锁:

“与时间有关的错误”(time related)。为了避免这种数据混乱,线程需要同步。

2.锁的使用注意事项

建议锁!对公共数据进行保护。所有线程【应该】在访问公共数据前先拿锁再访问。但锁本身不具备强制性。

3.借助互斥锁管理共享数据实现同步

       (1)不加锁:

#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
 
void *tfn(void *arg)
{
    srand(time(NULL));
 
    while (1) {
 
        printf("hello ");
        sleep(rand() % 3);  //模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误
        printf("world\n");
        sleep(rand() % 3);
    }
 
    return NULL;
}
 
int main(void)
{
    pthread_t tid;
    srand(time(NULL));
 
    pthread_create(&tid, NULL, tfn, NULL);
    while (1) {
 
        printf("HELLO ");
        sleep(rand() % 3);
        printf("WORLD\n");
        sleep(rand() % 3);
 
    }
    pthread_join(tid, NULL);
 
    return 0;
}
 
 
 
/*线程之间共享资源stdout*/

子父线程相互争夺cpu,出现数据混淆现象:

(2)加mutex:

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

   pthread_mutex_t 类型。(本质是结构体)

   1. pthread_mutex_t lock; 创建锁

   2  pthread_mutex_init; 初始化        1

   3. pthread_mutex_lock;加锁           1--    --> 0

   4. 访问共享数据(stdout)        

   5. pthrad_mutext_unlock();解锁      0++    --> 1

   6. pthead_mutex_destroy销毁锁

 

2)初始化互斥量:

      pthread_mutex_t mutex;

       1. pthread_mutex_init(&mutex, NULL);              动态初始化。

       2. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;    静态初始化。

3)*注意事项:

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

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

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

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

       try锁: 尝试加锁,成功--。失败,返回。同时设置错误号 EBUSY

       restrict关键字: 用来限定指针变量。被该关键字限定的指针变量所指向的内存操作,必须由本指针完成

(3)加锁步骤测试:

#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
 
pthread_mutex_t mutex;    //定义一把互斥锁,可以想象为一个int
 
void *tfn(void *arg)
{
    srand(time(NULL));
 
    while (1) {
        
        pthread_mutex_lock(&mutex);    //加锁  可以想象成锁--  (1-------  -->0)
        printf("hello ");
        sleep(rand() % 3);  //模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误
        printf("world\n");
        pthread_mutex_unlock(&mutex);    //解锁 可以想象为锁++  (0-------  -->1)
 
        sleep(rand() % 3);
    }
 
    return NULL;
}
 
int main(void)
{
    pthread_t tid;
    srand(time(NULL));
 
    int ret = pthread_mutex_init(&mutex,NULL);   //初始化互斥锁 可以认为锁的值为1
    if(ret != 0){
        fprintf(stderr,"mutex init error: %s\n",strerror(ret));
        exit(1);
    }
    
    pthread_create(&tid, NULL, tfn, NULL);
    while (1) {
 
        pthread_mutex_lock(&mutex);     //加锁  可以想象成锁--  (1-------  -->0)
        printf("HELLO ");
        sleep(rand() % 3);
        printf("WORLD\n");
 
        pthread_mutex_unlock(&mutex);    //解锁 可以想象为锁++  (0-------  -->1)
        sleep(rand() % 3);
 
    }
    pthread_join(tid, NULL);
    
    pthread_mutex_destroy(&mutex);    //销毁互斥锁 
    return 0;
}
 
 

三、读写锁:

1.读写锁函数原型:

   锁只有一把。以读方式给数据加锁——读锁。以写方式给数据加锁——写锁。

  读共享,写独占

   写锁优先级高

   相较于互斥量而言当读线程多的时候,提高访问效率

   pthread_rwlock_t  rwlock;

   pthread_rwlock_init(&rwlock, NULL);                //初始化读写锁

   pthread_rwlock_rdlock(&rwlock);        try         //读模式加锁

   pthread_rwlock_wrlock(&rwlock);        try        //写模式加锁

   pthread_rwlock_unlock(&rwlock);                     //解锁

   pthread_rwlock_destroy(&rwlock);·                  //销毁读写锁

2.读写锁原理:

 同时有多个线程对同一全局数据读、写操作

 
/* 3个线程不定时 "写" 全局资源,5个线程不定时 "读" 同一全局资源 */
 
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
 
int counter;                          //全局资源
pthread_rwlock_t rwlock;              //全局的读写锁
 
void *th_write(void *arg)
{
    int t;
    int i = (int)arg;
 
    while (1) {
        t = counter;                    // 保存写之前的值
        usleep(1000);
 
        pthread_rwlock_wrlock(&rwlock);    //以写模式加锁,写独占
        printf("=====write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
        pthread_rwlock_unlock(&rwlock);    //解锁
 
        usleep(9000);               // 给 r 锁提供机会
    }
    return NULL;
}
 
void *th_read(void *arg)
{
    int i = (int)arg;
 
    while (1) {
        pthread_rwlock_rdlock(&rwlock);    //读线程间,读锁共享
        printf("-----------------read %d: %lu: %d\n", i, pthread_self(), counter);
        pthread_rwlock_unlock(&rwlock);    //解锁
 
        usleep(2000);                // 给写锁提供机会
    }
    return NULL;
}
 
int main(void)
{
    int i;
    pthread_t tid[8];    //设置一个8个线程的数组
 
    pthread_rwlock_init(&rwlock, NULL);    //自定义读写锁
 
    for (i = 0; i < 3; i++)
        pthread_create(&tid[i], NULL, th_write, (void *)i);     //创建3个写线程
 
    for (i = 0; i < 5; i++)
        pthread_create(&tid[i+3], NULL, th_read, (void *)i);    //创建5个读线程
 
    for (i = 0; i < 8; i++)
        pthread_join(tid[i], NULL);             //回收8个线程
 
    pthread_rwlock_destroy(&rwlock);            //释放读写琐
 
    return 0;
}

四、**死锁:

1.发生死锁原因:

使用锁不恰当导致的现象

       1. 对一个锁反复lock

       2. 两个线程,各自持有一把锁,请求另一把。

2.避免死锁方法:

       1.保证资源获取顺序,要求每个线程获取资源的顺序一致

·        2.当得不到所有所需资源时,放弃已经获得的资源,等待

五、*条件变量

  本身不是锁!  但是通常结合锁来使用。 mutex

  pthread_cond_t cond;

   1.初始化条件变量:

       1. pthread_cond_init(&cond, NULL);               动态初始化。

       2. pthread_cond_t cond = PTHREAD_COND_INITIALIZER;    静态初始化。

   2.阻塞等待条件:

       pthread_cond_wait(&cond, &mutex);

      作用:  

           1) 阻塞等待条件变量满足

           2) 解锁已经加锁成功的信号量 (相当于 pthread_mutex_unlock(&mutex))

           3)  当条件满足,函数返回时,重新加锁信号量 (相当于, pthread_mutex_lock(&mutex);)

          1) 和 2)俩步为一个原子操作

   

   pthread_cond_signal():   唤醒阻塞在条件变量上的 (至少)一个线程

   pthread_cond_broadcast(): 唤醒阻塞在条件变量上的所有线程

3.能够借助条件变量,完成生成者消费者

(1)模型分析

(2)代码实现

/*借助条件变量模拟 生产者-消费者 问题*/
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
 
/*链表作为公享数据,需被互斥量保护*/
struct msg {
    struct msg *next;
    int num;
};
 
struct msg *head;
 
/* 静态初始化 一个条件变量 和 一个互斥量*/
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
 
void *consumer(void *p)//消费者函数
{
    struct msg *mp;
 
    for (;;) {
        pthread_mutex_lock(&lock);
        while (head == NULL) {           //头指针为空,说明没有节点    可以为if吗
            pthread_cond_wait(&has_product, &lock);//发生阻塞
        }
        mp = head;      
        head = mp->next;                 //模拟消费掉一个产品
        pthread_mutex_unlock(&lock);
 
        printf("-Consume %lu---%d\n", pthread_self(), mp->num);
        free(mp);
        sleep(rand() % 5);
    }
}
 
void *producer(void *p)//生产者函数
{
    struct msg *mp;
 
    for (;;) {
        mp = malloc(sizeof(struct msg));
        mp->num = rand() % 1000 + 1;        //模拟生产一个产品
        printf("-Produce ---------------------%d\n", mp->num);
 
        pthread_mutex_lock(&lock);
        mp->next = head;
        head = mp;
        pthread_mutex_unlock(&lock);
 
        pthread_cond_signal(&has_product);  //将等待在该条件变量上的 一个线程唤醒
        sleep(rand() % 5);
    }
}
 
int main(int argc, char *argv[])
{
    pthread_t pid, cid;
    srand(time(NULL));
 
    pthread_create(&pid, NULL, producer, NULL);//创建生产者
    pthread_create(&cid, NULL, consumer, NULL);//创建消费者
 
    pthread_join(pid, NULL);//回收生产者
    pthread_join(cid, NULL);//回收消费者
 
    return 0;
}

(3)运行效果

(4)一个生产者,多个消费者

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
 
void err_thread(int ret, char *str)
{
    if (ret != 0) {
        fprintf(stderr, "%s:%s\n", str, strerror(ret));
        pthread_exit(NULL);
    }
}
 
struct msg {
    int num;
    struct msg *next;
};
 
struct msg *head;
 
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;      // 定义/初始化一个互斥量
pthread_cond_t has_data = PTHREAD_COND_INITIALIZER;      // 定义/初始化一个条件变量
 
void *produser(void *arg)
{
    while (1) {
        struct msg *mp = malloc(sizeof(struct msg));
 
        mp->num = rand() % 1000 + 1;                        // 模拟生产一个数据`
        printf("--produce %d\n", mp->num);
 
        pthread_mutex_lock(&mutex);                         // 加锁 互斥量
        mp->next = head;                                    // 写公共区域
        head = mp;
        pthread_mutex_unlock(&mutex);                       // 解锁 互斥量
 
        pthread_cond_signal(&has_data);                     // 唤醒阻塞在条件变量 has_data上的线程.
 
        sleep(rand() % 3);
    }
 
    return NULL;
}
 
void *consumer(void *arg)
{
    while (1) {
        struct msg *mp;
 
        pthread_mutex_lock(&mutex);                         // 加锁 互斥量
        while (head == NULL) {
            pthread_cond_wait(&has_data, &mutex);           // 阻塞等待条件变量, 解锁
        }                                                   // pthread_cond_wait 返回时, 重新加锁 mutex
 
        mp = head;
        head = mp->next;
 
        pthread_mutex_unlock(&mutex);                       // 解锁 互斥量
        printf("---------consumer id: %lu :%d\n", pthread_self(), mp->num);
 
        free(mp);
        sleep(rand()%3);
    }
 
    return NULL;
}
 
int main(int argc, char *argv[])
{
    int ret;
    pthread_t pid, cid;
 
    srand(time(NULL));
 
    ret = pthread_create(&pid, NULL, produser, NULL);           // 生产者
    if (ret != 0) 
        err_thread(ret, "pthread_create produser error");
 
    ret = pthread_create(&cid, NULL, consumer, NULL);           // 消费者
    if (ret != 0) 
        err_thread(ret, "pthread_create consuer error");
    ret = pthread_create(&cid, NULL, consumer, NULL);           // 消费者
    if (ret != 0) 
        err_thread(ret, "pthread_create consuer error");
    ret = pthread_create(&cid, NULL, consumer, NULL);           // 消费者
    if (ret != 0) 
        err_thread(ret, "pthread_create consuer error");
 
    pthread_join(pid, NULL);
    pthread_join(cid, NULL);
 
    return 0;
}

4.条件变量的优点:

相较于 mutex 而言,条件变量可以减少竞争。

如直接使用 mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚 (链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引 起消费者之间的竞争。提高了程序效率

六、信号量:

1.基本操作

   应用于线程、进程间同步。

   相当于 初始化值为 N 的互斥量。  N值,表示可以同时访问共享数据区的线程数

   函数:

      sem_t sem;    定义类型。

      int sem_init(sem_t *sem, int pshared, unsigned int value);//初始化信号量

       参数:

           sem: 信号量

           pshared:   0: 用于线程间同步

                   

                               1: 用于进程间同步

           value:        N值:指定同时访问的线程数

     sem_destroy();        //销毁信号量

       sem_wait();        一次调用,做一次-- 操作, 当信号量的值为 0 时,再次 -- 就会阻塞。 (对比 pthread_mutex_lock//加锁

      sem_post();        一次调用,做一次++ 操作. 当信号量的值为 N 时, 再次 ++ 就会阻塞。(对比 pthread_mutex_unlock//解锁

注意:信号量的初值,决定了占用信号量的线程个数

2.生产者消费者信号量模型

使用信号量完成线程间同步,模拟生产者,消费者问题

/*信号量实现 生产者 消费者问题*/
 
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>
 
#define NUM 5               
 
int queue[NUM];                                     //全局数组实现环形队列
sem_t blank_number, product_number;                 //空格子信号量, 产品信号量
 
void *producer(void *arg)
{
    int i = 0;
 
    while (1) {
        sem_wait(&blank_number);                    //生产者将空格子数--,为0则阻塞等待
        queue[i] = rand() % 1000 + 1;               //生产一个产品
        printf("----Produce---%d\n", queue[i]);        
        sem_post(&product_number);                  //将产品数++
 
        i = (i+1) % NUM;                            //借助下标实现环形
        sleep(rand()%1);
    }
}
 
void *consumer(void *arg)
{
    int i = 0;
 
    while (1) {
        sem_wait(&product_number);                  //消费者将产品数--,为0则阻塞等待
        printf("-Consume---%d\n", queue[i]);
        queue[i] = 0;                               //消费一个产品 
        sem_post(&blank_number);                    //消费掉以后,将空格子数++
 
        i = (i+1) % NUM;
        sleep(rand()%3);
    }
}
 
int main(int argc, char *argv[])
{
    pthread_t pid, cid;
 
    sem_init(&blank_number, 0, NUM);                //初始化空格子信号量为5, 线程间共享 -- 0
    sem_init(&product_number, 0, 0);                //产品数为0
 
    pthread_create(&pid, NULL, producer, NULL);    //产生生产者
    pthread_create(&cid, NULL, consumer, NULL);    //产生消费者
 
    pthread_join(pid, NULL);
    pthread_join(cid, NULL);
 
    sem_destroy(&blank_number);        //回收生产者
    sem_destroy(&product_number);      //回收消费者
 
    return 0;
}


目录
相关文章
|
27天前
|
存储 缓存 监控
Linux缓存管理:如何安全地清理系统缓存
在Linux系统中,内存管理至关重要。本文详细介绍了如何安全地清理系统缓存,特别是通过使用`/proc/sys/vm/drop_caches`接口。内容包括清理缓存的原因、步骤、注意事项和最佳实践,帮助你在必要时优化系统性能。
173 78
|
1月前
|
Linux Shell 网络安全
Kali Linux系统Metasploit框架利用 HTA 文件进行渗透测试实验
本指南介绍如何利用 HTA 文件和 Metasploit 框架进行渗透测试。通过创建反向 shell、生成 HTA 文件、设置 HTTP 服务器和发送文件,最终实现对目标系统的控制。适用于教育目的,需合法授权。
63 9
Kali Linux系统Metasploit框架利用 HTA 文件进行渗透测试实验
|
27天前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
94 13
|
27天前
|
Ubuntu Linux C++
Win10系统上直接使用linux子系统教程(仅需五步!超简单,快速上手)
本文介绍了如何在Windows 10上安装并使用Linux子系统。首先,通过应用商店安装Windows Terminal和Linux系统(如Ubuntu)。接着,在控制面板中启用“适用于Linux的Windows子系统”并重启电脑。最后,在Windows Terminal中选择安装的Linux系统即可开始使用。文中还提供了注意事项和进一步配置的链接。
42 0
|
1月前
|
存储 Oracle 安全
服务器数据恢复—LINUX系统删除/格式化的数据恢复流程
Linux操作系统是世界上流行的操作系统之一,被广泛用于服务器、个人电脑、移动设备和嵌入式系统。Linux系统下数据被误删除或者误格式化的问题非常普遍。下面北亚企安数据恢复工程师简单聊一下基于linux的文件系统(EXT2/EXT3/EXT4/Reiserfs/Xfs) 下删除或者格式化的数据恢复流程和可行性。
|
21天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
52 1
|
3月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
68 1
|
3月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
47 3
|
3月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
29 2
|
3月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
47 2