linuxC线程同步问题

简介: linuxC线程同步问题

1. 互斥锁

1.1 创建

pthread_mutex_t mutex;  // 创建一个互斥锁,需要定义为全局变量

1.2 初始化

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

1.3 加锁

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex); // 获取不到会阻塞

1.4 解锁

#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);

以下行为会报错:

  • 对处于未锁定的互斥锁进行解锁操作
  • 解锁其他线程锁定的互斥锁

1.5 非阻塞加锁

#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);

参数 mutex 指向目标互斥锁,成功返回 0,失败返回一个非 0 值的错误码,如果目标互斥锁已经被其它线程锁住,则调用失败返回 EBUSY

1.6 销毁

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 不能销毁没有解锁的互斥锁
  • 不能销毁没有初始化的互斥锁

1.7 互斥锁属性

1.7.1 初始化和销毁

#include <pthread.h>
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);

1.7.2 类型

#include <pthread.h>
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
  • PTHREAD_MUTEX_NORMAL:一种标准的互斥锁类型,不做任何的错误检查或死锁检测。如果线程试图对已经由自己锁定的互斥锁再次进行加锁,则发生死锁;互斥锁处于未锁定状态,或者已由其它线程锁定,对其解锁会导致不确定结果。

PTHREAD_MUTEX_ERRORCHECK:此类互斥锁会提供错误检查。譬如这三种情况都会导致返回错误:线程试图对已经由自己锁定的互斥锁再次进行加锁(同一线程对同一互斥锁加锁两次),返回错误;线程对由其它线程锁定的互斥锁进行解锁,返回错误;线程对处于未锁定状态的互斥锁进行解锁,返回错误。这类互斥锁运行起来比较慢,因为它需要做错误检查,不过可将其作为调试工具,以发现程序哪里违反了互斥锁使用的基本原则。


PTHREAD_MUTEX_RECURSIVE:此类互斥锁允许同一线程在互斥锁解锁之前对该互斥锁进行多次加锁,然后维护互斥锁加锁的次数,把这种互斥锁称为递归互斥锁,但是如果解锁次数不等于加速次数,则是不会释放锁的;

  • 所以,如果对一个递归互斥锁加锁两次,然后解锁一次,那么这个互斥锁依然处于锁定状态,对它再次进行解锁之前不会释放该锁。
  • PTHREAD_MUTEX_DEFAULT : 此类互斥锁提供默认的行为和特性 。 使 用

PTHREAD_MUTEX_INITIALIZER 初 始 化 的互斥锁 , 或者调用参数arg为NULL 的pthread_mutexattr_init()函数所创建的互斥锁,都属于此类型。此类锁意在为互斥锁的实现保留

  • 最大灵活性, Linux 上 , PTHREAD_MUTEX_DEFAULT 类型互斥锁的行为与PTHREAD_MUTEX_NORMAL 类型相仿。

2. 条件变量

条件变量通常是和互斥锁一起使用的

2.1 创建

pthread_cond_t cond; // 定义为全局变量,这样多个线程才能够访问

2.2 初始化

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

2.3 通知等待条件变量的线程

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);  // 唤醒所有线程
int pthread_cond_signal(pthread_cond_t *cond);     // 只能唤醒一个线程

2.4 等待条件变量

调用pthread_cond_wait()函数时,调用者把互斥锁传递给函数,函数会自动把调用线程放到等待条件的线程列表上,然后将互斥锁解锁;当 pthread_cond_wait()被唤醒返回时,会再次锁住互斥锁。

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

mutex:参数 mutex 是一个 pthread_mutex_t 类型指针,指向一个互斥锁对象;前面开头便给大家介绍了,条件变量通常是和互斥锁一起使用,因为条件的检测(条件检测通常是需要访问共享资源的)是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的。

3. 自旋锁

互斥锁是基于自旋锁来实现的,所以自旋锁相较于互斥锁更加底层。

互斥锁在无法获取到锁时会让线程陷入阻塞等待状态;而自旋锁在无法获取到锁时,将会在原地“自旋”等待。

