一、互斥锁
临界资源概念:
不能同时访问的资源,比如写文件,只能由一个线程写,同时写会写乱。
比如外设打印机,打印的时候只能由一个程序使用。
外设基本上都是不能共享的资源。
生活中比如卫生间,同一时间只能由一个人使用。
必要性: 临界资源不可以共享
两种方法创建互斥锁,静态方式和动态方式
动态方式:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
其中mutexattr用于指定互斥锁属性,如果为NULL则使用缺省属性。
静态方式:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
锁的销毁:
int pthread_mutex_destroy(pthread_mutex_t *mutex)
在Linux中,互斥锁并不占用任何资源,
因此LinuxThreads中的 pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。
互斥锁的使用:
int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)
vim 设置代码全文格式化:gg=G
查看线程:
没有加互斥锁:
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
FILE* fp;
void* testattr(void* arg)
{
pthread_detach(pthread_self());
printf("This is testattr pthread\n");
char str[] = "You read testattr thread\n";
char c;
int i = 0;
while(1)
{
while(i<strlen(str))
{
c = str[i];
int ret = 0;
ret = fputc(c,fp);
i++;
usleep(1);
}
i = 0;
usleep(1);
}
pthread_exit("testattr exit");
}
void* testattr2(void* arg)
{
pthread_detach(pthread_self());
printf("This is testattr2 pthread\n");
char str[] = "I write testattr2 line\n";
char c;
int i = 0;
while(1)
{
while(i<strlen(str))
{
c = str[i];
int ret = 0;
ret = fputc(c,fp);
i++;
usleep(1);
}
i = 0;
usleep(1);
}
pthread_exit("testattr2 exit");
}
int main()
{
pthread_t pthread;
pthread_t pthread2;
int i = 0;
void* retv;
fp = fopen("1.txt","a+");
if(fp == NULL)
{
perror("fp");
exit(-1);
}
pthread_create(&pthread,NULL,testattr,NULL);
pthread_create(&pthread2,NULL,testattr2,NULL);
while(1)
{
sleep(1);
}
fclose(fp);
return 0;
}
1.txt中的值:
加上互斥锁:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
mutex
参数是一个指向互斥锁对象的指针,该锁对象必须是一个已经初始化的互斥锁。
pthread_mutex_lock
函数尝试对互斥锁进行加锁。如果互斥锁当前没有被锁住,那么调用将成功,该线程将对互斥锁进行加锁并立即返回。如果互斥锁当前被其他线程锁住,那么调用将被阻塞,直到互斥锁被释放。
在加锁之后,线程负责确保在对共享资源的访问完成后调用 pthread_mutex_unlock
函数进行解锁,以允许其他线程获得对互斥锁的访问。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
是一种静态初始化互斥锁的方式。
在使用互斥锁之前,必须对其进行初始化。
PTHREAD_MUTEX_INITIALIZER 是一个宏,它在编译时为互斥锁对象提供了默认的初始值。
#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
FILE* fp;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* testattr(void* arg)
{
pthread_detach(pthread_self());
printf("This is testattr pthread\n");
char str[] = "You read testattr thread\n";
char c;
int i = 0;
while(1)
{
pthread_mutex_lock(&mutex);
while(i<strlen(str))
{
c = str[i];
int ret = 0;
ret = fputc(c,fp);
i++;
usleep(1);
}
pthread_mutex_unlock(&mutex);
i = 0;
usleep(1);
}
pthread_exit("testattr exit");
}
void* testattr2(void* arg)
{
pthread_detach(pthread_self());
printf("This is testattr2 pthread\n");
char str[] = "I write testattr2 line\n";
char c;
int i = 0;
while(1)
{
pthread_mutex_lock(&mutex);
while(i<strlen(str))
{
c = str[i];
int ret = 0;
ret = fputc(c,fp);
i++;
usleep(1);
}
pthread_mutex_unlock(&mutex);
i = 0;
usleep(1);
}
pthread_exit("testattr2 exit");
}
int main()
{
pthread_t pthread;
pthread_t pthread2;
int i = 0;
void* retv;
fp = fopen("1.txt","a+");
if(fp == NULL)
{
perror("fp");
exit(-1);
}
pthread_create(&pthread,NULL,testattr,NULL);
pthread_create(&pthread2,NULL,testattr2,NULL);
while(1)
{
sleep(1);
}
fclose(fp);
return 0;
}
1.txt中的值:
动态方式创建互斥锁:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
其中mutexattr用于指定互斥锁属性,如果为NULL则使用缺省属性。
二、读写锁
必要性:提高线程执行效率
特性:
写者:写者使用写锁,如果当前没有读者,也没有其他写者,写者立即获得写锁;否则写者将等待,直到没有读者和写者。
读者:读者使用读锁,如果当前没有写者,读者立即获得读锁;否则读者等待,直到没有写者。
注意:
同一时刻只有一个线程可以获得写锁,同一时刻可以有多个线程获得读锁。
读写锁处于写锁状态时,所有试图对读写锁加锁的线程,不管是读者试图加读锁,还是写者试图加写锁,都会被阻塞。
读写锁处于读锁状态时,有写者试图加写锁时,之后的其他线程的读锁请求会被阻塞,以避免写者长时间的不写锁
初始化一个读写锁 pthread_rwlock_init
读锁定读写锁 pthread_rwlock_rdlock
非阻塞读锁定 pthread_rwlock_tryrdlock
写锁定读写锁 pthread_rwlock_wrlock
非阻塞写锁定 pthread_rwlock_trywrlock
解锁读写锁 pthread_rwlock_unlock
释放读写锁 pthread_rwlock_destroy
int pthread_detach(pthread_t thread); 成功:0;失败:错误号
作用:从状态上实现线程分离,注意不是指该线程独自占用地址空间。
线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后(不会产生僵尸线程),其退出状态不由其他线程获取,而直接自己自动释放(自己清理掉PCB的残留资源)。网络、多线程服务器常用。
示例代码:
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
pthread_rwlock_t rwlock;
FILE *fp;
void * read_func(void *arg){
pthread_detach(pthread_self());
printf("read thread\n");
char buf[32]={0};
while(1){
pthread_rwlock_rdlock(&rwlock);
while(fgets(buf,32,fp)!=NULL){
printf("%d,rd=%s\n",(int)arg,buf);
usleep(1000);
}
pthread_rwlock_unlock(&rwlock);
sleep(1);
}
}
void *func2(void *arg){
pthread_detach(pthread_self());
printf("This func2 thread\n");
char str[]="I write func2 line\n";
char c;
int i=0;
while(1){
pthread_rwlock_wrlock(&rwlock);
while(i<strlen(str))
{
c = str[i];
fputc(c,fp);
usleep(1);
i++;
}
pthread_rwlock_unlock(&rwlock);
i=0;
usleep(1);
}
pthread_exit("func2 exit");
}
void *func(void *arg){
pthread_detach(pthread_self());
printf("This is func1 thread\n");
char str[]="You read func1 thread\n";
char c;
int i=0;
while(1){
pthread_rwlock_wrlock(&rwlock);
while(i<strlen(str))
{
c = str[i];
fputc(c,fp);
i++;
usleep(1);
}
pthread_rwlock_unlock(&rwlock);
i=0;
usleep(1);
}
pthread_exit("func1 exit");
}
int main(){
pthread_t tid1,tid2,tid3,tid4;
void *retv;
int i;
fp = fopen("1.txt","a+");
if(fp==NULL){
perror("fopen");
return 0;
}
pthread_rwlock_init(&rwlock,NULL);
pthread_create(&tid1,NULL,read_func,1);
pthread_create(&tid2,NULL,read_func,2);
pthread_create(&tid3,NULL,func,NULL);
pthread_create(&tid4,NULL,func2,NULL);
while(1){
sleep(1);
}
}
-
文件和锁的初始化:
- 该程序以附加模式(
"a+"
)打开名为"1.txt"的文件,使用fopen
。如果文件不存在,它尝试创建它。 - 使用
pthread_rwlock_init
初始化读写锁。
- 该程序以附加模式(
-
线程函数:
read_func
:该函数负责从文件中读取。它不断从文件中读取行并将它们打印到控制台。它使用读锁以确保它可以与其他读取器并发读取,但在读取正在进行时不能写入。testattr
和testattr1
:这些函数不断将预定义的字符串写入文件。它们使用写锁以确保只有其中一个可以同时写入文件。
-
主函数:
- 它创建五个线程:两个用于读取(
pthread2
和pthread3
),三个用于写入(pthread
,pthread1
和主线程)。 - 主线程进入一个无限循环(
while(1)
)以使程序无限期运行。
- 它创建五个线程:两个用于读取(
-
线程分离:
- 使用
pthread_detach(pthread_self())
分离每个线程,以在线程退出时自动回收资源。
- 使用
-
线程同步:
- 读写操作由读写锁(
pthread_rwlock_rdlock
和pthread_rwlock_wrlock
)保护,以确保正确的同步并避免数据损坏。
- 读写操作由读写锁(
-
文件操作:
- 文件操作使用标准文件I/O函数(
fgets
用于读取,fputc
用于写入)。
- 文件操作使用标准文件I/O函数(
-
睡眠和延迟:
- 使用
usleep
和sleep
引入操作之间的延迟,以更好地展示并发行为。
- 使用
-
主函数中的无限循环:
- 通过带有
sleep(1)
延迟的无限循环,保持主线程处于活动状态。
- 通过带有
那读写的顺序是什么?
在这个程序中,有两个读线程 (read_func
) 和两个写线程 (testattr
和 testattr1
),以及主线程。读线程和写线程是同时运行的,并且使用了读写锁 (pthread_rwlock_t rwlock
) 来确保对文件的安全访问。
-
读线程 (
read_func
) 在一个无限循环中,通过pthread_rwlock_rdlock
获取读锁,然后通过fgets
从文件中读取内容。读操作完成后,通过pthread_rwlock_unlock
释放读锁。这表示读线程首先获取读锁,然后读取文件内容。 -
写线程 (
testattr
和testattr1
) 在一个无限循环中,通过pthread_rwlock_wrlock
获取写锁,然后通过fputc
向文件写入内容。写操作完成后,通过pthread_rwlock_unlock
释放写锁。这表示写线程首先获取写锁,然后写入文件内容。
总体而言,程序中的读线程和写线程是同时运行的,但是通过使用读写锁,可以确保对文件的安全访问,防止读和写操作之间的冲突。
注意:
读线程 (read_func) 在读取文件时获取读锁 (pthread_rwlock_rdlock),
这时其他读线程也可以同时获取读锁。
这允许多个读线程同时读取文件内容,因为读操作不会互斥。
在使用读写锁时,写线程获得写锁时会阻塞其他写线程和读线程,
以确保在写入文件时不会同时有其他线程读或写。
在这个程序中,当一个线程(写线程)获得写锁时,
其他线程(包括读线程和其他写线程)都会被阻塞,直到写线程释放写锁。
所以,写线程会独占地访问文件,直到它完成写入并释放写锁。
这种方式确保了写的原子性,防止多个写线程之间和读线程之间的竞争条件。
为什么需要读锁?
虽然读操作本身不会改变数据,但如果在读的同时有其他线程在写,就可能读到不一致或不准确的数据。
因此,在多线程环境下,为了确保数据的一致性,读操作也需要进行同步,而这就是使用读写锁的原因。
上述的程序中,pthread_rwlock_rdlock 用于获取读锁,而 pthread_rwlock_unlock 用于释放读锁。
这样做的目的是:
通过获取读锁,确保在读取文件内容时不会被写线程中断,避免了读线程和写线程之间的竞态条件。
释放读锁,以允许其他读线程或写线程访问文件。
使用读写锁可以实现多个线程同时读取文件,但在写线程写入时阻塞读线程,以保证对文件的安全访问。
这种同步机制确保了对共享资源的正确和一致的访问。
为什么此代码只需要一个读的回调函数?
在这个特定的程序中,可能只需要一个读回调函数。
读回调函数负责获取读锁,读取文件内容,然后释放读锁,以确保在读的过程中其他线程不会写入文件。
由于读操作本身不修改数据,多个读线程可以并发地执行。
写操作则需要更谨慎地处理,因为写线程在写入时需要独占访问,避免与其他写线程或读线程发生冲突。
因此,写回调函数需要获取写锁,执行写操作,然后释放写锁。
这里为什么需要usleep(1000)?
usleep(1000); 的目的是让读线程休眠一毫秒(1000微秒)。
这样的操作通常是为了减缓循环的执行速度,以避免过于频繁地执行文件读取操作。
usleep 函数用于在指定的微秒数内挂起当前线程的执行。