2 线程
推荐下面的网址进行学习:
https://computing.llnl.gov/tutorials/pthreads/
2.1 建立和使用线程
//线程的标识符
pthread_t thread;
//比较两个线程的ID
int pthread_equal(pthread_t t1,pthread_t t2);
//创建新进程函数
//If the pthread_create() routine succeeds it will return 0
//and put the new thread id into thread,
//otherwise an error number shall be returned indicating the error.
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start)(void *),void *arg);
//获得自己线程的ID
pthread_t pthread_self(void);
int sched_yield(void);
//终止函数
int pthread_exit(void *value_ptr);
//分离一个线程
int pthread_detach(pthread_t thread,);
//阻塞其调用者直到指定线程终止,然后,选择地保存线程的返回值。
//If the target thread thread is not detached and
//there are no other threads joined with the specified thread then
//the pthread_join() function suspends execution of the current thread
//and waits for the target thread thread to terminate.
//Otherwise the results are undefined.
//当pthread_join调用返回时,被连接线程就已被分离(detached),再也不能连接该线程了
int pthread_join(pthread_t thread,void **value_ptr);
1)分离一个正在运行的线程不会对线程带来任何影响,仅仅是通知系统当该线程结束时,其所属资源可以被回收。为确保终止线程的资源对进程可用,应该在每个线程结束时分离它们。
2)进程内的线程能够同时使用不同的堆栈执行不同的指令。尽管线程彼此独立的执行,它们总是共享同一个地址空间和文件描述符。共享地址空间使得线程间的通信高效。
2.2 线程的生命周期
初始线程与普通线程的区别:
1)如果普通线程从启动函数中返回,则线程终止而其他线程依然可以执行;但初始进程从main()函数返回时,进程终止(进程内的所有线程也终止)。如果希望在初始线程终止时,进程中的其他线程继续执行,则需要在初始线程中调用pthread_exit而不是从main函数返回。
2)大多数系统中,初始线程运行在默认进程堆栈上,该堆栈可以增长到足够的尺寸,而在某些实现中,普通线程的堆栈空间是受限的,如果线程堆栈溢出,则程序会因段错误或总线错误而失败。
线程在以下情况下时被阻塞:
1)试图加锁一个已经被锁住的互斥量。
2)等待某个条件变量。
3)调用Singwait等待尚未发生的信号。
4)执行无法立即完成的IO操作。
5)线程还会由于如内存页错误之类的系统操作而被阻塞。
3 同步
3.1 使用线程编写各种难度的程序,需要在线程间共享数据或者需要以一致的顺序在线程间执行一组操作。
3.2 不变量、临界区和谓词
不变量:是由程序作出的假设,特别是有关变量组间关系的假设。
临界区:指影响共享数据的代码段。
两者之间的关系:临界区总能够对应到一个数据不变量,反之亦然。例如,从队列中删除数据时,可以将删除数据的代码视为临界区,也可以将队列状态视为不变量。
谓词:是描述代码所需不变量的状态的语句。
3.3 互斥量
使线程同步最通用和常用的方法就是确保对相同(或相关)数据的内存访问“互斥的”进行,即一次只能允许一个线程写数据,其他线程必须等待。Pthreads使用一种特殊形式的Edsger Dijkstra信号灯—互斥量(mutex)。
3.3.1 创建和销毁互斥量
1)互斥量是使用pthread_mutex_t
类型的变量来表示的。一般,如果有其他文件使用互斥量,则将其声明为外部类型;如果仅在本文件内使用,则将其声明为静态类型。
2)可以使用宏PTHREAD_MUTEX_INITIALIZER
来声明具有默认属性的静态互斥量。如果可能,将互斥量和数据定义在一起为一个结构体。
3)可以使用pthread_mutex_init
调用来动态地初始化互斥量。当不再需要互斥量时,应该调用pthread_mutex_destroy
来释放它。
3.3.2 加锁和解锁互斥量
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//调用sched_yield的效果是将处理器交给另一个等待运行的线程,
//但是如果没有就绪的线程,则立即返回。
sched_yield ()
3.3.2.1 非阻塞式互斥量锁
1)当调用pthread_mutex_lock
加锁互斥量时,如果此时互斥量已近被锁住,则调用线程将被阻塞。
2)如果希望如果互斥量被锁住时,就执行另外的代码线路,则使用pthread_mutex_trylock
函数,当互斥量已被锁住时调用该函数讲返回错误代码EBUSY。
3.3.3 使用多个互斥量
加锁层次:针对死锁,可以从以下两种通用的解决方法:
1)固定加锁层次:所有需要同时加锁互斥量A和互斥量B的代码,必须首先加锁互斥量A,然后加锁互斥量B。
2)试加锁和回退:在锁住某个集合中的第一个互斥量后,使用pthread_mutex_trylock
来加锁集合中的其他互斥量,如果失败则将集合中所有已加锁互斥量释放,并重新加锁。
3.4 条件变量
https://computing.llnl.gov/tutorials/pthreads/#ConditionVariables
这里我要吐槽一下,<<POSIX多线程程序设计中文版>>
,不知道是原书的锅,还是翻译的锅。讲的晦涩难懂,不如直接看POSIX Threads Programming
简介明了。
下面直接翻译POSIX Threads Programming
里面的内容:
1)条件变量为线程同步提供了另一种方式。互斥锁通过控制线程访问数据来实现同步,条件变量允许线程根据数据的实际值进行同步。
2)在没有条件变量的情况下,程序员需要不断查询(某个条件是否为真),以检查是否满足条件。这可能非常耗费资源,因为线程在这个活动中会一直处于忙碌的工作中。条件变量是在没有查询的情况下实现相同目标的一种方法。
3)条件变量总是与互斥锁一起使用。
创建和释放条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
等待和唤醒条件变量
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
附录一个使用的小程序:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_t tid1,tid2;
bool wait = true;
void *watch(void *t)
{
pthread_mutex_lock(&mutex);
while(wait)
{
pthread_cond_wait(&cond,&mutex);
}
pthread_mutex_unlock(&mutex);
cout << "watch()" << endl;
pthread_exit(NULL);
}
void *inc(void *arg)
{
pthread_mutex_lock(&mutex);
wait = false;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
cout << "inc()" << endl;
pthread_exit(NULL);
}
int main(int argc,char **argv)
{
int status;
status = pthread_create(&tid1,NULL,watch,NULL);
if(status != 0)
{
cout<<"pthread 1 failed !"<<endl;
}
status = pthread_create(&tid2,NULL,inc,NULL);
if(status != 0)
{
cout<<"pthread 2 failed !"<<endl;
}
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
pthread_exit(NULL);
}
执行结果:
几点说明:
1)pthread_cond_wait(&cond,&mutex)
是一个原子操作,当它执行时,首先对mutex解锁,这样另外的线程才能得到锁来修改条件,pthread_cond_wait
解锁后,再将本身的线程/进程投入睡眠,另外,当该函数返回时,会再对mutex进行加锁,这样才能“执行某种操作”后unlock锁。
2)关于void *inc(void *arg)
线程是应该在pthread_cond_signal(&cond)
之前解锁,还是在pthread_cond_signal(&cond)
之后解锁,看实际需要。在之前解锁,则发送完信号后,void *watch(void *t)
线程由阻塞态转为可执行态;而之后解锁,执行完pthread_cond_signal(&cond)
后,如果接着执行void *watch(void *t)
线程,则pthread_cond_wait(&cond,&mutex)
会在返回前对mutex进行加锁,这时如果void *inc(void *arg)
尚未解锁,则void *watch(void *t)
线程会继续阻塞,知道void *inc(void *arg)
线程解锁为止。目前,见得例子大部分都是上述小程序的过程。