操作系统-线程详解

线程

基本知识

线程是一种轻量级的进程,其被包含在进程之中,是进程中实际的运作单位,是调度和任务执行的基本单位

进程和线程的区别?

  1. 开销方面:每个进程都有独立的代码和数据空间,进程切换的开销大,线程是轻量级进程,同一个进程的线程共享代码和数据空间,每个线程有自己的栈和寄存器,线程切换的开销小
  2. 所处环境:在操作系统中可以有多个进程同时执行,而在一个进程中可以有多个线程同时执行
  3. 内存分配:系统在运行时为每个进程分配不同的内存空间,而对线程而言除了CPU外,系统不会为线程分配内存(线程所使用的栈内存也是其所属进程的内存),线程组之间只能共享资源
  4. 包含关系:线程是进程的一部分,没有线程的进程可以看成单线程的

注:线程有自己的栈空间,但是堆空间是共享的

引入线程的目的是让进程的多个程序段可以同时执行,更好的利用CPU,也能加快程序执行

多进程和多线程?(尤其要说切换开销,调度,CPU利用率,资源共享,上下文切换等)

  1. 多进程是指系统中同时运行两个甚至多个进程,所谓同时运行多个进程需要用到并发技术,所有正在运行的进程轮流使用CPU,多进程各个进程相互独立,各个程序之间没有互相影响,进程数据共享复杂需要用到IPC,但是同步简单,进程创建、销毁和切换的代价高。
  2. 多线程即将一个进程分为多个独立的过程,即一个进程可以包含多个线程,其相对进程而言创建(因为不用拷贝到自己的内存)、销毁和切换的代价低,多线程共享进程的数据,不过每个线程还是拥有自己的堆栈和寄存器,但是线程之间的同步互斥则很复杂,而且一个线程的崩溃可能导致整个程序出问题
  3. 多线程适用于需要频繁的创建销毁的应用,适用于IO密集型应用(因为IO时线程阻塞,则不能让CPU闲下来,让其去调度其他线程),多线程更适用于单机多核场景
  4. 多进程模型的优势是CPU,所以其使用于CPU密集型场景,同时也更适用于多机分布场景中,适合多机扩展
线程同步

多线程同步包括:互斥锁、条件变量、信号量、读写锁、自旋锁

线程同步是因为多线程共享进程内存,那么可能每个线程看到不一致的数据,这样会导致一些问题

哪些情况需要同步?

当多个进程可能同时写某个变量,或者可能同时读写某个变量。只读变量不会出现同步问题

互斥量

其实就是一个互斥锁,一次只允许一个变量去访问资源

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_init(pthread_mutex_t* mutex, NULL);
int pthread_mutex_destroy(pthread_mutex_t* mutex);

pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
pthread_mutex_trylock(&mutex);
//trylock 函数如果有线程加锁了则返回-1,错误为EBUSY,如果加锁成功则返回0
pthread_mutex_timedlock(&mutex,const struct timespec* time);
//可以指定超时时间,如果无法获取锁则阻塞,如果阻塞超时则返回错误,这样可以避免死锁

当多个线程访问一个动态对象时,最好引入引用计数,从而能够访问结束后释放对象

使用互斥量要防止死锁的出现:所谓死锁是指线程/进程想要获取某个资源(锁)但是该资源被其他线程/进程所占有,但是其他线程/进程又想获取当前线程/进程的资源从而导致死锁

读写锁

读写锁可以有三种状态,即读加锁,写加锁,不加锁,一次只有一个线程可以占有写模式下的读写锁,但是多个线程可以同时占有读模式下的读写锁

当读写锁在写加锁时,所有尝试加锁的线程都会被阻塞,但是当在读加锁下时,如果其他线程想以读模式加锁,则可以得到访问权,但是如果以写模式加锁则会被阻塞,当读写锁以读模式被加锁时,此时如果某个线程想加写锁则会被阻塞,但是如果此时其他线程也想加读锁时,其也会被阻塞,这样可以防止写线程长期被阻塞得不到执行

读写锁的应用场景:

