当前位置: 首页 > news >正文

莱芜环保网站wordpress 短信插件

莱芜环保网站,wordpress 短信插件,台州手机网站制作,福田欧辉是国企吗Linux系统编程 day09 线程同步 1.互斥锁2.死锁3.读写锁4.条件变量#xff08;生产者消费者模型#xff09;5.信号量 1.互斥锁 互斥锁是一种同步机制#xff0c;用于控制多个线程对共享资源的访问#xff0c;确保在同一时间只有一个线程可以访问特定的资源或执行特定的操作… Linux系统编程 day09 线程同步 1.互斥锁2.死锁3.读写锁4.条件变量生产者消费者模型5.信号量 1.互斥锁 互斥锁是一种同步机制用于控制多个线程对共享资源的访问确保在同一时间只有一个线程可以访问特定的资源或执行特定的操作。如果没有互斥锁对于多个线程的程序则可能会出现一些未知的问题。比如下面有两个线程一个是打印“hello world”的线程一个是打印“HELLO WORLD”的线程。但是不同的是这个词汇是分两次分别打印的程序代码如下。 #include stdio.h #include stdlib.h #include unistd.h #include string.h #include sys/types.h #include pthread.h #include time.h// 打印 hello world void *mythread1(void *args) {while(1){printf(hello );sleep(rand() % 3);printf(world\n);sleep(rand() % 3);}pthread_exit(NULL); }// 打印HELLO WORLD void *mythread2(void *args) {while(1){printf(HELLO );sleep(rand() % 3);printf(WORLD\n);sleep(rand() % 3);}pthread_exit(NULL); }int main() {int ret 0;pthread_t thread1, thread2;// 初始化随机数种子srand(time(NULL));ret pthread_create(thread1, NULL, mythread1, NULL);if(ret ! 0){printf(pthread_create error: [%s]\n, strerror(ret));return -1;}ret pthread_create(thread2, NULL, mythread2, NULL);if(ret ! 0){printf(pthread_create error: [%s]\n, strerror(ret));return -1;}// 等待线程结束pthread_join(thread1, NULL);pthread_join(thread2, NULL);return 0; }将上述文件命名为01.pthread_lock.c使用命令make 01.pthread_lock则会自动编译01.pthread_lock.c生成01.pthread_lock运行该程序可以看到如下现象。 可以发现两个进程打印出来的“hello world”和“HELLO WORLD”并不是连续在一起的所以需要加互斥锁将打印的代码进行加锁。下面是互斥锁的一些函数。 /*** brief 定义一把互斥锁*/ pthread_mutex_t mutex_var;/** * brief 初始化互斥锁* param mutex 互斥锁* param mutexattr 互斥锁属性,传入NULL为默认属性* return 是否初始化成功初始化成功返回0初始化失败返回错误码* retval int*/ int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);/** * brief 互斥锁加锁* param mutex 互斥锁* return 是否加锁成功加锁成功返回0加锁失败会阻塞在这里发生错误返回错误码* retval int*/ int pthread_mutex_lock(pthread_mutex_t *mutex);/** * brief 互斥锁尝试加锁* param mutex 互斥锁* return 是否加锁成功加锁成功返回0加锁失败直接返回0发生错误返回错误码* retval int*/ int pthread_mutex_trylock(pthread_mutex_t *mutex);/** * brief 互斥锁解锁* param mutex 互斥锁* return 是否解锁成功解锁成功返回0解锁失败返回错误码* retval int*/ int pthread_mutex_unlock(pthread_mutex_t *mutex);/** * brief 摧毁互斥锁* param mutex 互斥锁* return 摧毁成功返回0摧毁失败返回错误码* retval int*/ int pthread_mutex_destroy(pthread_mutex_t *mutex);现在在每个线程运行的函数中加入互斥锁代码如下 #include stdio.h #include stdlib.h #include unistd.h #include string.h #include sys/types.h #include pthread.h #include time.h// 定义一把互斥锁 pthread_mutex_t mutex;// 打印 hello world void *mythread1(void *args) {while(1){// 加锁pthread_mutex_lock(mutex);printf(hello );sleep(rand() % 3);printf(world\n);sleep(rand() % 3);// 解锁pthread_mutex_unlock(mutex);}pthread_exit(NULL); }// 打印HELLO WORLD void *mythread2(void *args) {while(1){// 加锁pthread_mutex_lock(mutex);printf(HELLO );sleep(rand() % 3);printf(WORLD\n);sleep(rand() % 3);// 解锁pthread_mutex_unlock(mutex);}pthread_exit(NULL); }int main() {int ret 0;pthread_t thread1, thread2;// 初始化随机数种子srand(time(NULL));// 初始化互斥锁pthread_mutex_init(mutex, NULL);ret pthread_create(thread1, NULL, mythread1, NULL);if(ret ! 0){printf(pthread_create error: [%s]\n, strerror(ret));return -1;}ret pthread_create(thread2, NULL, mythread2, NULL);if(ret ! 0){printf(pthread_create error: [%s]\n, strerror(ret));return -1;}// 等待线程结束pthread_join(thread1, NULL);pthread_join(thread2, NULL);// 摧毁互斥锁pthread_mutex_destroy(mutex);return 0; }编译后运行如下结果 可以观察到这次的运行结果并不会出现线程间交叉打印的情况。 2.死锁 死锁是由程序员对互斥锁的使用不当而产生的并不是操作系统提供的一种机制。死锁的产生主要由两种形式。 第一种是自己锁自己也就是调用了两次加锁。也就是说线程1加锁了再没释放锁的时候线程1又申请加锁此时就会产生死锁且死锁阻塞到该位置还有一种情况是线程1加锁了但是线程1的整个程序里面没有释放锁的相关操作或者根本执行不到释放锁的代码此时若线程2申请加锁则程序阻塞在此处若线程1继续申请加锁则阻塞在线程1加锁处。 第二种是相互锁住线程1拥有A锁请求B锁线程2拥有B锁请求A锁这样就相互阻塞住了。 合理的编写程序能够避免死锁解决司所有一下一些方法 1.让线程按照一定的顺序去访问共享资源。2.在访问其它锁的时候先释放自己的锁。3.调用pthread_mutex_trylock该函数若加锁不成功会立刻返回此时就不会造成阻塞死等。 3.读写锁 读写锁也叫共享独占锁读时共享写时独占极大地提高了程序运行的效率。也就是当读写锁以读模式锁住的时候它是共享的当读写锁以写模式锁住的时候是独占的。读写锁适合的场景是读的次数远大于写的情况下的。 读写锁有以下一些特性 1.读写锁是写模式加锁的时候在解锁前所有要对该锁加锁的线程都会阻塞。2.读写锁是读模式加锁的时候如果线程以读模式加锁则会成功以写模式加锁会阻塞。3.读写锁是读模式加锁的时候如果既有读线程也有写线程则尝试加锁都会被阻塞。若锁被释放则写线程会请求锁成功而读线程会继续阻塞。也就是读写都有的时候优先满足写模式的锁也就是写比读优先级更高。 下面是读写锁的接口函数 /*** brief 定义读写锁变量*/ pthread_rwlock_t rwlock_var;/*** brief 读写锁初始化* * 初始化读写锁 * param rwlock 读写锁* param attr 读写锁属性传入NULL为默认属性* return 初始化成功返回0反之返回错误码* retval int*/ int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);/*** brief 读写锁加读锁* param rwlock 读写锁* return 成功加锁返回0失败则返回错误码锁被占用则一直阻塞* retval int*/ int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);/*** brief 读写锁尝试加读锁* param rwlock 读写锁* return 成功加锁返回0失败则返回错误码锁被占用也会直接返回* retval int*/ int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);/*** brief 读写锁加写锁* param rwlock 读写锁* return 成功加锁返回0失败则返回错误码锁被占用则一直阻塞* retval int*/ int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);/*** brief 读写锁尝试加写锁* param rwlock 读写锁* return 成功加锁返回0失败则返回错误码锁被占用也会直接返回* retval int*/ int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);/*** brief 读写锁解锁* param rwlock 读写锁* return 成功解锁返回0失败则返回错误码* retval int*/ int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);/*** brief 摧毁读写锁* param rwlock 读写锁* return 成功摧毁返回0失败则返回错误码* retval int*/ int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);下面举一个关于读写锁的例子有3个写线程5个读线程写线程修改数字读线程读取数字输出到终端。代码如下 #include stdio.h #include stdlib.h #include string.h #include unistd.h #include time.h #include sys/types.h #include pthread.h// 线程总数 #define THREAD_COUNT 8// 定义读写锁 pthread_rwlock_t rwlock;// 计数 int number 0;// 读线程回调函数 void *thread_read(void *args) {int i *(int *)args;int cur;while(1){// 读写锁加读锁pthread_rwlock_rdlock(rwlock);cur number;printf([%d]-R: [%d]\n, i, cur);// 读写锁加写锁pthread_rwlock_unlock(rwlock);sleep(rand() % 3);} }// 写线程回调函数 void *thread_write(void *args) {int i *(int *)args;int cur;while(1){// 读写锁加写锁pthread_rwlock_wrlock(rwlock);cur number;cur 1;number cur;printf([%d]-W: [%d]\n, i, cur);// 读写锁解锁pthread_rwlock_unlock(rwlock);sleep(rand() % 3);} }int main() {int ret 0;int arr[THREAD_COUNT]; // 记录第几个线程pthread_t threads[THREAD_COUNT]; // 线程数组// 初始化随机数种子srand(time(NULL));// 初始化读写锁pthread_rwlock_init(rwlock, NULL);// 创建3个写线程for(int i 0; i 3; i){arr[i] i;ret pthread_create(threads[i], NULL, thread_write, arr[i]);if(ret ! 0){printf(pthread_create error: [%s]\n, strerror(ret));return -1;}}// 创建5个读线程for(int i 3; i THREAD_COUNT; i){arr[i] i;ret pthread_create(threads[i], NULL, thread_read, arr[i]);if(ret ! 0){printf(pthread_create error: [%s]\n, strerror(ret));return -1;}}// 回收子线程for(int i 0; i THREAD_COUNT; i){pthread_join(threads[i], NULL);}// 释放锁pthread_rwlock_destroy(rwlock);return 0; }编译程序运行的结果如下 可以发现读线程输出的数字都是写线程修改后的并不会出现读线程的数据是不符合写进程修改后的。如果没有加读写锁则会让读写乱套当写线程修改完数据之后读线程也可能正好输出修改前的值。 4.条件变量生产者消费者模型 条件变量主要是用于生产者消费者模型的。所谓生产者消费者模型就是生产者负责生成东西而消费者负责对生产者生产者的东西用于消费。在这种情况下生产者消费的东西其实也就是临界资源所以需要使用互斥锁进行访问。而在生产者消费者模型中无法确定是生产者先访问还是消费者先访问所以在生产者没有生产东西的时候消费者也可能需要进行消费或者生产者生产的进度会更不上消费者消耗的进度。因此为了保证消费者每次消费都是在生产者生成东西之后或者生产的东西有剩余所以需要使用到条件变量此时原来并行的生产者消费者就变成了串行的生产者消费者。关于条件变量的接口如下 /*** brief 条件变量定义定义一个条件变量*/ pthread_cond_t cond_var;/*** brief 初始化条件变量* param cond 条件变量* param cond_attr 条件变量属性传入NULL为默认属性* return 初始化成功返回0失败返回错误码* retval int*/ int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);/*** brief 唤醒至少一个阻塞在该条件变量上的线程* param cond 条件变量* return 唤醒成功返回0失败返回0* retval int*/ int pthread_cond_signal(pthread_cond_t *cond);/*** brief 唤醒所有阻塞在该条件变量上的线程* param cond 条件变量* return 唤醒成功返回0失败返回0* retval int*/ int pthread_cond_broadcast(pthread_cond_t *cond);/*** brief 阻塞等待条件变量满足* * 当条件变量不满足的时候会阻塞线程并进行解锁* 当条件变量满足的时候会解除线程阻塞并加锁* param cond 条件变量* param mutex 互斥锁* return 成功返回0失败返回0* retval int*/ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);/*** brief 销毁条件变量* param cond 条件变量* return 销毁成功返回0失败返回0* retval int*/ int pthread_cond_destroy(pthread_cond_t *cond);需要注意的是条件变量本身并不是锁但是它可以造成线程的阻塞所以通常需要与互斥锁配合使用。使用互斥锁是为了保护共享数据使用条件变量可以使线程阻塞等待某个条件的发生当条件满足的时候解除阻塞。 下面给一个关于生产者消费者的例子有一个链表生产者负责不断地从堆区申请空间开辟内存并且存入数据并将节点放到链表上而消费者负责在链表上不断地读取数据并且释放节点代码如下所示。 #include stdio.h #include stdlib.h #include string.h #include unistd.h #include time.h #include pthread.h// 链表节点定义 typedef struct node {int data;struct node *next; } node_t;// 互斥锁 pthread_mutex_t mutex; // 条件变量 pthread_cond_t cond; // 头结点 node_t *head NULL;// 生产者线程 void *thread_product(void *args) {while(1){// 申请节点node_t *pnode (node_t *)malloc(sizeof(node_t));if(pnode NULL){perror(malloc error);exit(-1);}pnode-data rand() % 1000;// 加互斥锁pthread_mutex_lock(mutex);pnode-next head;head pnode;printf([Productor]: %d\n, pnode-data);// 解锁pthread_mutex_unlock(mutex);// 通知消费者线程pthread_cond_signal(cond);sleep(rand() % 3);}pthread_exit(NULL); }// 消费者线程 void *thread_consume(void *args) {while(1){// 加互斥锁pthread_mutex_lock(mutex);if(head NULL){// 条件满足解除阻塞并加锁// 条件不满足阻塞并解锁pthread_cond_wait(cond, mutex);}node_t *pnode head;head pnode-next;printf([Consumer]: %d\n, pnode-data);free(pnode);pnode NULL;// 解锁pthread_mutex_unlock(mutex);sleep(rand() % 3);}pthread_exit(NULL); }int main() {int ret;// 生产者与消费者线程pthread_t thread_productor, thread_consumer;// 初始化随机数种子srand(time(NULL));// 初始化条件变量与互斥锁pthread_mutex_init(mutex, NULL);pthread_cond_init(cond, NULL);ret pthread_create(thread_productor, NULL, thread_product, NULL);if(ret ! 0){printf(pthread_create error: [%s]\n, strerror(ret));return -1;}ret pthread_create(thread_consumer, NULL, thread_consume, NULL);if(ret ! 0){printf(pthread_create error: [%s]\n, strerror(ret));return -1;}// 回收线程pthread_join(thread_productor, NULL);pthread_join(thread_consumer, NULL);// 销毁互斥锁与条件变量pthread_mutex_destroy(mutex);pthread_cond_destroy(cond);return 0; }运行可以发现消费者消费的是生产者最后一次生产出来的。 如果将上述的代码改为多个消费者线程和多个生产者线程的时候则上述的代码就会出现Segmentation fault (core dumped)的情况。首先需要分析这个问题是如何产生的。这个问题的原因就是访问了非法的空间内存而这种情况只会在消费者线程中出现。而经过定位可以发现对NULL指针进行了操作也就是head pnode-next; printf([Consumer]: %d\n, pnode-data);。首先在生产者线程中调用了pthread_cond_signal函数的时候会通知一个或者多个消费者线程唤醒满足而此时这几个线程都不在处于阻塞状态就会进行解锁当连续多个消费者线程依次解锁的时候向下运行就会导致访问到非法空间。比如链表中只有一个节点而此时有三个消费者线程处于阻塞状态且阻塞在pthread_cond_wait处。此时生产者线程调用了pthread_cond_signal则会唤醒这消费者三个线程的一个或者是两个又或者是三个。此时三个的条件变量都满足第一个消费者线程抢到了锁消费了一个结点。而此时生产者线程没有创造新的节点由于第二个线程已经满足了条件变量此时它抢到了互斥锁则也会向下执行由于前面的线程已经消费了唯一的一个线程所以此时就会访问到非法的内存空间。应对这种情况则是在pthread_cond_wait后面对头结点再一次进行判断若满足条件则向下执行不满足条件则解锁并使用continue进入下一次循环。以5个生产者线程10个消费者线程为例代码如下所示 #include stdio.h #include stdlib.h #include unistd.h #include pthread.h #include time.h #include string.h// 线程数量 #define PRODUCER_THREAD_COUNT 5 // 生产线程数量 #define CONSUMER_THREAD_COUNT 10 // 消费线程数量// 链表节点 struct node {int data;struct node *next; };// 头结点 struct node *head NULL;// 互斥锁 pthread_mutex_t mutex;// 条件变量 pthread_cond_t cond;void *thread_producer(void *arg) {struct node *pnode NULL;int idx (int)(long)arg;while(1){pnode (struct node *)malloc(sizeof(struct node));if(pnode NULL){perror(malloc error);exit(-1);}pnode-data rand() % 100;// 加锁pthread_mutex_lock(mutex);pnode-next head;head pnode;printf(P[%d]: %d\n, idx, pnode-data);pthread_mutex_unlock(mutex);pthread_cond_signal(cond);sleep(rand() % 3);}pthread_exit(NULL); }void *thread_consumer(void *arg) {struct node *pnode NULL;int idx (int)(long)arg;while(1){ pthread_mutex_lock(mutex);if(head NULL){pthread_cond_wait(cond, mutex);}// 若头节点为空if(head NULL){// 解锁pthread_mutex_unlock(mutex);continue;}pnode head;head head-next;printf(C[%d]: %d\n, idx, pnode-data);pthread_mutex_unlock(mutex);free(pnode);pnode NULL;sleep(rand() % 3);}pthread_exit(NULL); }int main() {int ret 0;pthread_t producer_thread[PRODUCER_THREAD_COUNT], consumer_thread[CONSUMER_THREAD_COUNT];srand((unsigned int)time(NULL));// 初始化互斥锁pthread_mutex_init(mutex, NULL);// 初始化条件变量pthread_cond_init(cond, NULL);// 创建生产者线程for(int i 1; i PRODUCER_THREAD_COUNT; i){ret pthread_create(producer_thread[i - 1], NULL, thread_producer, (void *)(long)i);if(ret ! 0){printf(pthread_create error: %s\n, strerror(ret));exit(-1);}}// 创建消费者线程for(int i 1; i CONSUMER_THREAD_COUNT; i){ ret pthread_create(consumer_thread[i - 1], NULL, thread_consumer, (void *)(long)i);if(ret ! 0){printf(pthread_create error: %s\n, strerror(ret));exit(-1);}}// 回收线程for(int i 0; i PRODUCER_THREAD_COUNT; i){pthread_join(producer_thread[i], NULL);}for(int i 0; i CONSUMER_THREAD_COUNT; i){pthread_join(consumer_thread[i], NULL);}// 摧毁互斥锁pthread_mutex_destroy(mutex);return 0; }运行结果如下 5.信号量 信号量可以理解为是升级版的互斥锁因为互斥锁只能运行一个线程进行访问而信号量可以支持多个线程或者进程。关于信号量的接口如下所示 /*** brief 定义信号量*/ sem_t sem_var;/*** brief 初始化信号量* param sem 信号量* param pshared 0表示线程同步1表示进程同步* param value 最多有多少个线程操作共享数据* return 成功返回0失败返回-1并设置errno的值。* retval int*/ int sem_init(sem_t *sem, int pshared, unsigned int value);/*** brief 相当于sem--当sem为0的时候引起阻塞即加锁。* param sem 信号量* return 成功返回0失败返回-1并设置error的值* retval int*/ int sem_wait(sem_t *sem);/*** brief 与sem_wait一样但是不会阻塞* param sem 信号量* return 成功返回0失败返回-1并设置error的值* retval int*/ int sem_trywait(sem_t *sem);/*** brief 相当于sem* param sem 信号量* return 成功返回0失败返回-1并设置errno的值* retval int*/ int sem_post(sem_t *sem);/*** brief 摧毁信号量* param sem 信号量* return 成功返回0失败返回-1并设置errno的值*/ int sem_destroy(sem_t *sem);什么时候用信号量呢也就是当一个共享资源能够被多个现成进行操作的时候。比如停车场里面会有很多个车位可以同时允许多个车位进行停靠代码如下所示 #include stdio.h #include stdlib.h #include time.h #include string.h #include unistd.h #include pthread.h #include semaphore.h// 车位数量 #define PARKING_COUNT 10// 线程数量 #define ENTRY_PARKING_THREAD_COUNT 10 // 进入停车场线程 #define EXIT_PARKING_THREAD_COUNT 10 // 离开停车场线程// 结点模拟停车场 struct park {int use; // 是否被使用int parking_space; // 停车位编号int license_plate_number; // 停车车牌号 }park[PARKING_COUNT];// 信号量 sem_t entry_sem; // 进入停车场信号量 sem_t exit_sem; // 离开停车场信号量// 进入停车场线程 void *thread_entryParking(void *arg) {// 标记是否停车int parking_space -1;int license_plate_number 0;while(1){// 车位标志parking_space 0;license_plate_number rand() % 10000 10000;printf(车牌号[%d]驶来了\n, license_plate_number);sleep(rand() % 5);// 开始停车sem_wait(entry_sem);// 寻找车位for(int i 0; i PARKING_COUNT; i){if(park[i].use 0){parking_space park[i].parking_space;break;}}// 没有找到停车位if(parking_space 0){sem_post(entry_sem);printf(车牌号[%d]因为没有车位离开了\n, license_plate_number);continue;}// 开始停车park[parking_space - 1].license_plate_number license_plate_number;park[parking_space - 1].use 1;printf(车牌号[%d]停进了停车场的[%d]车位\n, license_plate_number, park[parking_space - 1].parking_space);// 通知可以有车离开sem_post(exit_sem);}pthread_exit(NULL); }// 离开停车场 void *thread_exitParking(void *arg) {// 车位int parking_space 0;while(1){sleep(rand() % 5);// 随机车位parking_space rand() % PARKING_COUNT 1;sem_wait(exit_sem);// 该车位没有车继续下一次if(park[parking_space - 1].use 0){sem_post(exit_sem);continue;}printf(车牌号[%d]离开了停车场的[%d]车位\n, park[parking_space - 1].license_plate_number, park[parking_space - 1].parking_space);// 标记车位没有使用park[parking_space - 1].use 0;// 可停车数加1sem_post(entry_sem);}pthread_exit(NULL); }int main() {int ret 0;// 进入停车场线程pthread_t entryParking_thread[ENTRY_PARKING_THREAD_COUNT];// 离开停车场线程pthread_t exitParking_thread[EXIT_PARKING_THREAD_COUNT];srand((unsigned int)time(NULL));// 初始化信号量sem_init(entry_sem, 0, PARKING_COUNT);sem_init(exit_sem, 0, 0);// 初始化停车场memset(park, 0x00, sizeof park);for(int i 0; i PARKING_COUNT; i){park[i].parking_space i 1;}// 创建停车线程for(int i 0; i ENTRY_PARKING_THREAD_COUNT; i){ret pthread_create(entryParking_thread[i], NULL, thread_entryParking, NULL);if(ret ! 0){printf(pthread_create error: %s\n, strerror(ret));exit(-1);}}// 创建离开停车场线程for(int i 0; i EXIT_PARKING_THREAD_COUNT; i){ret pthread_create(exitParking_thread[i], NULL, thread_exitParking, NULL);if(ret ! 0){printf(pthread_create error: %s\n, strerror(ret));exit(-1);}}// 回收线程for(int i 0; i ENTRY_PARKING_THREAD_COUNT; i){pthread_join(entryParking_thread[i], NULL);}for(int i 0; i EXIT_PARKING_THREAD_COUNT; i){pthread_join(exitParking_thread[i], NULL);}// 摧毁信号量sem_destroy(entry_sem);sem_destroy(exit_sem);return 0; }运行上述代码结果如下
http://www.w-s-a.com/news/160179/

