目录
引言
一、多线程设计
多线程模拟抢票
二、互斥锁
互斥量的接口
修改抢票代码
锁的原理
锁的封装:RAII
引言
随着信息技术的飞速发展,计算机软件正变得越来越复杂,对性能和响应速度的要求也日益提高。在这样的背景下,多线程编程作为一种提高程序执行效率和资源利用率的技术,已经成为了现代软件开发中的重要组成部分。然而,多线程技术在带来性能提升的同时,也引入了一系列复杂的问题,如资源共享、数据一致性和线程间的协调。为了确保多线程环境下的程序正确性和稳定性,线程同步与线程互斥成为了我们必须深入探讨的关键领域。上文讲解了用户级线程(linux是用户级线程,win是内核级线程)本文将引导读者了解多线程编程的基本概念,探讨线程同步与线程互斥的重要性,并分析如何在多线程应用程序中有效地管理和控制线程行为。
一、多线程设计
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <string>using namespace std;class threadData
{
public:threadData(const string& name) :_threadname(name){}public:string _threadname;
};void* threadRountine(void* arg)
{pthread_detach(pthread_self()); //detach thread from main thread,但是不能保证主线程最后退出!threadData* td = static_cast<threadData*>(arg);int i = 0;while (i < 5){cout << td->_threadname << " ,test_i = " << i << ", &test_i = " << (&i)<< endl;i++; sleep(1);}delete td;return nullptr;
}int main()
{vector<pthread_t> tids;for (int i = 0; i < 5; i++){pthread_t tid;threadData* td = new threadData("Thread" + to_string(i));pthread_create(&tid, nullptr, threadRountine, td);tids.push_back(tid);sleep(1);}return 0;
}
可以观察到,栈内的数据是线程独享的。每个线程都有自己独立的栈结构.地址不同,变量的数值也不同,同时哪个线程先执行也是未知的。
需要注意的是,我们观察到有的线程并没有跑完5个完整的循环,这是因为:pthread_detach函数不能保证主线程最后退出,但是pthread_join函数能保证主线程最后退出,如果主线程退出,那么进程就会退出。
其实在线程当中没有秘密(都在一个地址空间)。我们可以定义一个全局的变量,让线程中使用这个变量,并实例化这个变量,我们就可以在主线程、其他线程获得这个数据。
虽然可以,但是线程独立栈上的数据不要透露出去!-----保证独立,但不能保证私有!
即,如何让一个全局变量拥有线程私有的数值呢?----线程的局部存储技术
__thread:这是编译器支持的编译选项。只能定义内置类型,禁止自定义
发现地址并不一致。因此对g_val ++ 的时候,g_val属于线程私有。而且之前的地址很小,在全局区。现在的地址很大,就像是在堆栈一样大。
主函数:已初始化全局区 线程函数:共享区
用途:减少相似代码的多次编写
多线程模拟抢票
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <string>using namespace std;int tickets = 100;class threadData
{
public:threadData(int number) :_threadname("Thread" + to_string(number)){}public:string _threadname;
};void* GetTickets(void* arg)
{threadData* td = static_cast<threadData*>(arg);while (true){if (tickets > 0){usleep(10000); cout << td->_threadname << " got a ticket : " << tickets << endl; tickets--;}elsebreak;}cout << td->_threadname << " : quit" << endl;return nullptr;
}int main()
{vector<pthread_t> tids;vector<threadData*> tds;for (int i = 0; i < 5; i++){pthread_t tid;threadData* td = new threadData(i);tds.push_back(td);pthread_create(&tid, nullptr, GetTickets, td);tids.push_back(tid);}for (int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr); //原型:int pthread_join(pthread_t thread, void **retval);delete tds[i];} return 0;
}
我们设置了100张tickets,让5个线程去抢票。
发现抢票的逻辑出错了。
原因:
1.显示器是一个共享资源,没有得到保护,各个线程抢占显示器资源,导致打印出错。
2.tickets是一个共享资源,多个线程并发访问的时候,会导致临界资源收不到保护。作为临界资源,必须保证一次只能被一个执行流进入访问。并发访问是指多个执行单元(如线程、进程或任务)在同一时间段内试图同时访问共享资源(如内存、文件、数据库等)的情况
--操作需要三部才能完成
--操作不是原子的,不是安全的
二、互斥锁
互斥量的接口
pthread_mutex_t这是库给我们提供的一种数据类型
可以定义成全局,用这个宏初始化
定义成全局之后,不需要destroy销毁
不是全局,就需要借助函数初始化和销毁
初始化函数的第二个参数是属性相关,可以不用管,设置为nullptr
并发访问需要保证所有线程申请的是同一把锁。
加锁和解锁之间的区域,就可以保证数据是安全的访问的
为了让tickets数据是安全的,在访问tickets数据的阶段就需要加锁。
我们成功加锁之后,这个tickets全局变量就变成了临界资源。
在我们的代码中,只有一小部分在访问临界资源,这一部分叫做临界区
互斥:在任何时刻,只让一个执行流去访问临界资源。
锁:拒绝并发,串行执行,保护数据的安全。
锁之间的代码是原子性的
/*
并发(Concurrency)
并发是指系统能够处理多个任务同时存在的能力。在并发的情况下,任务可以交替进行,即一个任务开始执行,在它执行的过程中,可以暂停并让另一个任务执行,然后第一个任务可以在适当的时候恢复执行。并发强调的是任务管理的灵活性,而不是速度。
并发的一些关键点包括:
时间共享:多个任务共享同一个处理器的执行时间。
上下文切换:操作系统可以在任务之间快速切换,某个时间段同时处理多个任务。
协调:并发系统需要处理任务之间的资源共享和通信问题。
并行(Parallelism)
并行是指同时执行多个任务的能力。并行执行通常需要多个处理器或多核处理器,这样每个处理器可以同时处理一个任务或任务的一部分(多个cpu,可以一次从执行队列调出多个线程去执行)。
并行的一些关键点包括:
同时性:多个任务或多个部分同时在不同的处理器上执行。
硬件依赖:并行执行通常需要特定的硬件支持,如多核CPU或分布式系统。
*/
修改抢票代码
增加上锁保护机制。
class threadData
{
public:threadData(int number, pthread_mutex_t* mutex) :_threadname("Thread" + to_string(number)),_mutex(mutex){}public:string _threadname;pthread_mutex_t* _mutex;
};
需要在任何形式的访问共享资源前加锁。抢完一次票需要解锁。
解锁需要在break之前,防止break之后直接跳出循环,这样最后一次循环中申请的锁就无法解锁。
现象
发现票都被两个线程抢完了,虽然不会出现tickets出错,但是出现了逻辑出错。
为什么不把锁放在while外部呢?---当然不可以
逻辑上:1.不可以把票一次性--完 2.增加了执行负担,降低并发效率
执行临界区的代码需要先申请锁,这个锁只有一把(所有线程只能看到同一把锁)。
申请锁成功才能去执行临界资源,否则执行流会在锁处阻塞。
申请锁失败:1.正常申请失败 2.锁被其他人拿走……这个时候线程会等待锁资源。
虽然票不是负数,但是被一个线程抢完了。
加上休眠时间
只要不访问临界区了,就可以解锁。
这样就可以保证规则地去抢锁。
/*
锁与时间片:当一个线程获得锁并开始执行被加锁的代码块时,它可能会占用一个或多个时间片。如果在执行过程中时间片用尽,操作系统可能会进行线程调度,将该线程挂起,并将CPU时间分配给其他线程。但是,即使发生线程切换,持有锁的线程在释放锁之前,其他线程是无法进入该锁保护的代码块的。
也就说,就算锁资源在执行的过程中因为时间消耗完毕而被切换,那么其他执行流在执行的时候(线程是被执行调度的基本单位),也禁止访问“被锁住”的资源,从而保证资源的安全性,从而在现象上达成了原子性的现象
*/
全局锁:不需要初始化与销毁
(用全局的宏初始化)内部加锁就直接用全局的锁就可以。
即:省去了init与destory函数
但是还是得手动lock与unlock。
这个时候就不需要类内封装锁,使用全局的锁就可以
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
之前代码的错误:
锁被释放之后,那么在循环又接着去申请锁,导致不断地申请释放锁,从而导致资源一直被锁在一个执行流(线程)之中。
大白话就是:你这个线程离这个锁更近,每个线程竞争锁的能力是不同的。
当修改之后,这个进程在休眠期间,其他进程就有机会去竞争锁资源。
我们对于锁的申请是允许并发(一段时间内,各个执行流都有机会被执行)的,只不过上锁(lock)这句代码是原子的。
条件变量:线程同步解决饥饿问题
锁的原理
锁也是共享的。我们对于锁的申请是允许并发(一段时间内,各个执行流都有机会被执行)的,只不过上锁(lock)这句代码是原子的。
但是
该部分将着重解决两个问题,第一个问题就是锁的原理问题。
问题2
答案是可以切换的
这是锁通过对线程的控制完成的。
问题一解答:锁的原理
唤醒其他线程,让锁的内容从0变1(上锁经历了xchg之后,锁的内容为0),这样就恢复到了锁初始化的状态,让其他线程有能力申请锁。
锁的封装:RAII
#pragma once#include <pthread.h>class Mutex
{
public:Mutex(pthread_mutex_t* lock): _lock(lock){}void Lock(){pthread_mutex_lock(_lock);}void Unlock(){pthread_mutex_unlock(_lock);}~Mutex(){}private:pthread_mutex_t* _lock;
};class LockGuard // RAII class for locking and unlocking a mutex---需要传入指针
{
public:LockGuard(pthread_mutex_t* mutex): _mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex _mutex;
};
在使用的时候,我们应该在外面建立一个锁的指针,就像是智能指针一样。
注意:
1,限定作用域,出了作用域之后,这个临时对象会自动调用析构去解锁
2,业务的处理时间不应该在if内,不应该在加锁内部。
发现正确抢完了票。
由于打印在锁的内部,所以显示器资源也被保护了。