8.线程-竞争故障
/*
@:创建20个线程,同时对1个文件内的数字进行加1
*/
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<string.h>
#define THRNUM 20
#define FNAME "./1.txt"
#define LINESIZE 1024
static void *thr_add(void *p)
{
FILE *fp;
char linebuf[LINESIZE];
fp = fopen(FNAME, "r+");
if(NULL == fp)
{
perror("fopen()");
exit(1);
}
fgets(linebuf, LINESIZE, fp);
fseek(fp, 0, SEEK_SET); //文件内容指针重新回到开头
//相当于所有线程都在此刻休眠1s,然后同时对旧值做加1操作
//通过sleep放大竞争故障
sleep(1);
fprintf(fp, "%d\n", atoi(linebuf) + 1);
fclose(fp);
pthread_exit(NULL);
}
int main()
{
pthread_t tid[THRNUM];
int i, j, err;
for(i = 0; i < THRNUM; i++)
{
err = pthread_create(&tid[i], NULL, thr_add, NULL);
if(err)
{
fprintf(stderr, "%d pthread_create() %s\n", i, strerror(err));
for(j = 0; j < i; j++)
pthread_join(tid[j], NULL);
exit(1);
}
}
for(i = 0; i < THRNUM; i++)
pthread_join(tid[i], NULL);
exit(0);
}
旧值为44,结果则为45,与预期结果64不符合。
1.多个线程同时打开文件没有问题。问题出现在同时读和写,因为有可能有的线程在读的时候,其他线程在写。
2.为了保证每个线程都能把内容写到文件里面去,要么每个线程单独做fclose,要么在fprintf与fclose之间加上fflush,在fprintf后立即把缓冲区内容刷新到文件里面去。(fprintf是写入到文件里面取得,全缓冲)。
修改:
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<string.h>
#define THRNUM 20
#define FNAME "./1.txt"
#define LINESIZE 1024
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
static void *thr_add(void *p)
{
FILE *fp;
char linebuf[LINESIZE];
fp = fopen(FNAME, "r+");
if(NULL == fp)
{
perror("fopen()");
exit(1);
}
pthread_mutex_lock(&mut);
fgets(linebuf, LINESIZE, fp);
fseek(fp, 0, SEEK_SET); //文件内容指针重新回到开头
sleep(1);
fprintf(fp, "%d\n", atoi(linebuf) + 1);
fclose(fp);
pthread_mutex_unlock(&mut);
pthread_exit(NULL);
}
int main()
{
pthread_t tid[THRNUM];
int i, j, err;
for(i = 0; i < THRNUM; i++)
{
err = pthread_create(&tid[i], NULL, thr_add, NULL);
if(err)
{
fprintf(stderr, "%d pthread_create() %s\n", i, strerror(err));
for(j = 0; j < i; j++)
pthread_join(tid[j], NULL);
exit(1);
}
}
for(i = 0; i < THRNUM; i++)
pthread_join(tid[i], NULL);
pthread_mutex_destroy(&mut);
exit(0);
}
9.线程同步
linux下为了多线程同步,通常用到锁的概念。
1)posix下抽象了一个锁类型的结构:ptread_mutex_t。通过对该结构的操作,来判断资源是否可以访问。顾名思义,加锁(lock)后,别人就无法打开,只有当锁没有关闭(unlock)的时候才能访问资源。
即对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
2)使用互斥锁(互斥)可以使线程按顺序执行。通常,互斥锁通过确保一次只有一个线程执行代码的临界段来同步多个线程。互斥锁还可以保护单线程代码。
3)要更改缺省的互斥锁属性,可以对属性对象进行声明和初始化。通常,互斥锁属性会设置在应用程序开头的某个位置,以便可以快速查找和轻松修改。
9.1 ptread_mutex_init()函数
#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
对锁进行初始化,是以动态方式创建互斥锁的;
函数成功执行后,锁被初始化为未锁住态。
参数:
mutex:
要操作的锁结构类型对象
attr:
新建锁的属性,如果为NULL,则使用默认的互斥锁属性,默认属性为快速互斥锁。
返回值:
成功完成之后会返回零,失败返回非零。
互斥锁的属性:
互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。
PTHREAD_MUTEX_TIMED_NP
缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
解锁:可以是同进程内任何线程
PTHREAD_MUTEX_RECURSIVE_NP
嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
解锁:必须由加锁者解锁
PTHREAD_MUTEX_ERRORCHECK_NP
检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
解锁:必须由加锁者解锁才有效,否则返回EPERM
PTHREAD_MUTEX_ADAPTIVE_NP
适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
解锁:可以是同进程内任何线程
9.2 静态创建互斥锁
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER
9.3 pthread_mutex_destroy()函数
#include<pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex)
用于注销一个互斥锁,释放锁占用的资源 ,仅当锁处于解锁状态才能注销。
返回值:
成功返回0,失败返回错误码。如果注销加锁的互斥锁,则会返回错误码EBUSY。
9.4 pthread_mutex_lock()函数
#include<pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex)
工作机制:
阻塞调用。线程调用该函数让互斥锁上锁,如果该互斥锁已被另一个线程锁定和拥有,则调用该线程将阻塞,直到该互斥锁变为可用为止。
返回值:
成功返回0,失败返回非0;
9.5 pthread_mutex_trylock()函数
#include<pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex)
工作机制:
非阻塞调用模式。如果互斥量没被锁住, 函数将把互斥量加锁, 并获得对共享资源的访问权限; 如果互斥量被锁住了, 函数将不会阻塞等待而直接返回EBUSY,表示共享资源处于忙状态。
返回值:
成功返回0,失败返回非0;
9.6 pthread_mutex_unlock()函数
#include<pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex)
解锁
返回值:
成功返回0,失败返回非0;
9.7 pthread_once()函数
#include<pthread.h>
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));
功能:
本函数使用初值为PTHREAD_ONCE_INIT的once_control变量保证init_routine()函数在本进程执行序列中仅执行一次。
1)在多线程环境中,有些事仅需要执行一次。通常当初始化应用程序时,可以比较容易地将其放在main函数中。但当你写一个库时,就不能在main里面初始化了,你可以用静态初始化,但使用一次初始化(pthread_once)会比较容易些。
2)在多线程编程环境下,尽管pthread_once()调用会出现在多个线程中,init_routine()函数仅执行一次,究竟在哪个线程中执行是不定的,是由内核调度来决定。
3)Linux Threads使用互斥锁和条件变量保证由pthread_once()指定的函数执行且仅执行一次,而once_control表示是否执行过。
4)如果once_control的初值不是PTHREAD_ONCE_INIT(Linux Threads定义为0),pthread_once() 的行为就会不正常。
5)在LinuxThreads中,实际"一次性函数"的执行状态有三种:NEVER(0)、IN_PROGRESS(1)、DONE(2),如果once初值设为1,则由于所有pthread_once()都必须等待其中一个激发"已执行一次"信号,因此所有pthread_once ()都会陷入永久的等待中;如果设为2,则表示该函数已执行过一次,从而所有pthread_once()都会立即返回0。
10.锁链例程
/*
4个线程同时往终端分别打印a, b, c, d;
打印结果要是abcdabcd...
5s中后进程结束;
处理办法:锁链
*/
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<string.h>
#define THRNUM 4
static pthread_mutex_t mut[THRNUM];
static int next(int n)
{
if(n + 1 == THRNUM)
return 0;
else
return n+1;
}
static void *thr_func(void *p)
{
int n = (int)p;
int c = 'a' + n;
while(1)
{
pthread_mutex_lock(&mut[n]);
write(1, &c, 1);
pthread_mutex_unlock(&mut[next(n)]);
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid[THRNUM];
int i, j, err;
//初始化4把锁,并分别把创建的4个线程锁上,然后线程各自跳到thr_func, 各自调用
//pthread_mutex_lock试图在加锁,因此在main线程中已经被加锁,所以线程会被阻塞在这里
//main线程运行到pthread_mutex_unlock(&mut[0]), 线程tid[0]在main线程中解锁,在thr_func就可以
//执行加锁操作,write后,解锁tid[1]...链式反应
//加锁和解锁针对的对象是一段代码,而不是一个变量
for(i = 0; i < THRNUM; i++)
{
pthread_mutex_init(&mut[i], NULL); //初始化锁
pthread_mutex_lock(&mut[i]); //加锁
err = pthread_create(&tid[i], NULL, thr_func, (void *)i);
if(err)
{
fprintf(stderr, "%d pthread_create() %s\n", i, strerror(err));
for(j = 0; j < i; j++)
pthread_join(tid[j], NULL);
exit(1);
}
}
pthread_mutex_unlock(&mut[0]);
alarm(5);
for(i = 0; i < THRNUM; i++)
pthread_join(tid[i], NULL);
exit(0);
}
11.线程任务池
注意点:
1)注意临界区的跳转语句break,continue, 函数调用。如果是跳转到临界区外,一定要解锁后在跳转,否则很可能产生死锁。
2)sched_yield()函数的使用
不足:
程序运行有些慢,采用的查询法,上、下游都处于忙等的状态
优化方法:通知法,上游等通知num=0则放数,下游等上游把任务放进去之后,发通知唤醒空闲的线程。
/*
上游main线程:把30000000 - 30000200之间的数丢给全局变量num
num: >0,抛给任务池;=0,表示没有要接收或者抛出的值;=-1 数据丢完,退出main线程
下游任务池:从num处取数,并进行计算
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM 4
//程序运行有些慢,采用的查询法,上、下游都处于忙等的状态
//优化方法:通知法,上游等通知num=0则放数,下游等上游把任务放进去之后,发通知唤醒空闲的线程
static int num = 0;
static pthread_mutex_t mut_num = PTHREAD_MUTEX_INITIALIZER;
static void *thr_prime(void *p)
{
int i, j, mark;
while(1)
{
pthread_mutex_lock(&mut_num);
while(num == 0)
{
pthread_mutex_unlock(&mut_num);
sched_yield();
pthread_mutex_lock(&mut_num);
}
if(num == -1)
{
//注意临界区的跳转语句break,continue, 函数调用。
//如果是跳转到临界区外,一定要解锁后在跳转
//此处用Break,相当于mut_num是在被lock的状态下跳转的 ,需要先解锁,否则会产生死锁
pthread_mutex_unlock(&mut_num);
break;
}
i = num;
num = 0;
pthread_mutex_unlock(&mut_num);
mark = 1;
for(j = 2; j < i/2; j++)
{
if(i % j == 0)
{
mark = 0;
break;
}
}
if(mark)
printf("[%d]%d is a primer\n", (int)p, i);
}
pthread_exit(NULL);
}
int main()
{
int i, j, err;
pthread_t tid[THRNUM];
for(i = 0; i < THRNUM; i++)
{
err = pthread_create(&tid[i], NULL, thr_prime, (void *)i);
if(err)
{
fprintf(stderr, "%d pthread_create(): %s\n", i, strerror(err));
for(j = 0; j < i; j++)
{
pthread_join(tid[j], NULL);
}
exit(1);
}
}
//下发任务,加锁后如果发现任务没被抢走,则解锁然后在加锁,
for(i = LEFT; i <= RIGHT; i++)
{
pthread_mutex_lock(&mut_num);
while(num != 0) //任务没被抢走为真
{
pthread_mutex_unlock(&mut_num);
sched_yield();//可以理解为时间非常短的sleep,并且不会引起调度颠簸
pthread_mutex_lock(&mut_num);
}
num = i; //下发任务
pthread_mutex_unlock(&mut_num);
}
pthread_mutex_lock(&mut_num);
//等待最后一个数计算完毕
while(num != 0)
{
pthread_mutex_unlock(&mut_num);
sched_yield();
pthread_mutex_lock(&mut_num);
}
num = -1;
pthread_mutex_unlock(&mut_num);
for(i = 0; i <= THRNUM; i++)
{
pthread_join(tid[i], NULL);
}
pthread_mutex_destroy(&mut_num);
exit(0);
}