读写锁适用于读次数远大于写次数的场景,这样可以提高并行性,例如某个线程想读取某个变量,但是又害怕读取之后该变量被修改(那么后序使用该变量可能会出现问题),此时可以通过加读写锁,从而提高并行性,而不是加互斥锁(例如当需要以读的形式遍历某个数组,此时需要加读锁以防止队列被修改

pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
pthread_rwlock_destroy(&rwlock);

pthread_rwlock_rdlock(&rwlock);//读加锁
pthread_rwlock_wrlock(&rwlock);//写加锁
pthread_rwlock_unlock(&rwlock);//解锁
pthread_rwlock_tryrdlock(&rwlock);
pthread_rwlock_trywrlock(&rwlock);

pthread_mutex_timedrdlock(&rwlock,const struct timespec* time);
pthread_mutex_timedwrlock(&rwlock,const struct timespec* time);
自旋锁

自旋锁与普通的锁的区别在于当自旋锁尝试获取锁的所有权时会以忙等的形式不断地循环检查锁是否可用,在多处理器环境下对持有锁时间较短地程序而言,使用自旋锁代替一般地互斥锁效率更高,因为普通锁则会将线程阻塞唤醒,从而带来一些效率问题(线程上下文切换)

缺点:

  1. 某个线程持有自旋锁时间过长,则导致其他等待获取锁的线程进入循环等待,消耗CPU,使用不当可能导致CPU使用率极高
  2. 自旋锁不公平,无法满足等待时间最长的线程优先获取锁

注意:获取自旋锁的线程不要执行可能阻塞的操作,不然会导致其他自旋阻塞的线程等待时间变长

pthread_spinlock_t spinlock;
pthread_spin_init(&spinlock, int psharde);
pthread_spin_destroy(&spinlock);

pthread_spin_lock(&spinlock);
pthread_spin_trylock(&spinlock);
pthread_spin_unlock(&spinlock);
//加锁成功则返回0
条件变量

条件变量是利用线程间共享的变量进行同步的一种机制,即条件变量通过某个变量阻塞线程,直到特定条件为真线程才唤醒,条件变量和互斥锁一起使用,其对于条件的测试是在互斥锁的保护下进程的。

条件变量的使用如下:

线程加锁后测试条件为假,线程阻塞,同时解锁,当某个线程修改了条件为真后,唤醒阻塞线程,同时加锁

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_init(&cond, NULL);
pthread_cond_destroy(&cond);

pthread_cond_wait(&cond, &mutex);
pthread_cond_timewait(&cond, &mutex, const struct timespec* time);//超时返回-1,同时重新加锁
pthread_cond_signal(&cond);//唤醒某一个
pthread_cond_broadcast(&cond);//唤醒所有

关于条件变量的几个问题?

  1. 为什么条件变量需要加锁?
    • 条件本身就是公共资源,对公共资源的方便需要加锁
    • 其次pthread_cond_wait实际上分为两步操作,即将线程挂在等待队列中,然后解锁,如果不传入锁,即在线程挂起前解锁,则在解锁和挂起线程之间如果插入了一个生产者线程生产了一个线程并进行了notify,而notify后该线程才挂起,则该notify就丢失了,所以根本还是为了避免竞争
  2. 条件变量唤醒时,是先signal再解锁还是先解锁再signal?
    • 如果先解锁,则可能此时恰好又来了一个消费者将刚刚的产品给拿走了,则下面的signal不起作用(浙这也是一种虚假唤醒),或者此时另外一个线程加锁了,则signal唤醒的线程将无法加锁继续阻塞
    • 如果后解锁,则可能唤醒时消费者无法获取到锁从而继续阻塞,但是linux解决了这个问题,linux维护两个队列,一个cond_wait和mutex_lock队列,signal只是将cond_wait队列中的线程移动到mutex_lock而不会返回到用户空间
    • 所以推荐使用后解锁
  3. 为什么条件的测试while,而不是if?
    • 为了防止虚假唤醒,在多核CPU下signal可能会唤醒多个线程,此时如果用if,则多个线程去执行操作可能出现错误,而用while的话,由于刚刚生产者的消费品已经被第一个线程所消费,所以此时虚假唤醒的线程会继续阻塞,即多个被唤醒的线程先获得的锁的那个可以继续执行,其可能将刚刚生产者生产的产品给取走了,这个时候下一个被唤醒的线程获得锁后必须先再判断条件是否满足,否则可能出问题
    • 同样2中所说signal之前解锁也可能出现虚假唤醒
    • 总之虚假唤醒是系统的问题,但是修复这个问题会导致条件变量性能降低,还不如用while
  4. 总之个人认为,当某个线程被signal唤醒后,会先尝试去加锁,如果此时的锁还没有释放则会阻塞,等到释放后才继续运行,而此时的条件可能已经改变了,所以需要while条件
信号量

信号量就是一个计数器,用于控制线程对共享资源的访问,对信号量的操作即PV操作,P操作时如果信号量为0则阻塞,否则信号量减1,程序继续运行,当V时如果有线程阻塞则唤醒,否则信号量加一

信号量和互斥锁的区别:

  1. 互斥量用于线程的互斥,而信号量用于线程的同步
    • 线程同步和互斥的区别?
  2. 互斥量只能是0/1,其确保了一次只能有一个线程去访问资源,但是信号量可以大于0,其可以用于多个资源和多个线程间的互斥问题,即实现了多个线程去访问多个(同类)资源,当信号量为单值时,其也成为了互斥量
  3. 互斥量必须由同一个线程进行加锁和释放,但是信号量可以是一个线程P/V,而其他线程V/P
发布了23 篇原创文章 · 获赞 4 · 访问量 2122

猜你喜欢

转载自blog.csdn.net/hdadiao/article/details/104614689