https://blog.youkuaiyun.com/liu5320102/article/details/50764645
https://blog.youkuaiyun.com/a987073381/article/details/52029070
https://blog.youkuaiyun.com/qq_41248872/article/details/82991949(讲解含图)
https://www.cnblogs.com/kellerfz/p/7862122.html(函数实现)
线程概念巩固
在Linux中,多线程的本质仍是进程,它与进程的区别:
进程:独立地址空间,拥有PCB
线程:也有PCB,但没有独立的地址空间(共享)
注:进程控制块(PCB Process Control Block)
线程的特点:
1,线程是轻量级进程,有PCB,创建线程使用的底层函数和进程一样,都是clone
2,从内核看进程和线程是一样的,都有各自不同的PCB
3,进程可以蜕变成线程
4,在LINUX中,线程是最小的执行单位,进程是最小的分配资源单位
查看指定线程的LWP号命令:
1 |
|
线程优点:
提高程序并发性
开销小
数据通信,共享数据方便
线程缺点:
库函数 ,不稳定
调试,编写困难,GDB
对信号支持不好
线程属性,可以在一开始就设置好分离态,具体在下面的代码有说明!
线程同步,主要有互斥锁mutex,读写锁,条件变量,信号量
C语言中使用多线程的函数
对象 | 操作 | Linux Pthread API | Windows SDK 库对应 API |
---|---|---|---|
线程 | 创建 | pthread_create | CreateThread |
退出 | pthread_exit | ThreadExit | |
等待 | pthread_join | WaitForSingleObject | |
互斥锁 | 创建 | pthread_mutex_init | CreateMutex |
销毁 | pthread_mutex_destroy | CloseHandle | |
加锁 | pthread_mutex_lock | WaitForSingleObject | |
解锁 | pthread_mutex_unlock | ReleaseMutex | |
条件 | 创建 | pthread_cond_init | CreateEvent |
销毁 | pthread_cond_destroy | CloseHandle | |
触发 | pthread_cond_signal | SetEvent | |
广播 | pthread_cond_broadcast | SetEvent / ResetEvent | |
等待 | pthread_cond_wait / pthread_cond_timedwait | SingleObjectAndWait |
多线程开发在 Linux 平台上已经有成熟的 Pthread 库支持。其涉及的多线程开发的最基本概念主要包含四点:线程,互斥锁,条件变量、读写锁。其中,线程操作又分线程的创建,退出,等待 3 种。互斥锁则包括 4 种操作,分别是创建,销毁,加锁和解锁。条件操作有 5 种操作:创建,销毁,触发,广播和等待。其他的一些线程扩展概念,如信号灯等,都可以通过上面的三个基本元素的基本操作封装出来。
创建线程
int pthread_create(pthread_t * tid, const pthread_attr_t * attr, void * ( * func) (void * ), void * arg);
其返回值是一个整数,若创建进程成功返回0,否则,返回其他错误代码,也是正整数。
创建线程需要的参数:
- 线程变量名:
pthread_t *
类型,是标示线程的id,一般是无符号整形,这里也可以是引用类型,目的是用于返回创建线程的ID - 线程的属性指针:制定线程的属性,比如线程优先*级,初始栈大小等,通常情况使用的都是指针。
- 创建线程的程序代码:一般是函数指针,进程创建后执行该函数指针指向的函数。
- 程序代码的参数:若线程执行的函数包含由若干个参数,需要将这些参数封装成结构体,并传递给它指针。
创建线程的函数的形式如下
结束线程
结束进程的函数定义如下:
void pthread_exit (void *status);
参数是指针类型,用于存储线程结束后返回状态。
线程等待
在 Linux 平台下,当处理线程结束时需要注意的一个问题就是如何让一个线程善始善终,让其所占资源得到正确释放。在 Linux 平台默认情况下,虽然各个线程之间是相互独立的,一个线程的终止不会去通知或影响其他的线程。但是已经终止的线程的资源并不会随着线程的终止而得到释放,我们需要调用 pthread_join() 来获得另一个线程的终止状态并且释放该线程所占的资源。 Pthread_join() 函数的定义:
int pthread_join (pthread_t tid, void ** status);
- 第一个参数表示要等待的进程的id;
- 第二参数表示要等待的进程的返回状态,是个二级指针。
调用该函数的线程将挂起,等待 th 所表示的线程的结束。 thread_return 是指向线程 th 返回值的指针。需要注意的是 th 所表示的线程必须是 joinable 的,即处于非 detached(游离)状态;并且只可以有唯一的一个线程对 th 调用 pthread_join() 。如果 th 处于 detached 状态,那么对 th 的 pthread_join() 调用将返回错误。
如果你压根儿不关心一个线程的结束状态,那么也可以将一个线程设置为 detached 状态,从而来让操作系统在该线程结束时来回收它所占的资源。将一个线程设置为 detached 状态可以通过两种方式来实现。一种是调用 pthread_detach() 函数,可以将线程 th 设置为 detached 状态。
pthread_detach 函数定义:
int pthread_detach(pthread_t th);
另一种方法是在创建线程时就将它设置为 detached 状态,首先初始化一个线程属性变量,然后将其设置为 detached 状态,最后将它作为参数传入线程创建函数 pthread_create(),这样所创建出来的线程就直接处于 detached 状态。方法如下。
pthread_t tid;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, THREAD_FUNCTION, arg);
总之为了在使用 Pthread 时避免线程的资源在线程结束时不能得到正确释放,从而避免产生潜在的内存泄漏问题,在对待线程结束时,要确保该线程处于 detached 状态,否着就需要调用 pthread_join() 函数来对其进行资源回收。
线程创建后怎么执行,新线程和老线程谁先执行这些不是程序来决定,而是由操作系统进行调度的,但是在编程的时候我们常常需要多个线程配合工作,比如在结束某个线程之前,需要等待另外一个线程的处理结果(返回状态等信息),这时候就需要使用线程等待函数
函数pthread_join用来等待一个线程的结束。函数原型为:
extern int pthread_join __P ((pthread_t __th, void **__thread_return));
第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的线程将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。一个线程的结束有两种途径,一种是象我们上面的例子一样,函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit来实现。它的函数原型为:
extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
唯一的参数是函数的返回代码,只要pthread_exit中的参数retval不是NULL,这个值将被传递给 thread_return。最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH。
总结:pthread_join用于等待一个线程的结束,也就是主线程中要是加了这段代码,就会在加代码的位置卡主,直到这个线程执行完毕才往下走。
pthread_exit用于强制退出一个线程(非执行完毕退出),一般用于线程内部。
结合用法:
一般都是pthread_exit在线程内退出,然后返回一个值。这个时候就跳到主线程的pthread_join了(因为一直在等你结束),这个返回值会直接送到pthread_join,实现了主与分线程的通信。
注意事项:
exit()是进程退出,如果在线程函数中调用exit,那该线程的进程也就挂了。会导致该线程所在进程的其他线程也挂掉,比较严重
这个线程退出的返回值的格式是void*,无论是什么格式都要强转成void*才能返回出来主线程(pthread_exit((void*)tmp);),而这个时候pthread_join就去接这个值,我们传进去一个void*的地址也就是&(void*),传地址进去接值是接口类函数常用的做法,有同样效果的做法是引用&,但是这个做法一来值容易被误改,二来不规范,所以定义一个类型然后把地址传进去修改value。回到题目,这里返回的void*是一个指针类型,必须强转成对应的指针才能用。
举个例子,如果是char* = “mimida”;传出来的tmp,必须(char*)tmp一下。
而如果是int* a = new int(3888);这种类型返回的tmp,必须*(int*)tmp一下才能用。
最重要的一点,你定义的类型和最后出来的类型一定要一致,不然很容易出现问题。也就是你定义了int*,最后强转出来的一定是*(int*)。
别void* a = (void*)10;这种诡异的格式(我就中过招),一开始是什么就转成什么!(这个规则同时也适用于线程数据里的set和get)
实例:
https://blog.youkuaiyun.com/zqixiao_09/article/details/50298693
https://blog.youkuaiyun.com/modiziri/article/details/41961595
其他关于进程的函数
-
返回当前线程ID
pthread_t pthread_self (void);
用于返回当前进程的ID -
制定线程变成分裂状态
int pthread_detach (pthread_t tid);
参数是指定线程的ID,指定的ID的线程变成分离状态;若指定线程是分离状态,则 如果线程退出,那么它所有的资源都将释放,如果线程不是分离状态,线程必须保留它的线程ID、退出状态,直到其他线程对他调用的pthread_join()
函数
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
void print_message_func(void *ptr);
int main()
{
int tmp1,tmp2;
void *retival;
pthread_t thread1,thread2;
char *message1 = "thread1";
char *message2 = "thread2";
int ret_thread1,ret_thread2;
ret_thread1 = pthread_create(&thread1,NULL,(void *)&print_message_func,(void *)message1);
if(ret_thread1 == 0)
printf("create thread 1 true\n");
else
printf("create thread 1 false\n");
tmp1 = pthread_join(thread1,&retival);
printf("thread 1 return value (tmp1) is %d\n",tmp1);
if(tmp1 != 0)
printf("cannot join with thread 1\n");
ret_thread2 = pthread_create(&thread2,NULL,(void *)&print_message_func,(void *)message2);
if(ret_thread2 == 0)
printf("create thread 2 true\n");
else
printf("create thread 2 false\n");
tmp2 = pthread_join(thread2,&retival);
printf("thread 2 return value (tmp2) is %d\n",tmp2);
if(tmp2 != 0)
printf("cannot join with thread 2\n");
}
void print_message_func(void *ptr)
{
for(int i=0;i<5;++i)
{
printf("%s:%d\n",(char*)ptr,i);
}
}
线程属性
线程属性,在创建时分离代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
void *mythread(void *args)
{
printf("child thread id==[%ld]\n", pthread_self());
}
int main()
{
pthread_t thread;
//线程属性
pthread_attr_t attr;
//线程属性初始化
pthread_attr_init(&attr);
//设置线程到分离属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
//创建一个线程
int ret = pthread_create(&thread, &attr, mythread, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
printf("main thread id==[%ld]\n", pthread_self());
sleep(1);
ret = pthread_join(thread, NULL);
if(ret!=0)
{
printf("pthread_join error, [%s]\n", strerror(ret));
}
//释放线程属性
pthread_attr_destroy(&attr);
return 0;
}
线程通信
因为同一进程的不同线程共享同一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段,所以线程之间可以方便、快速地共享信息。只需要将数据复制到共享(全局或堆)变量中即可。不过,要避免出现多个线程试图同时修改同一份信息。
主要由于多个线程可能更改全局变量,因此全局变量最好声明violate
- 使用消息实现通信
在Windows程序设计中,每一个线程都可以拥有自己的消息队列(UI线程默认自带消息队列和消息循环,工作线程需要手动实现消息循环),因此可以采用消息进行线程间通信sendMessage,postMessage。
1)定义消息#define WM_THREAD_SENDMSG=WM_USER+20;
2)添加消息函数声明afx_msg int OnTSendmsg();
3)添加消息映射ON_MESSAGE(WM_THREAD_SENDMSG,OnTSM)
4)添加OnTSM()的实现函数;
5)在线程函数中添加PostMessage消息Post函数
- 使用事件CEvent类实现线程间通信:Event对象有两种状态:有信号和无信号,线程可以监视处于有信号状态的事件,以便在适当的时候执行对事件的操作。
1)创建一个CEvent类的对象:CEvent threadStart;它默认处在未通信状态;
2)threadStart.SetEvent();使其处于通信状态;
3)调用WaitForSingleObject()来监视CEvent对象
线程间的同步方式
各个线程可以访问进程中的公共变量,资源,所以使用多线程的过程中需要注意的问题是如何防止两个或两个以上的线程同时访问同一个数据,以免破坏数据的完整性。数据之间的相互制约包括
1、直接制约关系,即一个线程的处理结果,为另一个线程的输入,因此线程之间直接制约着,这种关系可以称之为同步关系
2、间接制约关系,即两个线程需要访问同一资源,该资源在同一时刻只能被一个线程访问,这种关系称之为线程间对资源的互斥访问,某种意义上说互斥是一种制约关系更小的同步
同步机制的封装
-
syn_pthread.h
#ifndef _SYN_PTHREAD_H_
#define _SYN_PTHREAD_H_
#include <exception>
#include <pthread.h>
#include <semaphore.h>
/*封装信号量*/
class sem{
public:
sem()//创建并初始化信号量
{
if(sem_init(&m_sem,0,0)!=0)
{
throw std::exception();//初始化函数没有返回值抛出异常
}
}
~sem()//销毁信号量
{
sem_destroy(&m_sem);
}
bool wait()//等待信号量
{
return sem_wait(&m_sem)==0;
}
bool post()//增加信号量
{
return sem_post(&m_sem)==0;
}
private:
sem_t m_sem;
};
/*封装互斥锁*/
class locker
{
public:
locker()//创建并初始化互斥锁
{
if(pthread_mutex_init(&m_mutex,NULL)!=0)
{
throw std::exception();
}
~locker()//销毁互斥锁
{
pthread_mutex_destroy(&m_mutex);
}
bool lock()//获取互斥锁
{
return pthread_mutex_lock(&m_mutex)==0;
}
bool unlock()//释放互斥锁
{
return pthread_mutex_unlock(&m_mutex)==0;
}
private:
pthread_mutex_t m_mutex;
};
/*封装条件变量*/
class cond{
public:
cond()//创建并初始化条件变量
{
if(pthread_mutex_init(&m_mutex,NULL!=0)
throw std::exception();
if(pthread_cond_init(&m_cond,NULL)!=0)
{
//构造函数出问题就立即释放已经成功分配的资源
pthread_mutex_destroy(&m_mutex);
throw std::exception();
}
}
~cond()//销毁条件变量
{
pthread_mutex_destroy(&m_mutex);
pthread_cond_destroy(&m_cond);
}
bool wait()//等待条件变量
{
int ret=0;
pthread_mutex_lock(&m_mutex);
ret=pthread_cond_wait(&m_cond,&m_mutex);
pthread_mutex_unlock(&m_mutex);
return ret==0;
}
bool signal()//唤醒等待条件变量的线程
{
return pthread_cond_signal(&m_cond)==0;
}
private:
pthread_mutex_t m_mutex;
pthread_cond_t m_cond;
};
#endif
多线程的同步与互斥
锁机制
多线程之间可能需要互斥的访问一些全局变量,这就需要互斥的来访问,这些需要共享访问的字段被称作是临界资源,访问临界资源的程序段称作是临界区。
实现线程间的互斥与同步机制的是锁机制,下面是常用的锁机制的函数和类。
pthread_mutex_t mutex
锁对象pthread_mutex_init(&mutex,NULL)
在主线程中初始化锁为解锁状态pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
编译时初始化锁位解锁状态pthread_mutex_lock(&mutex)(阻塞加锁)
访问临界区加锁操作- pthread_mutex_trylock( &mutex)(非阻塞加锁); pthread_mutex_lock() 类似,不同的是在锁已经被占据时返回 EBUSY 而不是挂起等待。
pthread_mutex_unlock(&mutex)
: 访问临界区解锁操作
互斥锁熟悉
线程和进程的同步对象(互斥量,读写锁,条件变量)都具有属性。在修改属性前都需要对该结构进行初始化。使用后要把该结构回收。我们用pthread_ mutexattr_init函数对pthread_mutexattr结构进行初始化,用pthread_mutexattr_destroy函数对该结构进行回收。
名称:: | pthread_mutexattr_init/pthread_mutexattr_destroy |
功能: | 初始化/回收pthread_mutexattr_t结构 |
头文件: | #include <pthread.h> |
函数原形: | int pthread_mutexattrattr_init(pthread_mutexattr_t *attr); int pthread_mutexattrattr_destroy( pthread_mutexattr_t *attr ); |
参数: | attr pthread_mutexattr_t结构变量 |
返回值: | 若成功返回0,若失败返回错误编号。 |
pthread_mutexattr_init将属性对象的值初始化为缺省值。并分配属性对象占用的内存空间。
attr中pshared属性表示用这个属性对象创建的互斥锁的作用域,它的取值可以是PTHREAD_PROCESS_PRIVATE(缺省值,表示由这个属性对象创建的互斥锁只能在进程内使用)或PTHREAD_PROCESS_SHARED。
互斥量属性分为共享互斥量属性和类型互斥量属性。两种属性分别由不同的函数得到并由不同的函数进行修改。pthread_mutexattr_getpshared和pthread_mutexattr_setpshared函数可以获得和修改共享互斥量属性。pthread_mutexattr_gettype和pthread_mutexattr_settype函数可以获得和修改类型互斥量属性。下面我们分别介绍。
名称:: | pthread_mutexattr_gettype/pthread_mutexattr_settype |
功能: | 获得/修改类型互斥量属性 |
头文件: | #include <pthread.h> |
函数原形: | int pthread_mutexattrattr_ getpshared ( const pthread_attr_t *restrict attr,int*restrict pshared); int pthread_mutexattrattr_ setpshared ( const pthread_attr_t *restrict attr,int pshared); |
参数: |
|
返回值: | 若成功返回0,若失败返回错误编号。 |
pthread_mutexattr_gettype函数可以获得互斥锁类型属性。缺省的互斥锁类型属性是PTHREAD_MUTEX_DEFAULT。
合法的类型属性值有:
PTHREAD_MUTEX_NORMAL;
PTHREAD_MUTEX_ERRORCHECK;
PTHREAD_MUTEX_RECURSIVE;
PTHREAD_MUTEX_DEFAULT。
类型说明:
PTHREAD_MUTEX_NORMAL
这种类型的互斥锁不会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会引起这个线程的死锁。如果试图解锁一个由别的线程锁定的互斥锁会引发不可预料的结果。如果一个线程试图解锁已经被解锁的互斥锁也会引发不可预料的结果。
PTHREAD_MUTEX_ERRORCHECK
这种类型的互斥锁会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会返回一个错误代码。如果试图解锁一个由别的线程锁定的互斥锁将会返回一个错误代码。如果一个线程试图解锁已经被解锁的互斥锁也将会返回一个错误代码。
PTHREAD_MUTEX_RECURSIVE
如果一个线程对这种类型的互斥锁重复上锁,不会引起死锁,一个线程对这类互斥锁的多次重复上锁必须由这个线程来重复相同数量的解锁,这样才能解开这个互斥锁,别的线程才能得到这个互斥锁。如果试图解锁一个由别的线程锁定的互斥锁将会返回一个错误代码。如果一个线程试图解锁已经被解锁的互斥锁也将会返回一个错误代码。这种类型的互斥锁只能是进程私有的(作用域属性为PTHREAD_PROCESS_PRIVATE)。
PTHREAD_MUTEX_DEFAULT
这种类型的互斥锁不会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会引起不可预料的结果。如果试图解锁一个由别的线程锁定的互斥锁会引发不可预料的结果。如果一个线程试图解锁已经被解锁的互斥锁也会引发不可预料的结果。POSIX标准规定,对于某一具体的实现,可以把这种类型的互斥锁定义为其他类型的互斥锁。
注意点
1、互斥量需要时间来加锁和解锁。锁住较少互斥量的程序通常运行得更快。所以,互斥量应该尽量少,够用即可,每个互斥量保护的区域应则尽量大。
2、互斥量的本质是串行执行。如果很多线程需要领繁地加锁同一个互斥量,则线程的大部分时间就会在等待,这对性能是有害的。如果互斥量保护的数据(或代码)包含彼此无关的片段,则可以特大的互斥量分解为几个小的互斥量来提高性能。这样,任意时刻需要小互斥量的线程减少,线程等待时间就会减少。所以,互斥量应该足够多(到有意义的地步),每个互斥量保护的区域则应尽量的少。
死锁问题
一个线程需要访问两个或者更多不同的共享资源,而每个资源又有不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就可能发生死锁。死锁就是指多个线程/进程因竞争资源而造成的一种僵局(相互等待),若无外力作用,这些进程都将无法向前推进。
情况一:如果一个线程对同一个互斥量进行加锁两次,那么它就会陷入死锁状态,因为想要对互斥量加锁,必须保证互斥量没有被锁,不然就只能等待,当我加第二次锁的时候,发现互斥量已经加锁了,就阻塞的等待,但是因为没有解锁,就会一直等下去
这个问题与互斥锁的中的默认 recursive 属性有关。解决问题的方法就是显式地在互斥变量初始化时将设置起 recursive 属性。基于此,以上代码其实稍作修改就可以很好的运行,只需要在初始化锁的时候加设置一个属性。请看清单 2 。
清单 2. 设置互斥锁 recursive 属性实例
1 2 3 4 |
|
因此,建议尽量设置 recursive 属性以初始化 Linux 的互斥锁,这样既可以解决同一线程递归加锁的问题,又可以避免很多情况下死锁的发生。这样做还有一个额外的好处,就是可以让 Windows 和 Linux 下让锁的表现统一。
情况二:程序中使用一个以上的互斥量时,如果允许一个线程一直占有第一个互斥量,并且在试图锁住第二个互斥量时处于阻塞状态,但是拥有第二个互斥量的线程也在试图锁住第一个互斥量.就造成两个线程都无法向前运行,就会产生死锁
死锁的处理策略:
1、预防死锁:破坏死锁产生的四个条件:互斥条件、不剥夺条件、请求和保持条件以及循环等待条件。
2、避免死锁:在每次进行资源分配前,应该计算此次分配资源的安全性,如果此次资源分配不会导致系统进入不安全状态,那么将资源分配给进程,否则等待。
当一个进程申请使用资源的时候,银行家算法通过先 试探 分配给该进程资源,然后通过安全性算法判断分配后的系统是否处于安全状态,若不安全则试探分配作废,让该进程继续等待。
算法:银行家算法(https://blog.youkuaiyun.com/qq_33414271/article/details/80245715)
3、检测死锁:检测到死锁后通过资源剥夺、撤销进程、进程回退等方法解除死锁。
实例 不加锁访问互斥全局变量
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int sharei = 0;
void increase_num(void);
int main()
{
int ret;
pthread_t thread1,thread2,thread3;
ret = pthread_create(&thread1,NULL,(void *)&increase_num,NULL);
ret = pthread_create(&thread2,NULL,(void *)&increase_num,NULL);
ret = pthread_create(&thread3,NULL,(void *)&increase_num,NULL);
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
pthread_join(thread3,NULL);
printf("sharei = %d\n",sharei);
return 0;
}
void increase_num(void)
{
long i,tmp;
for(i =0;i<=10000;++i)
{
tmp = sharei;
tmp = tmp + 1;
sharei = tmp;
}
}
编译运行结果,多运行几次,发现结果都不一样。这就是因为对于全局变量,没有添加互斥锁,导致的问题。
实例 访问全局变量添加互斥锁
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int sharei = 0;
void increase_num(void);
// add mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int main()
{
int ret;
pthread_t thread1,thread2,thread3;
ret = pthread_create(&thread1,NULL,(void *)&increase_num,NULL);
ret = pthread_create(&thread2,NULL,(void *)&increase_num,NULL);
ret = pthread_create(&thread3,NULL,(void *)&increase_num,NULL);
pthread_join(thread1,NULL);
pthread_join(thread2,NULL);
pthread_join(thread3,NULL);
printf("sharei = %d\n",sharei);
return 0;
}
void increase_num(void)
{
long i,tmp;
for(i =0;i<=10000;++i)
{
// lock
if(pthread_mutex_lock(&mutex) != 0)
{
perror("pthread_mutex_lock");
exit(EXIT_FAILURE);
}
tmp = sharei;
tmp = tmp + 1;
sharei = tmp;
// unlock
if(pthread_mutex_unlock(&mutex) != 0)
{
perror("pthread_mutex_unlock");
exit(EXIT_FAILURE);
}
}
}
添加互斥锁后,就发现,多次运行的结果都是一样的。
- 其实这里的加锁不是对共享变量(全局变量)或者共享内存进行保护,这里的加锁实际上是对临界区的控制,所谓的临界区就是访问临界资源的那一段代码,这段代码对临界资源进行多种操作,正确的情况是不允许这段代码执行到一半,处理器使用权就被其他线程抢走,所以这段代码具有原子性,即要么执行,要么不执行,不能执行到一半就被抢走处理权,这样就会造成共享数据被污染。
- 还有一点,添加锁来控制临界区是有代价的,这个代价表现出来就是时间的额外开销,内部过程是因为要保护现场,会利用一些资源,也需要处理器处理的时间。
注:
-
临界资源
多线程之间可能需要互斥的访问一些全局变量,这就需要互斥的来访问,这些需要共享访问的字段被称作是 临界资源 -
临界区
就是访问临界资源的那一段代码称作临界区 -
互斥量是一个内核对象。
互斥量用来确保一个线程独占一个资源的访问。
互斥量与临界区非常相似,并且互斥量可以用于不同进程中的线程互斥访问。 -
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。
信号量:用于多线程间的同步
读写锁
读写锁(reader-writer lock)与互斥量类似,不过读写锁允许更高的并行性
互斥量只有两种状态,要么是不加锁状态,要么就是锁状态,而且一次只有一个线程可以对其加锁
读写锁有三个状态:
①读模式下的加锁状态
②写模式下的加锁状态
③不加锁状态
读写锁的特征:
·一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程占有读模式的读写锁
·当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞.
·当读写锁是读加锁状态时,所有试图以读模式对它进行加锁的线程都可以访问,但是任何以希望以写模式对锁进行加锁的线程都会被阻塞,直到所有线程释放他们的读锁
·需要注意的一点.当读写锁处于读模式的状态时,线程试图以写模式获取上锁时,读写锁会阻塞后面的读模式的锁请求,这样可以避免读模式的锁被长期占用.
·读写锁适合于对数据结构读的次数远大于写的情况
·读写锁也叫共享互斥锁,当读写锁以读模式锁住时,就可以说成是以共享模式锁住的,当读写锁以写模式锁住时,就可以说成是以互斥模式锁住的
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *rwlockattr);//初始化读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);//销毁读写锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//读模式锁定读写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//写模式锁定读写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//解锁读写锁
//读写锁也有条件版本
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);
所有函数的返回值:成功返回0.失败返回错误编号
eg.pthread_rwlock_t q_lock;
pthread_rwlock_init(&q_lock, NULL);
pthread_rwlock_rdlock(&q_lock);
...
pthread_rwlock_unlock(&q_lock);
pthread_rwlock_detroy(&q_lock);
实例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
int number = 0;
//定义一把读写锁
pthread_rwlock_t rwlock;
void *fun_write(void *args)
{
int i = *(int *)args;
int n;
while(1)
{
//加写锁
pthread_rwlock_wrlock(&rwlock);
n = number;
n++;
//sleep(rand()%3);
number = n;
printf("W->[%d]:[%d]\n", i, number);
//解写锁
pthread_rwlock_unlock(&rwlock);
sleep(rand()%3);
}
pthread_exit(NULL);
}
void *fun_read(void *args)
{
int i = *(int *)args;
while(1)
{
//加读锁
pthread_rwlock_rdlock(&rwlock);
printf("R->[%d]:[%d]\n", i, number);
//解锁
pthread_rwlock_unlock(&rwlock);
sleep(rand()%3);
}
pthread_exit(NULL);
}
int main()
{
int i;
int ret;
int n = 8;
int arr[8];
pthread_t thread[8];
//读写锁初始化
pthread_rwlock_init(&rwlock, NULL);
//创建3个写线程
for(i=0; i<3; i++)
{
arr[i] = i;
ret = pthread_create(&thread[i], NULL, fun_write, (void *)&arr[i]);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
}
//创建5个读线程
for(i=3; i<n; i++)
{
arr[i] = i;
ret = pthread_create(&thread[i], NULL, fun_read, (void *)&arr[i]);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
}
for(i=0; i<n; i++)
{
//回收子线程
pthread_join(thread[i], NULL);
}
//释放读写锁资源
pthread_rwlock_destroy(&rwlock);
return 0;
}
信号量机制
锁机制使用是有限制的,锁只有两种状态,即加锁和解锁,对于互斥的访问一个全局变量,这样的方式还可以对付,但是要是对于其他的临界资源,比如说多台打印机等,这种方式显然不行了。
信号量机制在操作系统里面学习的比较熟悉了,信号量是一个整数计数器,其数值表示空闲临界资源的数量。
当有进程释放资源时,信号量增加,表示可用资源数增加;当有进程申请到资源时,信号量减少,表示可用资源数减少。这个时候可以把锁机制认为是0-1信号量。
关于信号量机制的函数。
int sem_init(sem_t * sem, int pshared, unsigned int value);
初始化信号量
-
- 成功返回0,失败返回-1;
-
- 参数sem:表示指向信号结构的指针。
-
- 参数pshared:不是0 的时候该信号量在进程间共享,否则只能在当前进程的所有线程间共享。
-
- 参数value:信号量的初始值。
int sem_wait(sem_t *sem);
信号量减一操作,有线程申请资源
-
- 成功返回0,否则返回-1
-
- 参数sem:指向一个信号量的指针
int sem_post(sem_t *sem);
信号量加一操作,有线程释放资源
-
- 成功返回0,否则返回-1
-
- 参数sem:指向一个信号量指针
int sem_destroy(sem_t *sem);
销毁信号量。
-
- 成功返回0,否则返回-1
-
- 参数sem:指向一个信号量的指针。
实例 生产者消费者
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define MAXSIZE 10
int stack[MAXSIZE];
int size =0;
sem_t sem;
void privide_data(void)
{
int i;
for(i =0;i<MAXSIZE;++i)
{
stack[i] = i;
//sem_consumer++
sem_post(&sem);
}
}
void handle_data(void)
{
int i;
while((i = size ++) <MAXSIZE)
{
//sem_consumer--, 若为0则阻塞
sem_wait(&sem);
printf("cross : %d X %d = %d \n",stack[i],stack[i],stack[i] * stack[i]);
sleep(1);
}
}
int main()
{
pthread_t privider,handler;
sem_init(&sem,0,0);
pthread_create(&privider,NULL,(void *)&privide_data,NULL);
pthread_create(&handler,NULL,(void *)&handle_data,NULL);
pthread_join(privider,NULL);
pthread_join(handler,NULL);
sem_destroy(&sem);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
typedef struct node
{
int data;
struct node *next;
}NODE;
//链表头节点指针
NODE *head = NULL;
sem_t sem_consumer;
sem_t sem_producer;
//生产者线程处理函数
void *producer(void *args)
{
NODE *pNode = NULL;
while(1)
{
pNode = (NODE *)malloc(sizeof(NODE));
if(pNode==NULL)
{
perror("malloc error\n");
exit(1);
}
pNode->data = rand()%1000;
//sem_producer--, 若为0则阻塞
sem_wait(&sem_producer);
pNode->next = head;
head=pNode;
printf("P:[%d]\n", head->data);
//sem_consumer++
sem_post(&sem_consumer);
sleep(rand()%3);
}
}
//消费者线程处理函数
void *consumer(void *args)
{
NODE *pNode = NULL;
while(1)
{
//sem_consumer--, 若为0则阻塞
sem_wait(&sem_consumer);
printf("C:[%d]\n", head->data);
pNode = head;
head = head->next;
//sem_producer++
sem_post(&sem_producer);
free(pNode);
pNode = NULL;
sleep(rand()%3);
}
}
int main(int argc, char *argv[])
{
int ret;
pthread_t thread1;
pthread_t thread2;
//信号量初始化
sem_init(&sem_producer, 0, 5);
sem_init(&sem_consumer, 0, 0);
//创建生产者线程
ret = pthread_create(&thread1, NULL, producer, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//创建消费者线程
ret = pthread_create(&thread2, NULL, consumer, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//主线程回收子线程
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
//释放信号量资源
sem_destroy(&sem_producer);
sem_destroy(&sem_consumer);
return 0;
}
这段代码是经典的生产者消费者问题,只有当生产者把资源放入存储区,消费者才能取得。
条件变量
条件变量是线程可用的另一种同步机制。互斥量用于上锁,条件变量则用于等待,并且条件变量总是需要与互斥量一起使用,运行线程以无竞争的方式等待特定的条件发生。
条件变量本身是由互斥量保护的,线程在改变条件变量之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种变化,因为互斥量必须在锁定之后才能计算条件。
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);//初始化条件变量
int pthread_cond_destroy(pthread_cond_t *cond);//销毁条件变量
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);//无条件等待条件变量变为真
int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *tsptr);//在给定时间内,等待条件变量变为真
//唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond); //可以唤起所有等待该条件的线程
int pthread_cond_signal(pthread_cond_t *cond);//至少能唤起一个等待的线程
版权声明:本文为优快云博主「Qregi」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/Qregi/article/details/82084162
eg.pthread_mutex_t mutex;
pthread_cond_t cond;
...
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
...
pthread_mutex_unlock(&mutex);
...
注意:
1. pthread_cond_wait 执行的流程首先将这个mutex解锁, 然后等待条件变量被唤醒, 如果没有被唤醒, 该线程将一直休眠, 也就是说, 该线程将一直阻塞在这个pthread_cond_wait调用中, 而当此线程被唤醒时, 将自动将这个mutex加锁,然后再进行条件变量判断(原因是“惊群效应”,如果是多个线程都在等待这个条件,而同时只能有一个线程进行处理,此时就必须要再次条件判断,以使只有一个线程进入临界区处理。),如果满足,则线程继续执行。
2.调用者给pthread_cond_wait函数传入一个互斥量mutex,这个互斥量会对条件进行保护,这个函数自动把调用线程放到等待条件的线程列表上,对互斥量进行解锁,然后等待条件变量触发.这是线程挂起,不占用CPU时间,直到条件变量被触发.这个函数返回前会自动重新给互斥量加锁
3.互斥量的解锁和在条件变量上挂起都是自动进行的.因此条件变量触发之前,所有线程都要对互斥量进行加锁.
使用条件变量的优点:
① 可以避免线程竞争,由于线程之间是异步进行的,所以每个线程得到锁的概率依照线程的优先级来决定,如果想要让某个线程执行特定的任务,就可以利用条件变量,让系统立即调度拥有特定条件变量的线程,避免线程竞争
② 可以提高效率,如果一个线程得到锁,那么其他的线程得不到锁,就会不停的去查询锁有没有被释放,这样会浪费大量的资源,利用条件变量可以使得不到锁的线程处于阻塞的等待状态,直到被使用pthread_cond_signal函数通知,可以提高效率
实例 生产消费者模型(条件变量)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
typedef struct node
{
int data;
struct node *next;
}NODE;
//链表头节点指针
NODE *head = NULL;
//互斥锁
pthread_mutex_t mutex;
//条件变量
pthread_cond_t cond;
//生产者线程处理函数
void *producer(void *args)
{
NODE *pNode = NULL;
while(1)
{
pNode = (NODE *)malloc(sizeof(NODE));
if(pNode==NULL)
{
perror("malloc error\n");
exit(1);
}
pNode->data = rand()%1000;
//lock共享资源
pthread_mutex_lock(&mutex);
pNode->next = head;
head=pNode;
printf("P:[%d]\n", head->data);
//对共享资源解锁
pthread_mutex_unlock(&mutex);
//使用条件变量解除对线程到阻塞
pthread_cond_signal(&cond);
sleep(rand()%3);
}
}
//消费者线程处理函数
void *consumer(void *args)
{
NODE *pNode = NULL;
while(1)
{
//lock共享资源
pthread_mutex_lock(&mutex);
if(head==NULL)
{
//条件不满足阻塞等待head不为空
pthread_cond_wait(&cond, &mutex);
}
printf("C:[%d]\n", head->data);
pNode = head;
head = head->next;
//对共享资源解锁
pthread_mutex_unlock(&mutex);
free(pNode);
pNode = NULL;
sleep(rand()%3);
}
}
int main(int argc, char *argv[])
{
int ret;
pthread_t thread1;
pthread_t thread2;
pthread_mutex_t mutex;
pthread_cond_t cond;
//初始化互斥锁
pthread_mutex_init(&mutex, NULL);
//初始化条件变量
pthread_cond_init(&cond, NULL);
//创建生产者线程
ret = pthread_create(&thread1, NULL, producer, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//创建消费者线程
ret = pthread_create(&thread2, NULL, consumer, NULL);
if(ret!=0)
{
printf("pthread_create error, [%s]\n", strerror(ret));
return -1;
}
//主线程回收子线程
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
//释放锁资源
pthread_mutex_destroy(&mutex);
//释放条件变量资源
pthread_cond_destroy(&cond);
return 0;
}
实例2 顺序打印奇偶数
在两个线程处理函数之中,比如处理偶数的函数中,如果当前value值是偶数,就打印并把value自增1,然后使用pthread_cond_signal函数通知条件变量odd,如果不为偶数,就使用pthread_cond_wait函数阻塞的等待
这里也是同样的做法
这是最后的结果:
注意 Linux 平台上触发条件变量的自动复位问题
条件变量的置位和复位有两种常用模型:第一种模型是当条件变量置位(signaled)以后,如果当前没有线程在等待,其状态会保持为置位(signaled),直到有等待的线程进入被触发,其状态才会变为复位(unsignaled),这种模型的采用以 Windows 平台上的 Auto-set Event 为代表。其状态变化如图 1 所示:
第二种模型则是 Linux 平台的 Pthread 所采用的模型,当条件变量置位(signaled)以后,即使当前没有任何线程在等待,其状态也会恢复为复位(unsignaled)状态。其状态变化如图 2 所示:
具体来说,Linux 平台上 Pthread 下的条件变量状态变化模型是这样工作的:调用 pthread_cond_signal() 释放被条件阻塞的线程时,无论存不存在被阻塞的线程,条件都将被重新复位,下一个被条件阻塞的线程将不受影响。而对于 Windows,当调用 SetEvent 触发 Auto-reset 的 Event 条件时,如果没有被条件阻塞的线程,那么条件将维持在触发状态,直到有新的线程被条件阻塞并被释放为止。
这种差异性对于那些熟悉 Windows 平台上的条件变量状态模型而要开发 Linux 平台上多线程的程序员来说可能会造成意想不到的尴尬结果。试想要实现一个旅客坐出租车的程序:旅客在路边等出租车,调用条件等待。出租车来了,将触发条件,旅客停止等待并上车。一个出租车只能搭载一波乘客,于是我们使用单一触发的条件变量。这个实现逻辑在第一个模型下即使出租车先到,也不会有什么问题,其过程如图 3 所示:
然而如果按照这个思路来在 Linux 上来实现,代码看起来可能是清单 3 这样。
清单 3. Linux 出租车案例代码实例
……
// 提示出租车到达的条件变量
pthread_cond_t taxiCond;
// 同步锁
pthread_mutex_t taxiMutex;
// 旅客到达等待出租车
void * traveler_arrive(void * name) {
cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;
pthread_mutex_lock(&taxiMutex);
pthread_cond_wait (&taxiCond, &taxtMutex);
pthread_mutex_unlock (&taxtMutex);
cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;
pthread_exit( (void *)0 );
}
// 出租车到达
void * taxi_arrive(void *name) {
cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl;
pthread_cond_signal(&taxtCond);
pthread_exit( (void *)0 );
}
void main() {
// 初始化
taxtCond= PTHREAD_COND_INITIALIZER;
taxtMutex= PTHREAD_MUTEX_INITIALIZER;
pthread_t thread;
pthread_attr_t threadAttr;
pthread_attr_init(&threadAttr);
pthread_create(&thread, & threadAttr, taxt_arrive, (void *)( ” Jack ” ));
sleep(1);
pthread_create(&thread, &threadAttr, traveler_arrive, (void *)( ” Susan ” ));
sleep(1);
pthread_create(&thread, &threadAttr, taxi_arrive, (void *)( ” Mike ” ));
sleep(1);
return 0;
}
清单 4. 程序结果输出
1 2 3 4 |
|
其过程如图 4 所示:
图 4. 采用 Linux 条件变量模型的出租车实例流程
通过对比结果,你会发现同样的逻辑,在 Linux 平台上运行的结果却完全是两样。对于在 Windows 平台上的模型一, Jack 开着出租车到了站台,触发条件变量。如果没顾客,条件变量将维持触发状态,也就是说 Jack 停下车在那里等着。直到 Susan 小姐来了站台,执行等待条件来找出租车。 Susan 搭上 Jack 的出租车离开,同时条件变量被自动复位。
但是到了 Linux 平台,问题就来了,Jack 到了站台一看没人,触发的条件变量被直接复位,于是 Jack 排在等待队列里面。来迟一秒的 Susan 小姐到了站台却看不到在那里等待的 Jack,只能等待,直到 Mike 开车赶到,重新触发条件变量,Susan 才上了 Mike 的车。这对于在排队系统前面的 Jack 是不公平的,而问题症结是在于 Linux 平台上条件变量触发的自动复位引起的一个 Bug 。
条件变量在 Linux 平台上的这种模型很难说好坏。但是在实际开发中,我们可以对代码稍加改进就可以避免这种差异的发生。由于这种差异只发生在触发没有被线程等待在条件变量的时刻,因此我们只需要掌握好触发的时机即可。最简单的做法是增加一个计数器记录等待线程的个数,在决定触发条件变量前检查下该变量即可。改进后 Linux 函数如清单 5 所示。
清单 5. Linux 出租车案例代码实例
……
// 提示出租车到达的条件变量
pthread_cond_t taxiCond;
// 同步锁
pthread_mutex_t taxiMutex;
// 旅客人数,初始为 0
int travelerCount=0;
// 旅客到达等待出租车
void * traveler_arrive(void * name) {
cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;
pthread_mutex_lock(&taxiMutex);
// 提示旅客人数增加
travelerCount++;
pthread_cond_wait (&taxiCond, &taxiMutex);
pthread_mutex_unlock (&taxiMutex);
cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;
pthread_exit( (void *)0 );
}
// 出租车到达
void * taxi_arrive(void *name)
{
cout<< ” Taxi ” <<(char *)name<< ” arrives. ” <<endl;
while(true)
{
pthread_mutex_lock(&taxiMutex);
// 当发现已经有旅客在等待时,才触发条件变量
if(travelerCount>0)
{
pthread_cond_signal(&taxtCond);
pthread_mutex_unlock (&taxiMutex);
break;
}
pthread_mutex_unlock (&taxiMutex);
}
pthread_exit( (void *)0 );
}
注意条件返回时互斥锁的解锁问题因此我们建议在 Linux 平台上要出发条件变量之前要检查是否有等待的线程,只有当有线程在等待时才对条件变量进行触发。
在 Linux 调用 pthread_cond_wait 进行条件变量等待操作时,我们增加一个互斥变量参数是必要的,这是为了避免线程间的竞争和饥饿情况。但是当条件等待返回时候,需要注意的是一定不要遗漏对互斥变量进行解锁。
Linux 平台上的 pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) 函数返回时,互斥锁 mutex 将处于锁定状态。因此之后如果需要对临界区数据进行重新访问,则没有必要对 mutex 就行重新加锁。但是,随之而来的问题是,每次条件等待以后需要加入一步手动的解锁操作。正如前文中乘客等待出租车的 Linux 代码如清单 6 所示:
清单 6. 条件变量返回后的解锁实例
void * traveler_arrive(void * name) {
cout<< ” Traveler: ” <<(char *)name<< ” needs a taxi now! ” <<endl;
pthread_mutex_lock(&taxiMutex);
pthread_cond_wait (&taxiCond, &taxtMutex);
pthread_mutex_unlock (&taxtMutex);
cout<< ” Traveler: ” << (char *)name << ” now got a taxi! ” <<endl;
pthread_exit( (void *)0 );
}
等待的绝对时间问题
超时是多线程编程中一个常见的概念。例如,当你在 Linux 平台下使用 pthread_cond_timedwait() 时就需要指定超时这个参数,以便这个 API 的调用者最多只被阻塞指定的时间间隔。pthread_cond_timedwait() 函数定义
1 2 3 |
|
参数 abstime 在这里用来表示和超时时间相关的一个参数,但是需要注意的是它所表示的是一个绝对时间,而不是一个时间间隔数值,只有当系统的当前时间达到或者超过 abstime 所表示的时间时,才会触发超时事件。
相对时间到绝对时间转换实例
1 2 3 4 5 6 7 |
|
Linux 的绝对时间看似简单明了,却是开发中一个非常隐晦的陷阱。而且一旦你忘了时间转换,可以想象,等待你的错误将是多么的令人头疼:如果忘了把相对时间转换成绝对时间,相当于你告诉系统你所等待的超时时间是过去式的 1970 年 1 月 1 号某个时间段,于是操作系统毫不犹豫马上送给你一个 timeout 的返回值,然后你会举着拳头抱怨为什么另外一个同步线程耗时居然如此之久,并一头扎进寻找耗时原因的深渊里。
互斥锁、条件变量和信号量的区别:
互斥锁:互斥,一个线程占用了某个资源,那么其它的线程就无法访问,直到这个线程解锁,其它线程才可以访问。
条件变量:同步,一个线程完成了某一个动作就通过条件变量发送信号告诉别的线程,别的线程再进行某些动作。条件变量必须和互斥锁配合使用。
信号量:同步,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。而且信号量有一个更加强大的功能,信号量可以用作为资源计数器,把信号量的值初始化为某个资源当前可用的数量,使用一个之后递减,归还一个之后递增。
另外还有以下几点需要注意:
1、信号量可以模拟条件变量,因为条件变量和互斥量配合使用,相当于信号量模拟条件变量和互斥量的组合。在生产者消费者线程池中,生产者生产数据后就会发送一个信号 pthread_cond_signal通知消费者线程,消费者线程通过pthread_cond_wait等待到了信号就可以继续执行。这是用条件变量和互斥锁实现生产者消费者线程的同步,用信号量一样可以实现!
2、信号量可以模拟互斥量,因为互斥量只能为加锁或解锁(0 or 1),信号量值可以为非负整数,也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量时,就完成一个资源的互斥访问。前面说了,信号量主要用做多线程多任务之间的同步,而同步能够控制线程访问的流程,当信号量为单值时,必须有线程释放,其他线程才能获得,同一个时刻只有一个线程在运行(注意,这个运行不一定是访问资源,可能是计算)。如果线程是在访问资源,就相当于实现了对这个资源的互斥访问。
3、互斥锁是为上锁而优化的;条件变量是为等待而优化的; 信号量既可用于上锁,也可用于等待,因此会有更多的开销和更高的复杂性。
4、互斥锁,条件变量都只用于同一个进程的各线程间,而信号量(有名信号量)可用于不同进程间的同步。当信号量用于进程间同步时,要求信号量建立在共享内存区。
5、互斥量必须由同一线程获取以及释放,信号量和条件变量则可以由一个线程释放,另一个线程得到。
6、信号量的递增和减少会被系统自动记住,系统内部的计数器实现信号量,不必担心丢失,而唤醒一个条件变量时,如果没有相应的线程在等待该条件变量,此次唤醒会被丢失。
自旋锁
自旋锁(spinlock)与互斥量 类似,但是它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等阻塞状态.
自旋锁可以用于以下情况:锁被持有的时间端,而且线程并不希望在重新调度上花太多的成本
自旋锁用于多个CPU系统中,在单处理器系统中,自旋锁不起锁的作用,只是禁止或启用内核抢占.在自旋锁忙等待期间,内核抢占机制还是有效的,等待自旋锁释放的线程可能被更高优先级的线程抢占CPU
自旋锁基于共享变量。一个线程通过给共享变量设置一个值来获取锁,其他等待线程查询共享变量是否为0来确定锁现是否可用,然后在忙等待的循环中"自旋"直到锁可用为止
自旋锁的接口与互斥量类似,这使得它可以比较容易的从一个替换为另一个.可以用pthread_spin_init函数对自旋锁进行初始化,使用pthread_spin_destroy函数进行自旋锁的反复初始化
int pthread_spin_init(pthread_spinlock_t* lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t* lock);
int pthread_spin_lock(pthread_spinlokc_t* lock);
int pthread_spin_trylock(pthread_spinlokc_t* lock);
int pthread_spin_unlock(pthread_spinlokc_t* lock);
自旋锁初始化函数中的参数pshared表示进程共享属性,表明自旋锁是如何获取的.
如果pshared被设为PTHREAD_PROCESS_SHARED,则自旋锁可以被访问锁底层的内存的线程获取,即使线程属于不同的进程
如果被设置为PTHREAD_PROCESS_PRIVATE,自旋锁就只能被初始化该锁的进程内部的线程获取
自旋锁的注意事项:
1.临界区代码不要存在睡眠情况(主要因为发生睡眠不可预知睡眠多长时间,另外长时间睡眠,导致即将进入临界区其他线程,长时间得不到自旋锁,无休止自旋,从而导致死锁),所以临界区调用导致睡眠函数,不能选择自旋锁。
2.保证进入临界区的线程,不发生内核抢占。(这一点不必担心,持有自旋锁情况,Linux内核不进行抢占)
3.临界区代码,执行时间不能太长。(因为其他线程,如果要进入话,导致自旋,过多消耗CPU资源)
4.选择自旋锁时,也要注意中断情况(上半部分中断(硬件中断)和下半部分中断(软中断),中断会抢占即中断到来时,打断目前临界区代码执行,转往执行中断代码),当中断要进入自旋锁保护临界区代码时,将导致线程与中断发生死锁可能。
屏障
屏障(barrier)是用户协调多个线程并行工作的同步机制.屏障允许每个线程等待,直到所有的合作线程都达到某一点,然后从该点继续执行.
初始化屏障时, 可以使用count参数指定, 在允许所有线程继续运行之前, 必须到达屏障的线程数目. 屏障属性attr设置为NULL表示使用默认属性.
int pthread_barrier_destroy(pthread_barrier_t *barrier);
int pthread_barrier_init(pthread_barrier_t *restrict barrier,
const pthread_barrierattr_t *restrict attr, unsigned count);
int pthread_barrier_wait(pthread_barrier_t *barrier);
调用pthread_barrier_wait的线程在屏障技术count未满足条件时,会进入休眠状态.如果该线程是最后一个调用pthread_barrier_wait的线程,就满足了屏障计数,所有的线程都被唤醒.
对于一个任意线程,pthread_barrier_wait函数返回PTHREAD_BARRIER_SERIAL_THREAD.剩下的线程看到的返回值是0,这使得一个线程可以作为主线程,它可以工作在其他所有线程已完成的工作结果上.