目录
进程和线程
线程和进程是一对有意义的概念,主要区别和联系如下:
- 进程是操作系统进行资源分配的基本单位,进程拥有完整的虚拟空间。进行系统资源分配的时候,除了CPU资源之外,不会给线程分配独立的资源,线程所需要的资源需要共享。
- 线程是进程的一部分,如果没有进行显式地线程分配,可以认为进程是单线程的;如果进程中建立了线程,则可以认为系统是多线程的。
- 多线程和多进程是两种不同的概念,虽然二者都是并行完成功能。但是,多个线程之间像内存、变量等资源可以通过简单的办法共享,多进程则不同,进程间的共享方式有限。
- 进程有进程控制表PCB,系统通过PCB对进程进行调度;线程有线程控制表TCB.但是,TCB所表示的状态比PCB要少得多。
线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
Linux下线程创建函数pthread_ create()
函数pthread_create()用于创建一个线程。
在pthread_create()函数调用时,传入的参数有线程属性、线程函数、线程函数变量,用于生成一个某种特性的线程, 线程中执行线程函数。创建线程使用函数pthread_ create(),它的原型为:
#include <pthread.h>
int pthread_create( pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),//函数指针void *arg );
- thread:用于标识一个线程,它是一个pthread_ t类型的变量,在头文件pthreadtypes.h中定义:typedef unsigned long int pthread t;
- attr:这个参数用于设置线程的属性,设置为空,采用了默认属性。
- start_ routine:当线程的资源分配成功后,线程中所运行的单元,一般设置为自己编写的一个函数start_routine()。
- arg: 线程函数运行时传入的参数,将一个run的参数传入用于控制线程的结束。
当创建线程成功时,函数返回0;若不为0,则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。错误代码EAGAIN表示系统中的线程数量达到了上限,错误代码EINVAL表示线程的属性非法。
线程创建成功后,新创建的线程按照参数3和参数4确定一个运行函数,原来的线程在线程创建函数返回后继续运行下一行代码。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void *threadRun(void *args)
{
while (true)
{
sleep(1);
cout << "我是子线程..." << endl;
}
}
int main()
{
pthread_t t1;
pthread_create(&t1, nullptr, threadRun, nullptr);
while (true)
{
sleep(1);
cout << "我是主线程..." << endl;
}
}
在编译时,我们需要链接线程库libpthread
g++ -o myphread myphread .cc -lpthread
从运行结果可以看到结果并不规律,主要是两个线程争夺CPU资源造成的。
线程的等待函数pthread_ join()
为什么需要线程等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
函数pthread_join()用来等待一 个线程运行结束。这个函数是阻塞函数,一直等到被等待的线程结束为止,函数才返回并且收回被等待线程的资源。函数原型为:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
- thread:线程的标识符,即pthread_create()函数创建成功的值。
- retval:线程返回值,它是一个指针,可以用来存储被等待线程的返回值。
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的:
1. 如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调用pthread_ cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_ CANCELED。
3. 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
4. 如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
2. 线程可以调用pthread_ exit终止自己。
3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
函数pthread_exit()
void pthread_exit(void *retval);
线程终止函数,跟进程一样,无返回值,线程结束的时候无法返回到它的调用者(自身)。
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
函数pthread_cancel()
取消一个执行中的线程
int pthread_cancel(pthread_t thread);
返回值:成功返回0;失败返回错误码 。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
static int retvalue;//线程返回值
void *threadRun1(void *args)
{
cout << "我是子线程1..." << endl;
retvalue = 1;
return (void *)&retvalue;
}
void *threadRun2(void *args)
{
cout << "我是子线程2..." << endl;
retvalue = 2;
pthread_exit((void *)&retvalue);
}
void *threadRun3(void *arg)
{
while (1)
{
cout << "我是子线程3..." << endl;
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
void *ret;
// threadRun1 return
pthread_create(&tid, NULL, threadRun1, NULL);
pthread_join(tid, &ret);
cout << "子线程1返回... 返回值:" << *(int *)ret << endl;
// threadRun2 exit
pthread_create(&tid, NULL, threadRun2, NULL);
pthread_join(tid, &ret);
cout << "子线程2返回... 返回值:" << *(int *)ret << endl;
// threadRun3 cancel by other
pthread_create(&tid, NULL, threadRun3, NULL);
sleep(3);
pthread_cancel(tid);
pthread_join(tid, &ret);
if (ret == PTHREAD_CANCELED)
cout << "子线程3返回... 返回值:PTHREAD_CANCELED" << endl;
else
cout << "子线程3返回... 返回值:NULL" << endl;
}
分离线程pthread_detach()
int pthread_detach(pthread_t thread);
线程的分离状态决定线程的终止方法。线程的分离状态有分离线程和非分离线程两种。
在上面的例子中,线程建立的时候没有设置属性,默认终止方法为非分离状态。在这种情况下,需要等待创建线程的结束。只有当pthread_join()函数返回时,线程才算终止,并且释放线程创建的时候系统分配的资源。
分离线程不用其他线程等待,当前线程运行结束后线程就结束了,并且马上释放资源。线程的分离方式可以根据需要,选择适当的分离状态。
当将一个线程设置为分离线程时,如果线程的运行非常快,可能在pthread_create()函数返回之前就终止了。由于一个线程在终止以后可以将线程号和系统资源移交给其他的线程使用,此时再使用函数pthread_create()获得的线程号进行操作会发生错误。
理解线程库和线程ID
线程库是为了给用户提供创建线程的接口。线程库会被映射进程到共享区,我们进程中的线程可以随时访问库中的代码和数据。在库里存在相关描述线程的结构体。
#include<iostream> #include<string> #include<unistd.h> using namespace std; std::string hexAddr(pthread_t tid) { char buffer[64]; snprintf(buffer, sizeof(buffer), "0x%x", tid); return buffer; } void *threadRoutine(void* args) { string name = static_cast<const char*>(args); int cnt = 5; while(cnt) { cout << name << " : " << cnt-- << " : " << hexAddr(pthread_self()) << " &cnt: " << &cnt << endl; sleep(1); } return nullptr; } int main() { pthread_t t1, t2, t3; pthread_create(&t1, nullptr, threadRoutine, (void*)"thread 1"); // 线程被创建的时候,谁先执行不确定! pthread_create(&t2, nullptr, threadRoutine, (void*)"thread 2"); // 线程被创建的时候,谁先执行不确定! pthread_create(&t3, nullptr, threadRoutine, (void*)"thread 3"); // 线程被创建的时候,谁先执行不确定! pthread_join(t1, nullptr); pthread_join(t2, nullptr); pthread_join(t3, nullptr); return 0; }
线程间同步
在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问 题,叫做同步。POSIX信号量是用于同步操作,达到无冲突的访问共享资源目的。 POSIX可以用于线程间同步。
信号量
信号量(semaphore)是操作系统中最常用的同步原语之一。自旋锁是一种实现忙等待的锁,而信号量则允许进程进入睡眠状态。简单来说,信号量是一个计数器,它支持两个操作原语,即P和V操作。P和V原指荷兰语中的两个单词,分别表示减少和增加,后来美国人把它改成down和up,现在Linux内核中也叫这两个名字。
信号量中经典的例子莫过于生产者和消费者问题,它是操作系统发展历史上经典的进程同步问题,最早由Dijkstra提出。假设生产者生产商品,消费者购买商品,通常消费者需要到实体商店或者网上商城购买商品。用计算机来模拟这个场景,一个线程代表生产者, 另外一个线程代表消费者,缓冲区代表商店。生产者生产的商品被放置到缓冲区中以供应给消费者线程消费,消费者线程从缓冲区中获取物品,然后释放缓冲区。若生产者线程生产商品时发现没有空闲缓冲区可用,那么生产者必须等待消费者线程释放出一个空闲缓冲区。若消费者线程购买商品时发现商店没货了,那么消费者必须等待,直到新的商品生产出来。如果采用自旋锁机制,那么当消费者发现商品没货时,他就搬个凳子坐在商店门口一直等送货员送货过来;如果采用信号量机制,那么商店服务员会记录消费者的电话,等到货了通知消费者来购买。显然,在现实生活中,如果是面包这类很快可以做好的商品,大家愿意在商店里等;如果是家电等商品,大家肯定不会在商店里等。
线程的信号量与进程的信号量类似,但是使用线程的信号量可以高效地完成基于线程的资源计数。信号量实际上是一个非负的整数计数器,用来实现对公共资源的控制。在公共资源增加的时候,信号量的值增加;公共资源消耗的时候,信号量的值减少;只有当信号量大于0的时候,才能访问信号量所代表的公共资源。
信号量的主要函数有信号量初始化函数sem_init()、 信号量的销毁函数sem_destroy()、
信号量的增加函数sem_pom()、信号量的减少函数sem_wait()等。 还有一个函数sem_trywait(),它的含义与互斥的函数pthread_mutex_trylock()是一致的,先对资源是否可用进行判断。函数的原型在头文件文件semaphore.h中定义。
1.线程信号量初始化函数sem_int()
sem_int()函数用来初始化一个信号量。它的原型为:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
- sem指向信号量结构的一个指针, 当信号量初始化完成的时候,可以使用这个指针进行信号量的增加减少操作;
- 参数pshared用于表示信号量的共享类型,不为0时这个信号量可以在进程间共享,否则这个信号量只能在在当前进程的多个线程之间共享;
- 参数value用于设置信号量初始化的时候信号量的值。
2.线程信号量增加函数sem_post()
sem_post(函数的作用是增加信号量的值,每次增加的值为1。当有线程等待这个信号量的时候,等待的线程将返回。函数的原型为:
#include <semaphore.h>
int sem_post (sem_t *sem) ;
3.线程信号量等待函数sem_wait()
sem_wait()函数的作用是减少信号量的值,如果信号量的值为0,则线程会一直阻塞到信号量的值大于0为止。sem_wait()函数每次使信号量的值减少1,当信号量的值为0时不再减少。函数原型为:
#include <semaphore. h>
int sem_wait (sem_t *sem) ;
4.线程信号量销毁函数sem_destroy()
sem_destroy()函数用来释放信号量sem,函数原型为:
#include <semaphore.h>
int sem_destroy(sem_t *sem) ;
5.线程信号量的例子
下面来看一个使用信号量的例子。在mutex的例子中,使用了一个全局变量来计数,在这个例子中,使用信号量来做相同的工作,其中一个线程增加信号量来模仿生产者,另一个线程获得信号量来模仿消费者。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
sem_t sem;
int running = 1;//线程运行控制
int cnt = 5;
//生产者
void *producter_f (void *arg)
{
int semval = 0;
while (running)
{
usleep(1);
sem_post(&sem);//信号量增加
sem_getvalue(&sem,&semval);//获取信号量的值
printf("生产,总数量:%d\n",semval);
}
}
//消费者
void *consumer_f (void *arg)
{
int semval = 0;
while (running)
{
usleep(1);
sem_wait(&sem);//信号量减少
sem_getvalue(&sem,&semval);//获取信号量的值
printf("消费,总数量:%d\n",semval);
}
}
int main ()
{
pthread_t consumer_t;//消费者线程参数
pthread_t producter_t;//生产者线程参数
sem_init(&sem , 0, 16);//信号量初始化
pthread_create(&producter_t, NULL, producter_f, NULL);//建立生产者线程
pthread_create(&consumer_t, NULL,consumer_f, NULL) ;//建立消费者线程
usleep(1) ;//等待
running =0;//设置线程退出值
pthread_join(consumer_t, NULL);//等待消费者线程退出
pthread_join (producter_t, NULL) ;//等待生产者线程退出
sem_destroy(&sem);//信号量销毁
return 0;
}
从执行结果可以看出,各个线程间存在竞争关系。而数值并未按产生一个消耗一个的顺序显示出来,而是以交叉的方式进行,有的时候产生多个后再消耗多个,造成这种现象的原因是信号量的产生和消耗线程对CPU竞争的结果。
信号量有一个有趣的特点,它可以同时允许任意数量的锁持有者。信号量初始化函数为sem_init(struct semaphore *sem, int count),其中count的值可以大于或等于1。当count大于1时,表示允许在同一时刻至多有count 个锁持有者,这种信号量叫作计数信号量(countingsemaphore);当count等于1时,同一时刻仅允许一个CPU持有锁,这种信号量叫作互斥信号量或者二进制信号量(binary semaphore)。 在Linux内核中,大多使用count值为1的信号量。相比自旋锁,信号量是一个允许睡眠的锁。信号量适用于一些情况复杂、加锁时间比较长的应用场景, 如内核与用户空间复杂的交互行为等。
线程间的互斥
进程线程间的互斥相关背景概念
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界自娱的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,会带来一些问题。
代码模拟抢票过程:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
int tickets = 100;
void *threadRoutine(void *name)
{
string tname = static_cast<const char*>(name);
while(true)
{
if(tickets > 0)
{
usleep(2000); // 模拟抢票花费的时间
cout << tname << " get a ticket: " << tickets-- << endl;
}
else
{
break;
}
}
return nullptr;
}
int main()
{
pthread_t t[4];
int n = sizeof(t)/sizeof(t[0]);
for(int i = 0; i < n; i++)
{
char *data = new char[64];
snprintf(data, 64, "thread-%d", i+1);
pthread_create(t+i, nullptr, threadRoutine, data);
}
for(int i = 0; i < n; i++)
{
pthread_join(t[i], nullptr);
}
return 0;
}
我们发现由于多线程的并发访问票直接被抢到了负数,这在现实中是不合理的。 当票只剩下一张时,有多个线程访问这个临界资源,所以我们要对临界资源进行加锁。
互斥锁
信号量根据初始count的大小,可以分为计数信号量和互斥信号量。根据著名的洗手间理论,信号量相当于一个可以同时容纳N个人的洗手间,只要洗手间人不满,其他人就可以进去,如果人满了,其他人就要在外面等待。互斥锁类似于街边的移动洗手间,每次只能进去一个人,里面的人出来后才能让排队中的下一个人进去。既然互斥锁类似于count值等于1的信号量,为什么内核社区要重新开发互斥锁,而不是复用信号量的机制呢?
在设计之初,信号量在Linux内核中的实现没有任何问题,但是互斥锁相对于信号量要简单轻便一些。 在锁争用激烈的测试场景下,互斥锁比信号量执行速度更快,可扩展性更好。另外,mutex 数据结构的定义比信号量小。这些都是在互斥锁设计之初的优点。互斥锁上的一些优化方案(如自旋等待)已经移植到了读写信号量中。
线程互斥的函数介绍
与线程互斥有关的函数原型和初始化的常量如下,主要包含互斥的初始化方式宏定义、互斥的初始化函数pthread_mutx_init()、 互斥的锁定函数pthread_mutex_lock()、 互斥的预锁定函数pthread_mutx_trylock()、互斥的解锁函数pthread_mutx_unlock()、 互斥的销毁函数pthread_mutex_destroy()。
函数pthread_mutx_init(),初始化一 个 mutex变量,结构pthread_mutex_ t为系统内部私有的数据类型,在使用时直接用pthread_mutex_t就可以了,因为系统可能对其实现进行修改。属性为NULL,表明使用默认属性。
pthread_ mutex_lock()函数声明开始用互斥锁上锁,此后的代码直至调用pthread_mutx_unlock()函数为止,均不能执行被保护区域的代码,也就是说,在同时间内只能有一个线程执行。当一个线程执行到pthread_mutex _lock()函数处时,如果该锁此时被另一个线程使用,此线程被阻塞,即程序将等待另一个线程释放此互斥锁。互斥锁使用完毕后记得要释放资源,调用pthread_mutex_destroy()函数进行释放。
互斥锁是用来保护一段临界区的,它可以保证某时间段内只有一个线程在执行一段代码或者访问某个资源。下面一段代码是一个生产者/消费者的实例程序,生产者生产数据,消费者消耗数据,它们共用一个变量, 每次只有一个线程访问此公共变量。
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
int tickets = 100;
class TData
{
public:
TData(const string &name, pthread_mutex_t *mutex):_name(name), _pmutex(mutex)
{}
~TData()
{}
public:
string _name;
pthread_mutex_t *_pmutex;
};
void *threadRoutine(void *args)
{
TData *td = static_cast<TData*>(args);
while(true)
{
pthread_mutex_lock(td->_pmutex);
if(tickets > 0)
{
usleep(2000); // 模拟抢票花费的时间
cout << td->_name << " get a ticket: " << tickets-- << endl;
pthread_mutex_unlock(td->_pmutex);
}
else
{
pthread_mutex_unlock(td->_pmutex);
break;
}
}
return nullptr;
}
int main()
{
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, nullptr);
pthread_t tids[4];
int n = sizeof(tids)/sizeof(tids[0]);
for(int i = 0; i < n; i++)
{
char name[64];
snprintf(name, 64, "thread-%d", i+1);
TData *td = new TData(name, &mutex);
pthread_create(tids+i, nullptr, threadRoutine, td);
}
for(int i = 0; i < n; i++)
{
pthread_join(tids[i], nullptr);
}
pthread_mutex_destroy(&mutex);
return 0;
}
互斥锁比信号量的实现要高效很多。
- 互斥锁最先实现自旋等待机制。
- 互斥锁在睡眠之前尝试获取锁。
- 互斥锁通过实现MCS锁来避免多个CPU争用锁而导致CPU高速缓存行颠簸现象。
正是因为互斥锁的简洁性和高效性,所以互斥锁的使用场景比信号量要更严格,使用互斥锁需要注意的约束条件如下。
- 同一时刻只有一个线程可以持有互斥锁。
- 只有锁持有者可以解锁。 不能在一个进程中持有互斥锁, 而在另外一个进程中释放它。因此互斥锁不适合内核与用户空间复杂的同步场景,信号量和读写信号量比较适合。
- 不允许递归地加锁和解锁。
- 当进程持有互斥锁时,进程不可以退出。
- 互斥锁必须使用官方接口函数来初始化。
- 互斥锁可以睡眠,所以不允许在中断处理程序或者中断下半部(如tasklet、定时器等)中使用。
在实际项目中,该如何选择自旋锁、信号量和互斥锁呢?
在中断上下文中可以毫不犹像地使用自旋锁,如果临界区有睡眠、隐含睡眠的动作及内核接口函数,应避免选择自旋锁。在信号量和互斥锁中该如何选择呢?除非代码场景不符合上述互斥锁的约束中的某一条, 否则可以优先使用互斥锁。
使用C++对线程进行封装
#include <iostream>
#include <string>
#include <cstdlib>
#include <pthread.h>
class Thread
{
public:
typedef enum
{
NEW = 0,
RUNNING,
EXITED
} ThreadStatus;
typedef void (*func_t)(void *);
public:
Thread(int num, func_t func, void *args) : _tid(0), _status(NEW), _func(func), _args(args)
{
char name[128];
snprintf(name, sizeof(name), "thread-%d", num);
_name = name;
}
int status() { return _status; }
std::string threadname() { return _name; }
pthread_t threadid()
{
if (_status == RUNNING)
return _tid;
else
{
return 0;
}
}
// 类的成员函数,具有默认参数this,需要static
// 但是会有新的问题:static成员函数,无法直接访问类属性和其他成员函数,将this指针传过来
static void *runHelper(void *args)
{
Thread *ts = (Thread*)args; // 拿到了当前对象
// _func(_args);
(*ts)();
return nullptr;
}
void operator ()() //仿函数
{
if(_func != nullptr) _func(_args);
}
void run()
{
int n = pthread_create(&_tid, nullptr, runHelper, this);
if(n != 0) exit(1);
_status = RUNNING;
}
void join()
{
int n = pthread_join(_tid, nullptr);
if( n != 0)
{
std::cerr << "main thread join thread " << _name << " error" << std::endl;
return;
}
_status = EXITED;
}
~Thread()
{}
private:
pthread_t _tid;
std::string _name;
func_t _func; // 线程未来要执行的回调
void *_args;
ThreadStatus _status;
};
使用C++对互斥锁进行封装
#include <iostream>
#include <pthread.h>
class Mutex // 自己不维护锁,有外部传入
{
public:
Mutex(pthread_mutex_t *mutex):_pmutex(mutex)
{}
void lock()
{
pthread_mutex_lock(_pmutex);
}
void unlock()
{
pthread_mutex_unlock(_pmutex);
}
~Mutex()
{}
private:
pthread_mutex_t *_pmutex;
};
class LockGuard // 自己不维护锁,由外部传入
{
public:
LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
{
_mutex.lock();
}
~LockGuard()
{
_mutex.unlock();
}
private:
Mutex _mutex;
};
使用
#include <iostream>
#include <string>
#include <unistd.h>
#include "Thread.hpp"
#include "lockGuard.hpp"
using namespace std;
int tickets = 100; // 全局变量,共享对象
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 在外边定义的锁
void threadRoutine(void *args)
{
std::string message = static_cast<const char *>(args);
while (true)
{
{
LockGuard lockguard(&mutex); // RAII 风格的锁
if (tickets > 0)
{
usleep(2000);
cout << message << " get a ticket: " << tickets-- << endl; // 临界区
}
else
{
break;
}
}
}
}
int main()
{
Thread t1(1, threadRoutine, (void *)"hello world");
Thread t2(2, threadRoutine, (void *)"hello leiyaling");
t1.run();
t2.run();
t1.join();
t2.join();
return 0;
}
互斥锁能够保证线程是安全的,因为锁只有加锁和解锁,是原子的。
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
常见的线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
- 可重入函数体内使用了静态的数据结构
常见可重入的情况
- 不使用全局变量或静态变量
- 不使用用malloc或者new开辟出的空间
- 不调用不可重入函数 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
- 函数是可重入的,那就是线程安全的;
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题 。
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
- 可重入函数是线程安全函数的一种。
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
读写锁
上述介绍的信号量有一个明显的缺点——没有区分临界区的读写属性。读写锁通常允多个线程并发地读访问临界区,但是写访间只限制于一个线程。读写锁能有效地提高并发性在多处理器系统中允许有多个读者同时访问共享资源,但写者是排他性的,读写锁具有如下特性。
- 允许多个读者同时进入临界区,但同一时刻写者不能进入。
- 同一时刻只允许一个写者进入临界区。
- 读者和写者不能同时进入临界区。
死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
死锁四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配