什么是线程?
进程:一个正在执行的程序,这是资源分配的最小单位。但它无法同时处理多个事情,如何解决?引入线程。
进程不是不能处理多个事情,而是进程是资源的拥有者,创建、撤销、切换存 在较大的时空开销,因此需要引入线程。还有就是引入多处理机(SMP),可满足多个运行单位,而多个进程并行开销过大。
线程:
轻量级的进程,程序执行的最小单位,系统独立调度和分配CPU的基本单位,它是进程中的一个实体,一个进程可以有多个线程,这些线程共享进程的所有资源,线程本身只包含一点必不可少的资源。
一些术语:
并发:同一时刻,只能有一条指令执行,但多个进程指令轮转,使得宏观上看起来同时执行效果。
看起来同时发生,单核。
并行,同一时刻,有多条指令在多个处理器上同时执行的
真正的同时发生。
同步:彼此有信赖关系的调用,不应该“同时发生”,同步就是阻止那些“同时发生”的事情。
这其实是从逆向说明的,也是应用最多的场景。同步是一种机制。
异步:任何两个彼此独立的操作是异步的,表明事情独立的发生。
线程ID
线程 | 进程 | |
标识符类型 | pthread_t | pid_t |
获取ID | pthgread_self() | getpid() |
创建 | pthread_create() | fork() |
pthread_t:在FreeBSOS和Mac OS10.3是一个结构体
在linux 是一个unsigned long int
线程的基本操作:
创建线程:编译时需要连接库libpthread
int pthread_create(pthread_t *restrict tidp,
const pthread_attr_t *restrict attr,
void *(*start_routine)(void *),
void *restrict arg)
第一个参数:新线程的ID,如果成功,则新线程的id回填充到tidp指向的内存
第二个参数:线程属性(调度策略、继承性,分离性...)
第三个参数:回调函数(新线程要执行的函数)
第四个参数:回调函数的参数
返回值:成功0,失败返回错误码 详见:cat /usr/include/asm-generic/errno.h
主线程:1、main,在线程代码中,这个特殊的执行流被称作主线程,你可以在初始线程中做任何普通线程可以做的事情。
2、特殊在于main返回时会进程结束。可以在主线程中调用pthread_exit,这样进程就会等待所有线程结束时才终止。
3、接受参数的方式是通过argc和argv,而普通线程只有一个参数void *.
4、在绝大多数情况下,主线程的堆栈不受限制,而普通线程的堆栈,一旦溢出就会产生错误。
主线程的创建:
1、随着进程的创建而创建
2、其他线程可以通过调用pthread_create创建
3、新线程,可能在当前进程创建前已经运行了,或创建返回时已经运行完毕了,具体需要看进程的调度。
线程的四个基本状态
就绪:线程能够运行,但是在等待可用的处理器。
运行:可能有多个线程在运行
阻塞:等待处理器以外的其他条件
终止:线程从启动函数中返回,或者调用pthread_exit函数,或者被取消。
回收:
线程的分离属性:
分离一个正在运行的线程并不影响它,仅仅告诉系统当线程结束的时候,其所属的资源可以回收。不被分离,就不会被回收,称作“僵尸线程”。创建线程时默认的非分离的。
回收会释放掉所有系统资源和进程资源。其中,系统资源由系统去做,而程序资源则由程序员去释放。如由malloc或mmap内存或条件,但用到互斥量的话,最好是线程,结束时解锁,由其他线程释放。
线程的基本控制:
线程的终止:
1、exit:危险的,任何一个线程调用的exit,那么就会线束整个进程。
2、普通线程的三种退出方式:return /被同一个进程的其他线程取消/调用pthread_exit(void *rval),rval:是退出码。
线程的连接:
int pthread_join(pthread_t tid,void **rval)
调用该函数的线程会一直阻塞,直到指定的线程tid调用pthread_exit,
参数tid就是指定线程的id
参数rval是指定线程的返回码,如果线程被取消,那么rval被置为PTHREAD_CANCELED
成功返回0,失败回错误码
调用pthread_join会使指定的线程片于分离状态,如果指定的线程已经处于分离状态,那么调用就会失败。也就是说,如果连接成功的话,其他线程是无法调用它了。
pthread_detach可分离一个线程,线程可以自己分离自己
int pthread_detach(pthread_t thread);成功返回0,失败返回错误码
线程的取消:
int pthread_cancel(pthread_t tid)
取消tid指定的线程,成功0。只是发送一个请求,并不等待线程终止,而且即使发送成功也不意味着tid一定会终止。
取消状态,就是线程对取消信号的处理方式,忽略或者响应,线程创建时默认响应取消信号。
int pthread_setcancelstate(int state,int *oldstate);//可设置对cancel信号的反应,有两种状态:
PTHREAD_CANCEL_ENABLE(缺省);CANCELED状态
PTHREAD_CANCEL_DISABLE;忽略信号,继续运行
old_state 如果不为NULL则存入原来的Cancel状态以使恢复。
取消类型:线程对取消信号的响应方式,立即或延时取消,默认j 延时取消
int pthread_setcanceltype(int type,int *oldtype)
PTHREAD_CANCEL_DEFFERED,仅当Cancel状态为Enable时有效,表示收到信号后继续运行至下一个取消点再退出
PTHREAD_CANCEL_ASYCHRONOUSUS立即执行取消动作(退出)
oldtype不为NULL,则存入旧的类型。
什么是取消点呢?
取消一个线程,它通常需要被取消线程的配合。线程会查看自己是否有取消请求,如果有就主动退出,这些查看是否有取消的地方称为取消点。很多函数都需要取消点,如printf()
向进程发送信号:
int pthread_kill(pthread_t thread,int sig);
向线程发送signal,大部分signal默认动作都是终止进程的运行,才用sigaction()去抓信号并加上处理函数。
向指定的ID线程发送sig信号,如果线程代码内不做处理,则按信号默认的行为影响整个进程。
如果int sig的参数不是0,那一定要清楚到底要干什么,而且一定要实现线程的信号处理函数,否则就会影响整个进程。
sig==0,是一个保留信号,作用是用来判断线程是不是还活着。
进程信号处理:
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
给信号signum设置一个处理函数,处理函数在sigaction 中指定
act sa_mask 信号屏蔽
act sa_handler信号 集 处理 程序
int sigemptyset(sigset_t *set);清 空 信号 集
int sigfillset(sigset_t *set);将 所 有 信号 加 入 信 号 集
int sigaddset(sigset_t *set,int signum);增加 一 个 信 号 到 信号 集
int sigdelset(sigset_t *set int signum);删 除 一 个 信 号 到 信号 集 。
多线 程 信号屏蔽处理
/*int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);//进 程 */
int pthread_sigmask(int how,const sigset_t *set,sigset_t *oldset);
how=SIG_BLOCK;向当 前 的 信号 掩 码中添加set,其中 set表示要 阻塞 的 信号 组 。
SIG_UNELOCK:向当前的信号掩码中删除set,其中set表示要取消阻塞的信号组。
SIG_SETMASK:将当前的信号掩码替换为set, 其中set表示新的信号掩码。
在多线程中,新线程的当前信号掩码会继承创造它的那个线程的信号掩码。
一般情况下,被阻塞的 信号将不能中断此线程的执行;除非此信号的产生是因为程序运行出错加SIGSEGV;另外不能被忽略处理的信号SIGKILL和SGISTOP 也无法被阻塞。
清除操作:
线程可以安排它退出时的清理操作,与进程atexit安排进程退出时需要调用的函数类似。
线程可以安排多个线程处理程序,记录在栈中。
pthread_cleanup_push(void(*rtn)(void*),void *agrs)//注册处理程序
pthread_cleanup_pop(int excute)//清除处理程序
成对出现的,否则编译无法通过。
触发调用的情况:
1、调用pthread_exit
2、响应取消请求
3、用非零参数调用pthread_cleanup_pop
线程的同步:很重要
为什么使用互斥量?
多个线程共享内存,需要每个线程看到相同的视图。当一个线程修改变量时,另一个线程读或修改就可能出问题。解决就是互斥量,确保他们不会访问到无效的变量。
2020-1-8,把存量先一个个过一遍,还行,210的开发板已经重新烧了安卓和CE,并且,串口打印乱码的问题也解决了,其实也不能说是解决,只是把宇泰的串口脚定义和开发板上带的线不同,宇泰的是1,2脚接收和发送,而开发板是执地的标准的定义,2、3脚。造成打印的各种问题。算是初试了把linux,逐渐破除对linux的神密感。开局良好!!!
互斥量其实就是一把锁。
互斥量用pthread_mutex_t类型的数据表示,使用前需要包含头文件/usr/include/bits/pthreadtypes.h
1、调用pthread_mutex_init()初始化
2、静态分配的互斥量,,可置为常量PTHREAD_MUTEX_INTIAUZER
3、动态分配的互斥量在释放内存之前需要调用pthread_mutex_destroy()进行释放。
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict atrr);
第一个参数是要初始化的互斥量,第二个属性,默认为NULL
int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER.
加锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
成功返回0,失败错误码,会导致线程阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex);
成功返回0,失败错误码,不会导致线程阻塞
解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
成功0,失败错误码。
读写锁:对于互斥一个时刻只有一个线程访问的改进,更高的并行性。对于一个变量读取,完全可以让多个线程可以同时进行。
类型:pthread_rwlock_t rwlock
具有三个状态:读模式下加锁、写模式下加锁,不加锁
写加锁时,所有其他线程都会被阻塞
读加锁,所有读的线程都获得访问权,但要试图写的话,得等到所有线程释放锁
但一直读占用的话,若有线程加写锁,则会阻塞读线程,给写线程一个机会。适合于,读比较多,写比较少的情况。
初始化:
int pthreaad_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockatrr_t *restrict attr);
销毁:
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
成功返回0,失败错误码。
读加锁:
int pthread_rwlock_rollock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
写加锁:
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlockt_t rwlock);
解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
成功0,错误码。
条件变量:一种机制,对于互斥量被锁住以后发现当前的线程还是无法完成自己的操作,应该释放互斥量让其他线程工作
1、轮询,不停的查询你需要的条件
2、让系统帮你查询条件,使用条件变量pthread_cond_t cond
条件变量初始化
1、pthread_cond_t cond=PTHREAD_COND_INTILIZER
2、int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condatrr_t*restrict attr);
销毁
int pthread_cond_destroy(pthread_cond_t *cond);
使用时需要配合互斥量
int pthread_cond_wait(pthread_cond_t *retrict cond,pthread_mutex_t *restrict mutex);
使用方式:
pthread _mutex_lock(&mutex)
while或if(线程执行的条件是否成立)
pthread_cond_wait(&cond, &mutex);
线程执行
pthread_mutex_unlock(&mutex);
需要解释的有两点,为什么要加锁,以及为什么可以使用while和if。首先解释第一点,有两个方面,线程在执行的部分访问的是进程的资源,有可能有多个线程需要访问它,为了避免由于线程并发执行所引起的资源竞争,所以要让每个线程互斥的访问公有资源,但是细心一下就会发现,如果while或者if判断的时候,不满足线程的执行条件,那么线程便会调用pthread_cond_wait阻塞自己,但是它持有的锁怎么办呢,如果他不归还操作系统,那么其他线程将会无法访问公有资源。
pthread_cond_wait的内部实现机制,当pthread_cond_wait被调用线程阻塞的时候,pthread_cond_wait会自动释放互斥锁。释放互斥锁的时机是什么呢:是线程从调用pthread_cond_wait到操作系统把他放在线程等待队列之后,这样做有一个很重要的原因,就是mutex的第二个作用,保护条件。想一想,线程是并发执行的,如果在没有把被阻塞的线程A放在等待队列之前,就释放了互斥锁,这就意味着其他线程比如线程B可以获得互斥锁去访问公有资源,这时候线程A所等待的条件改变了,但是它没有被放在等待队列上,导致A忽略了等待条件被满足的信号。倘若在线程A调用pthread_cond_wait开始,到把A放在等待队列的过程中,都持有互斥锁,其他线程无法得到互斥锁,就不能改变公有资源。这就保证了线程A被放在等待队列上之后才会有公有资源被改变的信号传递给等待队列。
接下来讲解使用while和if判断线程执行条件是否成立的区别。一般来说,在多线程资源竞争的时候,在一个使用资源的线程里面(消费者)判断资源是否可用,不可用便调用pthread_cond_wait,在另一个线程里面(生产者)如果判断资源可用的话,则调用pthread_cond_signal发送一个资源可用信号。但是在wait成功之后,资源就一定可以被使用么,答案是否定的,如果同时有两个或者两个以上的线程正在等待此资源,wait返回后,资源可能已经被使用了,在这种情况下,应该使用:
while(resource == FALSE)
pthread_cond_wait(&cond, &mutex);
如果之后一个消费者,那么使用if就可以了。解释一下原因,分解pthread_cond_wait的动作为以下几步:
1,线程放在等待队列上,解锁
2,等待 pthread_cond_signal或者pthread_cond_broadcast信号之后去竞争锁
3,若竞争到互斥索则加锁。
上面讲到,有可能多个线程在等待这个资源可用的信号,信号发出后只有一个资源可用,但是有A,B两个线程都在等待,B比较速度快,获得互斥锁,然后加锁,消耗资源,然后解锁,之后A获得互斥锁,但他回去发现资源已经被使用了,它便有两个选择,一个是去访问不存在的资源,另一个就是继续等待,那么继续等待下去的条件就是使用while,要不然使用if的话pthread_cond_wait返回后,就会顺序执行下去。
下面来讲一下:pthread_cond_wait和pthread_cond_singal是怎样配对使用的:
等待线程:
pthread_cond_wait前要先加锁
pthread_cond_wait内部会解锁,然后等待条件变量被其它线程激活
pthread_cond_wait被激活后会再自动加锁
激活线程:
加锁(和等待线程用同一个锁)
pthread_cond_signal发送信号(阶跃信号前最好判断有无等待线程)
解锁
激活线程的上面三个操作在运行时间上都在等待线程的pthread_cond_wait函数内部。
另一个函数:
int pthread_cond_timedwait(pthread_cond_t *cond_interface, pthread_mutex_t * mutex, const timespec *abstime)
abstime是一个绝对时间(即:现在在时间+需要等待的时间),Linux中常用的时间结构有struct timespec 和 struct timeval
struct timespec
{
time_t tv_sec; /* Seconds. */
long tv_nsec; /* Nanoseconds. */
};
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};
两者的区别是:timespec的第二个参数是纳秒数,而timeval的第二个参数是毫秒数。
pthread_cond_timedwait允许线程就阻塞时间设置一个限制值。指定了这个函数必须返回时的系统时间,即使当相对条件下还没有收到信号。
如果发生这种超时情况,此函数返回ETIMEDOUT错误。
使用绝对时间而不是时间差的好处:若函数过早返回(或许因为捕捉了某个信号),那么同一函数无需改变其参数中的timespec结构的内容就能再次被调用。
linux的线程高级控制:linux线程高级属性
一次性初始化:
有些事需要且只能执行一次(比如互斥量初始化)。
通常当初始化应用程序时,可以比较容易地将其放在main函数中。但当你写一个库函数时,就不能在main里面初始化了,你可以用静态初始化,但使用一次初始(pthread_once_t
)会比较容易些。
首先要定义一个pthread_once_t
变量,这个变量要用宏PTHREAD_ONCE_INIT
初始化。然后创建一个与控制变量相关的初始化函数。
pthread_once_t once_control = PTHREAD_ONCE_INIT;
void init_routine()
{
//初始化互斥量
//初始化读写锁
......
}
接下来就可以在任何时刻调用pthread_once
函数
int pthread_once(pthread_once_t* once_control, void (*init_routine)(void));
功能:本函数使用初值为PTHREAD_ONCE_INIT
的once_control
变量保证init_routine()
函数在本进程执行序列中仅执行一次。在多线程编程环境下,尽管pthread_once()
调用会出现在多个线程中,init_routine()
函数仅执行一次,究竟在哪个线程中执行是不定的,是由内核调度来决定。
Linux Threads使用互斥锁和条件变量保证由pthread_once()
指定的函数执行且仅执行一次。
实际"一次性函数"的执行状态有三种:NEVER(0)
、IN_PROGRESS(1)
、DONE (2)
,用once_control
来表示pthread_once()
的执行状态:
1、如果once_control
初值为0,那么pthread_once
从未执行过,init_routine()
函数会执行。
2、如果once_control
初值设为1,则由于所有pthread_once()
都必须等待其中一个激发"已执行一次"信号, 因此所有pthread_once ()
都会陷入永久的等待中,init_routine()
就无法执行
3、如果once_control
设为2,则表示pthread_once()
函数已执行过一次,从而所有pthread_once()
都会立即 返回,init_routine()
就没有机会执行
当pthread_once
函数成功返回,once_control
就会被设置为2
线程属性:
线程属性标识符:pthread_attr_t 包含在 pthread.h 头文件中。
//线程属性结构如下:
typedef struct
{
int etachstate; //线程的分离状态
int schedpolicy; //线程调度策略
structsched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;
属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。
分离状态:决定了一个线程以什么方式终止自己。
我们已经在前面已经知道,在默认情况下线程是非分离状态的,这种情况
下,原有的线程等待创建的线程结束。只有当pthread_join() 函数返回
时,创建的线程才算终止,才能释放自己占用的系统资源。
分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,
马上释放系统资源。
通俗的说也就是:我们知道一般我们要等待(pthread_join)一个线程的结束,
主要是想知道它的结束状态,否则等待一般是没有什么意义的!但是if有一
些线程的终止态我们压根就不想知道,那么就可以使用“分离”属性,那么我
们就无须等待管理,只要线程自己结束了,自己释放src就可以咯!这样更
方便!
#include <pthread.h>
int pthread_attr_getdetachstate(const pthread_attr_t * attr, int * detachstate);
int pthread_attr_setdetachstate(pthread_attr_t * attr, int detachstate);
参数:attr:线程属性变量
detachstate:分离状态属性
若成功返回0,若失败返回-1。
设置的时候可以有两种选择:
<1>.detachstate参数为:PTHREAD_CREATE_DETACHED 分离状态启动
<2>.detachstate参数为:PTHREAD_CREATE_JOINABLE 正常启动线程
设置线程分离属性的步骤:
1、定义线程属性变量pthread_attr_t attr
2、初始化attr,pthread_attr_init(&attr)
3、设置线程为分离或非分离 pthread_attr_setdetachstate(&attr,detachstate)
4、创建线程pthread_create(&tid,&attr,thread_fun,NULL)
所有的线程都支持线程的分离状态属性。
线程栈属性:
POSIX.1定义了两个常量_POSIX_THREAD_ATTR_STACKADDR 和_POSIX_THREAD_ATTR_STACKSIZE检测系统是否支持栈属性。也可以给sysconf函数传递_SC_THREAD_ATTR_STACKADDR或 _SC_THREAD_ATTR_STACKSIZE来进行检测。
当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstack和pthread_attr_getstack两个函数分别设置和获取线程的栈地址。
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize); 成功:0;失败:错误号
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize); 成功:0;失败:错误号
参数: attr:指向一个线程属性的指针
stackaddr:返回获取的栈地址
stacksize:返回获取的栈大小
线程堆栈的大小,默认的是字节
函数pthread_attr_setstackaddr和pthread_attr_getstackaddr分别用来设置和得
到线程堆栈的位置。
int pthread_attr_getstacksize(const pthread_attr_t *,size_t * stacksize);
int pthread_attr_setstacksize(pthread_attr_t *attr ,size_t *stacksize);
参数:attr 线程属性变量
stacksize 堆栈大小
若成功返回0,若失败返回-1。
线程堆栈的地址
#include <pthread.h>
int pthread_attr_getstackaddr(const pthread_attr_t *attr,void **stackaddf);
int pthread_attr_setstackaddr(pthread_attr_t *attr,void *stackaddr);
参数:attr 线程属性变量
stackaddr 堆栈地址
若成功返回0,若失败返回-1。
注意:pthread_attr_getstackaddr已经过期,现在使用的是:pthread_attr_getstack
警戒缓冲区
函数pthread_attr_getguardsize和pthread_attr_setguardsize分别用来设置和得
到线程栈末尾的警戒缓冲区大小。
#include <pthread.h>
int pthread_attr_getguardsize(const pthread_attr_t *restrict attr,size_t *restrict
guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr ,size_t *guardsize);
若成功返回0,若失败返回-1。
值得注意:
线程属性guardsize控制着线程栈末尾之后以避免栈溢出的扩展内存
大小。这个属性默认设置为PAGESIZE(也就是4096)个字节。可以把guardsize线
程属性设为0,从而不允许属性的这种特征行为发生:在这种情况
下不会提供警戒缓存区。同样地,如果对线程属性stackaddr作了
修改,系统就会认为我们会自己管理栈,并使警戒栈缓冲区机制无
效,等同于把guardsize线程属性设为0。
线程的同步属性:
一、互斥量的属性
就像线程有属性一样,线程的同步互斥量也有属性,比较重要的是进程共享属性和类型属性。互斥量的属性用pthread_mutexattr_t类型的数据
表示,当然在使用之前必须进行初始化,使用完成之后需要进行销毁:
1)、互斥量初始化
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
2)、互斥量销毁
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
二、互斥量的进程共享属性
进程共享属性有两种值:
1)、PTHREAD_PROCESS_PRIVATE,这个是默认值,同一个进程中的多个线程访问同一个同步对象
2)、PTHREAD_PROCESS_SHARED, 这个属性可以使互斥量在多个进程中进行同步,如果互斥量在多进程的共享内存区域,那么具有这个属性的
互斥量可以同步多进程
设置互斥量进程共享属性
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
进程共享属性需要检测系统是否支持,可以检测宏_POSIX_THREAD_PROCESS_SHARED
三、互斥量的类型属性
类型属性
互斥量类型 没有解锁时再次加锁 不占用是解锁 已解锁时解锁
PTHREAD_MUTEX_NORMAL 死锁 未定义 未定义
PTHREAD_MUTEX_ERRORCHEK 返回错误 返回错误 返回错误
PTHREAD_MUTEX_RECURSIVE 允许 返回错误 返回错误
PTHREAD_MUTEX_DEFAULT 未定义 未定义 未定义
获取/设置互斥量的类型属性
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
四、读写锁与条件变量的属性
1、读写锁也有属性,它只有一个进程共享属性
读写锁属性初始化
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
设置读写锁进程共享属性
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr, int *restrict pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
线程的私有数据:
线程私有数据(Thread-specific data,TSD):存储和查询与某个线程相关数据的一种机制。
在进程内的所有线程都共享相同的地址空间,即意味着任何声明为静态或外部变量,或在进程堆声明的变量,都可以被进程内所有的线程读写。
一个线程真正拥有的唯一私有存储是处理器寄存器,栈在“主人”故意暴露给其他线程时也是共享的。
有时需要提供线程私有数据:可以跨多个函数访问(全局);仅在某个线程有效(私有)(即在线程里面是全局)。例如:errno。
进程内的所有线程共享进程的数据空间,因此全局变量为所有线程所共有。但有时线程也需要保存自己的私有数据,这时可以创建线程私有数据(Thread-specific Date)TSD来解决。在线程内部,私有数据可以被各个函数访问,但对其他线程是屏蔽的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量。(即在线程里面是全局变量)
要使线程外的函数不能访问这些数据,而线程内的函数使用这些数据就像线程内的全局变量一样,这些数据在一个线程内部是全局的,一般用线程私有数据的地址作为线程内各个函数访问该数据的入口。
线程私有数据采用了一种被称为一键多值的技术,即一个键对应多个数值。访问数据时都是通过键值来访问,好像是对一个变量进行访问,其实是在访问不同的数据。使用线程私有数据时,首先要为每个线程私有数据创建一个相关联的键。在各个线程内部,都使用这个公用的键来指代线程数据,但是在不同的线程中,这个键代表的数据是不同的。操作线程私有数据的函数主要有4个:pthread_key_create(创建一个键),pthread_setspecific(为一个键设置线程私有数据),pthread_getspecific(从一个键读取线程私有数据),pthread_key_delete(删除一个键)。
创建一个键:
int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));//返回值:若成功则返回0,否则返回错误编号
在分配(malloc)线程私有数据之前,需要创建和线程私有数据相关联的键(key),这个键的功能是获得对线程私有数据的访问权。
如果创建一个线程私有数据键,必须保证pthread_key_create对于每个Pthread_key_t变量仅仅被调用一次,因为如果一个键被创建两次,其实是在创建两个不同的键,第二个键将覆盖第一个键,第一个键以及任何线程可能为其关联的线程私有数据值将丢失。
创建新键时,每个线程的私有数据地址设为NULL。
注意:创建的键存放在keyp指向的内存单元,这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程私有数据地址进行关联。
除了创建键以外,pthread_key_create可以选择为该键关联析构函数,当线程退出时,如果线程私有数据地址被置为非NULL值,那么析构函数就会被调用。
注意:析构函数参数为退出线程的私有数据的地址。如果私有数据的地址为NULL,就说明没有析构函数与键关联即不需要调用该析构函数。
当线程调用pthread_exit或者线程执行返回,正常退出时,析构函数就会被调用,但是如果线程调用了exit、_exit、Exit函数或者abort或者其它非正常退出时,就不会调用析构函数。
线程通常使用malloc为线程私有数据分配空间,析构函数通常释放已分配的线程私有数据的内存。
线程可以为线程私有数据分配多个键,每个键都可以有一个析构函数与它关联。各个键的析构函数可以互不相同,当然它们也可以使用相同的析构函数。
线程退出时,线程私有数据的析构函数将按照操作系统实现定义的顺序被调用。析构函数可能调用另外一个函数,而该函数可能创建新的线程私有数据而且把这个线程私有数据和当前的键关联起来。当所有的析构函数都调用完成以后,系统会检查是否有非NULL的线程私有数据值与键关联,如果有的话,再次调用析构函数,这个过程一直重复到线程所有的键都为NULL值线程私有数据,或者已经做了PTHREAD_DESTRUCTOR_ITERATIONS中定义的最大次数的尝试。
取消键与线程私有数据之间的关联:
int pthread_delete(pthread_key_t *keyp);//返回值:若成功则返回0,否则返回错误编号
注意调用pthread_delete不会激活与键关联的析构函数。删除线程私有数据键的时候,不会影响任何线程对该键设置的线程私有数据值,甚至不影响调用线程当前键值,所以容易造成内存泄露(因为键不与私有数据关联了,当线程正常退出的时候不会调用键的析构函数,最终导致线程的私有数据这块内存没有释放)。使用已经删除的私有数据键将导致未定义的行为。
注意:对于每个pthread_key_t变量(即键)必须仅调用一次pthread_key_create。如果一个键创建两次,其实是在创建不同的键,第二个键将覆盖第一个,第一个键与任何线程可能为其设置的值将一起永远的丢失。所以,pthread_key_create放在主函数中执行;或每个线程使用pthread_once来创建键。
线程私有数据与键关联:
int pthread_setspecific(pthread_key_t key,const void *value);//返回值:若成功则返回0,否则返回错误编号
void* pthread_getspecific(pthread_key_t key);//返回值:线程私有数据地址;若没有值与键关联则返回NULL
如果没有线程私有数据值与键关联,pthread_getspecific键返回NULL,可以依据此来确定是否调用pthread_setspecific。
注意:两个线程对自己的私有数据操作是互相不影响的。也就是说,虽然 key 是同名且全局,但访问的内存空间并不是相同的一个。key 就像是一个数据管理员,线程的私有数据只是到他那去注册,让它知道你这个数据的存在。
fork函数与多线程
问题:
1. 虽然只将发起fork()调用的线程复制到子进程中,但全局变量的状态以及所有的pthreads对象(如互斥量、条件变量等)都会在子进程中得以保留, 这就造成一个危险的局面。例如:一个线程在fork()被调用前锁定了某个互斥量,且对某个全局变量的更新也做到了一半,此时fork()被调用,所有数 据及状态被拷贝到子进程中,那么子进程中对该互斥量就无法解锁(因为其并非该互斥量的属主),如果再试图锁定该互斥量就会导致死锁,这是多线程编程中最不 愿意看到的情况。同时,全局变量的状态也可能处于不一致的状态,因为对其更新的操作只做到了一半对应的线程就消失了。fork()函数被调用之后,子进程 就相当于处于signal handler之中,此时就不能调用线程安全的函数(用锁机制实现安全的函数),除非函数是可重入的,而只能调用异步信号安全(async- signal-safe)的函数。fork()之后,子进程不能调用:
- malloc(3)。因为malloc()在访问全局状态时会加锁。
- 任何可能分配或释放内存的函数,包括new、map::insert()、snprintf() ……
- 任何pthreads函数。你不能用pthread_cond_signal()去通知父进程,只能通过读写pipe(2)来同步。
- printf()系列函数,因为其他线程可能恰好持有stdout/stderr的锁。
- 除了man 7 signal中明确列出的“signal安全”函数之外的任何函数。
2. 因为并未执行清理函数和针对线程局部存储数据的析构函数,所以多线程情况下可能会导致子进程的内存泄露。另外,子进程中的线程可能无法访问(父进程中)由其他线程所创建的线程局部存储变量,因为(子进程)没有任何相应的引用指针。
由于这些问题,推荐在多线程程序中调用fork()的唯一情况是:其后立即调用exec()函数执行另一个程序,彻底隔断子进程与父进程的关系。由新的进程覆盖掉原有的内存,使得子进程中的所有pthreads对象消失。
对于那些必须执行fork(),而其后又无exec()紧随其后的程序来说,pthreads API提供了一种机制:fork()处理函数。利用函数pthread_atfork()来创建fork()处理函数。pthread_atfork()声明如下:
#include <pthread.h>
// Upon successful completion, pthread_atfork() shall return a value of zero; otherwise, an error number shall be returned to indicate the error.
// @prepare 新进程产生之前被调用
// @parent 新进程产生之后在父进程被调用
// @child 新进程产生之后,在子进程被调用
int pthread_atfork (void (*prepare) (void), void (*parent) (void), void (*child) (void));
该函数的作用就是往进程中注册三个函数,以便在不同的阶段调用,有了这三个参数,我们就可以在对应的函数中加入对应的处理功能。同时需要注意的是,每次调用pthread_atfork()函数会将prepare添加到一个函数列表中,创建子进程之前会(按与注册次序相反的顺序)自动执行该函数列表中函数。parent与child也会被添加到一个函数列表中,在fork()返回前,分别在父子进程中自动执行(按注册的顺序)。
服务器的创建
TCP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
int socket(int domain,int type,int protocol);
//domain:该参数一般被设置为AF_INET,表示使用的是IPv4地址。还有更多选项可以利用man查看该函数
//type:该参数也有很多选项,例如SOCK_STREAM表示面向流的传输协议,SOCK_DGRAM表示数据报,我们这里实现的是TCP,因此选用SOCK_STREAM,如果实现UDP可选SOCK_DGRAM
//protocol:协议类型,一般使用默认,设置为0
该函数用于打开一个网络通讯接口,出错则返回-1,成功返回一个socket(文件描述符),应用进程就可以像读写文件一样调用read/write在网络上收发数据
2、设置socket属性,用函数setsockopt(); * 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();
int bind(int sockfd,const struct sockaddr*addr,socklen_t addrlen);
//sockfd:服务器打开的sock
//后两个参数可以参考第四部分的介绍
服务器所监听的网络地址和端口号一般是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind来绑定一个固定的网络地址和端口号。bind成功返回0,出错返回-1。
bind()的作用:将参数sockfd和addr绑定在一起,是sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。
4、开启监听,用函数listen();
int listen(int sockfd,int backlog);
//sockfd的含义与bind中的相同。
//backlog参数解释为内核为次套接口排队的最大数量,这个大小一般为5~10,不宜太大(是为了防止SYN攻击)
该函数仅被服务器端使用,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。
5、接收客户端上来的连接,用函数accept();
int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
//addrlen是一个传入传出型参数,传入的是调用者的缓冲区cliaddr的长度,以避免缓冲区溢出问题;传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给cliaddr参数传NULL,表示不关心客户端的地址。
典型的服务器程序是可以同时服务多个客户端的,当有客户端发起连接时,服务器就调用accept()返回并接收这个连接,如果有大量客户端发起请求,服务器来不及处理,还没有accept的客户端就处于连接等待状态。
三次握手完成后,服务器调用accept()接收连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
过程:
服务器:首先调用socket()创建一个套接字用来通讯,其次调用bind()进行绑定这个文件描述符,并调用listen()用来监听端口是否有客户端请求来,如果有,就调用accept()进行连接,否则就继续阻塞式等待直到有客户端连接上来。连接建立后就可以进行通信了。
客户端:调用socket()分配一个用来通讯的端口,接着就调用connect()发出SYN请求并处于阻塞等待服务器应答状态,服务器应答一个SYN-ACK分段,客户端收到后从connect()返回,同时应答一个ACK分段,服务器收到后从accept()返回,连接建立成功。客户端一般不调用bind()来绑定一个端口号,并不是不允许bind(),服务器也不是必须要bind()。
数据结构:struct sockaddr_in addr; 定义一个ip地址
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
sin_family指代协议族,在socket编程中只能是AF_INET
sin_port存储端口号(使用网络字节顺序)
sin_addr存储IP地址,使用in_addr这个数据结构
sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。
s_addr按照网络字节顺序存储IP地址
IPv4 和 IPv6 的地址格式定义在“netinet/in.h”中,IPv4用sockaddr_in结构体表示,包括16位端口号和32位IP地址;IPv6用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些控制字段。
UNIX Domain Socket的地址格式定义在sys/un.h中,用sockaddr_un结构体表示。各种socket地址结构体的开头都是相同的,前16位表⽰示整个结构体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类型。IPv4、IPv6和UNIX Domain Socket的地 址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。因此socket API可以接受各种类型的sockaddr结构体指针做参数,例 如bind、accept、connect等函数,这些函数的参数应该设计成void 类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还没有void 类型,因此这写函数的参数都用struct sockaddr*类型表示,在传参之前需要强制类型转换(在bind函数中就有用到)。
sockaddr_in中的成员struct in_addr sin_addr表示32位的IP地址。但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示和in_addr表示之间转换。
字符串转in_addr的函数:
网络字节序:
TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。
#include<arpa/inet.h>
//将主机字节序转换为网络字节序
uint32_t htonl(uint32_t hostlong);//将32长整数从主机字节序转换为网络字节序,
//如果主机字节序是小端,则函数会做相应大小
//端转换后返回;如果主机字节序是大端,则函
//数不做转换,将参数原封不动返回。。。下同
uint16_t htons(uint16_t hostshort);
//将网络字节序转换为主机字节序
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
// h表示主机(host),n表示网络(net),l表示32位长整数,s表示16短整数。
TCP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4、设置要连接的对方的IP地址和端口等属性;
5、连接服务器,用函数connect();
int connect(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
这个函数只需要有客户端程序来调用,调用该函数后表明连接服务器,这里的参数都是对方的地址。connect()成功返回0,出错返回-1。
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
8、关闭监听;
注:socket
1、socket即为套接字,在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一的标识网络通讯中的一个进程,“IP地址+TCP或UDP端口号”就为socket。
2、在TCP协议中,建立连接的两个进程(客户端和服务器)各自有一个socket来标识,则这两个socket组成的socket pair就唯一标识一个连接。
3、socket本身就有“插座”的意思,因此用来形容网络连接的一对一关系,为TCP/IP协议设计的应用层编程接口称为socket API。