线程标识符类型:pthread_t
获取线程id:pthread_self()
一.创造线程:
int pthread_create(pthread_t *restrict,const pthread_attr_t *restrict attr,void*(*start_routine)(void *),void *restrict arg)
第一个参数:新线程的id,如果成功则新线程的id回填充到tid指向的内存
第二个参数:线程属性(调度策略,继承性,分离性...)
第三个参数:回调函数(新线程要执行的函数)
第四个参数:回调函数的参数
返回值:成功返回0,失败返回错误码
注意:编译时需要链接libpthread
例子:
void print_id(char *s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid is %u.tid is0x%x\n",s,pid,tid);
}
void *pthread_fun(void *arg)
{
print_id(arg);
return (void *)0;
}
int main()
{
pthread_t ntid;
int err;
err =pthread_creat(&tid,NULL,thread_fun,"new thread");
if(err!=0)
{
printf("creat new threadfailed\n");
return 0;
}
print_id("mainthread:");
sleep(2);
return 0;
}
二.线程的回收
1.线程的分离属性:分离一个正在运行的线程并不影响它,仅仅是通知系统当前线程结束时,其所属资源可以回收。一个没有被分离的线程在终止时会保留它的虚拟内存,包括他们的堆2.栈和其他系统资源,有时这种线程被称为僵尸线程,创建线程时默认时非分离的。
3.如果线程具有分离属性,线程终止时会被立刻回收,回收将释放掉所有在线程终止时未释放的系统资源和进程资源
4.终止被分离的线程会释放所有的系统资源,但是你务必释放由该线程占有的程序资源。由malloc或者mmap分配的内存可以在任何时候由任何线程释放,条件变量,互斥量,信号灯可以由任何线程销毁,只要他们被解锁了或者没有线程等待。但是只有互斥量的主人才能解锁它,所以在线程终止前,你需要解锁互斥量。
三.线程退出
1.如果进程中的任意一个线程调用了exit,_Exit,_exit那么整个进程就会终止
2.普通的单个线程有以下三种方式退出,这样不会终止进程:
(1)从启动例程返回,返回值是线程的退出码(即调用return函数)
(2)线程可以被统一进程中的其他线程取消
(3)线程调用pthread_exit(void *ravl)函数,ravl是退出码
四.线程的链接
int pthread_join(pthread_t tid,void *rval)
功能:调用该函数的线程会一直阻塞,知道指定的线程tid调用pthread_exit从启动例程返回或者被取消
参数一:就是指定线程的tid
参数二:是指定线程的返回码,如果线程被取消,那么rval被置为PTHREAD_CANCELED
返回值:成功返回0,失败返回错误码
注意:一个可结合的线程能够被其他线程收回其资源和杀死;在被其他线程回收之前,它的存储器资源(如栈)是不释放的。相反,一个分离的线程是不能被其他线程回收或杀死的,它的存储器资源在它终止时由系统自动释放。
注意:调用pthread_join会使指定的线程已经处于分离状态,如果指定的线程已经处于分离状态,那么调用会失败。pthread_detach可以分离一个线程,线程自己可以分离自己
int pthread_detach(pthread_t thread)
成功返回0,失败返回错误码
五.互斥量
(一)功能:为了让线程访问数据不冲突,这就需要对变量进行加锁,使得统一时刻只有一个线程可以访问变量。互斥量本质就是锁,访问共享资源前对互斥量加锁,访问完成后解锁。当互斥量加锁后,其他所有需要访问该互斥量的线程都将被阻塞。当互斥量解锁以后,所有因为这个互斥量阻塞的线程都将变为就绪态,第一个获得cpu的线程会获得互斥量,变为运行态,而其他需要访问该互斥量的线程会继续被阻塞,在这种方式下访问互斥量每次只有一个进程能够执行。
(二)类型:pthread_mutex_t
注意:在使用互斥量之前需要对互斥量进行初始化
1.如果是动态分配的互斥量,可以调用pthread_mutex_init()函数初始化
2.如果是静态分配的互斥量,还可以把它置为常量PTHREAD_MUTEX_INITIALIZER
例子:pthread_mutex_tmutex=PTHREAD_MUTEX_INITIALIZER
3.动态分配的互斥量在释放内存之前需要调用pthread_mutex_destroy()
4.int pthread_mutex_init(pthread_mutex_t *restrict mutex,constpthread_mutexattr_t *restrict attr)
第一个参数:要初始化的互斥量
第二个参数:互斥量的属性,默认为NULL
5.int pthread_mutex_destroy(pthread_mutex_t *mutex)
(三)互斥量的加锁:
1.int pthread_mutex_lock(pthread_mutex_t *mutex)
返回值:成功返回0,失败返回错误码
注意:如果互斥量已经被锁住,那么会导致该线程阻塞
2.int pthread_mutex_trylock(pthread_mutex_t *mutex)
返回值:如果成功返回0,失败返回错误码
注意:如果互斥量已经被锁住,不会导致线程阻塞
(四)互斥量的解锁:
int pthread_mutex_unlock(pthread_mutex_t *mutex)
返回值:成功返回0,失败返回错误码
六.读写锁
(一)读写锁与互斥量类似,不过读写锁有更高的并行性。互斥量要么加锁要么不加锁,而且同一时刻只允许一个线程对其加锁,对于一个变量的读取,完全可以让多个线程同时进行操作
(二)类型:pthread_rwlock_trwlock
1.读写锁有三种状态,读模式下加锁,不加锁,写模式下加锁。一次只有一个线程可以占有写模式下的读写锁,但是多个线程可以同时占有读模式的读写锁。
2.读写锁在写加锁状态时,在它被解锁之前,所有试图对这个锁加锁的线程都会阻塞。读写锁在读加锁状态时,所有试图以读模式对其加锁的线程都会获得访问权,但是如果线程希望以写模式对其加锁,它必须阻塞希望对它进行写加锁的线程,直到所有线程中的读加锁被释放
3.当读写锁以读模式加锁时,如果有线程试图以写模式对其加锁,那么读写锁会阻塞随后的读模式锁请求。这样可以避免读锁长期占用,而写锁达不到请求。
4.读写锁非常适合对数据结构读次数大于写次数的程序,当它以读模式锁住时,是以共享的方式锁住的。当它以写模式锁住时,是以独占的方式锁住的
(三)在使用读写锁之前需要对其进行初始化
1.int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,constpthread_rwlockattr_t *restrict attr)
第一个参数:要初始化的读写锁
第二个参数:属性
使用完之后要对读写锁进行销毁:
2.int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)
返回值:成功返回0,失败返回错误码
(四)读模式的加锁
int pthread_rwlock_rdlock(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_rwlock_t *rwlock)
(六)解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)
七.条件变量
类型:pthread_cond_t cond
注意:条件变量使用之前需要初始化
(一)初始化
1.静态初始化pthread_cond_tcond=PTHREAD_COND_INITIALIZER
2.动态初始化int pthread_cond_init(pthread_cond_t *restrictcond,const pthread_condattr_t *restrict attr)
条件变量使用完成后需要销毁:
int pthread_cond_destroy(pthread_cond_t *cond)
(二)条件变量的使用需要配合互斥量:
1.int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrictmutex)
使用pthread_cond_wait等待条件变为真。传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传递给函数
这个函数将线程放在等待条件的线程列表上,然后对互斥量进行解锁,这是个原子操作。当条件满足时这个函数返回,返回以后对互斥量加锁
2.int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t*restrict mutex,const struct timespec *restrict abstime)
这个函数与pthread_cond_wait类似,只是多一个timeout,如果到了指定的时间条件还不满足,那么就返回。时间用下面的结构体表示
struct timespec
{
time_t tv_sec;
long tv_nsec;
}
注意:这个时间时绝对时间。例如你要等待三分钟,就要把当前时间加上三分钟然后转换到timespec,而不是直接将三分钟转换到timespec
(三)当条件满足时,需要唤醒等待的线程
int pthread_cond_broadcast(pthread_cond_t *cond)
int pthread_cond_signal(pthread_cond_t *cond)
1.pthread_cond_broadcast唤醒等待条件的所有线程
2.pthread_cond_signal唤醒等待条件的某一线程
注意:一定要在条件改变以后再唤醒线程
例子:
void *fun(void *arg)
{
pthread_mutex_lock(p->lock);
if(p->readpos==writepos)
{
printf("consumer wait for notempty\n");
pthread_cond_wait(&p->notempty,&p->lock);
}
data=p->buffer[p->readpos];
pthread_mutex_unlock(p->lock);
}
上面条件的意思是当p->readpos==writepos时,等待另一个进程发送&p-notempty信号过来时,该进程才可以继续往下执行,要不然一直等待
八.一次性初始化
有些事需要且只能执行一次,比如互斥量初始化。通常当初始化应用程序时,可以比较容易地将其放在main()函数中。但当你写一个库函数时,就不能在main里面初始化了,你可以用静态初始化,但使用一次初始(pthread_once_t)会比较容易些。
首先要定义一个pthread_once_tonce_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。并且在初始化时可以把once_control设为0.1.2。
九.线程属性
(一)创建线程函数:intpthread_create(pthread_t *restrict,const pthread_attr_t *restrict attr,void*(*start_routine)(void *),void *restrict arg)
1.线程属性用pthread_attr_t类型的结构表示,在创建线程的时候可以不用传入NULL,而是传入一个pthread_attr_t结构,由用户自己来配置线程的属性
2.pthread_attr_t类型对应用程序是不透明的,也就是说应用程序不需要了解有关属性对象内部结构的任何细节,因而可以增加程序的可移植性
(二)介绍几种线程属性如下:
detachstate:线程的分离状态
guardsize:线程栈末尾的警戒区域大小(字节数)
stacksize:线程栈的最低字节
stacksize:线程栈的大小(字节数)
注意:并不是所有的系统都支持线程的这些属性,因此你需要检查当前系统是否支持你设置的属性。当然还有一些属性不包含在pthread_attr_t结构中,例如:线程的可取消状态,取消类型,并发度
(三)pthread_attr_t结构在使用之前需要初始化,使用完之后需要销毁
(四)线程属性的初始化
int pthread_attr_init(pthread_attr_t *attr)
(五)线程属性销毁
int pthread_attr_destroy(pthread_attr_t *attr)
如果在调用pthread_attr_init初始化属性的时候分配了内存空间,那么pthread_attr_destroy将释放内存空间。除此之外,pthread_attr_destroy还会用无效的值初始化pthread_attr_t对象,因此如果该属性对象被误用,会导致线程创建失败
(六)如果在创建线程的时候知道不需要知道线程的终止状态,那么可以修改pthread_attr_t结构体中的detachstate属性,让线程以分离状态启动。可以使用pthread_attr_setdetachstate函数来设置线程的分离状态属性。线程的分离属性有两种合法值:
1.PTHREAD_CREATE_DETACHED(分离的)
2.PTHREAD_CREATE_JOINABLE(非分离的,可链接的)
int pthread_attr_setdetachstate(pthread_attr_t *attr,int detachstate)(设置分离属性)
int pthread_attr_getdetachstate(pthread_attr_t *attr,int *detachstate)(获得线程的分离状态属性)
(七)设置线程分离属性的步骤
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)
所有系统都会支持线程的分离状态属性
例子:
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
pthread_create(&tid,&attr,thread_fun,,NULL);
pthread_attr_destroy(&attr);
(八)线程栈属性
1.对于进程来说,虚拟地址空间的大小是固定的,进程中只有一个栈,因此它的大小通常不是问题。但对线程来说,同样的虚拟地址被所有的线程共享。如果应用程序使用了太多的线程,导致线程栈累计超过可用的虚拟地址空间,这个时候就需要减少线程默认的栈大小。另外,如果线程分配了大量的自动变量或者线程的栈帧太深,那么这个时候需要的栈要比默认的大。
2.如果用完了虚拟地址空间,可以使用malloc或者mmap来为其他栈分配空间,并修改栈的位置。
(十)修改栈属性
int pthread_attr_setstack(pthread_attr_t *attr,void *stackaddr,size_tstacksize)
(十一)获取栈属性:
int pthread_attr_getstack(pthread_attr_t *attr,void **stackaddr,size_t*stacksize)
注意:参数stackaddr是栈的内存单元最低地址,参数stacksize是栈的大小。你要注意stackaddr并不一定是栈的开始,对于一些处理器,栈的地址是从高往低的,那么stackaddr是栈的结尾。
(十二)当然也可以单独获取或者修改栈的大小,而不去修改栈的地址。对于栈的大小设置,不能小于PTHREAD_STACK_MIN(需要头文件limit.h)
int pthread_attr_setstacksize(pthread_attr_t *attr,size_t stacksize);
int pthread_attr_getstacksize(pthread_attr_t *attr,size_t *stacksize);
对于栈的大小设置,在创建线程后,还可以修改。
(十三)对于遵循POSIX标准的系统来说,不一定支持线程的栈属性,因此你需要检查
1.在编译阶段使用
_POSIX_THREAD_ATTR_STACKADDR和_POSIX_THREAD_ATTR_STACKSIZE符号来检查系统是否支持线程栈属性,这些宏定义在/usr/include/bits/posix_opt.h
2.在运行阶段把
_SC_THREAD_ATTR_STACKADDR和_SC_THREAD_ATTR_STACKSIZE传递给sysconf函数检查系统对线程栈属性的支持
(十四)线程属性guardsize控制着线程栈末尾以后用以避免栈溢出的扩展内存大小,这个属性默认是PAGESIZE个字节。你可以把它设为0,这样就不会提供警戒缓冲区。同样的,如果你修改了stackaddr系统会认为你自己要管理栈,警戒缓冲区会无效。
1.设置guardsize:
int pthread_attr_setguardsize(pthread_attr_t *attr,size_t guardsize)
2.获取guardsize:
int pthread_attr_getguardsize(pthread_attr_t *attr,size_t *guardsize)
十.互斥量的属性
int pthread_mutex_init(pthread_mutex_t *restrict mutex,constpthread_mutexattr_t *restrict attr)
就像线程有属性一样,线程的同步互斥量也有属性,比较重要的是进程共享属性和类型属性。互斥量的属性用pthread_mutexattr_t类型的数据表示,当然在使用之前必须进行初始化,使用完成之后需要进行销毁
(一)互斥量初始化:
int pthread_mutexattr_init(pthread_mutexattr_t *attr)
(二)互斥量属性销毁:
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr)
(三)进程共享属性:
PTHREAD_PROCESS_PRIVATE,这个是默认值,同一个进程中的多个线程访问同一个同步对象
PTHREAD_PROCESS_SHARED,这个属性可以使互斥量在多个进程中进行同步,如果互斥量在多进程的共享内存区域,那么具有这个属性的互斥量可以同步多进程
(四)设置互斥量进程共享属性:
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,int pshared)
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr,int*restrict pshared)
(五)进程共享属性需要检测系统是否支持,可以检测宏_POSIX_THREAD_PROCESS_SHARED
(六)1.互斥量类型:PTHREAD_MUTEX_NORMAL,PTHREAD_MUTEX_ERRORCHEK,PTHREAD_MUTEX_RECURSIVE,PTHREAD_MUTEX_DEFAULT
2.设置互斥量的类型属性:
int pthread_mutexattr_settype(pthread_mutexattr_t *attr,int type)
int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr,int*restrict type)
十一.读写锁的属性
读写锁的属性设置和互斥量类似,只是读写锁只有一个属性,即共享属性
(一)读写锁属性初始化:
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr)
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr)
(二)设置读写锁属性:
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr,int pshared)
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr,int*restrict pshared)
十二.条件变量的属性
(一)条件变量属性初始化:
int pthread_condattr_init(pthread_condattr_t *attr)
int pthread_condattr_destroy(pthread_condattr_t *attr)
(二)设置条件变量属性:
int pthread_condattr_setpshared(pthread_condattr_t *attr,int pshared)
int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr,int*restrict pshared)
十三.线程的私有数据
1.应用程序设计中有必要提供一种变量,使得多个函数多个线程都可以访问这个变量(看起来是个全局变量),但是线程对这个变量的访问都不会彼此产生影响(貌似不是全局变量),但是你需要这样的数据,比如errno。那么这中数据就是线程的私有数据,尽管名字相同,但是每个线程访问的都是数据的副本。
2.在使用私有数据之前,你首先要创建一个与私有数据相关的键,要来获取对私有数据的访问权限。这个键的类型是pthread_key_t
int pthread_key_create(pthread_key_t *key,void(*destructor)(void *))
创建的键放在key指向的内存单元,destructor是与键相关的析构函数。当线程调用pthread_exit或者使用return返回,析构函数就会被调用。当析构函数调用的时候,它只有一个参数,这个参数是与key关联的那个数据的地址(也就是你的私有数据),因此你可以在析构函数中将这个数据销毁。
3.键使用完之后也可以销毁,当键销毁之后,与它关联的数据并没有销毁
int pthread_key_delete(pthread_key_t key)
4.有了键之后,你就可以将私有数据和键关联起来,这样就可以通过键来找到数据。所有的线程都可以访问这个键,但他们可以为键关联不同的数据。(这和名字一样而值不同的全局变量一样)
将私有数据与键关联:
int pthread_setspecific(pthread_key_t key,const void *value)
获取私有数据的地址,如果没有数据与key关联,那么返回空
void *pthread_getspecific(pthread_key_t key)
十四.线程与fork()
1.当线程调用fork函数时,就为子进程创建了整个进程地址空间的副本,子进程通过继承整个地址空间的副本,也会将父进程的互斥量、读写锁、条件变量的状态继承过来。也就是说,如果父进程中互斥量是锁着的,那么在子进程中互斥量也是锁着的(尽管子进程自己没来得及lock),这是非常不安全的,因为不是子进程自己锁住的,它无法解锁。
2.子进程内部只有一个线程,由父进程中调用fork()函数的线程副本构成。如果调用fork的线程将互斥量锁住,那么子进程会拷贝一个pthread_mutex_lock副本,这样子进程就有机会去解锁了。或者互斥量根本就没被加锁,这样也是可以的,但是你不能确保永远都是这样的情况
3.pthread_atfock函数给你创造了这样的条件,它会注册三个函数
int pthread_atfork(void(*prepare)(vodi),void (*parent)(void),void (*child)(void))
prepare是在fork调用之前会被调用的,parent在fork返回父进程之前调用,child在fork返回子进程之前调用,如果在prepare中加锁所有的互斥量,在parent和
child中解锁所有的互斥量,那么在fork返回之后,互斥量处于未加锁状态
4.可以用多个pthread_atfork函数,这是也就会有多个处理程序(prepare,parent,child)。多个prepare的执行顺序与注册顺序相反,而parent和child的执行顺序与注册顺序相同
多线程学习笔记
最新推荐文章于 2024-09-01 17:00:56 发布