锁主要有两个用途:
1 解决互斥访问资源
2 解决一个任务对另一个任务的依赖
关于linux内核:
linux内核只提供了轻量进程的支持,未实现线程模型。Linux是一种“多进程单线程”的操作系统。Linux本身只有进程的概念,而其所谓的“线程”本质上在内核里仍然是进程。
1、互斥锁
1.1 普通互斥锁
这里普通互斥锁指的是非递归互斥锁。
为了保护共享资源,使我们线程可以单独使用某个共享资源,使用之前先上锁,当其他进程要使用的时候,就需要等待到这个线程用完之后,再开锁。
接口与定义:
声明互斥锁:
pthread_mutex_t m;
初始化互斥锁:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
第一个参数:就是我们前面声明的锁,因为这个参数需要传递的是一个锁的指针,所以需要有一个取地址符。
第二个参数:是这个锁的属性,我们让它是默认的属性,这里设置为NULL
返回值:成功返回0, 失败返回-1
上锁:锁住某个资源
int pthread_mutex_lock(pthread_mutex_t *mutex); 这个函数是阻塞型。
int pthread_mutex_trylock(pthread_mutex_t *mutex); 这个是非阻塞型的。
返回值:成功返回0,失败返回-1
解锁:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回-1
销毁互斥锁:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
返回值:成功返回0, 失败返回-1
实例 启动两个线程对同一个全局数组写入:
#include#include #include #include #include pthread_mutex_t mutex; //定义互斥量 char* buf[5]; //字符指针数组 全局变量 int pos; //用于指定上面数组的下标 void *task(void *p) { //使用互斥量进行加锁 pthread_mutex_lock(&mutex); buf[pos] = (char *)p; sleep(1); pos++; //使用互斥量进行解锁 pthread_mutex_unlock(&mutex); } void *task2(void *p) { //使用互斥量进行加锁 pthread_mutex_lock(&mutex); buf[pos] = (char *)p; sleep(1); pos++; //使用互斥量进行解锁 pthread_mutex_unlock(&mutex); } int main(void) { //初始化互斥量, 默认属性 pthread_mutex_init(&mutex, NULL); pthread_spin_init(&spinMutex, NULL); //启动一个线程 向数组中存储内容 pthread_t tid, tid2; pthread_create(&tid, NULL, task, (void *)"zhangfei"); pthread_create(&tid2, NULL, task2, (void *)"guanyu"); //主线程进程等待,并且打印最终的结果 pthread_join(tid, NULL); pthread_join(tid2, NULL); pthread_mutex_destroy(&mutex); int i = 0; printf("字符指针数组中的内容是:"); for (i = 0; i < pos; ++i) { printf("%s ", buf[i]); }
输出:zhangfei guanyu
1.2 递归锁
递归锁也叫可重入锁(reentrant mutex),非递归锁也叫不可重入锁(non-reentrant mutex)。
同一个线程可以多次获取同一个递归锁,不会产生死锁。如果一个线程多次获取同一个非递归锁,则会产生死锁。
Windows下的Mutex和Critical Section是可递归的。
Linux下的pthread_mutex_t锁是默认是非递归的。可以通过设置PTHREAD_MUTEX_RECURSIVE属性,将pthread_mutex_t锁设置为递归锁。
//create recursive attribute pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); //set recursive attribute pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&g_mutex, &attr);
2、读写锁
读写锁也是用来控制互斥访问,特点是:
所以读写锁的规则就是:
读写锁适合于对资源的读次数比写次数多得多的情况。
读写锁的接口:
#include
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *rwlock,
const pthread_rwlockattr_t *attr);
// 申请读锁
int pthread_rwlock_rdlock(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);
// 销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
实例,两个读线程两个写线程,写互斥地对字符数组操作:
#include#include #include #include #include char cBuf[30]; pthread_rwlock_t rwlock; //读写锁 //write线程函数,负责往buf里面加字符 void *taskWrite(void *p) { while (1) { pthread_rwlock_wrlock(&rwlock); if (pos < 29) { cBuf[pos] = 'A'; pos++; printf("Write cBuf is: %s \n", cBuf); } pthread_rwlock_unlock(&rwlock); sleep(1); } } //write线程函数,负责从buf里面删除字符 void *taskWriteDel(void *p) { while (1) { pthread_rwlock_wrlock(&rwlock); if (pos > 0) { cBuf[pos-1] = 0; pos--; printf("WriteDel cBuf is: %s \n", cBuf); } pthread_rwlock_unlock(&rwlock); sleep(1); } } void *taskRead(void *p) { while (1) { pthread_rwlock_rdlock(&rwlock); if (pos > 0) { printf("read cBuf is: %s \n", cBuf); } pthread_rwlock_unlock(&rwlock); } } void *taskRead2(void *p) { while (1) { pthread_rwlock_rdlock(&rwlock); if (pos > 0) { printf("read2 cBuf is: %s \n", cBuf); } pthread_rwlock_unlock(&rwlock); } } int main(void) { pthread_t ptd1, ptd2, ptd3, ptd4; pos = 0; pthread_rwlock_init(&rwlock, NULL);//初始化一个读写锁 //创建线程 pthread_create(&ptd1, NULL, taskWrite, NULL); pthread_create(&ptd2, NULL, taskWriteDel, NULL); pthread_create(&ptd3, NULL, taskRead, NULL); pthread_create(&ptd4, NULL, taskRead2, NULL); //等待线程结束,回收其资源 pthread_join(ptd1, NULL); pthread_join(ptd2, NULL); pthread_join(ptd3, NULL); pthread_join(ptd4, NULL); pthread_rwlock_destroy(&rwlock);//销毁读写锁 re
结果:cBuf中的字符数量时而增加时而减少
3、自旋锁
自旋锁与互斥量功能一样,唯一一点不同的就是互斥量阻塞后休眠让出cpu,而自旋锁阻塞后不会让出cpu,会一直忙等待,直到得到锁。
接口上就是把互斥锁的mutex改成spin。
实例 两个线程各自数数:
#include#include #include #include #include pthread_spinlock_t spinMutex; void *task5(void *p) { int cur = 0; while (1) { //3.使用互斥量进行加锁 pthread_spin_lock(&spinMutex); printf("task5 %d\n", cur); cur++; sleep(0); //4.使用互斥量进行解锁 pthread_spin_unlock(&spinMutex); } } void *task6(void *p) { int cur = 0; while (1) { //3.使用互斥量进行加锁 pthread_spin_lock(&spinMutex); printf("task6 %d\n", cur); cur++; sleep(0); //4.使用互斥量进行解锁 pthread_spin_unlock(&spinMutex); } } int main(void) { pthread_spin_init(&spinMutex, NULL); pthread_t tid3, tid4; pthread_create(&tid3, NULL, task5, (void *)"zhangfei"); pthread_create(&tid4, NULL, task6, (void *)"zhangfei"); pthread_join(tid3, NULL); pthread_join(tid4, NULL); pthread_spin_destroy(&spinMute
4、条件变量
4.1 条件变量的原理和接口
条件变量用来自动阻塞一个线程,直 到某特殊情况发生为止。通常条件变量和互斥锁同时使用。线程在改变条件状态之前必须首先锁住互斥量。
接口:
#include
// 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond,
pthread_condattr_t *cond_attr);
// 阻塞等待
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
// 超时等待
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,
const timespec *abstime);
// 解除所有线程的阻塞
int pthread_cond_destroy(pthread_cond_t *cond);
// 至少唤醒一个等待该条件的线程
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒等待该条件的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);
实例, 一个写线程两个读线程,读完了就删,写完了就给读线程发信号:
#include#include #include #include #include int pos; //用于指定上面数组的下标 char cBuf[30]; //用作条件变量 pthread_cond_t tcond = PTHREAD_COND_INITIALIZER; pthread_mutex_t tmutex = PTHREAD_MUTEX_INITIALIZER; void *taskCondWrite(void *p) { while (1) { if (pos < 29) { cBuf[pos] = 'A'; pos++; printf("Write cBuf is: %s \n", cBuf); pthread_cond_signal(&tcond); sleep(1); } } } void *taskCondRead1(void *p) { while (1) { pthread_mutex_lock(&tmutex); pthread_cond_wait(&tcond, &tmutex); pthread_mutex_unlock(&tmutex); if (pos > 0) { printf("read1 cBuf is: %s \n", cBuf); } sleep(1); } } void *taskCondRead2(void *p) { while (1) { pthread_mutex_lock(&tmutex); pthread_cond_wait(&tcond, &tmutex); pthread_mutex_unlock(&tmutex); if (pos > 0) { printf("read2 cBuf is: %s \n", cBuf); } sleep(1); } } int main(void) { //一个写线程两个读线程,读完了就删,写完了就给读线程发信号,看是什么效果 pthread_t ptd1, ptd2, ptd3; pthread_create(&ptd1, NULL, taskCondWrite, NULL); pthread_create(&ptd2, NULL, taskCondRead1, NULL); pthread_create(&ptd3, NULL, taskCondRead2, NULL); //等待线程结束,回收其资源 pthread_join(ptd1, NULL); pthread_join(ptd2, NULL); pthread_join(ptd3, NULL); pthread_cond_destroy(&tmutex); r
4.2 虚假唤醒
在多核处理器下,pthread_cond_signal可能会激活多于一个线程。结果是,当一个线程调用pthread_cond_signal()后,多个调用pthread_cond_wait()或pthread_cond_timedwait()的线程返回。这种效应成为”虚假唤醒”(spurious wakeup)。
通常的解决方法是将if改为while:
mj中的实现:
void ArcConditionVariable::Wait(void) throw(ArcException) { status = pthread_mutex_lock(&_lock); while (_pred == 0) //signal和broadcast的时候设置成1,这个是用来防止虚假唤醒 { status = pthread_cond_wait(&_cond, &_lock); } _pred = 0; //最后加一个这个,还能保证,在broadcast的时候,都能往下进行么? status = pthread_mutex_unlock(&_lock); }
参考:https://www.jianshu.com/p/0eff666a4875
4.3 broadcast的例子
#include#include #include #include #include //用作条件变量 pthread_cond_t tcond = PTHREAD_COND_INITIALIZER; pthread_mutex_t tmutex = PTHREAD_MUTEX_INITIALIZER; int pred = 0; void *thread1_entry(void *arg) { while (1) { pthread_mutex_lock(&tmutex); printf("cond wait tcond1\n"); while (pred == 0) { pthread_cond_wait(&tcond, &tmutex); printf("recv cond1\n"); } pred = 0; pthread_mutex_unlock(&tmutex); } } void *thread2_entry(void *arg) { while (1) { pthread_mutex_lock(&tmutex); printf("cond wait tcond2\n"); while (pred == 0)//防止虚假唤醒 { pthread_cond_wait(&tcond, &tmutex); printf("recv cond2\n"); } pred = 0; pthread_mutex_unlock(&tmutex); } } void *thread3_entry(void *arg) { int ret; while (1) { pthread_mutex_lock(&tmutex); ret = pthread_cond_broadcast(&tcond); if (ret < 0) { printf("pthread_cond_broadcast error\n"); } else { printf("pthread_cond_broadcast !\n"); } pred = 1; pthread_mutex_unlock(&tmutex); sleep(2); } } int main(void) { //展示:thread3 通过broadcast唤醒thread1 和 thread2 各一次 pthread_t ptd1, ptd2, ptd3; pthread_create(&ptd1, NULL, thread1_entry, NULL); pthread_create(&ptd2, NULL, thread2_entry, NULL); pthread_create(&ptd3, NULL, thread3_entry, NULL); //等待线程结束,回收其资源 pthread_join(ptd1, NULL); pthread_join(ptd2, NULL); pthread_join(ptd3, NULL);
结果:
从结果可以看除在pred = 0之后同时被放行的线程不会执行
while (pred == 0)//防止虚假唤醒
这个循环体外面的内容。
所以要把需要执行的内容都放在这个循环体内。
5、信号量
信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。
与互斥锁在作用域上的区别:信号量: 进程间或线程间,互斥锁: 线程间。
编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞。PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。
接口:
#include
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 信号量 P 操作(减 1)
int sem_wait(sem_t *sem);
// 信号量P操作,限时等待(返回之前还是减一了的)
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
// 以非阻塞的方式来对信号量进行减 1 操作
int sem_trywait(sem_t *sem);
// 信号量 V 操作(加 1)
int sem_post(sem_t *sem);
// 获取信号量的值
int sem_getvalue(sem_t *sem, int *sval);
// 销毁信号量
int sem_destroy(sem_t *sem);
信号量可以用于同步和互斥:
5.1 信号量用于同步
用于同步的例子, 每写两个字符就可以读一次,并且每读一次对sem_p进行两次V操作
#include#include #include #include #include #include int pos; //用于指定上面数组的下标 char cBuf[30]; sem_t sem_g, sem_p; //定义两个信号量 void *pthread_g(void *arg) //此线程改变字符ch的值 { while (1) { sem_wait(&sem_g); if (pos < 29) { cBuf[pos] = 'A'; pos++; printf("Write cBuf is: %s \n", cBuf); sleep(1); } sem_post(&sem_p); } } void *pthread_p(void *arg) //此线程打印ch的值 { while (1) { sem_wait(&sem_p); sem_wait(&sem_p); if (pos > 0) //pthread_g要执行两次,才能走到这里 { printf("pthread_p cBuf is: %s \n", cBuf); } sleep(1); sem_post(&sem_g); sem_post(&sem_g); } } int main(void) { pthread_t tid1, tid2; sem_init(&sem_g, 0, 0); // 初始化信号量为0 sem_init(&sem_p, 0, 2); // 初始化信号量为2 // 创建两个线程 pthread_create(&tid1, NULL, pthread_g, NULL); pthread_create(&tid2, NULL, pthread_p, NULL); // 回收线程 pthread_join(tid1, NULL); pthread_join(tid2,
结果:
信号量用于同步的示意图:
5.2 信号量用于互斥
信号量用于互斥只需要一个信号量
// 信号量用于互斥实例 #include#include #include #include sem_t sem; //信号量 void printer(char *str) { sem_wait(&sem);//减一,p操作 while(*str) // 输出字符串(如果不用互斥,此处可能会被其他线程入侵) { putchar(*str); fflush(stdout); str++; sleep(1); } printf("\n"); sem_post(&sem);//加一,v操作 } void *thread_fun1(void *arg) { char *str1 = "hello"; printer(str1); } void *thread_fun2(void *arg) { char *str2 = "world"; printer(str2); } int main(void) { pthread_t tid1, tid2; sem_init(&sem, 0, 1); //初始化信号量,初始值为 1 //创建 2 个线程 pthread_create(&tid1, NULL, thread_fun1, NULL); pthread_create(&tid2, NULL, thread_fun2, NULL); //等待线程结束,回收其资源 pthread_join(tid1, NULL); pthread_join(tid2, NULL); sem_destroy(&sem); //销毁信号量 return 0; }
信号量用于互斥的示意图:
5.3 sem_timedwait的例子
第一个参数表示多少秒之后会发出一个SIGALRM,第二个参数表示多长时间会超时
#include#include #include #include #include #include #include #include sem_t sem; #define handle_error(msg) \ do { perror(msg); exit(EXIT_FAILURE); } while (0) static void handler(int sig) { write(STDOUT_FILENO, "sem_post() from handler\n", 24); if (sem_post(&sem) == -1) { write(STDERR_FILENO, "sem_post() failed\n", 18); _exit(EXIT_FAILURE); } } int main(int argc, char *argv[]) { struct sigaction sa; struct timespec ts; int s; if (argc != 3) { fprintf(stderr, "Usage: %s \n", argv[0]); exit(EXIT_FAILURE); } if (sem_init(&sem, 0, 0) == -1) handle_error("sem_init"); /* Establish SIGALRM handler; set alarm timer using argv[1] */ sa.sa_handler = handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if (sigaction(SIGALRM, &sa, NULL) == -1) handle_error("sigaction"); alarm(atoi(argv[1])); /* Calculate relative interval as current time plus number of seconds given argv[2] */ if (clock_gettime(CLOCK_REALTIME, &ts) == -1) handle_error("clock_gettime"); ts.tv_sec += atoi(argv[2]); printf("main() about to call sem_timedwait()\n"); while ((s = sem_timedwait(&sem, &ts)) == -1 && errno == EINTR) continue; /* Restart if interrupted by handler */ /* Check what happened */ if (s == -1) { if (errno == ETIMEDOUT) printf("sem_timedwait() timed out\n"); else perror("sem_timedwait"); } else printf("sem_timedwait() succeeded\n"); exit((s == 0) ? EXIT_SUCCESS : EXIT_
结果:
$ ./a.out 2 3 About to call sem_timedwait() sem_post() from handler sem_timedwait() succeeded $ ./a.out 2 1 About to call sem_timedwait() sem_timedwait() timed out
6、pthread标准的其它函数
6.1 pthread_self (void) ;
调用获取自己的线程ID:
6.2 pthread_exit (void * retval) ;
显式地退出线程
6.3 pthread_cance (pthread_t thread) ;
被其他线程用pthread_cance函数终止
7、Windows与Linux接口对比
事项 |
WIN32 |
Linux |
线程创建 |
CreateThread |
pthread_create |
线程终止 |
执行完成后退出;线程自身调用ExitThread函数即终止自己;被其他线程调用函数TerminateThread函数 |
执行完成后退出;由线程本身调用pthread_exit 退出;被其他线程调用函数pthread_cance终止 |
获取线程ID |
GetCurrentThreadId |
pthread_self |
创建互斥 |
CreateMutex |
pthread_mutex_init |
获取互斥 |
WaitForSingleObject、WaitForMultipleObjects |
pthread_mutex_lock |
释放互斥 |
ReleaseMutex |
phtread_mutex_unlock |
创建信号量 |
CreateSemaphore |
sem_init |
等待信号量 |
WaitForSingleObject |
sem_wait |
释放信号量 |
ReleaseSemaphore |
sem_post |
最后:linux带pthread编译:
g++ -lpthread mytestthread.cpp -o testt