相关文章:

  • 上海简站商贸有限公司福州哪家专业网站设计制作最好
  • 博客网站开发流程苏州专业做网站的公司哪家好
  • 四川手机网站建设西安 网站 高端 公司
  • 织梦大气绿色大气农业能源化工机械产品企业网站源码模版建筑工程知识零基础
  • 广州番禺网站公司v2017网站开发
  • 微信公众号怎么做微网站wordpress和dz
  • 西部数码网站管理助手 301福州搜索优化实力
  • 响应式网站介绍页面模板功能找不到
  • 公司网站如何seo自己做资讯网站
  • 天津网站建设软件开发招聘企业信用信息查询公示系统上海
  • 网站备案中做正品的网站
  • 网站建设0基础学起青海企业网站开发定制
  • 网站定制项目上海快速建站
  • 大型视频网站建设方案东莞企业网站建设开发
  • 西安php网站制作可以用AI做网站上的图吗
  • 网站开发工程师和前端企业网络推广公司
  • 泉州开发网站的公司有哪些电脑网页翻译
  • 河北省建设机械会网站首页刚做的网站怎么收录
  • 什么网站专门做自由行的framework7做网站
  • 网页设计与网站建设书籍包头住房与城乡建设局网站
  • 重庆网站建设平台免费猎头公司收费收费标准和方式
  • 形象设计公司网站建设方案书打开一个不良网站提示创建成功
  • 网站手机页面如何做网站关键字 优帮云
  • 免费的黄冈网站有哪些下载软件系统软件主要包括网页制作软件
  • 企业微站系统重庆高端网站建设价格
  • 有没有做衣服的网站吗网站自适应开发
  • 青海省制作网站专业专业定制网吧桌椅
  • 网站开发的项目17岁高清免费观看完整版
  • 手机网站建设多少钱一个门网站源码
  • 重庆 网站开发天津住房和城乡建设厅官方网站