互斥锁和自旋锁区别:


实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;

销上的区别:获取不到互斥锁会陷入阻塞状态(休眠),直到获取到锁时被唤醒;而获取不到自旋锁会在原地“自旋”,直到获取到锁;休眠与唤醒开销是很大的,所以互斥锁的开销要远高于自旋锁、自旋锁的效率远高于互斥锁;但如果长时间的“自旋”等待,会使得 CPU 使用效率降低,故自旋锁不适用于等待时间比较长的情况。

使用场景的区别:自旋锁在用户态应用程序中使用的比较少,通常在内核代码中使用比较多;因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能被抢占(内核中使用自旋锁会自动禁止抢占),一旦休眠意味着执行中断服务函数时主动交出了CPU 使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁!

3.1 创建

pthread_spin_t spinlock;

3.2 初始化

#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

pshard表示自旋锁的进程共享属性:

  • PTHREAD_PROCESS_SHARED: 该自旋锁可在多个进程中的线程之间共享
  • PTHREAD_PROCESS_PRIVATE: 只有在本进程的线程才能使用

3.3 加锁解锁

#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);  // 如果未能获取到锁,就立刻返回错误,错误码为 EBUSY
int pthread_spin_unlock(pthread_spinlock_t *lock);

4. 读写锁

读写锁的规则:

  • 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞。
  • 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。
    所以,读写锁非常适合于对共享数据读的次数远大于写的次数的情况。

4.1 创建

pthread_rwlock_t rwlock;

4.2 初始化

#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

4.3 读写锁上锁和解锁

#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

4.4 非阻塞方式加锁

#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

参数 rwlock 指向需要加锁的读写锁,加锁成功返回 0,加锁失败则返回 EBUSY。

4.5 属性

读写锁只有一个属性,那便是进程共享属性

#include <pthread.h>
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);

pshared:

  • PTHREAD_PROCESS_SHARED: 该读写锁可在多个进程中的线程之间共享
  • PTHREAD_PROCESS_PRIVATE: 只有在本进程的线程才能使用

疑问

条件变量哪里为什么要用while?, 而不用if判断

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<string.h>
int number = 0;
pthread_mutex_t mutex;
pthread_cond_t cond;
void *thread_func(void *arg)
{
  int id = (int)arg;
  printf("消费者.......\n");
  for (;;)
  {
    // 获取互斥锁(阻塞方式)
    pthread_mutex_lock(&mutex);
    // 获得条件变量(阻塞)
    while(number <= 0) {pthread_cond_wait(&cond, &mutex);}   // ????????????
    number--;
    printf("消费者%d--%d\n", id, number);
    // 释放互斥锁
    pthread_mutex_unlock(&mutex);
  }
}
int main(int argc, char **argv)
{
  int i = 0;
  int ret = 0;
  pthread_t tid_lis[5];
  // 初始化互斥锁
  pthread_mutex_init(&mutex, NULL);
  // 初始化条件变量
  pthread_cond_init(&cond, NULL);
  printf("debug\n");
  // 创建线程
  for (i=0;i<5;i++)
  {
    ret = pthread_create(&tid_lis[i], NULL, thread_func, (void *)(i+1));
    if (ret)
    {
      perror("pthread_create error");
      exit(-1);
    }
  }
  printf("生产者......\n");
  // 主线程作为生产者
  for(;;)
  {
    pthread_mutex_lock(&mutex);    // 获取锁
    number++;
    printf("生产者++\n");
    pthread_mutex_unlock(&mutex);  // 释放锁
    pthread_cond_signal(&cond);    // 发送“信号”
    //sleep(1);
  }
  pthread_mutex_destroy(&mutex);
  return 0;
}


目录
相关文章
|
Linux
LinuxC线程
LinuxC线程
87 0
|
18天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
48 1
|
3月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
65 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
|
3月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
54 1
|
3月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
62 1
|
3月前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
54 1
|
2月前
|
数据采集 Java Python
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
74 0